logstruct 0.1.12 → 0.2.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +139 -1
- data/README.md +76 -68
- data/lib/log_struct/concerns/configuration.rb +139 -91
- data/lib/log_struct/concerns/error_handling.rb +36 -6
- data/lib/log_struct/config_struct/error_handling_modes.rb +3 -1
- data/lib/log_struct/config_struct/filters.rb +116 -33
- data/lib/log_struct/config_struct/integrations.rb +0 -4
- data/lib/log_struct/config_struct/limits.rb +29 -0
- data/lib/log_struct/configuration.rb +2 -0
- data/lib/log_struct/enums/event.rb +3 -1
- data/lib/log_struct/enums/log_field.rb +5 -0
- data/lib/log_struct/formatter.rb +304 -56
- data/lib/log_struct/hash_utils.rb +33 -4
- data/lib/log_struct/integrations/action_mailer/error_handling.rb +78 -20
- data/lib/log_struct/integrations/action_mailer/fallback_error_handlers.rb +81 -0
- data/lib/log_struct/integrations/action_mailer/metadata_collection.rb +0 -20
- data/lib/log_struct/integrations/action_mailer.rb +22 -17
- data/lib/log_struct/integrations/active_job/log_subscriber.rb +177 -25
- data/lib/log_struct/integrations/active_job.rb +2 -7
- data/lib/log_struct/integrations/active_model_serializers.rb +7 -1
- data/lib/log_struct/integrations/active_record.rb +39 -17
- data/lib/log_struct/integrations/active_storage.rb +31 -31
- data/lib/log_struct/integrations/ahoy.rb +19 -15
- data/lib/log_struct/integrations/carrierwave.rb +55 -51
- data/lib/log_struct/integrations/dotenv.rb +22 -32
- data/lib/log_struct/integrations/good_job/logger.rb +30 -24
- data/lib/log_struct/integrations/good_job.rb +111 -40
- data/lib/log_struct/integrations/host_authorization.rb +47 -36
- data/lib/log_struct/integrations/lograge.rb +80 -4
- data/lib/log_struct/integrations/puma.rb +74 -34
- data/lib/log_struct/integrations/rack_error_handler/debug_exceptions_logging.rb +35 -0
- data/lib/log_struct/integrations/rack_error_handler/error_lines.rb +160 -0
- data/lib/log_struct/integrations/rack_error_handler/middleware.rb +14 -169
- data/lib/log_struct/integrations/rack_error_handler/parse_error_logging.rb +36 -0
- data/lib/log_struct/integrations/rack_error_handler.rb +20 -4
- data/lib/log_struct/integrations/rack_middleware.rb +23 -0
- data/lib/log_struct/integrations/rails_log_subscribers.rb +32 -0
- data/lib/log_struct/integrations/request_context/middleware.rb +4 -9
- data/lib/log_struct/integrations/shrine.rb +48 -73
- data/lib/log_struct/integrations/sidekiq/job_context_middleware.rb +32 -0
- data/lib/log_struct/integrations/sidekiq/logger.rb +17 -9
- data/lib/log_struct/integrations/sidekiq.rb +98 -7
- data/lib/log_struct/integrations/sorbet.rb +10 -46
- data/lib/log_struct/integrations.rb +3 -2
- data/lib/log_struct/log/{good_job/enqueue.rb → active_job/discard.rb} +13 -13
- data/lib/log_struct/log/active_job/enqueue.rb +0 -4
- data/lib/log_struct/log/{good_job/schedule.rb → active_job/error.rb} +15 -15
- data/lib/log_struct/log/{good_job/start.rb → active_job/retry.rb} +13 -13
- data/lib/log_struct/log/{good_job/finish.rb → active_job/retry_stopped.rb} +13 -17
- data/lib/log_struct/log/active_job.rb +4 -0
- data/lib/log_struct/log/active_storage/delete.rb +6 -2
- data/lib/log_struct/log/active_storage/download.rb +2 -4
- data/lib/log_struct/log/active_storage/exist.rb +2 -2
- data/lib/log_struct/log/active_storage/metadata.rb +4 -4
- data/lib/log_struct/log/active_storage/upload.rb +2 -10
- data/lib/log_struct/log/active_storage/url.rb +2 -6
- data/lib/log_struct/log/active_storage.rb +2 -3
- data/lib/log_struct/log/error.rb +24 -1
- data/lib/log_struct/log/good_job.rb +0 -4
- data/lib/log_struct/log/puma/error.rb +63 -0
- data/lib/log_struct/log/puma.rb +1 -0
- data/lib/log_struct/log/security/blocked_host.rb +0 -4
- data/lib/log_struct/log/shrine/exist.rb +0 -2
- data/lib/log_struct/log/shrine/metadata.rb +2 -4
- data/lib/log_struct/log/shrine/upload.rb +4 -0
- data/lib/log_struct/log/sidekiq/error.rb +63 -0
- data/lib/log_struct/log/{active_storage/stream.rb → sidekiq/log.rb} +17 -11
- data/lib/log_struct/log/sidekiq.rb +3 -45
- data/lib/log_struct/log.rb +4 -2
- data/lib/log_struct/logged_errors.rb +25 -0
- data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +2 -3
- data/lib/log_struct/param_filters.rb +51 -38
- data/lib/log_struct/params_errors.rb +78 -0
- data/lib/log_struct/rails_boot_banner_silencer.rb +7 -2
- data/lib/log_struct/railtie.rb +29 -14
- data/lib/log_struct/semantic_logger/color_formatter.rb +60 -53
- data/lib/log_struct/semantic_logger/concerns/log_methods.rb +65 -57
- data/lib/log_struct/semantic_logger/formatter.rb +79 -57
- data/lib/log_struct/semantic_logger/logger.rb +29 -53
- data/lib/log_struct/semantic_logger/reopen_after_fork.rb +40 -0
- data/lib/log_struct/semantic_logger/setup.rb +115 -112
- data/lib/log_struct/shared/serialize_common.rb +2 -7
- data/lib/log_struct/string_scrubber.rb +229 -20
- data/lib/log_struct/version.rb +1 -1
- data/lib/log_struct.rb +6 -4
- data/logstruct.gemspec +19 -10
- metadata +63 -11
- data/lib/log_struct/builders/active_job.rb +0 -87
- data/lib/log_struct/integrations/good_job/log_subscriber.rb +0 -189
- 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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
#
|
|
32
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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:
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
|
@@ -39,7 +44,8 @@ module LogStruct
|
|
|
39
44
|
)
|
|
40
45
|
)
|
|
41
46
|
rescue => e
|
|
42
|
-
|
|
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
|