logstruct 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -1
  3. data/README.md +15 -2
  4. data/lib/log_struct/boot_buffer.rb +28 -0
  5. data/lib/log_struct/builders/active_job.rb +84 -0
  6. data/lib/log_struct/concerns/configuration.rb +126 -13
  7. data/lib/log_struct/concerns/error_handling.rb +3 -7
  8. data/lib/log_struct/config_struct/filters.rb +18 -0
  9. data/lib/log_struct/config_struct/integrations.rb +8 -12
  10. data/lib/log_struct/configuration.rb +13 -0
  11. data/lib/log_struct/enums/event.rb +13 -0
  12. data/lib/log_struct/enums/log_field.rb +154 -0
  13. data/lib/log_struct/enums/source.rb +4 -1
  14. data/lib/log_struct/formatter.rb +29 -17
  15. data/lib/log_struct/integrations/action_mailer/error_handling.rb +3 -11
  16. data/lib/log_struct/integrations/action_mailer/event_logging.rb +22 -12
  17. data/lib/log_struct/integrations/active_job/log_subscriber.rb +52 -48
  18. data/lib/log_struct/integrations/active_model_serializers.rb +8 -14
  19. data/lib/log_struct/integrations/active_record.rb +35 -5
  20. data/lib/log_struct/integrations/active_storage.rb +59 -20
  21. data/lib/log_struct/integrations/ahoy.rb +2 -1
  22. data/lib/log_struct/integrations/carrierwave.rb +13 -16
  23. data/lib/log_struct/integrations/dotenv.rb +278 -0
  24. data/lib/log_struct/integrations/good_job/log_subscriber.rb +86 -136
  25. data/lib/log_struct/integrations/good_job/logger.rb +8 -10
  26. data/lib/log_struct/integrations/good_job.rb +5 -7
  27. data/lib/log_struct/integrations/host_authorization.rb +25 -4
  28. data/lib/log_struct/integrations/lograge.rb +20 -14
  29. data/lib/log_struct/integrations/puma.rb +482 -0
  30. data/lib/log_struct/integrations/rack_error_handler/middleware.rb +11 -18
  31. data/lib/log_struct/integrations/shrine.rb +44 -19
  32. data/lib/log_struct/integrations/sorbet.rb +48 -0
  33. data/lib/log_struct/integrations.rb +21 -0
  34. data/lib/log_struct/log/action_mailer/delivered.rb +99 -0
  35. data/lib/log_struct/log/action_mailer/delivery.rb +99 -0
  36. data/lib/log_struct/log/action_mailer.rb +30 -45
  37. data/lib/log_struct/log/active_job/enqueue.rb +125 -0
  38. data/lib/log_struct/log/active_job/finish.rb +130 -0
  39. data/lib/log_struct/log/active_job/schedule.rb +125 -0
  40. data/lib/log_struct/log/active_job/start.rb +130 -0
  41. data/lib/log_struct/log/active_job.rb +41 -54
  42. data/lib/log_struct/log/active_model_serializers.rb +72 -33
  43. data/lib/log_struct/log/active_storage/delete.rb +87 -0
  44. data/lib/log_struct/log/active_storage/download.rb +103 -0
  45. data/lib/log_struct/log/active_storage/exist.rb +93 -0
  46. data/lib/log_struct/log/active_storage/metadata.rb +93 -0
  47. data/lib/log_struct/log/active_storage/stream.rb +93 -0
  48. data/lib/log_struct/log/active_storage/upload.rb +118 -0
  49. data/lib/log_struct/log/active_storage/url.rb +93 -0
  50. data/lib/log_struct/log/active_storage.rb +32 -68
  51. data/lib/log_struct/log/ahoy.rb +67 -33
  52. data/lib/log_struct/log/carrierwave/delete.rb +115 -0
  53. data/lib/log_struct/log/carrierwave/download.rb +131 -0
  54. data/lib/log_struct/log/carrierwave/upload.rb +141 -0
  55. data/lib/log_struct/log/carrierwave.rb +37 -72
  56. data/lib/log_struct/log/dotenv/load.rb +76 -0
  57. data/lib/log_struct/log/dotenv/restore.rb +76 -0
  58. data/lib/log_struct/log/dotenv/save.rb +76 -0
  59. data/lib/log_struct/log/dotenv/update.rb +76 -0
  60. data/lib/log_struct/log/dotenv.rb +12 -0
  61. data/lib/log_struct/log/error.rb +58 -47
  62. data/lib/log_struct/log/good_job/enqueue.rb +126 -0
  63. data/lib/log_struct/log/good_job/error.rb +151 -0
  64. data/lib/log_struct/log/good_job/finish.rb +136 -0
  65. data/lib/log_struct/log/good_job/log.rb +131 -0
  66. data/lib/log_struct/log/good_job/schedule.rb +136 -0
  67. data/lib/log_struct/log/good_job/start.rb +136 -0
  68. data/lib/log_struct/log/good_job.rb +40 -141
  69. data/lib/log_struct/log/interfaces/additional_data_field.rb +1 -17
  70. data/lib/log_struct/log/interfaces/common_fields.rb +1 -39
  71. data/lib/log_struct/log/interfaces/public_common_fields.rb +1 -28
  72. data/lib/log_struct/log/interfaces/request_fields.rb +1 -33
  73. data/lib/log_struct/log/plain.rb +59 -34
  74. data/lib/log_struct/log/puma/shutdown.rb +80 -0
  75. data/lib/log_struct/log/puma/start.rb +120 -0
  76. data/lib/log_struct/log/puma.rb +10 -0
  77. data/lib/log_struct/log/request.rb +132 -48
  78. data/lib/log_struct/log/security/blocked_host.rb +141 -0
  79. data/lib/log_struct/log/security/csrf_violation.rb +131 -0
  80. data/lib/log_struct/log/security/ip_spoof.rb +141 -0
  81. data/lib/log_struct/log/security.rb +40 -70
  82. data/lib/log_struct/log/shared/add_request_fields.rb +1 -26
  83. data/lib/log_struct/log/shared/merge_additional_data_fields.rb +1 -22
  84. data/lib/log_struct/log/shared/serialize_common.rb +1 -33
  85. data/lib/log_struct/log/shared/serialize_common_public.rb +9 -9
  86. data/lib/log_struct/log/shrine/delete.rb +85 -0
  87. data/lib/log_struct/log/shrine/download.rb +90 -0
  88. data/lib/log_struct/log/shrine/exist.rb +90 -0
  89. data/lib/log_struct/log/shrine/metadata.rb +90 -0
  90. data/lib/log_struct/log/shrine/upload.rb +105 -0
  91. data/lib/log_struct/log/shrine.rb +10 -67
  92. data/lib/log_struct/log/sidekiq.rb +65 -26
  93. data/lib/log_struct/log/sql.rb +113 -106
  94. data/lib/log_struct/log.rb +29 -36
  95. data/lib/log_struct/multi_error_reporter.rb +80 -22
  96. data/lib/log_struct/param_filters.rb +50 -7
  97. data/lib/log_struct/rails_boot_banner_silencer.rb +123 -0
  98. data/lib/log_struct/railtie.rb +71 -0
  99. data/lib/log_struct/semantic_logger/formatter.rb +4 -2
  100. data/lib/log_struct/semantic_logger/setup.rb +34 -18
  101. data/lib/log_struct/shared/interfaces/additional_data_field.rb +22 -0
  102. data/lib/log_struct/shared/interfaces/common_fields.rb +39 -0
  103. data/lib/log_struct/shared/interfaces/public_common_fields.rb +29 -0
  104. data/lib/log_struct/shared/interfaces/request_fields.rb +39 -0
  105. data/lib/log_struct/shared/shared/add_request_fields.rb +28 -0
  106. data/lib/log_struct/shared/shared/merge_additional_data_fields.rb +27 -0
  107. data/lib/log_struct/shared/shared/serialize_common.rb +58 -0
  108. data/lib/log_struct/version.rb +1 -1
  109. data/lib/log_struct.rb +22 -4
  110. data/logstruct.gemspec +2 -1
  111. metadata +78 -9
  112. data/lib/log_struct/log/interfaces/message_field.rb +0 -20
  113. data/lib/log_struct/log_keys.rb +0 -102
@@ -0,0 +1,278 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # rubocop:disable Sorbet/ConstantsFromStrings
5
+ require_relative "../boot_buffer"
6
+ require "pathname"
7
+
8
+ begin
9
+ require "dotenv-rails"
10
+ rescue LoadError
11
+ # Dotenv-rails gem is not available, integration will be skipped
12
+ end
13
+
14
+ module LogStruct
15
+ module Integrations
16
+ # Dotenv integration: emits structured logs for load/update/save/restore events
17
+ module Dotenv
18
+ extend T::Sig
19
+ extend IntegrationInterface
20
+ @original_logger_setter = T.let(nil, T.nilable(UnboundMethod))
21
+
22
+ # Internal state holder to avoid duplicate subscriptions in a Sorbet-friendly way
23
+ State = ::Struct.new(:subscribed)
24
+ STATE = T.let(State.new(false), State)
25
+
26
+ sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
27
+ def self.setup(config)
28
+ # Subscribe regardless of dotenv gem presence so instrumentation via
29
+ # ActiveSupport::Notifications can be captured during tests and runtime.
30
+ subscribe!
31
+ true
32
+ end
33
+
34
+ class << self
35
+ extend T::Sig
36
+
37
+ sig { void }
38
+ def subscribe!
39
+ # Guard against double subscription
40
+ return if STATE.subscribed
41
+
42
+ instrumenter = defined?(::ActiveSupport::Notifications) ? ::ActiveSupport::Notifications : nil
43
+ return unless instrumenter
44
+
45
+ instrumenter.subscribe("load.dotenv") do |*args|
46
+ # Allow tests to stub Log::Dotenv.new to force an error path
47
+ LogStruct::Log::Dotenv.new
48
+ event = ::ActiveSupport::Notifications::Event.new(*args)
49
+ env = event.payload[:env]
50
+ abs = env.filename
51
+ file = begin
52
+ if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
53
+ Pathname.new(abs).relative_path_from(Pathname.new(::Rails.root.to_s)).to_s
54
+ else
55
+ abs
56
+ end
57
+ rescue
58
+ abs
59
+ end
60
+
61
+ ts = event.time ? Time.at(event.time) : Time.now
62
+ LogStruct.info(Log::Dotenv::Load.new(file: file, timestamp: ts))
63
+ rescue => e
64
+ if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env == "test"
65
+ raise
66
+ else
67
+ LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
68
+ end
69
+ end
70
+
71
+ instrumenter.subscribe("update.dotenv") do |*args|
72
+ LogStruct::Log::Dotenv.new
73
+ event = ::ActiveSupport::Notifications::Event.new(*args)
74
+ diff = event.payload[:diff]
75
+ vars = diff.env.keys.map(&:to_s)
76
+
77
+ ts = event.time ? Time.at(event.time) : Time.now
78
+ LogStruct.debug(Log::Dotenv::Update.new(vars: vars, timestamp: ts))
79
+ rescue => e
80
+ if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env == "test"
81
+ raise
82
+ else
83
+ LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
84
+ end
85
+ end
86
+
87
+ instrumenter.subscribe("save.dotenv") do |*args|
88
+ LogStruct::Log::Dotenv.new
89
+ event = ::ActiveSupport::Notifications::Event.new(*args)
90
+ ts = event.time ? Time.at(event.time) : Time.now
91
+ LogStruct.info(Log::Dotenv::Save.new(snapshot: true, timestamp: ts))
92
+ rescue => e
93
+ if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env == "test"
94
+ raise
95
+ else
96
+ LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
97
+ end
98
+ end
99
+
100
+ instrumenter.subscribe("restore.dotenv") do |*args|
101
+ LogStruct::Log::Dotenv.new
102
+ event = ::ActiveSupport::Notifications::Event.new(*args)
103
+ diff = event.payload[:diff]
104
+ vars = diff.env.keys.map(&:to_s)
105
+
106
+ ts = event.time ? Time.at(event.time) : Time.now
107
+ LogStruct.info(Log::Dotenv::Restore.new(vars: vars, timestamp: ts))
108
+ rescue => e
109
+ if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env == "test"
110
+ raise
111
+ else
112
+ LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
113
+ end
114
+ end
115
+
116
+ STATE.subscribed = true
117
+ end
118
+ end
119
+
120
+ # Early boot subscription to buffer structured logs until logger is ready
121
+ @@boot_subscribed = T.let(false, T::Boolean)
122
+ sig { void }
123
+ def self.setup_boot
124
+ return if @@boot_subscribed
125
+ return unless defined?(::ActiveSupport::Notifications)
126
+
127
+ instrumenter = if Object.const_defined?(:Dotenv)
128
+ dm = T.unsafe(Object.const_get(:Dotenv))
129
+ dm.respond_to?(:instrumenter) ? T.unsafe(dm).instrumenter : ::ActiveSupport::Notifications
130
+ else
131
+ ::ActiveSupport::Notifications
132
+ end
133
+
134
+ instrumenter.subscribe("load.dotenv") do |*args|
135
+ event = ::ActiveSupport::Notifications::Event.new(*args)
136
+ env = event.payload[:env]
137
+ abs = env.filename
138
+ file = begin
139
+ if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
140
+ Pathname.new(abs).relative_path_from(Pathname.new(::Rails.root.to_s)).to_s
141
+ else
142
+ abs
143
+ end
144
+ rescue
145
+ abs
146
+ end
147
+ ts = event.time ? Time.at(event.time) : Time.now
148
+ LogStruct::BootBuffer.add(Log::Dotenv::Load.new(file: file, timestamp: ts))
149
+ rescue => e
150
+ LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
151
+ end
152
+
153
+ instrumenter.subscribe("update.dotenv") do |*args|
154
+ event = ::ActiveSupport::Notifications::Event.new(*args)
155
+ diff = event.payload[:diff]
156
+ vars = diff.env.keys.map(&:to_s)
157
+ ts = event.time ? Time.at(event.time) : Time.now
158
+ LogStruct::BootBuffer.add(Log::Dotenv::Update.new(vars: vars, timestamp: ts))
159
+ rescue => e
160
+ LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
161
+ end
162
+
163
+ instrumenter.subscribe("save.dotenv") do |*args|
164
+ event = ::ActiveSupport::Notifications::Event.new(*args)
165
+ ts = event.time ? Time.at(event.time) : Time.now
166
+ LogStruct::BootBuffer.add(Log::Dotenv::Save.new(snapshot: true, timestamp: ts))
167
+ rescue => e
168
+ LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
169
+ end
170
+
171
+ instrumenter.subscribe("restore.dotenv") do |*args|
172
+ event = ::ActiveSupport::Notifications::Event.new(*args)
173
+ diff = event.payload[:diff]
174
+ vars = diff.env.keys.map(&:to_s)
175
+ ts = event.time ? Time.at(event.time) : Time.now
176
+ LogStruct::BootBuffer.add(Log::Dotenv::Restore.new(vars: vars, timestamp: ts))
177
+ rescue => e
178
+ LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
179
+ end
180
+
181
+ @@boot_subscribed = true
182
+ end
183
+
184
+ # Intercept Dotenv::Rails#logger= to defer replay until we resolve policy
185
+ sig { void }
186
+ def self.intercept_logger_setter!
187
+ return unless Object.const_defined?(:Dotenv)
188
+ # Do not intercept when LogStruct is disabled; allow original dotenv replay
189
+ return unless LogStruct.enabled?
190
+ dotenv_mod = T.unsafe(Object.const_get(:Dotenv))
191
+ return unless dotenv_mod.const_defined?(:Rails)
192
+ klass = T.unsafe(dotenv_mod.const_get(:Rails))
193
+ return if klass.instance_variable_defined?(:@_logstruct_replay_patched)
194
+
195
+ original = klass.instance_method(:logger=)
196
+ @original_logger_setter = original
197
+
198
+ mod = Module.new do
199
+ define_method :logger= do |new_logger|
200
+ # Defer replay: store desired logger, keep ReplayLogger as current
201
+ instance_variable_set(:@logstruct_pending_dotenv_logger, new_logger)
202
+ new_logger
203
+ end
204
+
205
+ define_method :logstruct_pending_dotenv_logger do
206
+ instance_variable_get(:@logstruct_pending_dotenv_logger)
207
+ end
208
+ end
209
+
210
+ klass.prepend(mod)
211
+ klass.instance_variable_set(:@_logstruct_replay_patched, true)
212
+ end
213
+
214
+ # Decide which boot logs to emit after user initializers
215
+ sig { void }
216
+ def self.resolve_boot_logs!
217
+ # If LogStruct is disabled, do not alter dotenv behavior at all
218
+ return unless LogStruct.enabled?
219
+ dotenv_mod = Object.const_defined?(:Dotenv) ? T.unsafe(Object.const_get(:Dotenv)) : nil
220
+ klass = dotenv_mod&.const_defined?(:Rails) ? T.unsafe(dotenv_mod.const_get(:Rails)) : nil
221
+
222
+ pending_logger = nil
223
+ railtie_instance = nil
224
+ if klass&.respond_to?(:instance)
225
+ railtie_instance = klass.instance
226
+ if railtie_instance.respond_to?(:logstruct_pending_dotenv_logger)
227
+ pending_logger = T.unsafe(railtie_instance).logstruct_pending_dotenv_logger
228
+ end
229
+ end
230
+
231
+ if LogStruct.enabled? && LogStruct.config.integrations.enable_dotenv
232
+ # Structured path
233
+ if pending_logger && railtie_instance
234
+ # Clear any buffered original logs
235
+ current_logger = railtie_instance.logger if railtie_instance.respond_to?(:logger)
236
+ if current_logger && current_logger.class.name.end_with?("ReplayLogger")
237
+ begin
238
+ logs = current_logger.instance_variable_get(:@logs)
239
+ logs.clear if logs.respond_to?(:clear)
240
+ rescue
241
+ # best effort
242
+ end
243
+ end
244
+ railtie_instance.config.dotenv.logger = pending_logger
245
+ end
246
+
247
+ # Detach original subscriber and subscribe runtime structured
248
+ if dotenv_mod&.const_defined?(:LogSubscriber)
249
+ T.unsafe(dotenv_mod.const_get(:LogSubscriber)).detach_from(:dotenv)
250
+ end
251
+ LogStruct::Integrations::Dotenv.subscribe!
252
+
253
+ require_relative "../boot_buffer"
254
+ LogStruct::BootBuffer.flush
255
+ else
256
+ # Original path: replay dotenv lines, drop structured buffer
257
+ if railtie_instance && @original_logger_setter
258
+ setter = @original_logger_setter
259
+ new_logger = pending_logger
260
+ if new_logger.nil? && ENV["RAILS_LOG_TO_STDOUT"].to_s.strip != ""
261
+ require "logger"
262
+ require "active_support/tagged_logging"
263
+ new_logger = ActiveSupport::TaggedLogging.new(::Logger.new($stdout)).tagged("dotenv")
264
+ end
265
+ setter.bind_call(railtie_instance, new_logger) if new_logger
266
+ end
267
+ require_relative "../boot_buffer"
268
+ LogStruct::BootBuffer.clear
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end
274
+
275
+ # Subscribe immediately to capture earliest dotenv events into BootBuffer
276
+ LogStruct::Integrations::Dotenv.setup_boot
277
+
278
+ # rubocop:enable Sorbet/ConstantsFromStrings
@@ -38,168 +38,118 @@ module LogStruct
38
38
  extend T::Sig
39
39
 
40
40
  # Job enqueued event
41
- sig { params(event: T.untyped).void }
41
+ sig { params(event: ::ActiveSupport::Notifications::Event).void }
42
42
  def enqueue(event)
43
- job_data = extract_job_data(event)
44
-
45
- log_entry = LogStruct::Log::GoodJob.new(
46
- event: Event::Enqueue,
47
- level: Level::Info,
48
- job_id: job_data[:job_id],
49
- job_class: job_data[:job_class],
50
- queue_name: job_data[:queue_name],
51
- arguments: job_data[:arguments],
52
- scheduled_at: job_data[:scheduled_at],
53
- priority: job_data[:priority],
54
- execution_time: event.duration,
55
- additional_data: {
56
- enqueue_caller: job_data[:caller_location]
57
- }
58
- )
59
-
60
- logger.info(log_entry)
43
+ payload = T.let(event.payload, T::Hash[Symbol, T.untyped])
44
+ job = payload[:job]
45
+ base_fields = build_base_fields(job, payload)
46
+ ts = event.time ? Time.at(event.time) : Time.now
47
+
48
+ logger.info(Log::GoodJob::Enqueue.new(
49
+ **base_fields.to_kwargs,
50
+ scheduled_at: (job&.scheduled_at ? Time.at(job.scheduled_at.to_i) : nil),
51
+ duration_ms: event.duration.to_f,
52
+ additional_data: {enqueue_caller: job&.enqueue_caller_location},
53
+ timestamp: ts
54
+ ))
61
55
  end
62
56
 
63
57
  # Job execution started event
64
- sig { params(event: T.untyped).void }
58
+ sig { params(event: ::ActiveSupport::Notifications::Event).void }
65
59
  def start(event)
66
- job_data = extract_job_data(event)
67
-
68
- log_entry = LogStruct::Log::GoodJob.new(
69
- event: Event::Start,
70
- level: Level::Info,
71
- job_id: job_data[:job_id],
72
- job_class: job_data[:job_class],
73
- queue_name: job_data[:queue_name],
74
- arguments: job_data[:arguments],
75
- executions: job_data[:executions],
76
- wait_time: job_data[:wait_time],
77
- scheduled_at: job_data[:scheduled_at],
60
+ payload = T.let(event.payload, T::Hash[Symbol, T.untyped])
61
+ job = payload[:job]
62
+ execution = payload[:execution] || payload[:good_job_execution]
63
+ base_fields = build_base_fields(job, payload)
64
+ ts = event.time ? Time.at(event.time) : Time.now
65
+
66
+ logger.info(Log::GoodJob::Start.new(
67
+ **base_fields.to_kwargs,
68
+ wait_ms: begin
69
+ wt = execution&.wait_time || calculate_wait_time(execution)
70
+ wt ? (wt.to_f * 1000.0) : nil
71
+ end,
72
+ scheduled_at: (job&.scheduled_at ? Time.at(job.scheduled_at.to_i) : nil),
78
73
  process_id: ::Process.pid,
79
- thread_id: Thread.current.object_id.to_s(36)
80
- )
81
-
82
- logger.info(log_entry)
74
+ thread_id: Thread.current.object_id.to_s(36),
75
+ timestamp: ts
76
+ ))
83
77
  end
84
78
 
85
79
  # Job completed successfully event
86
- sig { params(event: T.untyped).void }
80
+ sig { params(event: ::ActiveSupport::Notifications::Event).void }
87
81
  def finish(event)
88
- job_data = extract_job_data(event)
89
-
90
- log_entry = LogStruct::Log::GoodJob.new(
91
- event: Event::Finish,
92
- level: Level::Info,
93
- job_id: job_data[:job_id],
94
- job_class: job_data[:job_class],
95
- queue_name: job_data[:queue_name],
96
- executions: job_data[:executions],
97
- run_time: event.duration,
98
- finished_at: Time.now,
82
+ payload = T.let(event.payload, T::Hash[Symbol, T.untyped])
83
+ job = payload[:job]
84
+ base_fields = build_base_fields(job, payload)
85
+ start_ts = event.time ? Time.at(event.time) : Time.now
86
+ end_ts = event.end ? Time.at(event.end) : Time.now
87
+
88
+ logger.info(Log::GoodJob::Finish.new(
89
+ **base_fields.to_kwargs,
90
+ duration_ms: event.duration.to_f,
91
+ finished_at: end_ts,
99
92
  process_id: ::Process.pid,
100
93
  thread_id: Thread.current.object_id.to_s(36),
101
- additional_data: {
102
- result: job_data[:result]
103
- }
104
- )
105
-
106
- logger.info(log_entry)
94
+ additional_data: {result: payload[:result]},
95
+ timestamp: start_ts
96
+ ))
107
97
  end
108
98
 
109
99
  # Job failed with error event
110
- sig { params(event: T.untyped).void }
100
+ sig { params(event: ::ActiveSupport::Notifications::Event).void }
111
101
  def error(event)
112
- job_data = extract_job_data(event)
113
-
114
- log_entry = LogStruct::Log::GoodJob.new(
115
- event: Event::Error,
116
- level: Level::Error,
117
- job_id: job_data[:job_id],
118
- job_class: job_data[:job_class],
119
- queue_name: job_data[:queue_name],
120
- executions: job_data[:executions],
121
- exception_executions: job_data[:exception_executions],
122
- error_class: job_data[:error_class],
123
- error_message: job_data[:error_message],
124
- error_backtrace: job_data[:error_backtrace],
125
- run_time: event.duration,
102
+ payload = T.let(event.payload, T::Hash[Symbol, T.untyped])
103
+ job = payload[:job]
104
+ execution = payload[:execution] || payload[:good_job_execution]
105
+ exception = payload[:exception] || payload[:error]
106
+ ts = event.time ? Time.at(event.time) : Time.now
107
+ base_fields = build_base_fields(job, payload)
108
+
109
+ logger.error(Log::GoodJob::Error.new(
110
+ **base_fields.to_kwargs,
111
+ exception_executions: execution&.exception_executions,
112
+ err_class: exception&.class&.name,
113
+ error_message: exception&.message,
114
+ backtrace: exception&.backtrace&.first(20),
115
+ duration_ms: event.duration.to_f,
126
116
  process_id: ::Process.pid,
127
- thread_id: Thread.current.object_id.to_s(36)
128
- )
129
-
130
- logger.error(log_entry)
117
+ thread_id: Thread.current.object_id.to_s(36),
118
+ timestamp: ts
119
+ ))
131
120
  end
132
121
 
133
122
  # Job scheduled for future execution event
134
- sig { params(event: T.untyped).void }
123
+ sig { params(event: ::ActiveSupport::Notifications::Event).void }
135
124
  def schedule(event)
136
- job_data = extract_job_data(event)
137
-
138
- log_entry = LogStruct::Log::GoodJob.new(
139
- event: Event::Schedule,
140
- level: Level::Info,
141
- job_id: job_data[:job_id],
142
- job_class: job_data[:job_class],
143
- queue_name: job_data[:queue_name],
144
- arguments: job_data[:arguments],
145
- scheduled_at: job_data[:scheduled_at],
146
- priority: job_data[:priority],
147
- cron_key: job_data[:cron_key],
148
- execution_time: event.duration
149
- )
150
-
151
- logger.info(log_entry)
125
+ payload = T.let(event.payload, T::Hash[Symbol, T.untyped])
126
+ job = payload[:job]
127
+ base_fields = build_base_fields(job, payload)
128
+ ts = event.time ? Time.at(event.time) : Time.now
129
+
130
+ logger.info(Log::GoodJob::Schedule.new(
131
+ **base_fields.to_kwargs,
132
+ scheduled_at: (job&.scheduled_at ? Time.at(job.scheduled_at.to_i) : nil),
133
+ priority: job&.priority,
134
+ cron_key: job&.cron_key,
135
+ duration_ms: event.duration.to_f,
136
+ timestamp: ts
137
+ ))
152
138
  end
153
139
 
154
140
  private
155
141
 
156
- # Extract job data from ActiveSupport event payload
157
- sig { params(event: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
158
- def extract_job_data(event)
159
- payload = event.payload || {}
160
- job = payload[:job]
142
+ # Build BaseFields from job + payload (execution)
143
+ sig { params(job: T.untyped, payload: T::Hash[Symbol, T.untyped]).returns(Log::GoodJob::BaseFields) }
144
+ def build_base_fields(job, payload)
161
145
  execution = payload[:execution] || payload[:good_job_execution]
162
- exception = payload[:exception] || payload[:error]
163
-
164
- data = {}
165
-
166
- # Basic job information
167
- if job
168
- data[:job_id] = job.job_id if job.respond_to?(:job_id)
169
- data[:job_class] = job.job_class if job.respond_to?(:job_class)
170
- data[:queue_name] = job.queue_name if job.respond_to?(:queue_name)
171
- data[:arguments] = job.arguments if job.respond_to?(:arguments)
172
- data[:priority] = job.priority if job.respond_to?(:priority)
173
- data[:scheduled_at] = job.scheduled_at if job.respond_to?(:scheduled_at)
174
- data[:cron_key] = job.cron_key if job.respond_to?(:cron_key)
175
- data[:caller_location] = job.enqueue_caller_location if job.respond_to?(:enqueue_caller_location)
176
- end
177
-
178
- # Execution-specific information
179
- if execution
180
- data[:executions] = execution.executions if execution.respond_to?(:executions)
181
- data[:exception_executions] = execution.exception_executions if execution.respond_to?(:exception_executions)
182
- # Use existing wait_time if available, otherwise calculate it
183
- if execution.respond_to?(:wait_time) && execution.wait_time
184
- data[:wait_time] = execution.wait_time
185
- elsif execution.respond_to?(:created_at)
186
- data[:wait_time] = calculate_wait_time(execution)
187
- end
188
- data[:batch_id] = execution.batch_id if execution.respond_to?(:batch_id)
189
- data[:cron_key] ||= execution.cron_key if execution.respond_to?(:cron_key)
190
- end
191
-
192
- # Error information
193
- if exception
194
- data[:error_class] = exception.class.name
195
- data[:error_message] = exception.message
196
- data[:error_backtrace] = exception.backtrace&.first(20) # Limit backtrace size
197
- end
198
-
199
- # Result information
200
- data[:result] = payload[:result] if payload.key?(:result)
201
-
202
- data
146
+ Log::GoodJob::BaseFields.new(
147
+ job_id: job&.job_id,
148
+ job_class: job&.job_class,
149
+ queue_name: job&.queue_name,
150
+ arguments: job&.arguments,
151
+ executions: execution&.executions
152
+ )
203
153
  end
204
154
 
205
155
  # Calculate wait time from job creation to execution start
@@ -46,24 +46,22 @@ module LogStruct
46
46
  end
47
47
  end
48
48
 
49
- # Create a GoodJob log struct with the context
50
- log_struct = Log::GoodJob.new(
51
- event: Event::Log,
52
- level: LogStruct::Level.from_severity(level.to_s.upcase),
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
+ log_struct = Log::GoodJob::Log.new(
55
+ message: message || (block ? block.call : ""),
53
56
  process_id: ::Process.pid,
54
57
  thread_id: Thread.current.object_id.to_s(36),
55
58
  job_id: job_context[:job_id],
56
59
  job_class: job_context[:job_class],
57
60
  queue_name: job_context[:queue_name],
58
61
  executions: job_context[:executions],
59
- scheduled_at: job_context[:scheduled_at],
60
- priority: job_context[:priority],
61
- additional_data: {
62
- message: message || (block ? block.call : "")
63
- }
62
+ additional_data: extras
64
63
  )
65
64
 
66
- # Pass the struct to SemanticLogger
67
65
  super(log_struct, payload, &nil)
68
66
  end
69
67
  end
@@ -81,15 +81,13 @@ module LogStruct
81
81
  # Configure error handling for thread errors if GoodJob supports it
82
82
  if goodjob_module.respond_to?(:on_thread_error=)
83
83
  goodjob_module.on_thread_error = ->(exception) do
84
- # Log the error using our structured format
85
- log_entry = LogStruct::Log::GoodJob.new(
86
- event: Event::Error,
87
- level: Level::Error,
88
- error_class: exception.class.name,
84
+ log_entry = LogStruct::Log::GoodJob::Error.new(
85
+ err_class: exception.class.name,
89
86
  error_message: exception.message,
90
- error_backtrace: exception.backtrace
87
+ backtrace: exception.backtrace,
88
+ process_id: ::Process.pid,
89
+ thread_id: Thread.current.object_id.to_s(36)
91
90
  )
92
-
93
91
  goodjob_module.logger.error(log_entry)
94
92
  end
95
93
  end
@@ -34,6 +34,23 @@ module LogStruct
34
34
  return nil unless config.enabled
35
35
  return nil unless config.integrations.enable_host_authorization
36
36
 
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
+
37
54
  # Define the response app as a separate variable to fix block alignment
38
55
  response_app = lambda do |env|
39
56
  request = ::ActionDispatch::Request.new(env)
@@ -69,10 +86,14 @@ module LogStruct
69
86
  [FORBIDDEN_STATUS, RESPONSE_HEADERS, [RESPONSE_HTML]]
70
87
  end
71
88
 
72
- # Replace the default HostAuthorization app with our custom app for logging
73
- Rails.application.config.host_authorization = {
74
- response_app: response_app
75
- }
89
+ # Merge our response_app into existing host_authorization config to preserve excludes
90
+ existing = Rails.application.config.host_authorization
91
+ unless existing.is_a?(Hash)
92
+ existing = {}
93
+ end
94
+ existing = existing.dup
95
+ existing[:response_app] = response_app
96
+ Rails.application.config.host_authorization = existing
76
97
 
77
98
  true
78
99
  end