logstruct 0.1.1 → 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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +12 -7
  4. data/lib/log_struct/concerns/configuration.rb +52 -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/puma.rb +1 -6
  21. data/lib/log_struct/integrations/shrine.rb +21 -24
  22. data/lib/log_struct/integrations/sidekiq/logger.rb +8 -1
  23. data/lib/log_struct/log/action_mailer/delivered.rb +14 -49
  24. data/lib/log_struct/log/action_mailer/delivery.rb +14 -49
  25. data/lib/log_struct/log/action_mailer/error.rb +72 -0
  26. data/lib/log_struct/log/action_mailer.rb +15 -2
  27. data/lib/log_struct/log/active_job/enqueue.rb +9 -73
  28. data/lib/log_struct/log/active_job/finish.rb +9 -76
  29. data/lib/log_struct/log/active_job/schedule.rb +9 -73
  30. data/lib/log_struct/log/active_job/start.rb +9 -76
  31. data/lib/log_struct/log/active_job.rb +2 -2
  32. data/lib/log_struct/log/active_model_serializers.rb +5 -45
  33. data/lib/log_struct/log/active_storage/delete.rb +8 -46
  34. data/lib/log_struct/log/active_storage/download.rb +9 -55
  35. data/lib/log_struct/log/active_storage/exist.rb +9 -49
  36. data/lib/log_struct/log/active_storage/metadata.rb +9 -49
  37. data/lib/log_struct/log/active_storage/stream.rb +9 -49
  38. data/lib/log_struct/log/active_storage/upload.rb +9 -64
  39. data/lib/log_struct/log/active_storage/url.rb +9 -49
  40. data/lib/log_struct/log/active_storage.rb +2 -2
  41. data/lib/log_struct/log/ahoy.rb +5 -43
  42. data/lib/log_struct/log/carrierwave/delete.rb +15 -69
  43. data/lib/log_struct/log/carrierwave/download.rb +15 -77
  44. data/lib/log_struct/log/carrierwave/upload.rb +15 -83
  45. data/lib/log_struct/log/carrierwave.rb +13 -4
  46. data/lib/log_struct/log/dotenv/load.rb +5 -33
  47. data/lib/log_struct/log/dotenv/restore.rb +5 -33
  48. data/lib/log_struct/log/dotenv/save.rb +5 -33
  49. data/lib/log_struct/log/dotenv/update.rb +5 -33
  50. data/lib/log_struct/log/error.rb +7 -40
  51. data/lib/log_struct/log/good_job/enqueue.rb +9 -72
  52. data/lib/log_struct/log/good_job/error.rb +9 -89
  53. data/lib/log_struct/log/good_job/finish.rb +9 -78
  54. data/lib/log_struct/log/good_job/log.rb +11 -75
  55. data/lib/log_struct/log/good_job/schedule.rb +7 -78
  56. data/lib/log_struct/log/good_job/start.rb +7 -78
  57. data/lib/log_struct/log/good_job.rb +2 -2
  58. data/lib/log_struct/log/plain.rb +5 -32
  59. data/lib/log_struct/log/puma/shutdown.rb +5 -32
  60. data/lib/log_struct/log/puma/start.rb +5 -56
  61. data/lib/log_struct/log/request.rb +7 -90
  62. data/lib/log_struct/log/security/blocked_host.rb +12 -73
  63. data/lib/log_struct/log/security/csrf_violation.rb +6 -67
  64. data/lib/log_struct/log/security/ip_spoof.rb +6 -73
  65. data/lib/log_struct/log/shrine/delete.rb +6 -41
  66. data/lib/log_struct/log/shrine/download.rb +6 -44
  67. data/lib/log_struct/log/shrine/exist.rb +6 -44
  68. data/lib/log_struct/log/shrine/metadata.rb +8 -46
  69. data/lib/log_struct/log/shrine/upload.rb +6 -53
  70. data/lib/log_struct/log/sidekiq.rb +5 -42
  71. data/lib/log_struct/log/sql.rb +5 -65
  72. data/lib/log_struct/log.rb +2 -2
  73. data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +12 -1
  74. data/lib/log_struct/rails_boot_banner_silencer.rb +2 -9
  75. data/lib/log_struct/railtie.rb +1 -27
  76. data/lib/log_struct/semantic_logger/concerns/log_methods.rb +100 -0
  77. data/lib/log_struct/semantic_logger/logger.rb +46 -15
  78. data/lib/log_struct/semantic_logger/setup.rb +11 -7
  79. data/lib/log_struct/shared/{shared/add_request_fields.rb → add_request_fields.rb} +2 -2
  80. data/lib/log_struct/shared/{shared/merge_additional_data_fields.rb → merge_additional_data_fields.rb} +1 -1
  81. data/lib/log_struct/shared/{shared/serialize_common.rb → serialize_common.rb} +9 -3
  82. data/lib/log_struct/{log/shared → shared}/serialize_common_public.rb +2 -2
  83. data/lib/log_struct/version.rb +1 -1
  84. data/lib/log_struct.rb +18 -1
  85. data/logstruct.gemspec +1 -1
  86. metadata +9 -11
  87. data/lib/log_struct/integrations/action_mailer/callbacks.rb +0 -100
  88. data/lib/log_struct/log/shared/add_request_fields.rb +0 -4
  89. data/lib/log_struct/log/shared/merge_additional_data_fields.rb +0 -4
  90. 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: 0b926657fc690c72031cf72973642c3654264b3ea348f2212a625788b62d1ade
4
- data.tar.gz: 650b11e3b8b3e975feee474994ae9420b68da2eb933942f28cc495649d71f225
3
+ metadata.gz: 77b4f5cd95c84bd5b418bb95b1856af146e70a64e6d39e3a303e33e083faa64d
4
+ data.tar.gz: 670c515b7eb8fbe5320c3bc52e2a68234aff31f8633fc071286a1b4a816132b6
5
5
  SHA512:
6
- metadata.gz: 2e0339abeafbea5bf239799dcf1ca843f57aae004e356e407a18909d9c0b6e4d1302c897f358be7bf539b5e0534efd032319a0b3055ff29e169c1d3f22557193
7
- data.tar.gz: de385bc25a32f189044fbf1749e4511c1f9411d37a9af8628fc655e9706daba1a6b94b9482de756ef12557cc497deffa6f49a39ab9b434b3be6e25c7cb402214
6
+ metadata.gz: f7b81a7d6893f7db9b51c71d5623beedfa75d81e5c9c1ec51003db37a5873018427e81a86aaa2de3a73741cb372bea2d859980b96695592ee9249ea0def0fe9d
7
+ data.tar.gz: e8fd117ec5795acebba1a996d2777f25f2ac89afdbb4fa76cb18ec1faa5fea11800b9cf452178412eb0fabe77ca4bcc223dd516d53b1217031f7433361bdc41c
data/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ 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
+
19
+ ## [0.1.2] - 2025-10-03
20
+
21
+ Better default policy for when JSON logs are enabled: machines get JSON, humans get readable logs.
22
+ Enable LogStruct for production servers and test runs (both local and CI) to ensure tests catch production bugs.
23
+ Keep dev-friendly logging on local machines or when running interactive commands on production servers.
24
+
8
25
  ## [0.1.1] - 2025-09-29
9
26
 
10
27
  Added dotenv-rails integration. Many other fixes and improvements.
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 in production and test environments
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
@@ -58,11 +57,15 @@ Once initialized (and enabled), the gem automatically includes its modules into
58
57
  - Rails `config.filter_parameters` are merged into LogStruct's filters and then cleared (to avoid double filtering). Configure sensitive keys via `LogStruct.config.filters`.
59
58
  - When `RAILS_LOG_TO_STDOUT` is set, we log to STDOUT only. Otherwise, we log to STDOUT by default without adding a file appender to avoid duplicate logs.
60
59
 
61
- ### Development behavior
60
+ ### Default behavior by process type
62
61
 
63
- - Disabled by default in development. Enable explicitly via `LOGSTRUCT_ENABLED=true` or `LogStruct.configure { |c| c.enabled = true }`.
64
- - When enabled in development, LogStruct now defaults to production‑style JSON output so you can preview exactly what will be shipped in prod.
65
- - You can opt back into the colorful human formatter with:
62
+ - **Server processes** (`rails server`): JSON logging is enabled by default in production and test environments
63
+ - **Test runs** (`rails test`): JSON logging is enabled by default in test environment to ensure tests catch production bugs
64
+ - **Console** (`rails console`): JSON logging is disabled by default in all environments, providing human-readable logs instead
65
+ - **Other Rake tasks** (`rake db:migrate`, etc.): JSON logging is disabled by default in production, providing human-readable logs instead
66
+ - **Development environment**: Disabled by default for all process types. Enable explicitly via `LOGSTRUCT_ENABLED=true` or `LogStruct.configure { |c| c.enabled = true }`.
67
+
68
+ When enabled in development, LogStruct defaults to production‑style JSON output so you can preview exactly what will be shipped in prod. You can opt back into the colorful human formatter with:
66
69
 
67
70
  ```ruby
68
71
  LogStruct.configure do |c|
@@ -71,6 +74,8 @@ LogStruct.configure do |c|
71
74
  end
72
75
  ```
73
76
 
77
+ To force JSON logs in console or other Rake tasks (e.g., for debugging), set `LOGSTRUCT_ENABLED=true` in your environment.
78
+
74
79
  ## Documentation
75
80
 
76
81
  Please see the [documentation](https://logstruct.com/docs) for more details. (All code examples are type-checked and tested, and it's harder to keep a README up to date.)
@@ -10,6 +10,11 @@ module LogStruct
10
10
  module ClassMethods
11
11
  extend T::Sig
12
12
 
13
+ SERVER_COMMAND_ARGS = T.let(["server", "s"].freeze, T::Array[String])
14
+ CONSOLE_COMMAND_ARGS = T.let(["console", "c"].freeze, T::Array[String])
15
+ EMPTY_ARGV = T.let([].freeze, T::Array[String])
16
+ CI_FALSE_VALUES = T.let(["false", "0", "no"].freeze, T::Array[String])
17
+
13
18
  sig { params(block: T.proc.params(config: LogStruct::Configuration).void).void }
14
19
  def configure(&block)
15
20
  yield(config)
@@ -45,14 +50,20 @@ module LogStruct
45
50
  # - Sets enabled=true only when value is "true", "yes", "1", etc.
46
51
  # - Sets enabled=false when value is any other value
47
52
  # 2. Otherwise, check if current Rails environment is in enabled_environments
53
+ # AND one of: Rails::Server is defined, OR test environment with CI=true
54
+ # BUT NOT Rails::Console (to exclude interactive console)
48
55
  # 3. Otherwise, leave as config.enabled (defaults to true)
49
56
 
50
57
  # Then check if LOGSTRUCT_ENABLED env var is set
51
58
  config.enabled = if ENV["LOGSTRUCT_ENABLED"]
52
- # Override to true only if env var is "true"
53
59
  %w[true t yes y 1].include?(ENV["LOGSTRUCT_ENABLED"]&.strip&.downcase)
54
60
  else
55
- config.enabled_environments.include?(::Rails.env.to_sym)
61
+ is_console = console_process?
62
+ is_server = server_process?
63
+ ci_build?
64
+ in_enabled_env = config.enabled_environments.include?(::Rails.env.to_sym)
65
+
66
+ in_enabled_env && !is_console && (is_server || ::Rails.env.test?)
56
67
  end
57
68
  end
58
69
 
@@ -112,6 +123,45 @@ module LogStruct
112
123
 
113
124
  private
114
125
 
126
+ sig { returns(T::Boolean) }
127
+ def console_process?
128
+ return true if defined?(::Rails::Console)
129
+
130
+ current_argv.any? { |arg| CONSOLE_COMMAND_ARGS.include?(arg) }
131
+ end
132
+
133
+ sig { returns(T::Boolean) }
134
+ def server_process?
135
+ return true if logstruct_server_mode?
136
+
137
+ current_argv.any? { |arg| SERVER_COMMAND_ARGS.include?(arg) }
138
+ end
139
+
140
+ sig { returns(T::Boolean) }
141
+ def logstruct_server_mode?
142
+ ::LogStruct.server_mode?
143
+ end
144
+
145
+ sig { returns(T::Array[String]) }
146
+ def current_argv
147
+ raw = ::ARGV
148
+ strings = raw.map { |arg| arg.to_s }
149
+ T.let(strings, T::Array[String])
150
+ rescue NameError
151
+ EMPTY_ARGV
152
+ end
153
+
154
+ sig { returns(T::Boolean) }
155
+ def ci_build?
156
+ value = ENV["CI"]
157
+ return false if value.nil?
158
+
159
+ normalized = value.strip.downcase
160
+ return false if normalized.empty?
161
+
162
+ !CI_FALSE_VALUES.include?(normalized)
163
+ end
164
+
115
165
  sig { params(filter: T.untyped).returns(T.nilable(Symbol)) }
116
166
  def normalize_filter_symbol(filter)
117
167
  return filter if filter.is_a?(Symbol)
@@ -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