logstruct 0.1.2 → 0.1.4

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -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/rack_error_handler/middleware.rb +54 -16
  21. data/lib/log_struct/integrations/rack_error_handler.rb +3 -3
  22. data/lib/log_struct/integrations/shrine.rb +21 -24
  23. data/lib/log_struct/integrations/sidekiq/logger.rb +8 -1
  24. data/lib/log_struct/integrations.rb +25 -5
  25. data/lib/log_struct/log/action_mailer/delivered.rb +14 -49
  26. data/lib/log_struct/log/action_mailer/delivery.rb +14 -49
  27. data/lib/log_struct/log/action_mailer/error.rb +72 -0
  28. data/lib/log_struct/log/action_mailer.rb +15 -2
  29. data/lib/log_struct/log/active_job/enqueue.rb +9 -73
  30. data/lib/log_struct/log/active_job/finish.rb +9 -76
  31. data/lib/log_struct/log/active_job/schedule.rb +9 -73
  32. data/lib/log_struct/log/active_job/start.rb +9 -76
  33. data/lib/log_struct/log/active_job.rb +2 -2
  34. data/lib/log_struct/log/active_model_serializers.rb +5 -45
  35. data/lib/log_struct/log/active_storage/delete.rb +8 -46
  36. data/lib/log_struct/log/active_storage/download.rb +9 -55
  37. data/lib/log_struct/log/active_storage/exist.rb +9 -49
  38. data/lib/log_struct/log/active_storage/metadata.rb +9 -49
  39. data/lib/log_struct/log/active_storage/stream.rb +9 -49
  40. data/lib/log_struct/log/active_storage/upload.rb +9 -64
  41. data/lib/log_struct/log/active_storage/url.rb +9 -49
  42. data/lib/log_struct/log/active_storage.rb +2 -2
  43. data/lib/log_struct/log/ahoy.rb +5 -43
  44. data/lib/log_struct/log/carrierwave/delete.rb +15 -69
  45. data/lib/log_struct/log/carrierwave/download.rb +15 -77
  46. data/lib/log_struct/log/carrierwave/upload.rb +15 -83
  47. data/lib/log_struct/log/carrierwave.rb +13 -4
  48. data/lib/log_struct/log/dotenv/load.rb +5 -33
  49. data/lib/log_struct/log/dotenv/restore.rb +5 -33
  50. data/lib/log_struct/log/dotenv/save.rb +5 -33
  51. data/lib/log_struct/log/dotenv/update.rb +5 -33
  52. data/lib/log_struct/log/error.rb +7 -40
  53. data/lib/log_struct/log/good_job/enqueue.rb +9 -72
  54. data/lib/log_struct/log/good_job/error.rb +9 -89
  55. data/lib/log_struct/log/good_job/finish.rb +9 -78
  56. data/lib/log_struct/log/good_job/log.rb +11 -75
  57. data/lib/log_struct/log/good_job/schedule.rb +7 -78
  58. data/lib/log_struct/log/good_job/start.rb +7 -78
  59. data/lib/log_struct/log/good_job.rb +2 -2
  60. data/lib/log_struct/log/plain.rb +5 -32
  61. data/lib/log_struct/log/puma/shutdown.rb +5 -32
  62. data/lib/log_struct/log/puma/start.rb +5 -56
  63. data/lib/log_struct/log/request.rb +7 -90
  64. data/lib/log_struct/log/security/blocked_host.rb +12 -73
  65. data/lib/log_struct/log/security/csrf_violation.rb +6 -67
  66. data/lib/log_struct/log/security/ip_spoof.rb +6 -73
  67. data/lib/log_struct/log/shrine/delete.rb +6 -41
  68. data/lib/log_struct/log/shrine/download.rb +6 -44
  69. data/lib/log_struct/log/shrine/exist.rb +6 -44
  70. data/lib/log_struct/log/shrine/metadata.rb +8 -46
  71. data/lib/log_struct/log/shrine/upload.rb +6 -53
  72. data/lib/log_struct/log/sidekiq.rb +5 -42
  73. data/lib/log_struct/log/sql.rb +5 -65
  74. data/lib/log_struct/log.rb +2 -2
  75. data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +12 -1
  76. data/lib/log_struct/railtie.rb +11 -24
  77. data/lib/log_struct/semantic_logger/concerns/log_methods.rb +100 -0
  78. data/lib/log_struct/semantic_logger/logger.rb +46 -15
  79. data/lib/log_struct/semantic_logger/setup.rb +11 -7
  80. data/lib/log_struct/shared/{shared/add_request_fields.rb → add_request_fields.rb} +2 -2
  81. data/lib/log_struct/shared/{shared/merge_additional_data_fields.rb → merge_additional_data_fields.rb} +1 -1
  82. data/lib/log_struct/shared/{shared/serialize_common.rb → serialize_common.rb} +9 -3
  83. data/lib/log_struct/{log/shared → shared}/serialize_common_public.rb +2 -2
  84. data/lib/log_struct/version.rb +1 -1
  85. data/lib/log_struct.rb +4 -1
  86. data/logstruct.gemspec +1 -1
  87. metadata +9 -11
  88. data/lib/log_struct/integrations/action_mailer/callbacks.rb +0 -100
  89. data/lib/log_struct/log/shared/add_request_fields.rb +0 -4
  90. data/lib/log_struct/log/shared/merge_additional_data_fields.rb +0 -4
  91. data/lib/log_struct/log/shared/serialize_common.rb +0 -4
@@ -53,7 +53,7 @@ module LogStruct
53
53
 
54
54
  # Log the store operation with structured data
55
55
  log_data = Log::CarrierWave::Upload.new(
56
- storage: storage.class.name,
56
+ storage: storage.class.name.split("::").last.downcase.to_sym,
57
57
  file_id: identifier,
58
58
  filename: file.filename,
59
59
  mime_type: file.content_type,
@@ -62,11 +62,9 @@ module LogStruct
62
62
  uploader: self.class.name,
63
63
  model: model.class.name,
64
64
  mount_point: mounted_as.to_s,
65
- additional_data: {
66
- version: version_name.to_s,
67
- store_path: store_path,
68
- extension: file.extension
69
- }
65
+ version: version_name.to_s,
66
+ store_path: store_path,
67
+ extension: file.extension
70
68
  )
71
69
 
72
70
  ::Rails.logger.info(log_data)
@@ -85,7 +83,7 @@ module LogStruct
85
83
 
86
84
  # Log the retrieve operation with structured data
87
85
  log_data = Log::CarrierWave::Download.new(
88
- storage: storage.class.name,
86
+ storage: storage.class.name.split("::").last.downcase.to_sym,
89
87
  file_id: identifier,
90
88
  filename: file&.filename,
91
89
  mime_type: file&.content_type,
@@ -94,9 +92,9 @@ module LogStruct
94
92
  uploader: self.class.name,
95
93
  model: model.class.name,
96
94
  mount_point: mounted_as.to_s,
97
- additional_data: {
98
- version: version_name.to_s
99
- }
95
+ version: version_name.to_s,
96
+ store_path: store_path,
97
+ extension: file&.extension
100
98
  )
101
99
 
102
100
  ::Rails.logger.info(log_data)
@@ -49,7 +49,7 @@ module LogStruct
49
49
  **base_fields.to_kwargs,
50
50
  scheduled_at: (job&.scheduled_at ? Time.at(job.scheduled_at.to_i) : nil),
51
51
  duration_ms: event.duration.to_f,
52
- additional_data: {enqueue_caller: job&.enqueue_caller_location},
52
+ enqueue_caller: job&.enqueue_caller_location,
53
53
  timestamp: ts
54
54
  ))
55
55
  end
@@ -91,7 +91,7 @@ module LogStruct
91
91
  finished_at: end_ts,
92
92
  process_id: ::Process.pid,
93
93
  thread_id: Thread.current.object_id.to_s(36),
94
- additional_data: {result: payload[:result]},
94
+ result: payload[:result]&.to_s,
95
95
  timestamp: start_ts
96
96
  ))
97
97
  end
@@ -109,9 +109,9 @@ module LogStruct
109
109
  logger.error(Log::GoodJob::Error.new(
110
110
  **base_fields.to_kwargs,
111
111
  exception_executions: execution&.exception_executions,
112
- err_class: exception&.class&.name,
112
+ error_class: exception&.class&.name,
113
113
  error_message: exception&.message,
114
- backtrace: exception&.backtrace&.first(20),
114
+ backtrace: exception&.backtrace,
115
115
  duration_ms: event.duration.to_f,
116
116
  process_id: ::Process.pid,
117
117
  thread_id: Thread.current.object_id.to_s(36),
@@ -146,7 +146,7 @@ module LogStruct
146
146
  Log::GoodJob::BaseFields.new(
147
147
  job_id: job&.job_id,
148
148
  job_class: job&.job_class,
149
- queue_name: job&.queue_name,
149
+ queue_name: job&.queue_name&.to_sym,
150
150
  arguments: job&.arguments,
151
151
  executions: execution&.executions
152
152
  )
@@ -46,11 +46,6 @@ module LogStruct
46
46
  end
47
47
  end
48
48
 
49
- # Emit a GoodJob::Log event with context and extra fields as additional_data
50
- extras = {}
51
- extras[:scheduled_at] = job_context[:scheduled_at] if job_context.key?(:scheduled_at)
52
- extras[:priority] = job_context[:priority] if job_context.key?(:priority)
53
-
54
49
  log_struct = Log::GoodJob::Log.new(
55
50
  message: message || (block ? block.call : ""),
56
51
  process_id: ::Process.pid,
@@ -59,7 +54,8 @@ module LogStruct
59
54
  job_class: job_context[:job_class],
60
55
  queue_name: job_context[:queue_name],
61
56
  executions: job_context[:executions],
62
- additional_data: extras
57
+ scheduled_at: job_context[:scheduled_at],
58
+ priority: job_context[:priority]
63
59
  )
64
60
 
65
61
  super(log_struct, payload, &nil)
@@ -82,7 +82,7 @@ module LogStruct
82
82
  if goodjob_module.respond_to?(:on_thread_error=)
83
83
  goodjob_module.on_thread_error = ->(exception) do
84
84
  log_entry = LogStruct::Log::GoodJob::Error.new(
85
- err_class: exception.class.name,
85
+ error_class: exception.class.name,
86
86
  error_message: exception.message,
87
87
  backtrace: exception.backtrace,
88
88
  process_id: ::Process.pid,
@@ -3,6 +3,7 @@
3
3
 
4
4
  require "action_dispatch/middleware/host_authorization"
5
5
  require_relative "../enums/event"
6
+ require_relative "../log/security/blocked_host"
6
7
 
7
8
  module LogStruct
8
9
  module Integrations
@@ -34,23 +35,6 @@ module LogStruct
34
35
  return nil unless config.enabled
35
36
  return nil unless config.integrations.enable_host_authorization
36
37
 
37
- # In test environment, ensure HostAuthorization does not block requests
38
- # from the default integration test hosts. Allow all hosts explicitly.
39
- if ::Rails.env.test? && ::Rails.application.config.respond_to?(:hosts)
40
- begin
41
- ::Rails.application.config.hosts << /.*\z/
42
- rescue
43
- # best-effort; ignore if hosts not configurable
44
- end
45
- # Additionally, exclude all requests from HostAuthorization in test
46
- begin
47
- ::Rails.application.config.host_authorization ||= {}
48
- ::Rails.application.config.host_authorization[:exclude] = ->(_request) { true }
49
- rescue
50
- # best-effort
51
- end
52
- end
53
-
54
38
  # Define the response app as a separate variable to fix block alignment
55
39
  response_app = lambda do |env|
56
40
  request = ::ActionDispatch::Request.new(env)
@@ -58,32 +42,39 @@ module LogStruct
58
42
  # This can be helpful later when reviewing logs.
59
43
  blocked_hosts = env["action_dispatch.blocked_hosts"]
60
44
 
61
- # Create a security error to be handled
62
- blocked_host_error = ::ActionController::BadRequest.new(
63
- "Blocked host detected: #{request.host}"
64
- )
45
+ # Build allowed_hosts array
46
+ allowed_hosts_array = T.let(nil, T.nilable(T::Array[String]))
47
+ if blocked_hosts.respond_to?(:allowed_hosts)
48
+ allowed_hosts_array = blocked_hosts.allowed_hosts
49
+ end
50
+
51
+ # Get allow_ip_hosts value
52
+ allow_ip_hosts_value = T.let(nil, T.nilable(T::Boolean))
53
+ if blocked_hosts.respond_to?(:allow_ip_hosts)
54
+ allow_ip_hosts_value = blocked_hosts.allow_ip_hosts
55
+ end
65
56
 
66
- # Create request context hash
67
- context = {
57
+ # Create structured log entry for blocked host
58
+ log_entry = LogStruct::Log::Security::BlockedHost.new(
59
+ message: "Blocked host detected: #{request.host}",
68
60
  blocked_host: request.host,
69
- client_ip: request.ip,
70
- x_forwarded_for: request.x_forwarded_for,
71
- http_method: request.method,
72
61
  path: request.path,
62
+ http_method: request.method,
63
+ source_ip: request.ip,
73
64
  user_agent: request.user_agent,
74
- allowed_hosts: blocked_hosts.allowed_hosts,
75
- allow_ip_hosts: blocked_hosts.allow_ip_hosts
76
- }
77
-
78
- # Handle error according to configured mode (log, report, raise)
79
- LogStruct.handle_exception(
80
- blocked_host_error,
81
- source: Source::Security,
82
- context: context
65
+ referer: request.referer,
66
+ request_id: request.request_id,
67
+ x_forwarded_for: request.x_forwarded_for,
68
+ allowed_hosts: allowed_hosts_array&.empty? ? nil : allowed_hosts_array,
69
+ allow_ip_hosts: allow_ip_hosts_value
83
70
  )
84
71
 
72
+ # Log the blocked host
73
+ LogStruct.warn(log_entry)
74
+
85
75
  # Use pre-defined headers and response if we are only logging or reporting
86
- [FORBIDDEN_STATUS, RESPONSE_HEADERS, [RESPONSE_HTML]]
76
+ # Dup the headers so they can be modified by downstream middleware
77
+ [FORBIDDEN_STATUS, RESPONSE_HEADERS.dup, [RESPONSE_HTML]]
87
78
  end
88
79
 
89
80
  # Merge our response_app into existing host_authorization config to preserve excludes
@@ -50,7 +50,7 @@ module LogStruct
50
50
  Log::Request.new(
51
51
  http_method: data[:method]&.to_s,
52
52
  path: data[:path]&.to_s,
53
- format: data[:format]&.to_s,
53
+ format: data[:format]&.to_sym,
54
54
  controller: data[:controller]&.to_s,
55
55
  action: data[:action]&.to_s,
56
56
  status: status,
@@ -55,8 +55,14 @@ module LogStruct
55
55
  def call(env)
56
56
  return @app.call(env) unless LogStruct.enabled?
57
57
 
58
- # Try to process the request
58
+ request = ::ActionDispatch::Request.new(env)
59
+
59
60
  begin
61
+ # Trigger the same spoofing checks that ActionDispatch::RemoteIp performs after
62
+ # it is initialized in the middleware stack. We run this manually because we
63
+ # execute before that middleware and still want spoofing attacks to surface here.
64
+ perform_remote_ip_check!(request)
65
+
60
66
  @app.call(env)
61
67
  rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError => ip_spoof_error
62
68
  # Create a security log for IP spoofing
@@ -65,7 +71,7 @@ module LogStruct
65
71
  http_method: env["REQUEST_METHOD"],
66
72
  user_agent: env["HTTP_USER_AGENT"],
67
73
  referer: env["HTTP_REFERER"],
68
- request_id: env["action_dispatch.request_id"],
74
+ request_id: request.request_id,
69
75
  message: ip_spoof_error.message,
70
76
  client_ip: env["HTTP_CLIENT_IP"],
71
77
  x_forwarded_for: env["HTTP_X_FORWARDED_FOR"],
@@ -74,16 +80,9 @@ module LogStruct
74
80
 
75
81
  ::Rails.logger.warn(security_log)
76
82
 
77
- # Report the error
78
- context = extract_request_context(env)
79
- LogStruct.handle_exception(ip_spoof_error, source: Source::Security, context: context)
80
-
81
- # If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
82
- # If we are only logging or reporting these security errors, then return a default response
83
- [FORBIDDEN_STATUS, IP_SPOOF_HEADERS, [IP_SPOOF_HTML]]
83
+ [FORBIDDEN_STATUS, IP_SPOOF_HEADERS.dup, [IP_SPOOF_HTML]]
84
84
  rescue ::ActionController::InvalidAuthenticityToken => invalid_auth_token_error
85
85
  # Create a security log for CSRF error
86
- request = ::ActionDispatch::Request.new(env)
87
86
  security_log = Log::Security::CSRFViolation.new(
88
87
  path: request.path,
89
88
  http_method: request.method,
@@ -97,15 +96,15 @@ module LogStruct
97
96
  LogStruct.error(security_log)
98
97
 
99
98
  # Report to error reporting service and/or re-raise
100
- context = extract_request_context(env)
99
+ context = extract_request_context(env, request)
101
100
  LogStruct.handle_exception(invalid_auth_token_error, source: Source::Security, context: context)
102
101
 
103
102
  # If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
104
103
  # If we are only logging or reporting these security errors, then return a default response
105
- [FORBIDDEN_STATUS, CSRF_HEADERS, [CSRF_HTML]]
104
+ [FORBIDDEN_STATUS, CSRF_HEADERS.dup, [CSRF_HTML]]
106
105
  rescue => error
107
106
  # Extract request context for error reporting
108
- context = extract_request_context(env)
107
+ context = extract_request_context(env, request)
109
108
 
110
109
  # Create and log a structured exception with request context
111
110
  exception_log = Log.from_exception(Source::Rails, error, context)
@@ -119,9 +118,22 @@ module LogStruct
119
118
 
120
119
  private
121
120
 
122
- sig { params(env: T::Hash[String, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
123
- def extract_request_context(env)
124
- request = ::ActionDispatch::Request.new(env)
121
+ sig { params(request: ::ActionDispatch::Request).void }
122
+ def perform_remote_ip_check!(request)
123
+ action_dispatch_config = ::Rails.application.config.action_dispatch
124
+ check_ip = action_dispatch_config.ip_spoofing_check
125
+ return unless check_ip
126
+
127
+ proxies = normalized_trusted_proxies(action_dispatch_config.trusted_proxies)
128
+
129
+ ::ActionDispatch::RemoteIp::GetIp
130
+ .new(request, check_ip, proxies)
131
+ .to_s
132
+ end
133
+
134
+ sig { params(env: T::Hash[String, T.untyped], request: T.nilable(::ActionDispatch::Request)).returns(T::Hash[Symbol, T.untyped]) }
135
+ def extract_request_context(env, request = nil)
136
+ request ||= ::ActionDispatch::Request.new(env)
125
137
  {
126
138
  request_id: request.request_id,
127
139
  path: request.path,
@@ -133,6 +145,32 @@ module LogStruct
133
145
  # If we can't extract request context, return minimal info
134
146
  {error_extracting_context: error.message}
135
147
  end
148
+
149
+ sig { params(configured_proxies: T.untyped).returns(T.untyped) }
150
+ def normalized_trusted_proxies(configured_proxies)
151
+ if configured_proxies.nil? || (configured_proxies.respond_to?(:empty?) && configured_proxies.empty?)
152
+ return ::ActionDispatch::RemoteIp::TRUSTED_PROXIES
153
+ end
154
+
155
+ return configured_proxies if configured_proxies.respond_to?(:any?)
156
+
157
+ raise(
158
+ ArgumentError,
159
+ <<~EOM
160
+ Setting config.action_dispatch.trusted_proxies to a single value isn't
161
+ supported. Please set this to an enumerable instead. For
162
+ example, instead of:
163
+
164
+ config.action_dispatch.trusted_proxies = IPAddr.new("10.0.0.0/8")
165
+
166
+ Wrap the value in an Array:
167
+
168
+ config.action_dispatch.trusted_proxies = [IPAddr.new("10.0.0.0/8")]
169
+
170
+ Note that passing an enumerable will *replace* the default set of trusted proxies.
171
+ EOM
172
+ )
173
+ end
136
174
  end
137
175
  end
138
176
  end
@@ -19,9 +19,9 @@ module LogStruct
19
19
  return nil unless config.integrations.enable_rack_error_handler
20
20
 
21
21
  # Add structured logging middleware for security violations and errors
22
- # Need to insert after ShowExceptions to catch IP spoofing errors
23
- ::Rails.application.middleware.insert_after(
24
- ::ActionDispatch::ShowExceptions,
22
+ # Need to insert before RemoteIp to catch IP spoofing errors it raises
23
+ ::Rails.application.middleware.insert_before(
24
+ ::ActionDispatch::RemoteIp,
25
25
  Integrations::RackErrorHandler::Middleware
26
26
  )
27
27
 
@@ -37,50 +37,47 @@ module LogStruct
37
37
  end
38
38
 
39
39
  # Create structured log data
40
+ # Ensure storage is always a symbol
41
+ storage_sym = payload[:storage].to_sym
42
+
40
43
  log_data = case event_type
41
44
  when Event::Upload
42
45
  Log::Shrine::Upload.new(
43
- storage: payload[:storage],
46
+ storage: storage_sym,
44
47
  location: payload[:location],
45
- uploader: payload[:uploader],
48
+ uploader: payload[:uploader]&.to_s,
46
49
  upload_options: payload[:upload_options],
47
50
  options: payload[:options],
48
- duration_ms: event.duration,
49
- additional_data: payload.except(:storage, :location, :uploader, :upload_options, :options)
51
+ duration_ms: event.duration.to_f
50
52
  )
51
53
  when Event::Download
52
54
  Log::Shrine::Download.new(
53
- storage: payload[:storage],
55
+ storage: storage_sym,
54
56
  location: payload[:location],
55
- download_options: payload[:download_options],
56
- additional_data: payload.except(:storage, :location, :download_options)
57
+ download_options: payload[:download_options]
57
58
  )
58
59
  when Event::Delete
59
60
  Log::Shrine::Delete.new(
60
- storage: payload[:storage],
61
- location: payload[:location],
62
- additional_data: payload.except(:storage, :location)
61
+ storage: storage_sym,
62
+ location: payload[:location]
63
63
  )
64
64
  when Event::Metadata
65
- Log::Shrine::Metadata.new(
66
- storage: payload[:storage],
67
- location: payload[:location],
68
- metadata: payload[:metadata],
69
- additional_data: payload.except(:storage, :location, :metadata)
70
- )
65
+ metadata_params = {
66
+ storage: storage_sym,
67
+ metadata: payload[:metadata]
68
+ }
69
+ metadata_params[:location] = payload[:location] if payload[:location]
70
+ Log::Shrine::Metadata.new(**metadata_params)
71
71
  when Event::Exist
72
72
  Log::Shrine::Exist.new(
73
- storage: payload[:storage],
73
+ storage: storage_sym,
74
74
  location: payload[:location],
75
- exist: payload[:exist],
76
- additional_data: payload.except(:storage, :location, :exist)
75
+ exist: payload[:exist]
77
76
  )
78
77
  else
79
- Log::Shrine::Metadata.new(
80
- storage: payload[:storage],
81
- location: payload[:location],
82
- metadata: payload[:metadata]
83
- )
78
+ unknown_params = {storage: storage_sym, metadata: payload[:metadata]}
79
+ unknown_params[:location] = payload[:location] if payload[:location]
80
+ Log::Shrine::Metadata.new(**unknown_params)
84
81
  end
85
82
 
86
83
  # Pass the structured hash to the logger
@@ -23,11 +23,18 @@ module LogStruct
23
23
  # Override log methods to create Sidekiq log structs
24
24
  %i[debug info warn error fatal].each do |level|
25
25
  define_method(level) do |message = nil, payload = nil, &block|
26
+ # Ensure message is a String (Sidekiq may pass Hash for config logs)
27
+ msg = if message.nil?
28
+ block ? block.call.to_s : ""
29
+ else
30
+ message.to_s
31
+ end
32
+
26
33
  # Create a Sidekiq log struct with the message
27
34
  log_struct = Log::Sidekiq.new(
28
35
  level: LogStruct::Level.from_severity(level.to_s.upcase),
29
36
  event: Event::Log,
30
- message: message || (block ? block.call : ""),
37
+ message: msg,
31
38
  process_id: ::Process.pid,
32
39
  thread_id: Sidekiq.tid,
33
40
  context: ::Sidekiq::Context.current || {}
@@ -38,11 +38,25 @@ module LogStruct
38
38
  end
39
39
  end
40
40
 
41
- sig { void }
42
- def self.setup_integrations
41
+ sig { params(stage: Symbol).void }
42
+ def self.setup_integrations(stage: :all)
43
43
  config = LogStruct.config
44
44
 
45
- # Set up each integration with consistent configuration pattern
45
+ case stage
46
+ when :non_middleware
47
+ setup_non_middleware_integrations(config)
48
+ when :middleware
49
+ setup_middleware_integrations(config)
50
+ when :all
51
+ setup_non_middleware_integrations(config)
52
+ setup_middleware_integrations(config)
53
+ else
54
+ raise ArgumentError, "Unknown integration stage: #{stage}"
55
+ end
56
+ end
57
+
58
+ sig { params(config: LogStruct::Configuration).void }
59
+ def self.setup_non_middleware_integrations(config)
46
60
  Integrations::Lograge.setup(config) if config.integrations.enable_lograge
47
61
  Integrations::ActionMailer.setup(config) if config.integrations.enable_actionmailer
48
62
  Integrations::ActiveJob.setup(config) if config.integrations.enable_activejob
@@ -51,8 +65,6 @@ module LogStruct
51
65
  Integrations::GoodJob.setup(config) if config.integrations.enable_goodjob
52
66
  Integrations::Ahoy.setup(config) if config.integrations.enable_ahoy
53
67
  Integrations::ActiveModelSerializers.setup(config) if config.integrations.enable_active_model_serializers
54
- Integrations::HostAuthorization.setup(config) if config.integrations.enable_host_authorization
55
- Integrations::RackErrorHandler.setup(config) if config.integrations.enable_rack_error_handler
56
68
  Integrations::Shrine.setup(config) if config.integrations.enable_shrine
57
69
  Integrations::ActiveStorage.setup(config) if config.integrations.enable_activestorage
58
70
  Integrations::CarrierWave.setup(config) if config.integrations.enable_carrierwave
@@ -62,5 +74,13 @@ module LogStruct
62
74
  end
63
75
  Integrations::Puma.setup(config) if config.integrations.enable_puma
64
76
  end
77
+
78
+ sig { params(config: LogStruct::Configuration).void }
79
+ def self.setup_middleware_integrations(config)
80
+ Integrations::HostAuthorization.setup(config) if config.integrations.enable_host_authorization
81
+ Integrations::RackErrorHandler.setup(config) if config.integrations.enable_rack_error_handler
82
+ end
83
+
84
+ private_class_method :setup_non_middleware_integrations, :setup_middleware_integrations
65
85
  end
66
86
  end
@@ -9,9 +9,9 @@
9
9
  require "log_struct/shared/interfaces/common_fields"
10
10
  require "log_struct/shared/interfaces/additional_data_field"
11
11
  require "log_struct/shared/interfaces/request_fields"
12
- require "log_struct/shared/shared/serialize_common"
13
- require "log_struct/shared/shared/merge_additional_data_fields"
14
- require "log_struct/shared/shared/add_request_fields"
12
+ require "log_struct/shared/serialize_common"
13
+ require "log_struct/shared/merge_additional_data_fields"
14
+ require "log_struct/shared/add_request_fields"
15
15
  require_relative "../../enums/source"
16
16
  require_relative "../../enums/event"
17
17
  require_relative "../../enums/level"
@@ -21,11 +21,6 @@ module LogStruct
21
21
  module Log
22
22
  class ActionMailer
23
23
  class Delivered < T::Struct
24
- # typed: strict
25
- # frozen_string_literal: true
26
-
27
- extend T::Sig
28
-
29
24
  extend T::Sig
30
25
 
31
26
  # Shared/common fields
@@ -37,62 +32,32 @@ module LogStruct
37
32
  const :to, T.nilable(T::Array[String]), default: nil
38
33
  const :from, T.nilable(String), default: nil
39
34
  const :subject, T.nilable(String), default: nil
40
-
41
- # Event-specific fields
35
+ const :message_id, T.nilable(String), default: nil
36
+ const :mailer_class, T.nilable(String), default: nil
37
+ const :mailer_action, T.nilable(String), default: nil
38
+ const :attachment_count, T.nilable(Integer), default: nil
42
39
 
43
40
  # Additional data
44
41
  include LogStruct::Log::Interfaces::AdditionalDataField
45
42
  const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
46
43
  include LogStruct::Log::Shared::MergeAdditionalDataFields
47
44
 
48
- # Request fields (optional)
49
-
50
45
  # Serialize shared fields
51
46
  include LogStruct::Log::Interfaces::CommonFields
52
47
  include LogStruct::Log::Shared::SerializeCommon
53
48
 
54
- sig {
55
- params(to: T.untyped,
56
- from: T.untyped,
57
- subject: T.untyped).returns(T::Hash[LogStruct::LogField, T.untyped])
58
- }
59
- def self.base_hash(to: nil,
60
- from: nil,
61
- subject: nil)
62
- h = {}
49
+ sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
50
+ def to_h
51
+ h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
63
52
  h[LogField::To] = to unless to.nil?
64
53
  h[LogField::From] = from unless from.nil?
65
54
  h[LogField::Subject] = subject unless subject.nil?
55
+ h[LogField::MessageId] = message_id unless message_id.nil?
56
+ h[LogField::MailerClass] = mailer_class unless mailer_class.nil?
57
+ h[LogField::MailerAction] = mailer_action unless mailer_action.nil?
58
+ h[LogField::AttachmentCount] = attachment_count unless attachment_count.nil?
66
59
  h
67
60
  end
68
-
69
- sig {
70
- params(to: T.untyped,
71
- from: T.untyped,
72
- subject: T.untyped,
73
- additional_data: T.untyped,
74
- timestamp: T.untyped).returns(T::Hash[LogStruct::LogField, T.untyped])
75
- }
76
- def self.build(to: nil,
77
- from: nil,
78
- subject: nil,
79
- additional_data: nil,
80
- timestamp: Time.now)
81
- base_hash(to: to,
82
- from: from,
83
- subject: subject)
84
- end
85
-
86
- sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
87
- def to_h
88
- self.class.build(
89
- to: to,
90
- from: from,
91
- subject: subject,
92
- additional_data: additional_data,
93
- timestamp: timestamp
94
- )
95
- end
96
61
  end
97
62
  end
98
63
  end