logstruct 0.1.0 → 0.1.2

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 +11 -1
  3. data/README.md +23 -3
  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 +178 -15
  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 +477 -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 +116 -0
  98. data/lib/log_struct/railtie.rb +67 -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 +36 -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,477 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LogStruct
5
+ module Integrations
6
+ module Puma
7
+ extend T::Sig
8
+ extend T::Helpers
9
+
10
+ STATE = T.let(
11
+ {
12
+ installed: false,
13
+ boot_emitted: false,
14
+ shutdown_emitted: false,
15
+ handler_pending_started: false,
16
+ start_info: {
17
+ mode: nil,
18
+ puma_version: nil,
19
+ puma_codename: nil,
20
+ ruby_version: nil,
21
+ min_threads: nil,
22
+ max_threads: nil,
23
+ environment: nil,
24
+ pid: nil,
25
+ listening: []
26
+ }
27
+ },
28
+ T::Hash[Symbol, T.untyped]
29
+ )
30
+
31
+ class << self
32
+ extend T::Sig
33
+
34
+ sig { params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
35
+ def setup(config)
36
+ return nil unless config.integrations.enable_puma
37
+
38
+ # No stdout wrapping here.
39
+
40
+ # Ensure Puma is loaded so we can patch its classes
41
+ begin
42
+ require "puma"
43
+ rescue LoadError
44
+ # If Puma isn't available, skip setup
45
+ return nil
46
+ end
47
+
48
+ install_patches!
49
+
50
+ if ARGV.include?("server")
51
+ # Emit deterministic boot/started events based on CLI args
52
+ begin
53
+ port = T.let(nil, T.nilable(String))
54
+ ARGV.each_with_index do |arg, idx|
55
+ if arg == "-p" || arg == "--port"
56
+ port = ARGV[idx + 1]
57
+ break
58
+ elsif arg.start_with?("--port=")
59
+ port = arg.split("=", 2)[1]
60
+ break
61
+ end
62
+ end
63
+ si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
64
+ si[:pid] ||= Process.pid
65
+ si[:environment] ||= ((defined?(::Rails) && ::Rails.respond_to?(:env)) ? ::Rails.env : nil)
66
+ si[:mode] ||= "single"
67
+ if port && !T.cast(si[:listening], T::Array[T.untyped]).any? { |a| a.to_s.include?(":" + port.to_s) }
68
+ si[:listening] = ["tcp://127.0.0.1:#{port}"]
69
+ end
70
+ emit_boot_if_needed!
71
+ unless STATE[:started_emitted]
72
+ emit_started!
73
+ STATE[:started_emitted] = true
74
+ end
75
+ rescue => e
76
+ handle_integration_error(e)
77
+ end
78
+ begin
79
+ %w[TERM INT].each do |sig|
80
+ Signal.trap(sig) { emit_shutdown!(sig) }
81
+ end
82
+ rescue => e
83
+ handle_integration_error(e)
84
+ end
85
+ at_exit do
86
+ emit_shutdown!("Exiting")
87
+ rescue => e
88
+ handle_integration_error(e)
89
+ end
90
+
91
+ # Connection-based readiness: emit started once port is accepting connections
92
+ # No background threads or sockets; rely solely on parsing Puma output
93
+ end
94
+ true
95
+ end
96
+
97
+ sig { void }
98
+ def install_patches!
99
+ return if STATE[:installed]
100
+ STATE[:installed] = true
101
+
102
+ state_reset!
103
+
104
+ begin
105
+ begin
106
+ require "puma"
107
+ rescue => e
108
+ handle_integration_error(e)
109
+ end
110
+ puma_mod = ::Object.const_defined?(:Puma) ? T.unsafe(::Object.const_get(:Puma)) : nil # rubocop:disable Sorbet/ConstantsFromStrings
111
+ # rubocop:disable Sorbet/ConstantsFromStrings
112
+ if puma_mod&.const_defined?(:LogWriter)
113
+ T.unsafe(::Object.const_get("Puma::LogWriter")).prepend(LogWriterPatch)
114
+ end
115
+ if puma_mod&.const_defined?(:Events)
116
+ ev = T.unsafe(::Object.const_get("Puma::Events"))
117
+ ev.prepend(EventsPatch)
118
+ end
119
+ # Patch Rack::Handler::Puma.run to emit lifecycle logs using options
120
+ if ::Object.const_defined?(:Rack)
121
+ rack_mod = T.unsafe(::Object.const_get(:Rack))
122
+ if rack_mod.const_defined?(:Handler)
123
+ handler_mod = T.unsafe(rack_mod.const_get(:Handler))
124
+ if handler_mod.const_defined?(:Puma)
125
+ handler = T.unsafe(handler_mod.const_get(:Puma))
126
+ handler.singleton_class.prepend(RackHandlerPatch)
127
+ end
128
+ end
129
+ end
130
+ # Avoid patching CLI/Server; rely on log parsing
131
+ # Avoid patching CLI to minimize version-specific risks
132
+ # rubocop:enable Sorbet/ConstantsFromStrings
133
+ rescue => e
134
+ handle_integration_error(e)
135
+ end
136
+
137
+ # Rely on Puma patches to observe lines
138
+ end
139
+
140
+ sig { params(e: StandardError).void }
141
+ def handle_integration_error(e)
142
+ server_mode = ::LogStruct.server_mode?
143
+ if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env.test? && !server_mode
144
+ raise e
145
+ else
146
+ LogStruct.handle_exception(e, source: Source::Puma)
147
+ end
148
+ end
149
+
150
+ # No stdout interception
151
+
152
+ sig { void }
153
+ def state_reset!
154
+ STATE[:boot_emitted] = false
155
+ STATE[:shutdown_emitted] = false
156
+ STATE[:started_emitted] = false
157
+ STATE[:handler_pending_started] = false
158
+ STATE[:start_info] = {
159
+ mode: nil,
160
+ puma_version: nil,
161
+ puma_codename: nil,
162
+ ruby_version: nil,
163
+ min_threads: nil,
164
+ max_threads: nil,
165
+ environment: nil,
166
+ pid: nil,
167
+ listening: []
168
+ }
169
+ end
170
+
171
+ sig { params(line: String).returns(T::Boolean) }
172
+ def process_line(line)
173
+ l = line.to_s.strip
174
+ return false if l.empty?
175
+
176
+ # Suppress non-JSON rails banners
177
+ return true if l.start_with?("=> ")
178
+
179
+ # Ignore boot line
180
+ return true if l.start_with?("=> Booting Puma")
181
+
182
+ if l.start_with?("Puma starting in ")
183
+ # Example: Puma starting in single mode...
184
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:mode] = l.sub("Puma starting in ", "").sub(" mode...", "")
185
+ return true
186
+ end
187
+
188
+ if (m = l.match(/^(?:\*\s*)?Puma version: (\S+)(?:.*"([^\"]+)")?/))
189
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:puma_version] = m[1]
190
+ if m[2]
191
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:puma_codename] = m[2]
192
+ end
193
+ return true
194
+ end
195
+
196
+ if (m = l.match(/^\* Ruby version: (.+)$/))
197
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:ruby_version] = m[1]
198
+ return true
199
+ end
200
+
201
+ if (m = l.match(/^(?:\*\s*)?Min threads: (\d+)/))
202
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:min_threads] = m[1].to_i
203
+ return true
204
+ end
205
+
206
+ if (m = l.match(/^(?:\*\s*)?Max threads: (\d+)/))
207
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:max_threads] = m[1].to_i
208
+ return true
209
+ end
210
+
211
+ if (m = l.match(/^(?:\*\s*)?Environment: (\S+)/))
212
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:environment] = m[1]
213
+ return true
214
+ end
215
+
216
+ if (m = l.match(/^(?:\*\s*)?PID:\s+(\d+)/))
217
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:pid] = m[1].to_i
218
+ return true
219
+ end
220
+
221
+ if (m = l.match(/^\*?\s*Listening on (.+)$/))
222
+ si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
223
+ list = T.cast(si[:listening], T::Array[T.untyped])
224
+ address = T.must(m[1])
225
+ list << address unless list.include?(address)
226
+ # Emit started when we see the first listening address
227
+ if !STATE[:started_emitted]
228
+ emit_started!
229
+ STATE[:started_emitted] = true
230
+ end
231
+ return true
232
+ end
233
+
234
+ if l == "Use Ctrl-C to stop"
235
+ si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
236
+ # Fallback: if no listening address captured yet, infer from ARGV
237
+ if T.cast(si[:listening], T::Array[T.untyped]).empty?
238
+ begin
239
+ port = T.let(nil, T.untyped)
240
+ ARGV.each_with_index do |arg, idx|
241
+ if arg == "-p" || arg == "--port"
242
+ port = ARGV[idx + 1]
243
+ break
244
+ elsif arg.start_with?("--port=")
245
+ port = arg.split("=", 2)[1]
246
+ break
247
+ end
248
+ end
249
+ if port
250
+ si[:listening] << "tcp://127.0.0.1:#{port}"
251
+ end
252
+ rescue => e
253
+ handle_integration_error(e)
254
+ end
255
+ end
256
+ if !STATE[:started_emitted]
257
+ emit_started!
258
+ STATE[:started_emitted] = true
259
+ end
260
+ return false
261
+ end
262
+
263
+ if l.start_with?("- Gracefully stopping")
264
+ emit_shutdown!(l)
265
+ return true
266
+ end
267
+
268
+ if l.start_with?("=== puma shutdown:")
269
+ emit_shutdown!(l)
270
+ return true
271
+ end
272
+
273
+ if l == "- Goodbye!"
274
+ # Swallow
275
+ return true
276
+ end
277
+
278
+ if l == "Exiting"
279
+ emit_shutdown!(l)
280
+ return true
281
+ end
282
+
283
+ false
284
+ end
285
+
286
+ sig { void }
287
+ def emit_boot_if_needed!
288
+ # Intentionally no-op: we no longer emit a boot log
289
+ STATE[:boot_emitted] = true
290
+ end
291
+
292
+ # No server hooks; rely on parsing only
293
+
294
+ sig { void }
295
+ def emit_started!
296
+ si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
297
+ log = Log::Puma::Start.new(
298
+ mode: T.cast(si[:mode], T.nilable(String)),
299
+ puma_version: T.cast(si[:puma_version], T.nilable(String)),
300
+ puma_codename: T.cast(si[:puma_codename], T.nilable(String)),
301
+ ruby_version: T.cast(si[:ruby_version], T.nilable(String)),
302
+ min_threads: T.cast(si[:min_threads], T.nilable(Integer)),
303
+ max_threads: T.cast(si[:max_threads], T.nilable(Integer)),
304
+ environment: T.cast(si[:environment], T.nilable(String)),
305
+ process_id: T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:pid] || Process.pid,
306
+ listening_addresses: T.cast(T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:listening], T::Array[String]),
307
+ level: Level::Info,
308
+ timestamp: Time.now
309
+ )
310
+ LogStruct.info(log)
311
+ STATE[:handler_pending_started] = false
312
+ # Only use LogStruct; SemanticLogger routes to STDOUT in test
313
+ end
314
+
315
+ sig { params(_message: String).void }
316
+ def emit_shutdown!(_message)
317
+ return if STATE[:shutdown_emitted]
318
+ STATE[:shutdown_emitted] = true
319
+ log = Log::Puma::Shutdown.new(
320
+ process_id: T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:pid] || Process.pid,
321
+ level: Level::Info,
322
+ timestamp: Time.now
323
+ )
324
+ LogStruct.info(log)
325
+ # Only use LogStruct; SemanticLogger routes to STDOUT in test
326
+ # Let SemanticLogger appender write to STDOUT
327
+ end
328
+ end
329
+
330
+ # STDOUT interception is handled globally via StdoutFilter; keep Puma patches minimal
331
+
332
+ # Patch Puma::LogWriter to intercept log writes
333
+ module LogWriterPatch
334
+ extend T::Sig
335
+
336
+ sig { params(msg: String).returns(T.untyped) }
337
+ def log(msg)
338
+ consumed = ::LogStruct::Integrations::Puma.process_line(msg)
339
+ super unless consumed
340
+ end
341
+
342
+ sig { params(msg: String).returns(T.untyped) }
343
+ def write(msg)
344
+ any_consumed = T.let(false, T::Boolean)
345
+ msg.to_s.each_line do |l|
346
+ any_consumed = true if ::LogStruct::Integrations::Puma.process_line(l)
347
+ end
348
+ super unless any_consumed
349
+ end
350
+
351
+ sig { params(msg: String).returns(T.untyped) }
352
+ def <<(msg)
353
+ any_consumed = T.let(false, T::Boolean)
354
+ msg.to_s.each_line do |l|
355
+ any_consumed = true if ::LogStruct::Integrations::Puma.process_line(l)
356
+ end
357
+ super unless any_consumed
358
+ end
359
+
360
+ sig { params(msg: String).returns(T.untyped) }
361
+ def puts(msg)
362
+ consumed = ::LogStruct::Integrations::Puma.process_line(msg)
363
+ if consumed
364
+ # attempt to suppress; only forward if not consumed
365
+ return nil
366
+ end
367
+ if ::Kernel.instance_variables.include?(:@stdout)
368
+ io = T.unsafe(::Kernel.instance_variable_get(:@stdout))
369
+ return io.puts(msg)
370
+ end
371
+ super
372
+ end
373
+
374
+ sig { params(msg: String).returns(T.untyped) }
375
+ def info(msg)
376
+ consumed = ::LogStruct::Integrations::Puma.process_line(msg)
377
+ super unless consumed
378
+ end
379
+ end
380
+
381
+ # Patch Puma::Events as a fallback for some versions where Events handles output
382
+ module EventsPatch
383
+ extend T::Sig
384
+
385
+ sig { params(str: String).returns(T.untyped) }
386
+ def log(str)
387
+ consumed = ::LogStruct::Integrations::Puma.process_line(str)
388
+ super unless consumed
389
+ end
390
+ end
391
+
392
+ # Hook Rack::Handler::Puma.run to emit structured started/shutdown
393
+ module RackHandlerPatch
394
+ extend T::Sig
395
+
396
+ sig do
397
+ params(
398
+ app: T.untyped,
399
+ args: T.untyped,
400
+ block: T.nilable(T.proc.returns(T.untyped))
401
+ ).returns(T.untyped)
402
+ end
403
+ def run(app, *args, &block)
404
+ rest = args
405
+ options = T.let({}, T::Hash[T.untyped, T.untyped])
406
+ rest.each do |value|
407
+ next unless value.is_a?(Hash)
408
+ options.merge!(value)
409
+ end
410
+
411
+ begin
412
+ si = T.cast(::LogStruct::Integrations::Puma::STATE[:start_info], T::Hash[Symbol, T.untyped])
413
+ si[:mode] ||= "single"
414
+ si[:environment] ||= ((defined?(::Rails) && ::Rails.respond_to?(:env)) ? ::Rails.env : nil)
415
+ si[:pid] ||= Process.pid
416
+ si[:listening] ||= []
417
+ port = T.let(nil, T.untyped)
418
+ host = T.let(nil, T.untyped)
419
+ if options.respond_to?(:[])
420
+ port = options[:Port] || options["Port"] || options[:port] || options["port"]
421
+ host = options[:Host] || options["Host"] || options[:host] || options["host"]
422
+ end
423
+ if port
424
+ list = T.cast(si[:listening], T::Array[T.untyped])
425
+ list.clear
426
+ h = (host && host != "0.0.0.0") ? host : "127.0.0.1"
427
+ list << "tcp://#{h}:#{port}"
428
+ end
429
+ state = ::LogStruct::Integrations::Puma::STATE
430
+ state[:handler_pending_started] = true unless state[:started_emitted]
431
+ rescue => e
432
+ ::LogStruct::Integrations::Puma.handle_integration_error(e)
433
+ end
434
+
435
+ begin
436
+ Kernel.at_exit do
437
+ unless ::LogStruct::Integrations::Puma::STATE[:shutdown_emitted]
438
+ ::LogStruct::Integrations::Puma.emit_shutdown!("Exiting")
439
+ ::LogStruct::Integrations::Puma::STATE[:shutdown_emitted] = true
440
+ end
441
+ rescue => e
442
+ ::LogStruct::Integrations::Puma.handle_integration_error(e)
443
+ end
444
+ rescue => e
445
+ ::LogStruct::Integrations::Puma.handle_integration_error(e)
446
+ end
447
+
448
+ begin
449
+ result = super(app, **options, &block)
450
+ ensure
451
+ state = ::LogStruct::Integrations::Puma::STATE
452
+ if state[:handler_pending_started] && !state[:started_emitted]
453
+ begin
454
+ ::LogStruct::Integrations::Puma.emit_started!
455
+ state[:started_emitted] = true
456
+ rescue => e
457
+ ::LogStruct::Integrations::Puma.handle_integration_error(e)
458
+ ensure
459
+ state[:handler_pending_started] = false
460
+ end
461
+ end
462
+ end
463
+
464
+ result
465
+ end
466
+ end
467
+
468
+ # (No Launcher patch)
469
+
470
+ # No Server patch
471
+
472
+ # No InterceptorIO
473
+
474
+ # Removed EventsInitPatch and CLIPatch to avoid version-specific conflicts
475
+ end
476
+ end
477
+ end
@@ -60,21 +60,18 @@ module LogStruct
60
60
  @app.call(env)
61
61
  rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError => ip_spoof_error
62
62
  # Create a security log for IP spoofing
63
- security_log = Log::Security.new(
64
- event: Event::IPSpoof,
65
- message: ip_spoof_error.message,
66
- # Can't call .remote_ip on the request because that's what raises the error.
67
- # Have to pass the client_ip and x_forwarded_for headers.
68
- client_ip: env["HTTP_CLIENT_IP"],
69
- x_forwarded_for: env["HTTP_X_FORWARDED_FOR"],
63
+ security_log = Log::Security::IPSpoof.new(
70
64
  path: env["PATH_INFO"],
71
65
  http_method: env["REQUEST_METHOD"],
72
66
  user_agent: env["HTTP_USER_AGENT"],
73
67
  referer: env["HTTP_REFERER"],
74
- request_id: env["action_dispatch.request_id"]
68
+ request_id: env["action_dispatch.request_id"],
69
+ message: ip_spoof_error.message,
70
+ client_ip: env["HTTP_CLIENT_IP"],
71
+ x_forwarded_for: env["HTTP_X_FORWARDED_FOR"],
72
+ timestamp: Time.now
75
73
  )
76
74
 
77
- # Log the structured data
78
75
  ::Rails.logger.warn(security_log)
79
76
 
80
77
  # Report the error
@@ -87,15 +84,15 @@ module LogStruct
87
84
  rescue ::ActionController::InvalidAuthenticityToken => invalid_auth_token_error
88
85
  # Create a security log for CSRF error
89
86
  request = ::ActionDispatch::Request.new(env)
90
- security_log = Log::Security.new(
91
- event: Event::CSRFViolation,
92
- message: invalid_auth_token_error.message,
87
+ security_log = Log::Security::CSRFViolation.new(
93
88
  path: request.path,
94
89
  http_method: request.method,
95
90
  source_ip: request.remote_ip,
96
91
  user_agent: request.user_agent,
97
92
  referer: request.referer,
98
- request_id: request.request_id
93
+ request_id: request.request_id,
94
+ message: invalid_auth_token_error.message,
95
+ timestamp: Time.now
99
96
  )
100
97
  LogStruct.error(security_log)
101
98
 
@@ -111,11 +108,7 @@ module LogStruct
111
108
  context = extract_request_context(env)
112
109
 
113
110
  # Create and log a structured exception with request context
114
- exception_log = Log::Error.from_exception(
115
- Source::Rails,
116
- error,
117
- context
118
- )
111
+ exception_log = Log.from_exception(Source::Rails, error, context)
119
112
  LogStruct.error(exception_log)
120
113
 
121
114
  # Re-raise any standard errors to let Rails or error reporter handle it.
@@ -37,26 +37,51 @@ module LogStruct
37
37
  end
38
38
 
39
39
  # Create structured log data
40
- log_data = Log::Shrine.new(
41
- source: Source::Shrine,
42
- event: event_type,
43
- duration: event.duration,
44
- storage: payload[:storage],
45
- location: payload[:location],
46
- uploader: payload[:uploader],
47
- upload_options: payload[:upload_options],
48
- download_options: payload[:download_options],
49
- options: payload[:options],
50
- # Data is flattened by the JSON formatter
51
- additional_data: payload.except(
52
- :storage,
53
- :location,
54
- :uploader,
55
- :upload_options,
56
- :download_options,
57
- :options
40
+ log_data = case event_type
41
+ when Event::Upload
42
+ Log::Shrine::Upload.new(
43
+ storage: payload[:storage],
44
+ location: payload[:location],
45
+ uploader: payload[:uploader],
46
+ upload_options: payload[:upload_options],
47
+ options: payload[:options],
48
+ duration_ms: event.duration,
49
+ additional_data: payload.except(:storage, :location, :uploader, :upload_options, :options)
58
50
  )
59
- )
51
+ when Event::Download
52
+ Log::Shrine::Download.new(
53
+ storage: payload[:storage],
54
+ location: payload[:location],
55
+ download_options: payload[:download_options],
56
+ additional_data: payload.except(:storage, :location, :download_options)
57
+ )
58
+ when Event::Delete
59
+ Log::Shrine::Delete.new(
60
+ storage: payload[:storage],
61
+ location: payload[:location],
62
+ additional_data: payload.except(:storage, :location)
63
+ )
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
+ )
71
+ when Event::Exist
72
+ Log::Shrine::Exist.new(
73
+ storage: payload[:storage],
74
+ location: payload[:location],
75
+ exist: payload[:exist],
76
+ additional_data: payload.except(:storage, :location, :exist)
77
+ )
78
+ else
79
+ Log::Shrine::Metadata.new(
80
+ storage: payload[:storage],
81
+ location: payload[:location],
82
+ metadata: payload[:metadata]
83
+ )
84
+ end
60
85
 
61
86
  # Pass the structured hash to the logger
62
87
  # If Rails.logger has our Formatter, it will handle JSON conversion
@@ -17,6 +17,9 @@ module LogStruct
17
17
  def self.setup(config)
18
18
  return nil unless config.integrations.enable_sorbet_error_handlers
19
19
 
20
+ clear_sig_error_handler!
21
+ install_error_handler!
22
+
20
23
  # Install inline type error handler
21
24
  # Called when T.let, T.cast, T.must, etc. fail
22
25
  T::Configuration.inline_type_error_handler = lambda do |error, _opts|
@@ -44,6 +47,51 @@ module LogStruct
44
47
 
45
48
  true
46
49
  end
50
+
51
+ @installed = T.let(false, T::Boolean)
52
+
53
+ class << self
54
+ extend T::Sig
55
+
56
+ private
57
+
58
+ sig { void }
59
+ def install_error_handler!
60
+ return if installed?
61
+
62
+ T::Configuration.sig_builder_error_handler = lambda do |error, source|
63
+ LogStruct.handle_exception(error, source: source, context: nil)
64
+ end
65
+
66
+ @installed = true
67
+ end
68
+
69
+ sig do
70
+ returns(
71
+ T.nilable(
72
+ T.proc.params(error: StandardError, location: Thread::Backtrace::Location).void
73
+ )
74
+ )
75
+ end
76
+ def clear_sig_error_handler!
77
+ previous_handler = T.cast(
78
+ T::Configuration.instance_variable_get(:@sig_builder_error_handler),
79
+ T.nilable(
80
+ T.proc.params(error: StandardError, location: Thread::Backtrace::Location).void
81
+ )
82
+ )
83
+ T::Configuration.sig_builder_error_handler = nil
84
+
85
+ @installed = false
86
+
87
+ previous_handler
88
+ end
89
+
90
+ sig { returns(T::Boolean) }
91
+ def installed?
92
+ @installed
93
+ end
94
+ end
47
95
  end
48
96
  end
49
97
  end