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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -1
- data/README.md +4 -6
- data/lib/log_struct/concerns/configuration.rb +2 -2
- data/lib/log_struct/config_struct/integrations.rb +5 -0
- data/lib/log_struct/enums/log_field.rb +12 -1
- data/lib/log_struct/integrations/action_mailer/error_handling.rb +121 -27
- data/lib/log_struct/integrations/action_mailer/event_logging.rb +30 -14
- data/lib/log_struct/integrations/action_mailer/metadata_collection.rb +18 -24
- data/lib/log_struct/integrations/action_mailer.rb +13 -6
- data/lib/log_struct/integrations/active_job/log_subscriber.rb +2 -2
- data/lib/log_struct/integrations/active_storage.rb +8 -8
- data/lib/log_struct/integrations/ahoy.rb +2 -3
- data/lib/log_struct/integrations/carrierwave.rb +8 -10
- data/lib/log_struct/integrations/good_job/log_subscriber.rb +5 -5
- data/lib/log_struct/integrations/good_job/logger.rb +2 -6
- data/lib/log_struct/integrations/good_job.rb +1 -1
- data/lib/log_struct/integrations/host_authorization.rb +27 -36
- data/lib/log_struct/integrations/lograge.rb +1 -1
- data/lib/log_struct/integrations/shrine.rb +21 -24
- data/lib/log_struct/integrations/sidekiq/logger.rb +8 -1
- data/lib/log_struct/log/action_mailer/delivered.rb +14 -49
- data/lib/log_struct/log/action_mailer/delivery.rb +14 -49
- data/lib/log_struct/log/action_mailer/error.rb +72 -0
- data/lib/log_struct/log/action_mailer.rb +15 -2
- data/lib/log_struct/log/active_job/enqueue.rb +9 -73
- data/lib/log_struct/log/active_job/finish.rb +9 -76
- data/lib/log_struct/log/active_job/schedule.rb +9 -73
- data/lib/log_struct/log/active_job/start.rb +9 -76
- data/lib/log_struct/log/active_job.rb +2 -2
- data/lib/log_struct/log/active_model_serializers.rb +5 -45
- data/lib/log_struct/log/active_storage/delete.rb +8 -46
- data/lib/log_struct/log/active_storage/download.rb +9 -55
- data/lib/log_struct/log/active_storage/exist.rb +9 -49
- data/lib/log_struct/log/active_storage/metadata.rb +9 -49
- data/lib/log_struct/log/active_storage/stream.rb +9 -49
- data/lib/log_struct/log/active_storage/upload.rb +9 -64
- data/lib/log_struct/log/active_storage/url.rb +9 -49
- data/lib/log_struct/log/active_storage.rb +2 -2
- data/lib/log_struct/log/ahoy.rb +5 -43
- data/lib/log_struct/log/carrierwave/delete.rb +15 -69
- data/lib/log_struct/log/carrierwave/download.rb +15 -77
- data/lib/log_struct/log/carrierwave/upload.rb +15 -83
- data/lib/log_struct/log/carrierwave.rb +13 -4
- data/lib/log_struct/log/dotenv/load.rb +5 -33
- data/lib/log_struct/log/dotenv/restore.rb +5 -33
- data/lib/log_struct/log/dotenv/save.rb +5 -33
- data/lib/log_struct/log/dotenv/update.rb +5 -33
- data/lib/log_struct/log/error.rb +7 -40
- data/lib/log_struct/log/good_job/enqueue.rb +9 -72
- data/lib/log_struct/log/good_job/error.rb +9 -89
- data/lib/log_struct/log/good_job/finish.rb +9 -78
- data/lib/log_struct/log/good_job/log.rb +11 -75
- data/lib/log_struct/log/good_job/schedule.rb +7 -78
- data/lib/log_struct/log/good_job/start.rb +7 -78
- data/lib/log_struct/log/good_job.rb +2 -2
- data/lib/log_struct/log/plain.rb +5 -32
- data/lib/log_struct/log/puma/shutdown.rb +5 -32
- data/lib/log_struct/log/puma/start.rb +5 -56
- data/lib/log_struct/log/request.rb +7 -90
- data/lib/log_struct/log/security/blocked_host.rb +12 -73
- data/lib/log_struct/log/security/csrf_violation.rb +6 -67
- data/lib/log_struct/log/security/ip_spoof.rb +6 -73
- data/lib/log_struct/log/shrine/delete.rb +6 -41
- data/lib/log_struct/log/shrine/download.rb +6 -44
- data/lib/log_struct/log/shrine/exist.rb +6 -44
- data/lib/log_struct/log/shrine/metadata.rb +8 -46
- data/lib/log_struct/log/shrine/upload.rb +6 -53
- data/lib/log_struct/log/sidekiq.rb +5 -42
- data/lib/log_struct/log/sql.rb +5 -65
- data/lib/log_struct/log.rb +2 -2
- data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +12 -1
- data/lib/log_struct/railtie.rb +0 -22
- data/lib/log_struct/semantic_logger/concerns/log_methods.rb +100 -0
- data/lib/log_struct/semantic_logger/logger.rb +46 -15
- data/lib/log_struct/semantic_logger/setup.rb +11 -7
- data/lib/log_struct/shared/{shared/add_request_fields.rb → add_request_fields.rb} +2 -2
- data/lib/log_struct/shared/{shared/merge_additional_data_fields.rb → merge_additional_data_fields.rb} +1 -1
- data/lib/log_struct/shared/{shared/serialize_common.rb → serialize_common.rb} +9 -3
- data/lib/log_struct/{log/shared → shared}/serialize_common_public.rb +2 -2
- data/lib/log_struct/version.rb +1 -1
- data/lib/log_struct.rb +4 -1
- data/logstruct.gemspec +1 -1
- metadata +9 -11
- data/lib/log_struct/integrations/action_mailer/callbacks.rb +0 -100
- data/lib/log_struct/log/shared/add_request_fields.rb +0 -4
- data/lib/log_struct/log/shared/merge_additional_data_fields.rb +0 -4
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77b4f5cd95c84bd5b418bb95b1856af146e70a64e6d39e3a303e33e083faa64d
|
4
|
+
data.tar.gz: 670c515b7eb8fbe5320c3bc52e2a68234aff31f8633fc071286a1b4a816132b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
-
- **
|
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
|
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
|
-
|
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 ||
|
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
|
-
|
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
|
-
#
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
#
|
78
|
-
|
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
|
-
|
116
|
-
|
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
|
-
#
|
122
|
-
|
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
|
-
#
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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::
|
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
|
45
|
-
|
46
|
-
|
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
|
-
#
|
52
|
-
|
53
|
-
MetadataCollection.add_context_metadata(self,
|
54
|
-
|
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:
|
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:
|
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
|
16
|
-
if message
|
17
|
-
|
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
|
-
|
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
|
-
#
|
43
|
-
|
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
|
-
|
49
|
-
|
38
|
+
id_mapping.each do |ivar_name, log_key|
|
39
|
+
ivar = :"@#{ivar_name}"
|
40
|
+
next unless mailer.instance_variable_defined?(ivar)
|
50
41
|
|
51
|
-
|
52
|
-
|
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
|
58
|
-
if ::ActiveSupport::TaggedLogging.respond_to?(:current_tags)
|
59
|
-
|
60
|
-
|
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)
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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(
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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 =
|
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
|