logstruct 0.1.2 → 0.1.3

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -1
  3. data/README.md +4 -6
  4. data/lib/log_struct/concerns/configuration.rb +2 -2
  5. data/lib/log_struct/config_struct/integrations.rb +5 -0
  6. data/lib/log_struct/enums/log_field.rb +12 -1
  7. data/lib/log_struct/integrations/action_mailer/error_handling.rb +121 -27
  8. data/lib/log_struct/integrations/action_mailer/event_logging.rb +30 -14
  9. data/lib/log_struct/integrations/action_mailer/metadata_collection.rb +18 -24
  10. data/lib/log_struct/integrations/action_mailer.rb +13 -6
  11. data/lib/log_struct/integrations/active_job/log_subscriber.rb +2 -2
  12. data/lib/log_struct/integrations/active_storage.rb +8 -8
  13. data/lib/log_struct/integrations/ahoy.rb +2 -3
  14. data/lib/log_struct/integrations/carrierwave.rb +8 -10
  15. data/lib/log_struct/integrations/good_job/log_subscriber.rb +5 -5
  16. data/lib/log_struct/integrations/good_job/logger.rb +2 -6
  17. data/lib/log_struct/integrations/good_job.rb +1 -1
  18. data/lib/log_struct/integrations/host_authorization.rb +27 -36
  19. data/lib/log_struct/integrations/lograge.rb +1 -1
  20. data/lib/log_struct/integrations/shrine.rb +21 -24
  21. data/lib/log_struct/integrations/sidekiq/logger.rb +8 -1
  22. data/lib/log_struct/log/action_mailer/delivered.rb +14 -49
  23. data/lib/log_struct/log/action_mailer/delivery.rb +14 -49
  24. data/lib/log_struct/log/action_mailer/error.rb +72 -0
  25. data/lib/log_struct/log/action_mailer.rb +15 -2
  26. data/lib/log_struct/log/active_job/enqueue.rb +9 -73
  27. data/lib/log_struct/log/active_job/finish.rb +9 -76
  28. data/lib/log_struct/log/active_job/schedule.rb +9 -73
  29. data/lib/log_struct/log/active_job/start.rb +9 -76
  30. data/lib/log_struct/log/active_job.rb +2 -2
  31. data/lib/log_struct/log/active_model_serializers.rb +5 -45
  32. data/lib/log_struct/log/active_storage/delete.rb +8 -46
  33. data/lib/log_struct/log/active_storage/download.rb +9 -55
  34. data/lib/log_struct/log/active_storage/exist.rb +9 -49
  35. data/lib/log_struct/log/active_storage/metadata.rb +9 -49
  36. data/lib/log_struct/log/active_storage/stream.rb +9 -49
  37. data/lib/log_struct/log/active_storage/upload.rb +9 -64
  38. data/lib/log_struct/log/active_storage/url.rb +9 -49
  39. data/lib/log_struct/log/active_storage.rb +2 -2
  40. data/lib/log_struct/log/ahoy.rb +5 -43
  41. data/lib/log_struct/log/carrierwave/delete.rb +15 -69
  42. data/lib/log_struct/log/carrierwave/download.rb +15 -77
  43. data/lib/log_struct/log/carrierwave/upload.rb +15 -83
  44. data/lib/log_struct/log/carrierwave.rb +13 -4
  45. data/lib/log_struct/log/dotenv/load.rb +5 -33
  46. data/lib/log_struct/log/dotenv/restore.rb +5 -33
  47. data/lib/log_struct/log/dotenv/save.rb +5 -33
  48. data/lib/log_struct/log/dotenv/update.rb +5 -33
  49. data/lib/log_struct/log/error.rb +7 -40
  50. data/lib/log_struct/log/good_job/enqueue.rb +9 -72
  51. data/lib/log_struct/log/good_job/error.rb +9 -89
  52. data/lib/log_struct/log/good_job/finish.rb +9 -78
  53. data/lib/log_struct/log/good_job/log.rb +11 -75
  54. data/lib/log_struct/log/good_job/schedule.rb +7 -78
  55. data/lib/log_struct/log/good_job/start.rb +7 -78
  56. data/lib/log_struct/log/good_job.rb +2 -2
  57. data/lib/log_struct/log/plain.rb +5 -32
  58. data/lib/log_struct/log/puma/shutdown.rb +5 -32
  59. data/lib/log_struct/log/puma/start.rb +5 -56
  60. data/lib/log_struct/log/request.rb +7 -90
  61. data/lib/log_struct/log/security/blocked_host.rb +12 -73
  62. data/lib/log_struct/log/security/csrf_violation.rb +6 -67
  63. data/lib/log_struct/log/security/ip_spoof.rb +6 -73
  64. data/lib/log_struct/log/shrine/delete.rb +6 -41
  65. data/lib/log_struct/log/shrine/download.rb +6 -44
  66. data/lib/log_struct/log/shrine/exist.rb +6 -44
  67. data/lib/log_struct/log/shrine/metadata.rb +8 -46
  68. data/lib/log_struct/log/shrine/upload.rb +6 -53
  69. data/lib/log_struct/log/sidekiq.rb +5 -42
  70. data/lib/log_struct/log/sql.rb +5 -65
  71. data/lib/log_struct/log.rb +2 -2
  72. data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +12 -1
  73. data/lib/log_struct/railtie.rb +0 -22
  74. data/lib/log_struct/semantic_logger/concerns/log_methods.rb +100 -0
  75. data/lib/log_struct/semantic_logger/logger.rb +46 -15
  76. data/lib/log_struct/semantic_logger/setup.rb +11 -7
  77. data/lib/log_struct/shared/{shared/add_request_fields.rb → add_request_fields.rb} +2 -2
  78. data/lib/log_struct/shared/{shared/merge_additional_data_fields.rb → merge_additional_data_fields.rb} +1 -1
  79. data/lib/log_struct/shared/{shared/serialize_common.rb → serialize_common.rb} +9 -3
  80. data/lib/log_struct/{log/shared → shared}/serialize_common_public.rb +2 -2
  81. data/lib/log_struct/version.rb +1 -1
  82. data/lib/log_struct.rb +4 -1
  83. data/logstruct.gemspec +1 -1
  84. metadata +9 -11
  85. data/lib/log_struct/integrations/action_mailer/callbacks.rb +0 -100
  86. data/lib/log_struct/log/shared/add_request_fields.rb +0 -4
  87. data/lib/log_struct/log/shared/merge_additional_data_fields.rb +0 -4
  88. data/lib/log_struct/log/shared/serialize_common.rb +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a65754f5cc8e4753585ca448eec8e78b5f8586aba5161d94d5c5ccce20386909
4
- data.tar.gz: 9088ba81fe6319a9dfd1740949c87c7a793bd1535ae97a4d44bc98b0ca3b60b9
3
+ metadata.gz: 77b4f5cd95c84bd5b418bb95b1856af146e70a64e6d39e3a303e33e083faa64d
4
+ data.tar.gz: 670c515b7eb8fbe5320c3bc52e2a68234aff31f8633fc071286a1b4a816132b6
5
5
  SHA512:
6
- metadata.gz: 1bccedd39623795cef4059ec14029c6ed4dd9edba94b355c9147c78052e163d41f6d1d46988bf367bc1c726e0cabaab8f30c8cc36a5470f3b7d1f0c5463b56d6
7
- data.tar.gz: e0a3d5b784338c0d647ff03e12a92852e4c0de3fb2f93febba9df0bd3871f0b41ef262c1915db368a0b9db71afd0d005db05a325d2314f276dbd23e7b291f654
6
+ metadata.gz: f7b81a7d6893f7db9b51c71d5623beedfa75d81e5c9c1ec51003db37a5873018427e81a86aaa2de3a73741cb372bea2d859980b96695592ee9249ea0def0fe9d
7
+ data.tar.gz: e8fd117ec5795acebba1a996d2777f25f2ac89afdbb4fa76cb18ec1faa5fea11800b9cf452178412eb0fabe77ca4bcc223dd516d53b1217031f7433361bdc41c
data/CHANGELOG.md CHANGED
@@ -5,10 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.3] - 2025-10-11
9
+
10
+ ### Changed
11
+
12
+ - **Fix**: Changed storage, queue name, and format fields from `String` to `Symbol` type to match Rails conventions
13
+ - Affected log types: ActiveStorage, CarrierWave, Shrine (storage field), ActiveJob, GoodJob (queue_name field), Request (format field)
14
+ - JSON logging now enabled for all test runs (both local and CI) to ensure tests catch production bugs
15
+ - Previously only enabled for CI test runs, now always enabled in test environment
16
+ - This ensures local tests match CI behavior and catch serialization issues early
17
+ - Fixed host authorization app
18
+
8
19
  ## [0.1.2] - 2025-10-03
9
20
 
10
21
  Better default policy for when JSON logs are enabled: machines get JSON, humans get readable logs.
11
- Enable LogStruct for production servers or when running tests on CI.
22
+ Enable LogStruct for production servers and test runs (both local and CI) to ensure tests catch production bugs.
12
23
  Keep dev-friendly logging on local machines or when running interactive commands on production servers.
13
24
 
14
25
  ## [0.1.1] - 2025-09-29
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # LogStruct
2
2
 
3
- Adds JSON structured logging to any Rails app. Simply add the gem to your Gemfile and add an initializer to configure it. By default, your Rails app prints JSON logs to STDOUT (or to the configured destination when `RAILS_LOG_TO_STDOUT` is set). They're easy to search and filter, you can turn them into metrics and alerts, and they're great for building dashboards in CloudWatch, Grafana, or Datadog.
3
+ Adds secure JSON structured logging to any Rails app (>= 7.1). Simply add the gem to your Gemfile and add an initializer to configure it. By default, your Rails app prints JSON logs to STDOUT (or to the configured destination when `RAILS_LOG_TO_STDOUT` is set). They're easy to search and filter, you can turn them into metrics and alerts, and they're great for building dashboards in CloudWatch, Grafana, or Datadog.
4
4
 
5
5
  We support all your other favorite gems too, like Sidekiq, Sentry, and Shrine. (And if not, please open a PR!)
6
6
 
7
7
  ## Features
8
8
 
9
- - JSON logging enabled by default for server processes in production and test environments, and for CI test runs (automatically disabled for console, local tests, and other Rake tasks)
9
+ - JSON logging enabled by default for server processes in production and test environments (automatically disabled for console and other Rake tasks)
10
10
  - ActionMailer integration for email delivery logging
11
11
  - ActiveJob integration for job execution logging
12
12
  - Sidekiq integration for background job logging
@@ -17,7 +17,6 @@ We support all your other favorite gems too, like Sidekiq, Sentry, and Shrine. (
17
17
  - Sensitive data scrubbing for strings (inspired by the Logstop gem)
18
18
  - Host authorization logging for security violations
19
19
  - Rack middleware for enhanced error logging
20
- - ActionMailer delivery callbacks for Rails 7.0.x (backported from Rails 7.1)
21
20
  - Type checking with Sorbet and RBS annotations
22
21
 
23
22
  ## Installation
@@ -61,8 +60,7 @@ Once initialized (and enabled), the gem automatically includes its modules into
61
60
  ### Default behavior by process type
62
61
 
63
62
  - **Server processes** (`rails server`): JSON logging is enabled by default in production and test environments
64
- - **CI test runs** (`rails test` when `CI=true`): JSON logging is enabled by default to catch production bugs in your automated tests
65
- - **Local test runs** (`rails test` locally): JSON logging is disabled by default, providing human-readable logs for debugging
63
+ - **Test runs** (`rails test`): JSON logging is enabled by default in test environment to ensure tests catch production bugs
66
64
  - **Console** (`rails console`): JSON logging is disabled by default in all environments, providing human-readable logs instead
67
65
  - **Other Rake tasks** (`rake db:migrate`, etc.): JSON logging is disabled by default in production, providing human-readable logs instead
68
66
  - **Development environment**: Disabled by default for all process types. Enable explicitly via `LOGSTRUCT_ENABLED=true` or `LogStruct.configure { |c| c.enabled = true }`.
@@ -76,7 +74,7 @@ LogStruct.configure do |c|
76
74
  end
77
75
  ```
78
76
 
79
- To force JSON logs in console, local test runs, or other Rake tasks (e.g., for debugging), set `LOGSTRUCT_ENABLED=true` in your environment.
77
+ To force JSON logs in console or other Rake tasks (e.g., for debugging), set `LOGSTRUCT_ENABLED=true` in your environment.
80
78
 
81
79
  ## Documentation
82
80
 
@@ -60,10 +60,10 @@ module LogStruct
60
60
  else
61
61
  is_console = console_process?
62
62
  is_server = server_process?
63
- is_ci = ci_build?
63
+ ci_build?
64
64
  in_enabled_env = config.enabled_environments.include?(::Rails.env.to_sym)
65
65
 
66
- in_enabled_env && !is_console && (is_server || (::Rails.env.test? && is_ci))
66
+ in_enabled_env && !is_console && (is_server || ::Rails.env.test?)
67
67
  end
68
68
  end
69
69
 
@@ -24,6 +24,11 @@ module LogStruct
24
24
  # Default: true
25
25
  prop :enable_actionmailer, T::Boolean, default: true
26
26
 
27
+ # Map instance variables on mailer to ID fields in additional_data
28
+ # Default: { account: :account_id, user: :user_id }
29
+ # Example: { organization: :org_id, company: :company_id }
30
+ prop :actionmailer_id_mapping, T::Hash[Symbol, Symbol], factory: -> { {account: :account_id, user: :user_id} }
31
+
27
32
  # Enable or disable host authorization logging
28
33
  # Default: true
29
34
  prop :enable_host_authorization, T::Boolean, default: true
@@ -47,6 +47,8 @@ module LogStruct
47
47
  # Security-specific fields
48
48
  BlockedHost = new(:blocked_host)
49
49
  BlockedHosts = new(:blocked_hosts)
50
+ AllowedHosts = new(:allowed_hosts)
51
+ AllowIpHosts = new(:allow_ip_hosts)
50
52
  ClientIp = new(:client_ip)
51
53
  XForwardedFor = new(:x_forwarded_for)
52
54
 
@@ -54,9 +56,13 @@ module LogStruct
54
56
  To = new(:to)
55
57
  From = new(:from)
56
58
  Subject = new(:subject)
59
+ MessageId = new(:msg_id)
60
+ MailerClass = new(:mailer)
61
+ MailerAction = new(:mailer_action)
62
+ AttachmentCount = new(:attachments)
57
63
 
58
64
  # Error fields
59
- ErrClass = new(:err_class)
65
+ ErrorClass = new(:error_class)
60
66
  Backtrace = new(:backtrace)
61
67
 
62
68
  # Job-specific fields
@@ -82,6 +88,8 @@ module LogStruct
82
88
  Priority = new(:priority)
83
89
  CronKey = new(:cron_key)
84
90
  ErrorMessage = new(:error_message)
91
+ Result = new(:result)
92
+ EnqueueCaller = new(:enqueue_caller)
85
93
 
86
94
  # Dotenv fields
87
95
  File = new(:file)
@@ -117,6 +125,9 @@ module LogStruct
117
125
  # CarrierWave-specific fields
118
126
  Model = new(:model)
119
127
  MountPoint = new(:mount_point)
128
+ Version = new(:version)
129
+ StorePath = new(:store_path)
130
+ Extension = new(:ext)
120
131
 
121
132
  # SQL-specific fields
122
133
  Sql = new(:sql)
@@ -15,6 +15,9 @@ module LogStruct
15
15
  extend T::Sig
16
16
  extend ActiveSupport::Concern
17
17
 
18
+ sig { returns(T.nilable(T::Boolean)) }
19
+ attr_accessor :logstruct_mail_failed
20
+
18
21
  # NOTE: rescue_from handlers are checked in reverse order of declaration.
19
22
  # We want LogStruct handlers to be checked AFTER user handlers (lower priority),
20
23
  # so we need to add them BEFORE user handlers are declared.
@@ -47,6 +50,7 @@ module LogStruct
47
50
  # Just log the error without reporting or retrying
48
51
  sig { params(ex: StandardError).void }
49
52
  def log_and_ignore_error(ex)
53
+ self.logstruct_mail_failed = true
50
54
  log_email_delivery_error(ex, notify: false, report: false, reraise: false)
51
55
  end
52
56
 
@@ -67,20 +71,54 @@ module LogStruct
67
71
  # Handle an error from a mailer
68
72
  sig { params(mailer: T.untyped, error: StandardError, message: String).void }
69
73
  def log_structured_error(mailer, error, message)
70
- # Create a structured exception log with context
71
- context = {
72
- mailer_class: mailer.class.to_s,
73
- mailer_action: mailer.respond_to?(:action_name) ? mailer.action_name : nil,
74
- message: message
75
- }
74
+ # Get message if available
75
+ mailer_message = mailer.respond_to?(:message) ? mailer.message : nil
76
+
77
+ # Prepare universal mailer fields
78
+ message_data = {}
79
+ MetadataCollection.add_message_metadata(mailer, message_data)
80
+
81
+ # Prepare app-specific context data for additional_data
82
+ context_data = {}
83
+ MetadataCollection.add_context_metadata(mailer, context_data)
76
84
 
77
- # Create the structured exception log
78
- exception_data = Log.from_exception(Source::Mailer, error, context)
85
+ # Extract email fields
86
+ to = mailer_message&.to
87
+ from = mailer_message&.from&.first
88
+ subject = mailer_message&.subject
89
+ message_id = extract_message_id_from_mailer(mailer)
90
+
91
+ # Create ActionMailer-specific error struct
92
+ exception_data = Log::ActionMailer::Error.new(
93
+ to: to,
94
+ from: from,
95
+ subject: subject,
96
+ message_id: message_id,
97
+ mailer_class: mailer.class.to_s,
98
+ mailer_action: mailer.respond_to?(:action_name) ? mailer.action_name&.to_s : nil,
99
+ attachment_count: message_data[:attachment_count],
100
+ error_class: error.class,
101
+ message: message,
102
+ backtrace: error.backtrace,
103
+ additional_data: context_data.presence,
104
+ timestamp: Time.now
105
+ )
79
106
 
80
107
  # Log the structured error
81
108
  LogStruct.error(exception_data)
82
109
  end
83
110
 
111
+ # Extract message ID from the mailer
112
+ sig { params(mailer: T.untyped).returns(T.nilable(String)) }
113
+ def extract_message_id_from_mailer(mailer)
114
+ return nil unless mailer.respond_to?(:message)
115
+
116
+ mail_message = mailer.message
117
+ return nil unless mail_message.respond_to?(:message_id)
118
+
119
+ mail_message.message_id
120
+ end
121
+
84
122
  # Log when email delivery fails
85
123
  sig { params(error: StandardError, notify: T::Boolean, report: T::Boolean, reraise: T::Boolean).void }
86
124
  def log_email_delivery_error(error, notify: false, report: true, reraise: true)
@@ -98,9 +136,9 @@ module LogStruct
98
136
  sig { params(error: StandardError, reraise: T::Boolean).returns(String) }
99
137
  def error_message_for(error, reraise)
100
138
  if reraise
101
- "#{error.class}: Email delivery error, will retry. Recipients: #{recipients(error)}"
139
+ "#{error.class}: Email delivery error, will retry. Recipients: #{recipients(error)}. Error message: #{error.message}"
102
140
  else
103
- "#{error.class}: Cannot send email to #{recipients(error)}"
141
+ "#{error.class}: Cannot send email to #{recipients(error)}. Error message: #{error.message}"
104
142
  end
105
143
  end
106
144
 
@@ -112,19 +150,48 @@ module LogStruct
112
150
 
113
151
  # Report to error reporting service if requested
114
152
  if report
115
- context = {
116
- mailer_class: self.class.to_s,
117
- mailer_action: respond_to?(:action_name) ? action_name : nil,
118
- recipients: recipients(error)
119
- }
153
+ # Get message if available
154
+ mailer_message = respond_to?(:message) ? message : nil
120
155
 
121
- # Create an exception log for structured logging
122
- exception_data = Log.from_exception(Source::Mailer, error, context)
156
+ # Prepare universal mailer fields
157
+ message_data = {}
158
+ MetadataCollection.add_message_metadata(self, message_data)
159
+
160
+ # Prepare app-specific context data
161
+ context_data = {recipients: recipients(error)}
162
+ MetadataCollection.add_context_metadata(self, context_data)
163
+
164
+ # Extract email fields
165
+ to = mailer_message&.to
166
+ from = mailer_message&.from&.first
167
+ subject = mailer_message&.subject
168
+ message_id = extract_message_id_from_mailer(self)
169
+
170
+ # Create ActionMailer-specific error struct
171
+ exception_data = Log::ActionMailer::Error.new(
172
+ to: to,
173
+ from: from,
174
+ subject: subject,
175
+ message_id: message_id,
176
+ mailer_class: self.class.to_s,
177
+ mailer_action: respond_to?(:action_name) ? action_name&.to_s : nil,
178
+ attachment_count: message_data[:attachment_count],
179
+ error_class: error.class,
180
+ message: error.message,
181
+ backtrace: error.backtrace,
182
+ additional_data: context_data.presence,
183
+ timestamp: Time.now
184
+ )
123
185
 
124
186
  # Log the exception with structured data
125
187
  LogStruct.error(exception_data)
126
188
 
127
- # Call the error handler
189
+ # Call the error handler with flat context for compatibility
190
+ context = {
191
+ mailer_class: self.class.to_s,
192
+ mailer_action: respond_to?(:action_name) ? action_name : nil,
193
+ recipients: recipients(error)
194
+ }
128
195
  LogStruct.handle_exception(error, source: Source::Mailer, context: context)
129
196
  end
130
197
 
@@ -135,15 +202,42 @@ module LogStruct
135
202
  # Log a notification event that can be picked up by external systems
136
203
  sig { params(error: StandardError).void }
137
204
  def log_notification_event(error)
138
- # Create an error log data object
139
- exception_data = Log.from_exception(
140
- Source::Mailer,
141
- error,
142
- {
143
- mailer: self.class,
144
- action: action_name,
145
- recipients: recipients(error)
146
- }
205
+ # Get message if available
206
+ mailer_message = respond_to?(:message) ? message : nil
207
+
208
+ # Prepare universal mailer fields
209
+ message_data = {}
210
+ MetadataCollection.add_message_metadata(self, message_data)
211
+
212
+ # Prepare app-specific context data
213
+ context_data = {
214
+ mailer: self.class.to_s,
215
+ action: action_name&.to_s,
216
+ recipients: recipients(error)
217
+ }
218
+ MetadataCollection.add_context_metadata(self, context_data)
219
+
220
+ # Extract email fields
221
+ to = mailer_message&.to
222
+ from = mailer_message&.from&.first
223
+ subject = mailer_message&.subject
224
+ message_id = extract_message_id_from_mailer(self)
225
+
226
+ # Create ActionMailer-specific error struct
227
+ exception_data = Log::ActionMailer::Error.new(
228
+ to: to,
229
+ from: from,
230
+ subject: subject,
231
+ message_id: message_id,
232
+ mailer_class: self.class.to_s,
233
+ mailer_action: respond_to?(:action_name) ? action_name&.to_s : nil,
234
+ attachment_count: message_data[:attachment_count],
235
+ error_class: error.class,
236
+ message: error.message,
237
+ backtrace: error.backtrace,
238
+ additional_data: context_data.presence,
239
+ timestamp: Time.now,
240
+ level: Level::Info
147
241
  )
148
242
 
149
243
  # Log the error at info level since it's not a critical error
@@ -10,15 +10,27 @@ module LogStruct
10
10
  extend T::Sig
11
11
  extend T::Helpers
12
12
  requires_ancestor { ::ActionMailer::Base }
13
+ requires_ancestor { ErrorHandling }
13
14
 
14
15
  included do
15
- T.bind(self, ActionMailer::Callbacks::ClassMethods)
16
+ T.bind(self, T.class_of(::ActionMailer::Base))
16
17
 
17
18
  # Add callbacks for delivery events
18
19
  before_deliver :log_email_delivery
19
20
  after_deliver :log_email_delivered
20
21
  end
21
22
 
23
+ # When this module is prepended (our integration uses prepend), ensure callbacks are registered
24
+ if respond_to?(:prepended)
25
+ prepended do
26
+ T.bind(self, T.class_of(::ActionMailer::Base))
27
+
28
+ # Add callbacks for delivery events
29
+ before_deliver :log_email_delivery
30
+ after_deliver :log_email_delivered
31
+ end
32
+ end
33
+
22
34
  protected
23
35
 
24
36
  # Log when an email is about to be delivered
@@ -30,6 +42,9 @@ module LogStruct
30
42
  # Log when an email is delivered
31
43
  sig { void }
32
44
  def log_email_delivered
45
+ # Don't log delivered event if the delivery failed (error was handled with log_and_ignore_error)
46
+ return if logstruct_mail_failed
47
+
33
48
  log_mailer_event(Event::Delivered)
34
49
  end
35
50
 
@@ -41,17 +56,14 @@ module LogStruct
41
56
  # Get message (self refers to the mailer instance)
42
57
  mailer_message = message if respond_to?(:message)
43
58
 
44
- # Prepare data for the log entry
45
- data = {
46
- message_id: extract_message_id,
47
- mailer_class: self.class.to_s,
48
- mailer_action: action_name.to_s
49
- }.compact
59
+ # Prepare universal mailer fields
60
+ message_data = {}
61
+ MetadataCollection.add_message_metadata(self, message_data)
50
62
 
51
- # Add any additional metadata
52
- MetadataCollection.add_message_metadata(self, data)
53
- MetadataCollection.add_context_metadata(self, data)
54
- data.merge!(additional_data) if additional_data.present?
63
+ # Prepare app-specific context data for additional_data
64
+ context_data = {}
65
+ MetadataCollection.add_context_metadata(self, context_data)
66
+ context_data.merge!(additional_data) if additional_data.present?
55
67
 
56
68
  # Extract email fields (these will be filtered if email_addresses=true)
57
69
  to = mailer_message&.to
@@ -61,20 +73,24 @@ module LogStruct
61
73
  base_fields = Log::ActionMailer::BaseFields.new(
62
74
  to: to,
63
75
  from: from,
64
- subject: subject
76
+ subject: subject,
77
+ message_id: extract_message_id,
78
+ mailer_class: self.class.to_s,
79
+ mailer_action: action_name.to_s,
80
+ attachment_count: message_data[:attachment_count]
65
81
  )
66
82
 
67
83
  log = case event_type
68
84
  when Event::Delivery
69
85
  Log::ActionMailer::Delivery.new(
70
86
  **base_fields.to_kwargs,
71
- additional_data: data,
87
+ additional_data: context_data.presence,
72
88
  timestamp: Time.now
73
89
  )
74
90
  when Event::Delivered
75
91
  Log::ActionMailer::Delivered.new(
76
92
  **base_fields.to_kwargs,
77
- additional_data: data,
93
+ additional_data: context_data.presence,
78
94
  timestamp: Time.now
79
95
  )
80
96
  else
@@ -12,18 +12,11 @@ module LogStruct
12
12
  def self.add_message_metadata(mailer, log_data)
13
13
  message = mailer.respond_to?(:message) ? mailer.message : nil
14
14
 
15
- # Add recipient count if message is available
16
- if message
17
- # Don't log actual email addresses
18
- log_data[:recipient_count] = [message.to, message.cc, message.bcc].flatten.compact.count
19
-
20
- # Handle case when attachments might be nil
21
- log_data[:has_attachments] = message.attachments&.any? || false
22
- log_data[:attachment_count] = message.attachments&.count || 0
15
+ # Add attachment count if message is available
16
+ log_data[:attachment_count] = if message
17
+ message.attachments&.count || 0
23
18
  else
24
- log_data[:recipient_count] = 0
25
- log_data[:has_attachments] = false
26
- log_data[:attachment_count] = 0
19
+ 0
27
20
  end
28
21
  end
29
22
 
@@ -39,26 +32,27 @@ module LogStruct
39
32
 
40
33
  sig { params(mailer: T.untyped, log_data: T::Hash[Symbol, T.untyped]).void }
41
34
  def self.extract_ids_to_log_data(mailer, log_data)
42
- # Extract account ID if available
43
- if mailer.instance_variable_defined?(:@account)
44
- account = mailer.instance_variable_get(:@account)
45
- log_data[:account_id] = account.id if account.respond_to?(:id)
46
- end
35
+ # Use configured ID mapping from LogStruct configuration
36
+ id_mapping = LogStruct.config.integrations.actionmailer_id_mapping
47
37
 
48
- # Extract user ID if available
49
- return unless mailer.instance_variable_defined?(:@user)
38
+ id_mapping.each do |ivar_name, log_key|
39
+ ivar = :"@#{ivar_name}"
40
+ next unless mailer.instance_variable_defined?(ivar)
50
41
 
51
- user = mailer.instance_variable_get(:@user)
52
- log_data[:user_id] = user.id if user.respond_to?(:id)
42
+ obj = mailer.instance_variable_get(ivar)
43
+ log_data[log_key] = obj.id if obj.respond_to?(:id)
44
+ end
53
45
  end
54
46
 
55
47
  sig { params(log_data: T::Hash[Symbol, T.untyped]).void }
56
48
  def self.add_current_tags_to_log_data(log_data)
57
- # Get current tags from ActiveSupport::TaggedLogging if available
58
- if ::ActiveSupport::TaggedLogging.respond_to?(:current_tags)
59
- tags = T.unsafe(::ActiveSupport::TaggedLogging).current_tags
60
- log_data[:tags] = tags if tags.present?
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] || []
61
54
  end
55
+ log_data[:tags] = tags if tags.present?
62
56
 
63
57
  # Get request_id from ActionDispatch if available
64
58
  if ::ActionDispatch::Request.respond_to?(:current_request_id) &&
@@ -12,7 +12,6 @@ if defined?(::ActionMailer)
12
12
  require_relative "action_mailer/metadata_collection"
13
13
  require_relative "action_mailer/event_logging"
14
14
  require_relative "action_mailer/error_handling"
15
- require_relative "action_mailer/callbacks"
16
15
  end
17
16
 
18
17
  module LogStruct
@@ -37,11 +36,19 @@ module LogStruct
37
36
 
38
37
  # Register our custom observers and handlers
39
38
  # Registering these at the class level means all mailers will use them
40
- ActiveSupport.on_load(:action_mailer) { prepend LogStruct::Integrations::ActionMailer::MetadataCollection }
41
- ActiveSupport.on_load(:action_mailer) { prepend LogStruct::Integrations::ActionMailer::EventLogging }
42
- ActiveSupport.on_load(:action_mailer) { prepend LogStruct::Integrations::ActionMailer::ErrorHandling }
43
- ActiveSupport.on_load(:action_mailer) { prepend LogStruct::Integrations::ActionMailer::Callbacks }
44
- ActiveSupport.on_load(:action_mailer) { LogStruct::Integrations::ActionMailer::Callbacks.patch_message_delivery }
39
+ ActiveSupport.on_load(:action_mailer) do
40
+ prepend LogStruct::Integrations::ActionMailer::EventLogging
41
+ prepend LogStruct::Integrations::ActionMailer::ErrorHandling
42
+ prepend LogStruct::Integrations::ActionMailer::MetadataCollection
43
+ end
44
+
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
45
52
 
46
53
  true
47
54
  end
@@ -81,7 +81,7 @@ module LogStruct
81
81
  Log::ActiveJob::BaseFields.new(
82
82
  job_id: job.job_id,
83
83
  job_class: job.class.to_s,
84
- queue_name: job.queue_name,
84
+ queue_name: job.queue_name&.to_sym,
85
85
  executions: job.executions,
86
86
  provider_job_id: job.provider_job_id,
87
87
  arguments: ((job.class.respond_to?(:log_arguments?) && job.class.log_arguments?) ? job.arguments : nil)
@@ -98,7 +98,7 @@ module LogStruct
98
98
  logger.error(log_data)
99
99
  end
100
100
 
101
- sig { returns(::ActiveSupport::Logger) }
101
+ sig { returns(T.untyped) }
102
102
  def logger
103
103
  ::ActiveJob::Base.logger
104
104
  end
@@ -71,7 +71,7 @@ module LogStruct
71
71
  log_data = case event_type
72
72
  when Event::Upload
73
73
  Log::ActiveStorage::Upload.new(
74
- storage: service_name.to_s,
74
+ storage: service_name.to_sym,
75
75
  file_id: event.payload[:key]&.to_s,
76
76
  checksum: event.payload[:checksum]&.to_s,
77
77
  duration_ms: duration_ms,
@@ -82,7 +82,7 @@ module LogStruct
82
82
  )
83
83
  when Event::Download
84
84
  Log::ActiveStorage::Download.new(
85
- storage: service_name.to_s,
85
+ storage: service_name.to_sym,
86
86
  file_id: event.payload[:key]&.to_s,
87
87
  filename: event.payload[:filename],
88
88
  range: event.payload[:range],
@@ -90,36 +90,36 @@ module LogStruct
90
90
  )
91
91
  when Event::Delete
92
92
  Log::ActiveStorage::Delete.new(
93
- storage: service_name.to_s,
93
+ storage: service_name.to_sym,
94
94
  file_id: event.payload[:key]&.to_s
95
95
  )
96
96
  when Event::Metadata
97
97
  Log::ActiveStorage::Metadata.new(
98
- storage: service_name.to_s,
98
+ storage: service_name.to_sym,
99
99
  file_id: event.payload[:key]&.to_s,
100
100
  metadata: event.payload[:metadata]
101
101
  )
102
102
  when Event::Exist
103
103
  Log::ActiveStorage::Exist.new(
104
- storage: service_name.to_s,
104
+ storage: service_name.to_sym,
105
105
  file_id: event.payload[:key]&.to_s,
106
106
  exist: event.payload[:exist]
107
107
  )
108
108
  when Event::Stream
109
109
  Log::ActiveStorage::Stream.new(
110
- storage: service_name.to_s,
110
+ storage: service_name.to_sym,
111
111
  file_id: event.payload[:key]&.to_s,
112
112
  prefix: event.payload[:prefix]
113
113
  )
114
114
  when Event::Url
115
115
  Log::ActiveStorage::Url.new(
116
- storage: service_name.to_s,
116
+ storage: service_name.to_sym,
117
117
  file_id: event.payload[:key]&.to_s,
118
118
  url: event.payload[:url]
119
119
  )
120
120
  else
121
121
  Log::ActiveStorage::Metadata.new(
122
- storage: service_name.to_s,
122
+ storage: service_name.to_sym,
123
123
  file_id: event.payload[:key]&.to_s,
124
124
  metadata: event.payload[:metadata]
125
125
  )
@@ -17,7 +17,7 @@ module LogStruct
17
17
  extend T::Sig
18
18
 
19
19
  sig { params(name: T.untyped, properties: T.nilable(T::Hash[T.untyped, T.untyped]), options: T.untyped).returns(T.untyped) }
20
- def track(name, properties = nil, options = nil)
20
+ def track(name, properties = nil, options = {})
21
21
  result = super
22
22
  begin
23
23
  # Emit a lightweight structured log about the analytics event
@@ -32,8 +32,7 @@ module LogStruct
32
32
  properties: T.let(
33
33
  properties && properties.transform_keys { |k| k.to_sym },
34
34
  T.nilable(T::Hash[Symbol, T.untyped])
35
- ),
36
- additional_data: {}
35
+ )
37
36
  )
38
37
  )
39
38
  rescue => e