rails_semantic_logger 4.20.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +55 -98
  3. data/Rakefile +9 -4
  4. data/lib/rails_semantic_logger/action_controller/log_subscriber.rb +87 -16
  5. data/lib/rails_semantic_logger/action_mailer/log_subscriber.rb +36 -22
  6. data/lib/rails_semantic_logger/action_view/log_subscriber.rb +76 -42
  7. data/lib/rails_semantic_logger/active_job/log_subscriber.rb +216 -7
  8. data/lib/rails_semantic_logger/active_record/log_subscriber.rb +62 -160
  9. data/lib/rails_semantic_logger/appenders.rb +92 -0
  10. data/lib/rails_semantic_logger/engine.rb +68 -57
  11. data/lib/rails_semantic_logger/extensions/action_cable/tagged_logger_proxy.rb +61 -3
  12. data/lib/rails_semantic_logger/extensions/action_dispatch/debug_exceptions.rb +5 -14
  13. data/lib/rails_semantic_logger/extensions/active_job/logging.rb +2 -2
  14. data/lib/rails_semantic_logger/extensions/active_model_serializers/logging.rb +2 -2
  15. data/lib/rails_semantic_logger/extensions/active_support/logger.rb +24 -15
  16. data/lib/rails_semantic_logger/extensions/rails/server.rb +1 -1
  17. data/lib/rails_semantic_logger/options.rb +171 -20
  18. data/lib/rails_semantic_logger/rack/logger.rb +6 -13
  19. data/lib/rails_semantic_logger/sidekiq/defaults.rb +12 -27
  20. data/lib/rails_semantic_logger/sidekiq/job_logger.rb +29 -9
  21. data/lib/rails_semantic_logger/solid_queue/log_subscriber.rb +183 -0
  22. data/lib/rails_semantic_logger/version.rb +1 -1
  23. data/lib/rails_semantic_logger.rb +82 -27
  24. metadata +16 -22
  25. data/lib/rails_semantic_logger/delayed_job/plugin.rb +0 -11
  26. data/lib/rails_semantic_logger/extensions/active_support/log_subscriber.rb +0 -13
  27. data/lib/rails_semantic_logger/extensions/rack/server.rb +0 -12
  28. data/lib/rails_semantic_logger/extensions/rackup/server.rb +0 -12
  29. data/lib/rails_semantic_logger/extensions/sidekiq/sidekiq.rb +0 -87
@@ -1,42 +1,61 @@
1
+ # This subscriber is a reimplementation of Rails' own ActiveRecord::LogSubscriber that emits
2
+ # structured (message + payload) log entries instead of formatted text. When Rails changes its
3
+ # subscriber, those changes must be brought across here. Compare against the upstream source for
4
+ # each supported Rails version:
5
+ #
6
+ # Rails 8.1: https://github.com/rails/rails/blob/8-1-stable/activerecord/lib/active_record/log_subscriber.rb
7
+ # Rails 8.0: https://github.com/rails/rails/blob/8-0-stable/activerecord/lib/active_record/log_subscriber.rb
8
+ # Rails 7.2: https://github.com/rails/rails/blob/7-2-stable/activerecord/lib/active_record/log_subscriber.rb
9
+ #
10
+ # The upstream subscriber is functionally identical across Rails 7.2, 8.0, and 8.1 for everything
11
+ # that affects structured output. The only differences between those versions are in the internal
12
+ # `query_source_location` helper (used by `verbose_query_logs` to print the "↳ source" line) and a
13
+ # `:nodoc:` comment, neither of which changes the event payload. As a result no version-specific
14
+ # behavior is required here.
15
+ #
16
+ # As of Rails 8.1 there is also a parallel ActiveRecord::StructuredEventSubscriber (it emits
17
+ # structured events to Rails.event rather than text to Rails.logger). It is the authoritative,
18
+ # Rails-maintained reference for field names and payload shape; the fields below (e.g. :lock_wait
19
+ # and the strict_loading_violation payload) follow it. We do not use it (see CLAUDE.md), but diff
20
+ # against it when adding fields:
21
+ #
22
+ # Rails 8.1: https://github.com/rails/rails/blob/8-1-stable/activerecord/lib/active_record/structured_event_subscriber.rb
1
23
  module RailsSemanticLogger
2
24
  module ActiveRecord
3
25
  class LogSubscriber < ActiveSupport::LogSubscriber
4
26
  IGNORE_PAYLOAD_NAMES = %w[SCHEMA EXPLAIN].freeze
5
27
 
6
- class << self
7
- attr_reader :logger
8
- end
28
+ def strict_loading_violation(event)
29
+ return unless logger.debug?
9
30
 
10
- # Rails 7.1 stopped using runtime in log subscribers
11
- if Rails.version.to_f < 7.1
12
- def self.runtime=(value)
13
- ::ActiveRecord::RuntimeRegistry.sql_runtime = value
14
- end
31
+ payload = event.payload
32
+ owner = payload[:owner]
33
+ reflection = payload[:reflection]
15
34
 
16
- def self.runtime
17
- ::ActiveRecord::RuntimeRegistry.sql_runtime ||= 0
18
- end
35
+ log_payload = {owner: owner.name, association: reflection.name}
36
+ log_payload[:class] = reflection.klass.name unless reflection.polymorphic?
19
37
 
20
- def self.reset_runtime
21
- rt = runtime
22
- self.runtime = 0
23
- rt
24
- end
38
+ logger.debug(
39
+ message: reflection.strict_loading_violation_message(owner),
40
+ payload: log_payload
41
+ )
25
42
  end
26
43
 
27
44
  def sql(event)
28
- self.class.runtime += event.duration if self.class.respond_to?(:runtime)
29
45
  return unless logger.debug?
30
46
 
31
47
  payload = event.payload
32
48
  name = payload[:name]
33
49
  return if IGNORE_PAYLOAD_NAMES.include?(name)
34
50
 
35
- log_payload = {sql: payload[:sql]}
36
- log_payload[:binds] = bind_values(payload) unless (payload[:binds] || []).empty?
37
- log_payload[:allocations] = event.allocations if event.respond_to?(:allocations)
38
- log_payload[:cached] = event.payload[:cached]
39
- log_payload[:async] = true if event.payload[:async]
51
+ log_payload = {sql: payload[:sql]}
52
+ log_payload[:binds] = bind_values(payload) unless (payload[:binds] || []).empty?
53
+ log_payload[:allocations] = event.allocations
54
+ log_payload[:cached] = true if payload[:cached]
55
+ if payload[:async]
56
+ log_payload[:async] = true
57
+ log_payload[:lock_wait] = payload[:lock_wait] if payload[:lock_wait]
58
+ end
40
59
 
41
60
  log = {
42
61
  message: name,
@@ -54,137 +73,38 @@ module RailsSemanticLogger
54
73
 
55
74
  private
56
75
 
57
- @logger = SemanticLogger["ActiveRecord"]
58
-
59
76
  # When multiple values are received for a single bound field, it is converted into an array
60
77
  def add_bind_value(binds, key, value)
61
78
  key = key.downcase.to_sym unless key.nil?
62
79
 
63
- if rails_filter_params_include?(key)
64
- value = "[FILTERED]"
65
- elsif binds.key?(key)
66
- value = (Array(binds[key]) << value)
67
- end
68
-
80
+ value = (Array(binds[key]) << value) if binds.key?(key)
69
81
  binds[key] = value
70
82
  end
71
83
 
72
- def rails_filter_params_include?(key)
73
- filter_parameters = Rails.configuration.filter_parameters
74
-
75
- return filter_parameters.first.match? key if filter_parameters.first.is_a? Regexp
76
-
77
- filter_parameters.include? key
78
- end
79
-
80
84
  def logger
81
- self.class.logger
82
- end
83
-
84
- #
85
- # Rails 3,4,5 hell trying to get the bind values
86
- #
87
-
88
- def bind_values_v3(payload)
89
- binds = {}
90
- payload[:binds].each do |col, v|
91
- if col
92
- add_bind_value(binds, col.name, v)
93
- else
94
- binds[nil] = v
95
- end
96
- end
97
- binds
98
- end
99
-
100
- def bind_values_v4(payload)
101
- binds = {}
102
- payload[:binds].each do |col, v|
103
- attr_name, value = render_bind(col, v)
104
- add_bind_value(binds, attr_name, value)
105
- end
106
- binds
107
- end
108
-
109
- def bind_values_v5_0_0(payload)
110
- binds = {}
111
- payload[:binds].each do |attr|
112
- attr_name, value = render_bind(attr)
113
- add_bind_value(binds, attr_name, value)
114
- end
115
- binds
116
- end
117
-
118
- def bind_values_v5_0_3(payload)
119
- binds = {}
120
- casted_params = type_casted_binds(payload[:binds], payload[:type_casted_binds])
121
- payload[:binds].zip(casted_params).map do |attr, value|
122
- attr_name, value = render_bind(attr, value)
123
- add_bind_value(binds, attr_name, value)
124
- end
125
- binds
126
- end
127
-
128
- def bind_values_v5_1_5(payload)
129
- binds = {}
130
- casted_params = type_casted_binds(payload[:type_casted_binds])
131
- payload[:binds].zip(casted_params).map do |attr, value|
132
- attr_name, value = render_bind(attr, value)
133
- add_bind_value(binds, attr_name, value)
134
- end
135
- binds
85
+ ::ActiveRecord::Base.logger
136
86
  end
137
87
 
138
- def bind_values_v6_1(payload)
88
+ def bind_values(payload)
139
89
  binds = {}
140
90
  casted_params = type_casted_binds(payload[:type_casted_binds])
141
91
  payload[:binds].each_with_index do |attr, i|
142
- attr_name, value = render_bind(attr, casted_params[i])
92
+ filtered_value = filter(attribute_name(attr, i), casted_params[i])
93
+ attr_name, value = render_bind(attr, filtered_value)
143
94
  add_bind_value(binds, attr_name, value)
144
95
  end
145
96
  binds
146
97
  end
147
98
 
148
- def render_bind_v4_2(column, value)
149
- if column
150
- if column.binary?
151
- # This specifically deals with the PG adapter that casts bytea columns into a Hash.
152
- value = value[:value] if value.is_a?(Hash)
153
- value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>"
154
- end
155
-
156
- [column.name, value]
157
- else
158
- [nil, value]
99
+ def attribute_name(attr, index)
100
+ if attr.respond_to?(:name)
101
+ attr.name
102
+ elsif attr.respond_to?(:[]) && attr[index].respond_to?(:name)
103
+ attr[index].name
159
104
  end
160
105
  end
161
106
 
162
- def render_bind_v5_0_0(attribute)
163
- value =
164
- if attribute.type.binary? && attribute.value
165
- if attribute.value.is_a?(Hash)
166
- "<#{attribute.value_for_database.to_s.bytesize} bytes of binary data>"
167
- else
168
- "<#{attribute.value.bytesize} bytes of binary data>"
169
- end
170
- else
171
- attribute.value_for_database
172
- end
173
-
174
- [attribute.name, value]
175
- end
176
-
177
- def render_bind_v5_0_3(attr, value)
178
- if attr.is_a?(Array)
179
- attr = attr.first
180
- elsif attr.type.binary? && attr.value
181
- value = "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>"
182
- end
183
-
184
- [attr&.name, value]
185
- end
186
-
187
- def render_bind_v6_1(attr, value)
107
+ def render_bind(attr, value)
188
108
  case attr
189
109
  when ActiveModel::Attribute
190
110
  value = "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" if attr.type.binary? && attr.value
@@ -197,37 +117,19 @@ module RailsSemanticLogger
197
117
  [attr&.name || :nil, value]
198
118
  end
199
119
 
200
- def type_casted_binds_v5_0_3(binds, casted_binds)
201
- casted_binds || ::ActiveRecord::Base.connection.type_casted_binds(binds)
202
- end
120
+ # Filter sensitive bind values using the same parameter filter Rails uses for ActiveRecord,
121
+ # which is derived from ActiveRecord::Base.filter_attributes (config.filter_parameters).
122
+ def filter(name, value)
123
+ return value if name.nil?
203
124
 
204
- def type_casted_binds_v5_1_5(casted_binds)
205
- casted_binds.respond_to?(:call) ? casted_binds.call : casted_binds
125
+ filtered = ::ActiveRecord::Base.inspection_filter.filter_param(name.to_s, value)
126
+ # filter_param returns the same object when nothing was filtered, otherwise a mask object
127
+ # (a String delegate). Coerce the mask to a plain String for clean structured output.
128
+ filtered.equal?(value) ? value : filtered.to_s
206
129
  end
207
130
 
208
- if Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR.zero? && Rails::VERSION::TINY <= 2 # 5.0.0 - 5.0.2
209
- alias bind_values bind_values_v5_0_0
210
- alias render_bind render_bind_v5_0_0
211
- elsif Rails::VERSION::MAJOR == 5 &&
212
- ((Rails::VERSION::MINOR.zero? && Rails::VERSION::TINY <= 6) ||
213
- (Rails::VERSION::MINOR == 1 && Rails::VERSION::TINY <= 4)) # 5.0.3 - 5.0.6 && 5.1.0 - 5.1.4
214
- alias bind_values bind_values_v5_0_3
215
- alias render_bind render_bind_v5_0_3
216
- alias type_casted_binds type_casted_binds_v5_0_3
217
- elsif (Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR > 0) ||
218
- Rails::VERSION::MAJOR >= 7 # ~> 6.1.0 && >= 7.x.x
219
- alias bind_values bind_values_v6_1
220
- alias render_bind render_bind_v6_1
221
- alias type_casted_binds type_casted_binds_v5_1_5
222
- elsif Rails::VERSION::MAJOR >= 5 # ~> 5.1.5 && ~> 5.0.7 && 6.x.x
223
- alias bind_values bind_values_v5_1_5
224
- alias render_bind render_bind_v5_0_3
225
- alias type_casted_binds type_casted_binds_v5_1_5
226
- elsif Rails.version.to_i >= 4 # 4.x
227
- alias bind_values bind_values_v4
228
- alias render_bind render_bind_v4_2
229
- else # 3.x
230
- alias bind_values bind_values_v3
131
+ def type_casted_binds(casted_binds)
132
+ casted_binds.respond_to?(:call) ? casted_binds.call : casted_binds
231
133
  end
232
134
  end
233
135
  end
@@ -0,0 +1,92 @@
1
+ module RailsSemanticLogger
2
+ # Collects the appenders declared by the application via:
3
+ #
4
+ # config.rails_semantic_logger.appenders do |appenders|
5
+ # appenders.add(file_name: "log/#{Rails.env}.log", formatter: :json)
6
+ # appenders.add_server(io: $stdout, formatter: :color)
7
+ # appenders.add_console(io: $stderr, formatter: :color)
8
+ # end
9
+ #
10
+ # When at least one appender is declared this way, Rails Semantic Logger stops
11
+ # building its own default file appender (`format`, `ap_options`, `filter`, and
12
+ # `add_file_appender` no longer apply) and instead creates exactly the appenders
13
+ # declared here.
14
+ #
15
+ # The methods name the *context* in which the appender is created; the
16
+ # destination and formatting are ordinary `SemanticLogger.add_appender` arguments:
17
+ #
18
+ # #add - always created, during Rails initialization.
19
+ # #add_server - created only when serving requests (`rails server`, Sidekiq
20
+ # in server mode). Defaults to `$stdout`.
21
+ # #add_console - created only inside a `rails console` session. Defaults to
22
+ # `$stderr` so log output does not tangle with command results.
23
+ #
24
+ # Because each call appends to its context, any appender works in any context,
25
+ # and a context can have several (e.g. a server-only stdout *and* file appender).
26
+ class Appenders
27
+ include Enumerable
28
+
29
+ # Destination keys understood by SemanticLogger.add_appender. When none is
30
+ # supplied to #add_server / #add_console, the context's default stream is used.
31
+ DESTINATIONS = %i[io file_name appender logger metric].freeze
32
+
33
+ def initialize
34
+ @definitions = []
35
+ @server = []
36
+ @console = []
37
+ end
38
+
39
+ # Declare an appender. Accepts the same arguments (and optional block) as
40
+ # SemanticLogger.add_appender. Returns self so calls can be chained.
41
+ def add(**args, &block)
42
+ @definitions << [args, block]
43
+ self
44
+ end
45
+
46
+ # Declare an appender that is only created when the application is serving
47
+ # requests: `rails server`, Sidekiq in server mode, or when the app calls
48
+ # RailsSemanticLogger.add_server_appenders from its own server's boot hook.
49
+ # It is never created during a non-serving boot (rake tasks, runners,
50
+ # generators), so it only appears where it is useful.
51
+ #
52
+ # Accepts the same arguments (and optional block) as SemanticLogger.add_appender.
53
+ # When no destination is given it defaults to `$stdout`; the formatter defaults
54
+ # to `:color`.
55
+ def add_server(**args, &block)
56
+ @server << [defaults(args, io: $stdout), block]
57
+ self
58
+ end
59
+
60
+ # Declare an appender that is only created when running inside a `rails console`
61
+ # session. Identical to #add_server except it defaults to `$stderr`.
62
+ #
63
+ # Accepts the same arguments (and optional block) as SemanticLogger.add_appender.
64
+ def add_console(**args, &block)
65
+ @console << [defaults(args, io: $stderr), block]
66
+ self
67
+ end
68
+
69
+ # The appenders declared via #add_server / #add_console, each as
70
+ # [Hash args, Proc|nil block].
71
+ attr_reader :server, :console
72
+
73
+ # Yields each #add declaration as [Hash args, Proc|nil block].
74
+ def each(&)
75
+ @definitions.each(&)
76
+ end
77
+
78
+ def any?
79
+ @definitions.any? || @server.any? || @console.any?
80
+ end
81
+
82
+ private
83
+
84
+ # Apply the context default stream and formatter, without overriding a
85
+ # destination (io/file_name/...) the caller already supplied.
86
+ def defaults(args, io:)
87
+ args = {formatter: :color}.merge(args)
88
+ args[:io] = io unless DESTINATIONS.any? { |key| args.key?(key) }
89
+ args
90
+ end
91
+ end
92
+ end
@@ -42,9 +42,13 @@ module RailsSemanticLogger
42
42
  # own file loggers which would result in duplicate logging to the same log file
43
43
  Rails.logger = config.logger =
44
44
  begin
45
- if config.rails_semantic_logger.add_file_appender
45
+ if config.rails_semantic_logger.appenders?
46
+ # The application declared its own appenders; create those and skip the default file appender.
47
+ RailsSemanticLogger.add_appenders(config.rails_semantic_logger.appenders)
48
+ elsif config.rails_semantic_logger.add_file_appender
49
+ # DEPRECATED: This option is deprecated and will be removed in a future version.
46
50
  path = config.paths["log"].first
47
- FileUtils.mkdir_p(File.dirname(path)) unless File.exist?(File.dirname(path))
51
+ FileUtils.mkdir_p(File.dirname(path))
48
52
 
49
53
  # Add the log file to the list of appenders
50
54
  # Use the colorized formatter if Rails colorized logs are enabled
@@ -63,31 +67,42 @@ module RailsSemanticLogger
63
67
 
64
68
  app.formatter = formatter
65
69
  end
66
- SemanticLogger.add_appender(file_name: path, formatter: formatter, filter: config.rails_semantic_logger.filter)
70
+ SemanticLogger.add_appender(file_name: path, formatter: formatter,
71
+ filter: config.rails_semantic_logger.filter)
67
72
  end
68
73
 
69
74
  SemanticLogger[Rails]
70
75
  rescue StandardError => e
71
- # If not able to log to file, log to standard error with warning level only
76
+ # If unable to create the appenders, log to standard error with warning level only
72
77
  SemanticLogger.default_level = :warn
73
78
 
74
79
  SemanticLogger::Processor.logger = SemanticLogger::Appender::IO.new($stderr)
75
80
  SemanticLogger.add_appender(io: $stderr)
76
81
 
82
+ message =
83
+ if path
84
+ "Rails Error: Unable to access log file. Please ensure that #{path} exists and is chmod 0666."
85
+ else
86
+ "Rails Error: Unable to create the configured appenders."
87
+ end
77
88
  logger = SemanticLogger[Rails]
78
89
  logger.warn(
79
- "Rails Error: Unable to access log file. Please ensure that #{path} exists and is chmod 0666. " \
80
- "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed.",
90
+ "#{message} The log level has been raised to WARN and the output directed to STDERR " \
91
+ "until the problem is fixed.",
81
92
  e
82
93
  )
83
94
  logger
84
95
  end
85
96
 
97
+ # Logger and init-time appenders are now built; settings read above no
98
+ # longer take effect, so flag late changes (e.g. from config/initializers).
99
+ config.rails_semantic_logger.logger_initialized!
100
+
86
101
  # Replace Rails loggers
87
102
  %i[active_record action_controller action_mailer action_view].each do |name|
88
103
  ActiveSupport.on_load(name) { include SemanticLogger::Loggable }
89
104
  end
90
- ActiveSupport.on_load(:action_cable) { self.logger = SemanticLogger["ActionCable"] }
105
+ ActiveSupport.on_load(:action_cable) { self.logger = SemanticLogger[::ActionCable] }
91
106
  end
92
107
 
93
108
  # Before any initializers run, but after the gems have been loaded
@@ -105,7 +120,7 @@ module RailsSemanticLogger
105
120
  Mongo::Logger.logger = SemanticLogger[Mongo] if defined?(Mongo::Logger)
106
121
 
107
122
  # Replace the Resque Logger
108
- Resque.logger = SemanticLogger[Resque] if defined?(Resque) && Resque.respond_to?(:logger=)
123
+ Resque.logger = SemanticLogger[Resque] if defined?(Resque) && Resque.respond_to?(:logger=)
109
124
 
110
125
  # Replace the Sidekiq logger
111
126
  if config.rails_semantic_logger.replace_sidekiq_logger && defined?(::Sidekiq)
@@ -114,42 +129,44 @@ module RailsSemanticLogger
114
129
  end
115
130
 
116
131
  ::Sidekiq.configure_server do |config|
117
- config.logger = ::SemanticLogger[::Sidekiq]
118
- if config.respond_to?(:options)
119
- config.options[:job_logger] = RailsSemanticLogger::Sidekiq::JobLogger
120
- else
121
- config[:job_logger] = RailsSemanticLogger::Sidekiq::JobLogger
122
- end
132
+ config.logger = ::SemanticLogger[::Sidekiq]
133
+ config[:job_logger] = RailsSemanticLogger::Sidekiq::JobLogger
123
134
 
124
- # Add back the default console logger unless already added
125
- SemanticLogger.add_appender(io: $stdout, formatter: :color) unless SemanticLogger.appenders.console_output?
135
+ # Add back the default console logger (unless the app declared its own appenders, or one exists)
136
+ RailsSemanticLogger.add_server_appenders
126
137
 
127
138
  # Replace default error handler when present
128
139
  existing = RailsSemanticLogger::Sidekiq::Defaults.delete_default_error_handler(config.error_handlers)
129
140
  config.error_handlers << RailsSemanticLogger::Sidekiq::Defaults::ERROR_HANDLER if existing
130
141
  end
131
142
 
132
- if defined?(::Sidekiq::Job) && (::Sidekiq::VERSION.to_i != 5)
133
- ::Sidekiq::Job.singleton_class.prepend(RailsSemanticLogger::Sidekiq::Loggable)
134
- else
135
- ::Sidekiq::Worker.singleton_class.prepend(RailsSemanticLogger::Sidekiq::Loggable)
136
- end
143
+ ::Sidekiq::Job.singleton_class.prepend(RailsSemanticLogger::Sidekiq::Loggable)
144
+ end
145
+
146
+ # Replace the SolidQueue logger
147
+ if config.rails_semantic_logger.replace_solid_queue_logger &&
148
+ defined?(::SolidQueue) && ::SolidQueue.respond_to?(:logger=)
149
+ ::SolidQueue.logger = SemanticLogger[::SolidQueue]
137
150
  end
138
151
 
139
152
  # Replace the Sidetiq logger
140
153
  Sidetiq.logger = SemanticLogger[Sidetiq] if defined?(Sidetiq) && Sidetiq.respond_to?(:logger=)
141
154
 
142
155
  # Replace the DelayedJob logger
143
- if defined?(Delayed::Worker)
144
- Delayed::Worker.logger = SemanticLogger[Delayed::Worker]
145
- Delayed::Worker.plugins << RailsSemanticLogger::DelayedJob::Plugin
146
- end
156
+ Delayed::Worker.logger = SemanticLogger[Delayed::Worker] if defined?(Delayed::Worker)
147
157
 
148
158
  # Replace the Bugsnag logger
149
159
  Bugsnag.configure(false) { |config| config.logger = SemanticLogger[Bugsnag] } if defined?(Bugsnag)
150
160
 
151
161
  # Set the IOStreams PGP logger
152
- IOStreams::Pgp.logger = SemanticLogger["IOStreams::Pgp"] if defined?(IOStreams)
162
+ if defined?(IOStreams)
163
+ if IOStreams.respond_to?(:logger=)
164
+ IOStreams.logger = SemanticLogger[IOStreams]
165
+ else
166
+ # Older IOStreams versions
167
+ IOStreams::Pgp.logger = SemanticLogger[IOStreams::Pgp]
168
+ end
169
+ end
153
170
  end
154
171
 
155
172
  # After any initializers run, but after the gems have been loaded
@@ -207,14 +224,18 @@ module RailsSemanticLogger
207
224
  if config.rails_semantic_logger.quiet_assets && config.respond_to?(:assets) && config.assets.prefix
208
225
  assets_root = config.relative_url_root.to_s + config.assets.prefix
209
226
  assets_regex = %r(\A/{0,2}#{assets_root})
210
- RailsSemanticLogger::Rack::Logger.logger.filter = ->(log) { log.payload[:path] !~ assets_regex if log.payload }
227
+ RailsSemanticLogger::Rack::Logger.logger.filter = lambda do |log|
228
+ log.payload[:path] !~ assets_regex if log.payload
229
+ end
211
230
  end
212
231
 
213
232
  # Action View
214
233
  if defined?(::ActionView)
215
234
  require "action_view/log_subscriber"
216
235
 
217
- RailsSemanticLogger::ActionView::LogSubscriber.rendered_log_level = :info if config.rails_semantic_logger.rendered
236
+ if config.rails_semantic_logger.rendered
237
+ RailsSemanticLogger::ActionView::LogSubscriber.rendered_log_level = :info
238
+ end
218
239
  RailsSemanticLogger.swap_subscriber(
219
240
  ::ActionView::LogSubscriber,
220
241
  RailsSemanticLogger::ActionView::LogSubscriber,
@@ -226,7 +247,9 @@ module RailsSemanticLogger
226
247
  if defined?(::ActionController)
227
248
  require "action_controller/log_subscriber"
228
249
 
229
- RailsSemanticLogger::ActionController::LogSubscriber.action_message_format = config.rails_semantic_logger.action_message_format
250
+ subscriber = RailsSemanticLogger::ActionController::LogSubscriber
251
+ subscriber.action_message_format = config.rails_semantic_logger.action_message_format
252
+ subscriber.processing_log_level = :info if config.rails_semantic_logger.processing
230
253
  RailsSemanticLogger.swap_subscriber(
231
254
  ::ActionController::LogSubscriber,
232
255
  RailsSemanticLogger::ActionController::LogSubscriber,
@@ -245,46 +268,34 @@ module RailsSemanticLogger
245
268
  )
246
269
  end
247
270
 
248
- if config.rails_semantic_logger.replace_sidekiq_logger && defined?(::Sidekiq)
249
- require("rails_semantic_logger/extensions/sidekiq/sidekiq")
250
- end
251
- end
252
-
253
- #
254
- # Forking Frameworks
255
- #
256
-
257
- # Passenger provides the :starting_worker_process event for executing
258
- # code after it has forked, so we use that and reconnect immediately.
259
- if defined?(PhusionPassenger)
260
- PhusionPassenger.on_event(:starting_worker_process) do |forked|
261
- SemanticLogger.reopen if forked
271
+ # SolidQueue
272
+ if config.rails_semantic_logger.replace_solid_queue_logger && defined?(::SolidQueue::LogSubscriber)
273
+ RailsSemanticLogger.swap_subscriber(
274
+ ::SolidQueue::LogSubscriber,
275
+ RailsSemanticLogger::SolidQueue::LogSubscriber,
276
+ :solid_queue
277
+ )
262
278
  end
263
279
  end
264
280
 
265
- # Re-open appenders after Resque has forked a worker
266
- Resque.after_fork { |_job| ::SemanticLogger.reopen } if defined?(Resque.after_fork)
267
-
268
- # Re-open appenders after Spring has forked a process
269
- Spring.after_fork { |_job| ::SemanticLogger.reopen } if defined?(Spring.after_fork)
270
-
271
- # Re-open appenders after SolidQueue worker/dispatcher/scheduler has finished booting
272
- SolidQueue.on_start { ::SemanticLogger.reopen } if defined?(SolidQueue.on_start)
273
- SolidQueue.on_worker_start { ::SemanticLogger.reopen } if defined?(SolidQueue.on_worker_start)
274
- SolidQueue.on_dispatcher_start { ::SemanticLogger.reopen } if defined?(SolidQueue.on_dispatcher_start)
275
- SolidQueue.on_scheduler_start { ::SemanticLogger.reopen } if defined?(SolidQueue.on_scheduler_start)
281
+ # Forking frameworks (Passenger, Resque, Spring, SolidQueue, etc.) no longer
282
+ # need explicit reopen hooks: Semantic Logger v5 reopens appenders automatically
283
+ # in the child via a Process._fork / Process.daemon hook. Apps that opt out with
284
+ # `SemanticLogger.reopen_on_fork = false` are responsible for reopening themselves.
276
285
 
277
286
  console do |_app|
278
287
  # Don't use a background thread for logging
279
288
  SemanticLogger.sync!
280
289
  # Add a stderr logger when running inside a Rails console unless one has already been added.
281
- if config.rails_semantic_logger.console_logger && !SemanticLogger.appenders.console_output?
282
- SemanticLogger.add_appender(io: STDERR, formatter: :color)
283
- end
290
+ RailsSemanticLogger.add_console_appenders
284
291
 
285
292
  # Include method names on log entries in the console
286
293
  SemanticLogger.backtrace_level = SemanticLogger.default_level
287
294
  end
295
+
296
+ # Initialization is complete; the post-init settings have been consumed, so
297
+ # flag changes made after this point (e.g. at runtime) as having no effect.
298
+ config.rails_semantic_logger.fully_initialized!
288
299
  end
289
300
  end
290
301
  end
@@ -1,11 +1,69 @@
1
1
  require "action_cable/connection/tagged_logger_proxy"
2
2
 
3
+ # Upstream source (identical across supported versions):
4
+ # https://github.com/rails/rails/blob/v7.2.2/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb
5
+ # https://github.com/rails/rails/blob/v8.0.2/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb
6
+ # https://github.com/rails/rails/blob/v8.1.3/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb
3
7
  module ActionCable
4
8
  module Connection
5
9
  class TaggedLoggerProxy
6
- def tag(logger, &block)
7
- current_tags = tags - logger.tags
8
- logger.tagged(*current_tags, &block)
10
+ # Mirrors upstream #tag, including the `respond_to?(:tagged)` guard so a
11
+ # target logger without tagging support (e.g. when handed a non-tagged
12
+ # ActiveRecord::Base.logger by the worker pool) simply yields. Diverges in
13
+ # one place: upstream reads the already-applied tags via
14
+ # `logger.formatter.current_tags` (ActiveSupport::TaggedLogging), but
15
+ # Semantic Logger exposes them via `#tags`, so resolve from whichever the
16
+ # target logger supports. See #220.
17
+ def tag(logger, &)
18
+ if logger.respond_to?(:tagged)
19
+ current_tags = tags - applied_tags_for(logger)
20
+ logger.tagged(*current_tags, &)
21
+ else
22
+ yield
23
+ end
24
+ end
25
+
26
+ # Upstream defines the severity methods with a single-arg signature
27
+ # (`message = nil`), which discards Semantic Logger's richer payload and
28
+ # exception arguments and raises ArgumentError when they are supplied.
29
+ # Redefine them to forward the full Semantic Logger signature down to the
30
+ # wrapped logger, while still applying the connection's tags. See #220.
31
+ %i[debug info warn error fatal unknown].each do |severity|
32
+ define_method(severity) do |message = nil, payload = nil, exception = nil, &block|
33
+ tag(@logger) { forward(severity, message, payload, exception, &block) }
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ # Only forward the extra payload/exception arguments when the wrapped logger's
40
+ # method can accept them. A standard Ruby Logger (e.g. the one wrapped by
41
+ # ActionCable::Connection::TestCase) only accepts a single `progname` argument
42
+ # and raises ArgumentError given more, so fall back to the single-arg call. See #317.
43
+ def forward(severity, message, payload, exception, &)
44
+ if extended_signature?(@logger, severity)
45
+ @logger.public_send(severity, message, payload, exception, &)
46
+ else
47
+ @logger.public_send(severity, message, &)
48
+ end
49
+ end
50
+
51
+ def extended_signature?(logger, severity)
52
+ params = logger.method(severity).parameters
53
+ params.any? { |type, _| type == :rest } || params.count { |type, _| %i[req opt].include?(type) } >= 3
54
+ end
55
+
56
+ # Tags already applied to the target logger, so they are not duplicated.
57
+ # Semantic Logger exposes them via `#tags`; ActiveSupport::TaggedLogging
58
+ # via `formatter.current_tags`. Fall back to none when neither is present.
59
+ def applied_tags_for(logger)
60
+ if logger.respond_to?(:tags)
61
+ Array(logger.tags)
62
+ elsif logger.respond_to?(:formatter) && logger.formatter.respond_to?(:current_tags)
63
+ logger.formatter.current_tags
64
+ else
65
+ []
66
+ end
9
67
  end
10
68
  end
11
69
  end