logstruct 0.1.11 → 0.2.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +146 -1
  3. data/README.md +76 -68
  4. data/lib/log_struct/concerns/configuration.rb +139 -91
  5. data/lib/log_struct/concerns/error_handling.rb +36 -6
  6. data/lib/log_struct/config_struct/error_handling_modes.rb +3 -1
  7. data/lib/log_struct/config_struct/filters.rb +126 -22
  8. data/lib/log_struct/config_struct/integrations.rb +0 -4
  9. data/lib/log_struct/config_struct/limits.rb +29 -0
  10. data/lib/log_struct/configuration.rb +2 -0
  11. data/lib/log_struct/enums/event.rb +3 -1
  12. data/lib/log_struct/enums/log_field.rb +5 -0
  13. data/lib/log_struct/formatter.rb +304 -56
  14. data/lib/log_struct/hash_utils.rb +33 -4
  15. data/lib/log_struct/integrations/action_mailer/error_handling.rb +78 -20
  16. data/lib/log_struct/integrations/action_mailer/fallback_error_handlers.rb +81 -0
  17. data/lib/log_struct/integrations/action_mailer/metadata_collection.rb +0 -20
  18. data/lib/log_struct/integrations/action_mailer.rb +22 -17
  19. data/lib/log_struct/integrations/active_job/log_subscriber.rb +177 -25
  20. data/lib/log_struct/integrations/active_job.rb +2 -7
  21. data/lib/log_struct/integrations/active_model_serializers.rb +9 -3
  22. data/lib/log_struct/integrations/active_record.rb +45 -17
  23. data/lib/log_struct/integrations/active_storage.rb +31 -31
  24. data/lib/log_struct/integrations/ahoy.rb +19 -15
  25. data/lib/log_struct/integrations/carrierwave.rb +55 -51
  26. data/lib/log_struct/integrations/dotenv.rb +22 -32
  27. data/lib/log_struct/integrations/good_job/logger.rb +30 -24
  28. data/lib/log_struct/integrations/good_job.rb +111 -40
  29. data/lib/log_struct/integrations/host_authorization.rb +47 -36
  30. data/lib/log_struct/integrations/lograge.rb +39 -4
  31. data/lib/log_struct/integrations/puma.rb +74 -34
  32. data/lib/log_struct/integrations/rack_error_handler/debug_exceptions_logging.rb +35 -0
  33. data/lib/log_struct/integrations/rack_error_handler/error_lines.rb +160 -0
  34. data/lib/log_struct/integrations/rack_error_handler/middleware.rb +14 -169
  35. data/lib/log_struct/integrations/rack_error_handler/parse_error_logging.rb +36 -0
  36. data/lib/log_struct/integrations/rack_error_handler.rb +20 -4
  37. data/lib/log_struct/integrations/rack_middleware.rb +23 -0
  38. data/lib/log_struct/integrations/rails_log_subscribers.rb +32 -0
  39. data/lib/log_struct/integrations/request_context/middleware.rb +4 -9
  40. data/lib/log_struct/integrations/shrine.rb +48 -73
  41. data/lib/log_struct/integrations/sidekiq/job_context_middleware.rb +32 -0
  42. data/lib/log_struct/integrations/sidekiq/logger.rb +17 -9
  43. data/lib/log_struct/integrations/sidekiq.rb +98 -7
  44. data/lib/log_struct/integrations/sorbet.rb +10 -46
  45. data/lib/log_struct/integrations.rb +3 -2
  46. data/lib/log_struct/log/{good_job/enqueue.rb → active_job/discard.rb} +13 -13
  47. data/lib/log_struct/log/active_job/enqueue.rb +0 -4
  48. data/lib/log_struct/log/{good_job/schedule.rb → active_job/error.rb} +15 -15
  49. data/lib/log_struct/log/{good_job/start.rb → active_job/retry.rb} +13 -13
  50. data/lib/log_struct/log/{good_job/finish.rb → active_job/retry_stopped.rb} +13 -17
  51. data/lib/log_struct/log/active_job.rb +4 -0
  52. data/lib/log_struct/log/active_storage/delete.rb +6 -2
  53. data/lib/log_struct/log/active_storage/download.rb +2 -4
  54. data/lib/log_struct/log/active_storage/exist.rb +2 -2
  55. data/lib/log_struct/log/active_storage/metadata.rb +4 -4
  56. data/lib/log_struct/log/active_storage/upload.rb +2 -10
  57. data/lib/log_struct/log/active_storage/url.rb +2 -6
  58. data/lib/log_struct/log/active_storage.rb +2 -3
  59. data/lib/log_struct/log/error.rb +24 -1
  60. data/lib/log_struct/log/good_job.rb +0 -4
  61. data/lib/log_struct/log/puma/error.rb +63 -0
  62. data/lib/log_struct/log/puma.rb +1 -0
  63. data/lib/log_struct/log/security/blocked_host.rb +0 -4
  64. data/lib/log_struct/log/shrine/exist.rb +0 -2
  65. data/lib/log_struct/log/shrine/metadata.rb +2 -4
  66. data/lib/log_struct/log/shrine/upload.rb +4 -0
  67. data/lib/log_struct/log/sidekiq/error.rb +63 -0
  68. data/lib/log_struct/log/{active_storage/stream.rb → sidekiq/log.rb} +17 -11
  69. data/lib/log_struct/log/sidekiq.rb +3 -45
  70. data/lib/log_struct/log.rb +4 -2
  71. data/lib/log_struct/logged_errors.rb +25 -0
  72. data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +2 -3
  73. data/lib/log_struct/param_filters.rb +51 -38
  74. data/lib/log_struct/params_errors.rb +78 -0
  75. data/lib/log_struct/rails_boot_banner_silencer.rb +7 -2
  76. data/lib/log_struct/railtie.rb +29 -14
  77. data/lib/log_struct/semantic_logger/color_formatter.rb +60 -53
  78. data/lib/log_struct/semantic_logger/concerns/log_methods.rb +65 -57
  79. data/lib/log_struct/semantic_logger/formatter.rb +79 -57
  80. data/lib/log_struct/semantic_logger/logger.rb +29 -53
  81. data/lib/log_struct/semantic_logger/reopen_after_fork.rb +40 -0
  82. data/lib/log_struct/semantic_logger/setup.rb +115 -112
  83. data/lib/log_struct/shared/merge_additional_data_fields.rb +14 -1
  84. data/lib/log_struct/shared/serialize_common.rb +2 -7
  85. data/lib/log_struct/string_scrubber.rb +229 -20
  86. data/lib/log_struct/version.rb +1 -1
  87. data/lib/log_struct.rb +6 -4
  88. data/logstruct.gemspec +19 -10
  89. metadata +63 -11
  90. data/lib/log_struct/builders/active_job.rb +0 -84
  91. data/lib/log_struct/integrations/good_job/log_subscriber.rb +0 -179
  92. data/lib/log_struct/integrations/rack.rb +0 -31
@@ -15,6 +15,63 @@ module LogStruct
15
15
  extend T::Sig
16
16
  extend ActiveSupport::Concern
17
17
 
18
+ # ActionMailer::MailDeliveryJob passes errors to the mailer class (`handle_exception`), which
19
+ # looks the rescue_from handlers up on the class, so the handlers are defined there too.
20
+ # (ActiveSupport::Concern extends the class with ClassMethods when it's included or prepended.)
21
+ module ClassMethods
22
+ extend T::Sig
23
+
24
+ # Logs the error, and doesn't report or raise it
25
+ sig { params(ex: StandardError).void }
26
+ def log_and_ignore_error(ex)
27
+ ErrorHandling.handle_mailer_class_error(self, ex, report: false, reraise: false)
28
+ end
29
+
30
+ # Logs and reports the error, and doesn't raise it
31
+ sig { params(ex: StandardError).void }
32
+ def log_and_report_error(ex)
33
+ ErrorHandling.handle_mailer_class_error(self, ex, report: true, reraise: false)
34
+ end
35
+
36
+ # Logs the error and raises it again, reporting it too in the Report modes
37
+ sig { params(ex: StandardError).void }
38
+ def log_and_reraise_error(ex)
39
+ ErrorHandling.handle_mailer_class_error(self, ex, report: ErrorHandling.report_before_reraise?, reraise: true)
40
+ end
41
+ end
42
+
43
+ # log_and_reraise_error reports the error where standard_errors' mode reports the app's errors
44
+ # (Report, and ReportProduction in production). Elsewhere whatever rescues the raised error decides.
45
+ sig { returns(T::Boolean) }
46
+ def self.report_before_reraise?
47
+ mode = LogStruct.error_handling_mode_for(Source::Mailer)
48
+ mode == ErrorHandlingMode::Report || (mode == ErrorHandlingMode::ReportProduction && LogStruct.is_production?)
49
+ end
50
+
51
+ # An error that reaches the class handler from the delivery itself was logged (and reported)
52
+ # by the mailer's handler, which raised it again (log_and_reraise_error). It's raised again,
53
+ # without logging it twice, so the job fails as deliver_now would.
54
+ sig { params(mailer_class: T.untyped, error: StandardError, report: T::Boolean, reraise: T::Boolean).void }
55
+ def self.handle_mailer_class_error(mailer_class, error, report:, reraise:)
56
+ Kernel.raise error if LogStruct::LoggedErrors.logged?(error)
57
+
58
+ message = if reraise
59
+ "#{error.class}: Email delivery failed. Error message: #{error.message}"
60
+ else
61
+ "#{error.class}: Cannot send email. Error message: #{error.message}"
62
+ end
63
+ LogStruct.error(Log::ActionMailer::Error.new(
64
+ mailer_class: mailer_class.to_s,
65
+ error_class: error.class,
66
+ message: message,
67
+ backtrace: error.backtrace,
68
+ level: Level::Error
69
+ ))
70
+ LogStruct.report_error(error, source: Source::Mailer, context: {mailer_class: mailer_class.to_s}) if report
71
+
72
+ Kernel.raise error if reraise
73
+ end
74
+
18
75
  sig { returns(T.nilable(T::Boolean)) }
19
76
  attr_accessor :logstruct_mail_failed
20
77
 
@@ -47,23 +104,23 @@ module LogStruct
47
104
 
48
105
  protected
49
106
 
50
- # Just log the error without reporting or retrying
107
+ # Logs the error, and doesn't report or raise it
51
108
  sig { params(ex: StandardError).void }
52
109
  def log_and_ignore_error(ex)
53
110
  self.logstruct_mail_failed = true
54
111
  log_email_delivery_error(ex, notify: false, report: false, reraise: false)
55
112
  end
56
113
 
57
- # Log and report to error service, but doesn't reraise.
114
+ # Logs and reports the error, and doesn't raise it
58
115
  sig { params(ex: StandardError).void }
59
116
  def log_and_report_error(ex)
60
117
  log_email_delivery_error(ex, notify: false, report: true, reraise: false)
61
118
  end
62
119
 
63
- # Log, report to error service, and reraise for retry
120
+ # Logs the error and raises it again, reporting it too in the Report modes (report_before_reraise?)
64
121
  sig { params(ex: StandardError).void }
65
122
  def log_and_reraise_error(ex)
66
- log_email_delivery_error(ex, notify: false, report: true, reraise: true)
123
+ log_email_delivery_error(ex, notify: false, report: ErrorHandling.report_before_reraise?, reraise: true)
67
124
  end
68
125
 
69
126
  private
@@ -101,7 +158,8 @@ module LogStruct
101
158
  message: message,
102
159
  backtrace: error.backtrace,
103
160
  additional_data: context_data.presence,
104
- timestamp: Time.now
161
+ timestamp: Time.now,
162
+ level: Level::Error
105
163
  )
106
164
 
107
165
  # Log the structured error
@@ -127,8 +185,11 @@ module LogStruct
127
185
 
128
186
  # Use structured error logging
129
187
  log_structured_error(self, error, message)
188
+ # The mailer class handler gets the error if it's raised from here (re-raised, or raised by
189
+ # LogStruct.handle_exception in Raise mode), and mustn't log or report it again
190
+ LogStruct::LoggedErrors.mark(error)
130
191
 
131
- # Handle notifications and reporting
192
+ # Handle notifications, reporting and raising
132
193
  handle_error_notifications(error, notify, report, reraise)
133
194
  end
134
195
 
@@ -136,7 +197,7 @@ module LogStruct
136
197
  sig { params(error: StandardError, reraise: T::Boolean).returns(String) }
137
198
  def error_message_for(error, reraise)
138
199
  if reraise
139
- "#{error.class}: Email delivery error, will retry. Recipients: #{recipients(error)}. Error message: #{error.message}"
200
+ "#{error.class}: Email delivery failed. Recipients: #{recipients(error)}. Error message: #{error.message}"
140
201
  else
141
202
  "#{error.class}: Cannot send email to #{recipients(error)}. Error message: #{error.message}"
142
203
  end
@@ -148,21 +209,14 @@ module LogStruct
148
209
  # Log a notification event if requested
149
210
  log_notification_event(error) if notify
150
211
 
151
- # Report to error reporting service if requested
212
+ # Report to error reporting service if requested. The structured error line is already logged.
152
213
  if report
153
- context_data = {recipients: recipients(error)}
154
- exception_data = build_exception_data(error, Level::Error, context_data)
155
-
156
- # Log the exception with structured data
157
- LogStruct.error(exception_data)
158
-
159
- # Call the error handler with flat context for compatibility
160
214
  context = {
161
215
  mailer_class: self.class.to_s,
162
216
  mailer_action: respond_to?(:action_name) ? action_name : nil,
163
217
  recipients: recipients(error)
164
218
  }
165
- LogStruct.handle_exception(error, source: Source::Mailer, context: context)
219
+ LogStruct.report_error(error, source: Source::Mailer, context: context)
166
220
  end
167
221
 
168
222
  # Re-raise the error if requested
@@ -214,14 +268,18 @@ module LogStruct
214
268
  )
215
269
  end
216
270
 
271
+ # The formatter scrubs the addresses in the line
217
272
  sig { params(error: StandardError).returns(String) }
218
273
  def recipients(error)
219
- # Extract recipient info if available
274
+ # Some delivery errors (Postmark's, for example) have the recipients that failed
220
275
  if error.respond_to?(:recipients) && T.unsafe(error).recipients.present?
221
- T.unsafe(error).recipients.join(", ")
222
- else
223
- "unknown"
276
+ return T.unsafe(error).recipients.join(", ")
224
277
  end
278
+
279
+ # Otherwise the mail's own: to, cc and bcc (unknown when the mailer failed before it built the mail)
280
+ mail = respond_to?(:message) ? message : nil
281
+ destinations = mail.respond_to?(:destinations) ? Array(mail.destinations).compact : []
282
+ destinations.empty? ? "unknown" : destinations.join(", ")
225
283
  end
226
284
  end
227
285
  end
@@ -0,0 +1,81 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LogStruct
5
+ module Integrations
6
+ module ActionMailer
7
+ # The error handlers a mailer can name in rescue_from (with: :log_and_ignore_error), for processes
8
+ # where LogStruct doesn't set up ActionMailer (development, the console and Rake tasks by default).
9
+ # Without them those mailers raised NameError there. Each logs the error with the mailer's logger,
10
+ # then ignores it, reports it to Rails.error, or raises it again, as its name says.
11
+ module FallbackErrorHandlers
12
+ extend T::Sig
13
+ extend T::Helpers
14
+
15
+ requires_ancestor { ::ActionMailer::Base }
16
+
17
+ class << self
18
+ extend T::Sig
19
+
20
+ sig { params(logger: T.untyped, error: StandardError, report: T::Boolean, reraise: T::Boolean).void }
21
+ def handle(logger, error, report:, reraise:)
22
+ logger&.error("#{error.class}: #{error.message}")
23
+ ::Rails.error.report(error, handled: true, source: "logstruct.action_mailer") if report
24
+ Kernel.raise error if reraise
25
+ end
26
+ end
27
+
28
+ # The handlers, for a mailer and (through ClassMethods) for the mailer class, which
29
+ # ActionMailer::MailDeliveryJob passes an error to. Both have a logger.
30
+ module Handlers
31
+ extend T::Sig
32
+ extend T::Helpers
33
+
34
+ abstract!
35
+
36
+ sig { params(ex: StandardError).void }
37
+ def log_and_ignore_error(ex)
38
+ FallbackErrorHandlers.handle(handler_logger, ex, report: false, reraise: false)
39
+ end
40
+
41
+ sig { params(ex: StandardError).void }
42
+ def log_and_report_error(ex)
43
+ FallbackErrorHandlers.handle(handler_logger, ex, report: true, reraise: false)
44
+ end
45
+
46
+ sig { params(ex: StandardError).void }
47
+ def log_and_reraise_error(ex)
48
+ FallbackErrorHandlers.handle(handler_logger, ex, report: false, reraise: true)
49
+ end
50
+
51
+ private
52
+
53
+ sig { abstract.returns(T.untyped) }
54
+ def handler_logger
55
+ end
56
+ end
57
+
58
+ include Handlers
59
+
60
+ module ClassMethods
61
+ extend T::Sig
62
+ extend T::Helpers
63
+
64
+ requires_ancestor { T.class_of(::ActionMailer::Base) }
65
+
66
+ include Handlers
67
+
68
+ private
69
+
70
+ sig { override.returns(T.untyped) }
71
+ def handler_logger = logger
72
+ end
73
+
74
+ private
75
+
76
+ sig { override.returns(T.untyped) }
77
+ def handler_logger = logger
78
+ end
79
+ end
80
+ end
81
+ end
@@ -25,9 +25,6 @@ module LogStruct
25
25
  def self.add_context_metadata(mailer, log_data)
26
26
  # Add account ID information if available (but not user email)
27
27
  extract_ids_to_log_data(mailer, log_data)
28
-
29
- # Add any current tags from ActiveJob or ActionMailer
30
- add_current_tags_to_log_data(log_data)
31
28
  end
32
29
 
33
30
  sig { params(mailer: T.untyped, log_data: T::Hash[Symbol, T.untyped]).void }
@@ -43,23 +40,6 @@ module LogStruct
43
40
  log_data[log_key] = obj.id if obj.respond_to?(:id)
44
41
  end
45
42
  end
46
-
47
- sig { params(log_data: T::Hash[Symbol, T.untyped]).void }
48
- def self.add_current_tags_to_log_data(log_data)
49
- # Get current tags from thread-local storage or ActiveSupport::TaggedLogging
50
- tags = if ::ActiveSupport::TaggedLogging.respond_to?(:current_tags)
51
- T.unsafe(::ActiveSupport::TaggedLogging).current_tags
52
- else
53
- Thread.current[:activesupport_tagged_logging_tags] || []
54
- end
55
- log_data[:tags] = tags if tags.present?
56
-
57
- # Get job_id from ActiveJob if available
58
- if defined?(::ActiveJob::Logging) && ::ActiveJob::Logging.respond_to?(:job_id) &&
59
- T.unsafe(::ActiveJob::Logging).job_id.present?
60
- log_data[:job_id] = T.unsafe(::ActiveJob::Logging).job_id
61
- end
62
- end
63
43
  end
64
44
  end
65
45
  end
@@ -8,10 +8,10 @@ rescue LoadError
8
8
  end
9
9
 
10
10
  if defined?(::ActionMailer)
11
- require "logger"
12
11
  require_relative "action_mailer/metadata_collection"
13
12
  require_relative "action_mailer/event_logging"
14
13
  require_relative "action_mailer/error_handling"
14
+ require_relative "action_mailer/fallback_error_handlers"
15
15
  end
16
16
 
17
17
  module LogStruct
@@ -28,30 +28,35 @@ module LogStruct
28
28
  return nil unless config.enabled
29
29
  return nil unless config.integrations.enable_actionmailer
30
30
 
31
- # Silence default ActionMailer logs (we use our own structured logging)
32
- # This is required because we replace the logging using our own callbacks
33
- if defined?(::ActionMailer::Base)
34
- ::ActionMailer::Base.logger = ::Logger.new(File::NULL)
35
- end
36
-
37
- # Register our custom observers and handlers
38
- # Registering these at the class level means all mailers will use them
31
+ # Register our custom observers and handlers on all mailers. The hook runs right away
32
+ # if ActionMailer::Base is already loaded.
39
33
  ActiveSupport.on_load(:action_mailer) do
34
+ # LogStruct's delivery lines replace Rails' mail lines, which also log the whole mail at debug level.
35
+ # Mailers keep their logger, so an app's own logger calls in a mailer are still logged.
36
+ RailsLogSubscribers.detach(::ActionMailer::LogSubscriber, :action_mailer) if defined?(::ActionMailer::LogSubscriber)
37
+
40
38
  prepend LogStruct::Integrations::ActionMailer::EventLogging
41
39
  prepend LogStruct::Integrations::ActionMailer::ErrorHandling
42
40
  prepend LogStruct::Integrations::ActionMailer::MetadataCollection
43
41
  end
44
42
 
45
- # If ActionMailer::Base is already loaded, the on_load hooks won't run
46
- # So we need to apply the modules directly
47
- if defined?(::ActionMailer::Base)
48
- ::ActionMailer::Base.prepend(LogStruct::Integrations::ActionMailer::EventLogging)
49
- ::ActionMailer::Base.prepend(LogStruct::Integrations::ActionMailer::ErrorHandling)
50
- ::ActionMailer::Base.prepend(LogStruct::Integrations::ActionMailer::MetadataCollection)
51
- end
52
-
53
43
  true
54
44
  end
45
+
46
+ # Where LogStruct doesn't set up ActionMailer, mailers still get the error handlers they can name in
47
+ # rescue_from (see FallbackErrorHandlers)
48
+ sig { params(config: LogStruct::Configuration).void }
49
+ def self.setup_fallback_error_handlers(config)
50
+ return unless defined?(::ActionMailer)
51
+ return if config.enabled && config.integrations.enable_actionmailer
52
+
53
+ ActiveSupport.on_load(:action_mailer) do
54
+ next if include?(LogStruct::Integrations::ActionMailer::FallbackErrorHandlers)
55
+
56
+ include LogStruct::Integrations::ActionMailer::FallbackErrorHandlers
57
+ extend LogStruct::Integrations::ActionMailer::FallbackErrorHandlers::ClassMethods
58
+ end
59
+ end
55
60
  end
56
61
  end
57
62
  end
@@ -17,25 +17,38 @@ module LogStruct
17
17
 
18
18
  sig { params(event: ::ActiveSupport::Notifications::Event).void }
19
19
  def enqueue(event)
20
- job = T.cast(event.payload[:job], ::ActiveJob::Base)
21
- ts = EventTime.coerce_event_time(event.time)
22
- base_fields = build_base_fields(job)
23
- logger.info(Log::ActiveJob::Enqueue.new(
24
- **base_fields.to_kwargs,
25
- timestamp: ts
26
- ))
20
+ log_enqueue(event)
27
21
  end
28
22
 
29
23
  sig { params(event: ::ActiveSupport::Notifications::Event).void }
30
24
  def enqueue_at(event)
31
- job = T.cast(event.payload[:job], ::ActiveJob::Base)
25
+ log_enqueue(event)
26
+ end
27
+
28
+ # perform_all_later: a line for each job, as perform_later writes
29
+ sig { params(event: ::ActiveSupport::Notifications::Event).void }
30
+ def enqueue_all(event)
32
31
  ts = EventTime.coerce_event_time(event.time)
33
- base_fields = build_base_fields(job)
34
- logger.info(Log::ActiveJob::Schedule.new(
35
- **base_fields.to_kwargs,
36
- scheduled_at: job.scheduled_at,
37
- timestamp: ts
38
- ))
32
+ jobs = Array(event.payload[:jobs]).grep(::ActiveJob::Base)
33
+ error = event.payload[:exception_object]
34
+ if error
35
+ log_enqueue_all_error(error, jobs, ts)
36
+ return
37
+ end
38
+
39
+ # Adapters with their own enqueue_all (Sidekiq's) say only how many jobs they enqueued
40
+ if jobs.none? { |job| job.successfully_enqueued? || job.enqueue_error }
41
+ log_enqueued_count(jobs, event.payload[:enqueued_count], ts)
42
+ return
43
+ end
44
+
45
+ jobs.each do |job|
46
+ if job.successfully_enqueued?
47
+ log_enqueued(job, ts)
48
+ elsif job.enqueue_error
49
+ log_exception(job.enqueue_error, job)
50
+ end
51
+ end
39
52
  end
40
53
 
41
54
  sig { params(event: ::ActiveSupport::Notifications::Event).void }
@@ -45,16 +58,16 @@ module LogStruct
45
58
 
46
59
  if exception
47
60
  # Log the exception with the job context
48
- log_exception(exception, job, event)
49
- else
50
- ts = EventTime.coerce_event_time(event.time)
61
+ log_exception(exception, job)
62
+ # An aborted job didn't run: a before_perform callback stopped it
63
+ elsif !event.payload[:aborted]
51
64
  finished_at = EventTime.coerce_event_time(event.end)
52
65
  base_fields = build_base_fields(job)
53
66
  logger.info(Log::ActiveJob::Finish.new(
54
67
  **base_fields.to_kwargs,
55
68
  duration_ms: event.duration.to_f,
56
69
  finished_at: finished_at,
57
- timestamp: ts
70
+ timestamp: finished_at
58
71
  ))
59
72
  end
60
73
  end
@@ -74,8 +87,130 @@ module LogStruct
74
87
  ))
75
88
  end
76
89
 
90
+ # retry_on caught the exception and enqueued the job again
91
+ sig { params(event: ::ActiveSupport::Notifications::Event).void }
92
+ def enqueue_retry(event)
93
+ job = T.cast(event.payload[:job], ::ActiveJob::Base)
94
+ # retry_job can be called without an exception or a wait
95
+ error = event.payload[:error]
96
+ wait = event.payload[:wait]
97
+ logger.info(Log::ActiveJob::Retry.new(
98
+ **build_base_fields(job).to_kwargs,
99
+ error_class: error&.class,
100
+ message: error&.message,
101
+ wait_ms: wait.nil? ? nil : wait.to_f * 1000,
102
+ timestamp: EventTime.coerce_event_time(event.time)
103
+ ))
104
+ end
105
+
106
+ # retry_on ran out of attempts. The job then raises the exception (an error line), unless
107
+ # retry_on was given a block.
108
+ sig { params(event: ::ActiveSupport::Notifications::Event).void }
109
+ def retry_stopped(event)
110
+ job = T.cast(event.payload[:job], ::ActiveJob::Base)
111
+ error = T.cast(event.payload[:error], Exception)
112
+ logger.error(Log::ActiveJob::RetryStopped.new(
113
+ **build_base_fields(job).to_kwargs,
114
+ error_class: error.class,
115
+ message: error.message,
116
+ timestamp: EventTime.coerce_event_time(event.time)
117
+ ))
118
+ end
119
+
120
+ sig { params(event: ::ActiveSupport::Notifications::Event).void }
121
+ def discard(event)
122
+ job = T.cast(event.payload[:job], ::ActiveJob::Base)
123
+ error = T.cast(event.payload[:error], Exception)
124
+ logger.error(Log::ActiveJob::Discard.new(
125
+ **build_base_fields(job).to_kwargs,
126
+ error_class: error.class,
127
+ message: error.message,
128
+ timestamp: EventTime.coerce_event_time(event.time)
129
+ ))
130
+ end
131
+
77
132
  private
78
133
 
134
+ # Like Rails' own enqueue lines: a job the queue didn't take writes an error line, and one a
135
+ # before_enqueue callback stopped writes none
136
+ sig { params(event: ::ActiveSupport::Notifications::Event).void }
137
+ def log_enqueue(event)
138
+ job = T.cast(event.payload[:job], ::ActiveJob::Base)
139
+ # perform_later rescues ActiveJob::EnqueueError and keeps it on the job
140
+ error = event.payload[:exception_object] || job.enqueue_error
141
+ if error
142
+ # With the inline adapter, a job that fails raises out of perform_later, and its perform line has
143
+ # already logged the error
144
+ log_exception(error, job) unless LogStruct::LoggedErrors.logged?(error)
145
+ elsif !event.payload[:aborted]
146
+ log_enqueued(job, EventTime.coerce_event_time(event.time))
147
+ end
148
+ end
149
+
150
+ sig { params(job: ::ActiveJob::Base, ts: Time).void }
151
+ def log_enqueued(job, ts)
152
+ base_fields = build_base_fields(job)
153
+ scheduled_at = job.scheduled_at
154
+ if scheduled_at
155
+ logger.info(Log::ActiveJob::Schedule.new(**base_fields.to_kwargs, scheduled_at: scheduled_at, timestamp: ts))
156
+ else
157
+ logger.info(Log::ActiveJob::Enqueue.new(**base_fields.to_kwargs, timestamp: ts))
158
+ end
159
+ end
160
+
161
+ # perform_all_later raised: the adapter did (Redis down, say), or with the inline adapter a job did
162
+ sig { params(error: Exception, jobs: T::Array[::ActiveJob::Base], ts: Time).void }
163
+ def log_enqueue_all_error(error, jobs, ts)
164
+ # Rails' own enqueue loop (for adapters without enqueue_all) marks each job it tries, keeps an
165
+ # ActiveJob::EnqueueError on its job and goes on, and stops at a job that raises anything else
166
+ tried = jobs.reject { |job| job.successfully_enqueued?.nil? }
167
+ tried.each do |job|
168
+ enqueue_error = job.enqueue_error
169
+ if job.successfully_enqueued?
170
+ log_enqueued(job, ts)
171
+ elsif enqueue_error
172
+ log_exception(enqueue_error, job)
173
+ end
174
+ end
175
+ # With the inline adapter, the error is a job's own, which its perform line has logged
176
+ return if LogStruct::LoggedErrors.logged?(error)
177
+
178
+ raised_by = tried.last
179
+ if raised_by && !raised_by.successfully_enqueued? && raised_by.enqueue_error.nil?
180
+ log_exception(error, raised_by)
181
+ return
182
+ end
183
+
184
+ # Which job it was isn't known: an adapter's own enqueue_all (Sidekiq's) doesn't say which jobs it
185
+ # enqueued before it raised
186
+ classes = jobs.map { |job| job.class.to_s }.uniq.join(", ")
187
+ message = "perform_all_later of #{jobs.size} jobs (#{classes}) failed"
188
+ message += "; the queue adapter doesn't say which it enqueued" if tried.empty?
189
+ logger.error(Log::Error.new(
190
+ source: Source::Job,
191
+ error_class: error.class,
192
+ message: message,
193
+ error_message: error.message,
194
+ backtrace: error.backtrace,
195
+ level: Level::Error,
196
+ timestamp: ts
197
+ ))
198
+ LogStruct::LoggedErrors.mark(error)
199
+ end
200
+
201
+ # Every job's line when the adapter enqueued them all; when it didn't, it doesn't say which it enqueued
202
+ sig { params(jobs: T::Array[::ActiveJob::Base], enqueued_count: T.untyped, ts: Time).void }
203
+ def log_enqueued_count(jobs, enqueued_count, ts)
204
+ return if jobs.empty?
205
+
206
+ if enqueued_count == jobs.size
207
+ jobs.each { |job| log_enqueued(job, ts) }
208
+ else
209
+ classes = jobs.map { |job| job.class.to_s }.uniq.join(", ")
210
+ logger.warn("perform_all_later enqueued #{enqueued_count.to_i} of #{jobs.size} jobs (#{classes}); the queue adapter doesn't say which")
211
+ end
212
+ end
213
+
79
214
  sig { params(job: ::ActiveJob::Base).returns(Log::ActiveJob::BaseFields) }
80
215
  def build_base_fields(job)
81
216
  Log::ActiveJob::BaseFields.new(
@@ -83,19 +218,36 @@ module LogStruct
83
218
  job_class: job.class.to_s,
84
219
  queue_name: job.queue_name&.to_sym,
85
220
  executions: job.executions,
86
- provider_job_id: job.provider_job_id,
87
- arguments: ((job.class.respond_to?(:log_arguments?) && job.class.log_arguments?) ? job.arguments : nil)
221
+ # Backends use Strings or Integers (SolidQueue uses its job row ID)
222
+ provider_job_id: job.provider_job_id&.to_s,
223
+ arguments: logged_arguments(job)
88
224
  )
89
225
  end
90
226
 
91
- sig { params(exception: StandardError, job: ::ActiveJob::Base, _event: ::ActiveSupport::Notifications::Event).void }
92
- def log_exception(exception, job, _event)
93
- base_fields = build_base_fields(job)
94
- job_context = base_fields.to_kwargs
227
+ # ActionMailer's delivery job carries the mail's own arguments, which can be secrets: Devise passes its
228
+ # confirmation, password reset and invitation tokens as plain strings, which no key-based filter can find.
229
+ # For a mail, log the mailer, the mail and the delivery method only.
230
+ sig { params(job: ::ActiveJob::Base).returns(T.nilable(T::Array[T.untyped])) }
231
+ def logged_arguments(job)
232
+ return nil unless job.class.respond_to?(:log_arguments?) && job.class.log_arguments?
233
+ return job.arguments.first(3) if defined?(::ActionMailer::MailDeliveryJob) && job.is_a?(::ActionMailer::MailDeliveryJob)
95
234
 
96
- log_data = Log.from_exception(Source::Job, exception, job_context)
235
+ job.arguments
236
+ end
237
+
238
+ # perform_now lets any Exception through, not only StandardErrors
239
+ sig { params(exception: Exception, job: ::ActiveJob::Base).void }
240
+ def log_exception(exception, job)
241
+ log_data = Log::ActiveJob::Error.new(
242
+ **build_base_fields(job).to_kwargs,
243
+ error_class: exception.class,
244
+ message: exception.message,
245
+ backtrace: exception.backtrace
246
+ )
97
247
 
98
248
  logger.error(log_data)
249
+ # So a backend that reports the same exception (GoodJob's on_thread_error) doesn't log it again
250
+ LogStruct::LoggedErrors.mark(exception)
99
251
  end
100
252
 
101
253
  sig { returns(T.untyped) }
@@ -25,13 +25,8 @@ module LogStruct
25
25
  return nil unless config.integrations.enable_activejob
26
26
 
27
27
  ::ActiveSupport.on_load(:active_job) do
28
- if ::ActiveJob::LogSubscriber.respond_to?(:detach_from)
29
- # Detach the default text formatter
30
- ::ActiveJob::LogSubscriber.detach_from :active_job
31
- elsif ::ActiveSupport.respond_to?(:event_reporter)
32
- reporter = ::ActiveSupport.event_reporter
33
- reporter.unsubscribe(::ActiveJob::LogSubscriber) if reporter.respond_to?(:unsubscribe)
34
- end
28
+ # Detach the default text formatter
29
+ RailsLogSubscribers.detach(::ActiveJob::LogSubscriber, :active_job)
35
30
 
36
31
  # Attach our structured formatter
37
32
  Integrations::ActiveJob::LogSubscriber.attach_to :active_job
@@ -17,6 +17,11 @@ module LogStruct
17
17
  # Only activate if AMS appears to be present
18
18
  return nil unless defined?(::ActiveModelSerializers)
19
19
 
20
+ # AMS logs each render as a line of text too
21
+ if defined?(::ActiveModelSerializers::Logging::LogSubscriber)
22
+ ::ActiveModelSerializers::Logging::LogSubscriber.detach_from(:active_model_serializers)
23
+ end
24
+
20
25
  # Subscribe to common AMS notification names; keep broad but specific
21
26
  pattern = /\.active_model_serializers\z/
22
27
 
@@ -31,15 +36,16 @@ module LogStruct
31
36
  LogStruct.info(
32
37
  LogStruct::Log::ActiveModelSerializers.new(
33
38
  message: "ams.render",
34
- serializer: serializer&.to_s,
35
- adapter: adapter&.to_s,
39
+ serializer: serializer&.name,
40
+ adapter: adapter&.class&.name,
36
41
  resource_class: resource&.class&.name,
37
42
  duration_ms: duration_ms,
38
43
  timestamp: started
39
44
  )
40
45
  )
41
46
  rescue => e
42
- LogStruct.handle_exception(e, source: LogStruct::Source::Rails, context: {integration: :active_model_serializers})
47
+ # A problem with the log line is LogStruct's own, and never fails the render in production
48
+ LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :active_model_serializers})
43
49
  end
44
50
 
45
51
  true