Module: LogStruct::SemanticLogger::Setup

Extended by:
T::Sig
Defined in:
lib/log_struct/semantic_logger/setup.rb

Overview

Sets up SemanticLogger for a Rails app: its level, appenders and formatters, and the loggers it replaces

Constant Summary collapse

COMPONENT_LOAD_HOOKS =

Load hooks for the framework components whose loggers LogStruct replaces

T.let(
  %i[active_record action_controller action_mailer active_job action_view action_cable].freeze,
  T::Array[Symbol]
)
NOISY_LOGGERS =
/\A(ActionView|ActionController::RoutingError|ActiveRecord::SchemaMigration)/

Class Method Summary collapse

Class Method Details

.add_appenders(app) ⇒ void

This method returns an undefined value.

Parameters:

  • app (T.untyped)


99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/log_struct/semantic_logger/setup.rb', line 99

def self.add_appenders(app)
  config = LogStruct.config

  # Determine output destination
  io = determine_output(app)

  # Ruby buffers STDOUT when it isn't a terminal, so in a container lines waited until the buffer
  # filled or the process exited. (GoodJob's executable only syncs STDOUT when it writes its own
  # text logs there.)
  io.sync = true if io.equal?($stdout)

  appenders = []
  appenders << if Rails.env.development?
    if config.prefer_json_in_development
      # Default to production-style JSON in development when enabled
      ::SemanticLogger.add_appender(
        io: io,
        formatter: LogStruct::SemanticLogger::Formatter.new,
        filter: determine_filter
      )
    elsif config.enable_color_output
      # Opt-in colorful human formatter in development
      ::SemanticLogger.add_appender(
        io: io,
        formatter: LogStruct::SemanticLogger::ColorFormatter.new(
          color_map: config.color_map
        ),
        filter: determine_filter
      )
    else
      ::SemanticLogger.add_appender(
        io: io,
        formatter: LogStruct::SemanticLogger::Formatter.new,
        filter: determine_filter
      )
    end
  else
    # Use our custom JSON formatter in non-development environments
    ::SemanticLogger.add_appender(
      io: io,
      formatter: LogStruct::SemanticLogger::Formatter.new,
      filter: determine_filter
    )
  end

  # Log to Rails' log file too in development and test (config.local_environments), unless
  # RAILS_LOG_TO_STDOUT says not to. Production and other servers log to STDOUT only: nothing
  # rotates the file, and in a container it only fills the disk.
  local = config.local_environments.include?(Rails.env.to_sym)
  if local && app.config.paths["log"].first && ENV["RAILS_LOG_TO_STDOUT"].blank?
    appenders << ::SemanticLogger.add_appender(
      file_name: app.config.paths["log"].first,
      formatter: LogStruct::SemanticLogger::Formatter.new,
      filter: determine_filter
    )
  end
  # add_appender returns nil when it ignores a second STDOUT appender
  @appenders = appenders.compact
end

.configure_semantic_logger(app) ⇒ void

This method returns an undefined value.

Configures SemanticLogger and replaces Rails' loggers with LogStruct's:

  • the level from config.log_level (info in production, debug elsewhere)
  • the appenders for the environment (see add_appenders)
  • Rails.logger and the framework loggers (ActiveRecord, ActionController, ActiveJob, ...)

Parameters:

  • app (T.untyped) —

    The Rails application instance



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/log_struct/semantic_logger/setup.rb', line 46

def self.configure_semantic_logger(app)
  # Set SemanticLogger configuration
  ::SemanticLogger.application = Rails.application.class.module_parent_name
  ::SemanticLogger.environment = Rails.env

  # Determine log level from Rails config
  log_level = determine_log_level(app)
  ::SemanticLogger.default_level = log_level

  # Add appropriate appenders based on environment
  remove_appenders
  add_appenders(app)
  ReopenAfterFork.install!

  # Replace Rails.logger with SemanticLogger
  replace_rails_logger(app)
end

.configured? ⇒ Boolean

Returns:

  • (Boolean)


35
36
37
# File 'lib/log_struct/semantic_logger/setup.rb', line 35

def self.configured?
  !@logger.nil?
end

.determine_filter ⇒ T.proc.params(log: ::SemanticLogger::Log).returns(T::Boolean)?

With filter_noisy_loggers on, an appender filter that leaves out NOISY_LOGGERS' lines. SemanticLogger writes a line when a Proc filter returns true (a Regexp filter would keep only the names it matches).

Returns:

  • (T.proc.params(log: ::SemanticLogger::Log).returns(T::Boolean), nil)


178
179
180
181
182
# File 'lib/log_struct/semantic_logger/setup.rb', line 178

def self.determine_filter
  return nil unless LogStruct.config.filter_noisy_loggers

  ->(log) { !NOISY_LOGGERS.match?(log.name.to_s) }
end

.determine_log_level(app) ⇒ Symbol

Parameters:

  • app (T.untyped)

Returns:

  • (Symbol)


84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/log_struct/semantic_logger/setup.rb', line 84

def self.determine_log_level(app)
  level = if app.config.log_level
    app.config.log_level
  elsif Rails.env.production?
    :info
  elsif Rails.env.test?
    :debug
  else
    :debug
  end
  # Rails config.log_level can be a String or Symbol
  level.is_a?(String) ? level.to_sym : level
end

.determine_output(app) ⇒ T.untyped

Parameters:

  • app (T.untyped)

Returns:

  • (T.untyped)


160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/log_struct/semantic_logger/setup.rb', line 160

def self.determine_output(app)
  # Always honor explicit STDOUT directive
  return $stdout if ENV["RAILS_LOG_TO_STDOUT"].present?

  if Rails.env.test?
    # Use StringIO in test to keep stdout clean
    StringIO.new
  else
    # Use STDOUT for app logs in dev/production
    $stdout
  end
end

.reconfigure_appenders(app) ⇒ void

This method returns an undefined value.

Adds the appenders again, with the settings config/initializers changed after the logger was set up (prefer_json_in_development, enable_color_output, color_map, filter_noisy_loggers, local_environments). Appenders the app added in config/initializers stay.

Parameters:

  • app (T.untyped)


68
69
70
71
# File 'lib/log_struct/semantic_logger/setup.rb', line 68

def self.reconfigure_appenders(app)
  remove_appenders
  add_appenders(app)
end

.remove_appenders ⇒ void

This method returns an undefined value.

Removes the appenders add_appenders added, after writing the lines already queued for them



75
76
77
78
79
80
81
# File 'lib/log_struct/semantic_logger/setup.rb', line 75

def self.remove_appenders
  return if @appenders.empty?

  ::SemanticLogger.flush
  @appenders.each { |appender| ::SemanticLogger.remove_appender(appender) }
  @appenders = []
end

.replace_rails_logger(app) ⇒ void

This method returns an undefined value.

Replaces Rails.logger, ActiveSupport::LogSubscriber.logger, config.logger and the framework loggers (ActiveRecord, ActionController, ActionMailer, ActiveJob, ActionView, ActionCable) with LogStruct's SemanticLogger logger, so every Rails line is a LogStruct JSON line

Parameters:

  • app (T.untyped) —

    The Rails application instance



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/log_struct/semantic_logger/setup.rb', line 190

def self.replace_rails_logger(app)
  # Create new SemanticLogger instance
  logger = LogStruct::SemanticLogger::Logger.new("Rails")
  @logger = logger
  @original_rails_logger = Rails.logger
  @original_log_subscriber_logger = ActiveSupport::LogSubscriber.logger
  @original_app_logger = app.config.logger

  # Replace Rails.logger
  Rails.logger = logger

  # Rails assigns ActiveSupport::LogSubscriber.logger once during boot
  # (in the active_support.set_log_subscriber_logger initializer), so log
  # subscribers such as Lograge keep writing to the original boot logger
  # unless this reference is updated along with Rails.logger.
  ActiveSupport::LogSubscriber.logger = logger

  # Also replace the component loggers. Use load hooks rather than referencing the base
  # classes: loading them this early would apply their on_load hooks before the app's
  # initializers run, e.g. ActiveRecord would copy an empty filter_parameters into
  # filter_attributes.
  COMPONENT_LOAD_HOOKS.each do |hook|
    ActiveSupport.on_load(hook) { self.logger = logger }
  end

  # Store reference in app config
  app.config.logger = logger
end

.restore_rails_logger(app) ⇒ void

This method returns an undefined value.

Puts back the loggers that replace_rails_logger replaced. Used when an initializer disables LogStruct after the logger was replaced at boot.

Parameters:

  • app (T.untyped)


222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/log_struct/semantic_logger/setup.rb', line 222

def self.restore_rails_logger(app)
  logger = @logger
  return unless logger

  original_logger = @original_rails_logger
  Rails.logger = original_logger
  ActiveSupport::LogSubscriber.logger = @original_log_subscriber_logger
  app.config.logger = @original_app_logger
  remove_appenders

  # These hooks run after the ones that installed LogStruct's logger, including for
  # components that are already loaded
  COMPONENT_LOAD_HOOKS.each do |hook|
    ActiveSupport.on_load(hook) do
      self.logger = original_logger if self.logger.equal?(logger)
    end
  end

  @logger = nil
end