logstruct 0.0.2 → 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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -22
  3. data/README.md +25 -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/concerns/logging.rb +5 -5
  9. data/lib/log_struct/config_struct/filters.rb +18 -0
  10. data/lib/log_struct/config_struct/integrations.rb +16 -12
  11. data/lib/log_struct/configuration.rb +13 -0
  12. data/lib/log_struct/enums/event.rb +13 -0
  13. data/lib/log_struct/enums/log_field.rb +154 -0
  14. data/lib/log_struct/enums/source.rb +4 -1
  15. data/lib/log_struct/formatter.rb +29 -17
  16. data/lib/log_struct/integrations/action_mailer/error_handling.rb +3 -11
  17. data/lib/log_struct/integrations/action_mailer/event_logging.rb +22 -12
  18. data/lib/log_struct/integrations/active_job/log_subscriber.rb +52 -48
  19. data/lib/log_struct/integrations/active_model_serializers.rb +49 -0
  20. data/lib/log_struct/integrations/active_record.rb +35 -5
  21. data/lib/log_struct/integrations/active_storage.rb +59 -20
  22. data/lib/log_struct/integrations/ahoy.rb +54 -0
  23. data/lib/log_struct/integrations/carrierwave.rb +13 -16
  24. data/lib/log_struct/integrations/dotenv.rb +278 -0
  25. data/lib/log_struct/integrations/good_job/log_subscriber.rb +86 -136
  26. data/lib/log_struct/integrations/good_job/logger.rb +8 -10
  27. data/lib/log_struct/integrations/good_job.rb +5 -7
  28. data/lib/log_struct/integrations/host_authorization.rb +25 -4
  29. data/lib/log_struct/integrations/lograge.rb +20 -14
  30. data/lib/log_struct/integrations/puma.rb +482 -0
  31. data/lib/log_struct/integrations/rack_error_handler/middleware.rb +11 -18
  32. data/lib/log_struct/integrations/shrine.rb +44 -19
  33. data/lib/log_struct/integrations/sorbet.rb +48 -0
  34. data/lib/log_struct/integrations.rb +25 -0
  35. data/lib/log_struct/log/action_mailer/delivered.rb +99 -0
  36. data/lib/log_struct/log/action_mailer/delivery.rb +99 -0
  37. data/lib/log_struct/log/action_mailer.rb +30 -45
  38. data/lib/log_struct/log/active_job/enqueue.rb +125 -0
  39. data/lib/log_struct/log/active_job/finish.rb +130 -0
  40. data/lib/log_struct/log/active_job/schedule.rb +125 -0
  41. data/lib/log_struct/log/active_job/start.rb +130 -0
  42. data/lib/log_struct/log/active_job.rb +41 -54
  43. data/lib/log_struct/log/active_model_serializers.rb +94 -0
  44. data/lib/log_struct/log/active_storage/delete.rb +87 -0
  45. data/lib/log_struct/log/active_storage/download.rb +103 -0
  46. data/lib/log_struct/log/active_storage/exist.rb +93 -0
  47. data/lib/log_struct/log/active_storage/metadata.rb +93 -0
  48. data/lib/log_struct/log/active_storage/stream.rb +93 -0
  49. data/lib/log_struct/log/active_storage/upload.rb +118 -0
  50. data/lib/log_struct/log/active_storage/url.rb +93 -0
  51. data/lib/log_struct/log/active_storage.rb +32 -68
  52. data/lib/log_struct/log/ahoy.rb +88 -0
  53. data/lib/log_struct/log/carrierwave/delete.rb +115 -0
  54. data/lib/log_struct/log/carrierwave/download.rb +131 -0
  55. data/lib/log_struct/log/carrierwave/upload.rb +141 -0
  56. data/lib/log_struct/log/carrierwave.rb +37 -72
  57. data/lib/log_struct/log/dotenv/load.rb +76 -0
  58. data/lib/log_struct/log/dotenv/restore.rb +76 -0
  59. data/lib/log_struct/log/dotenv/save.rb +76 -0
  60. data/lib/log_struct/log/dotenv/update.rb +76 -0
  61. data/lib/log_struct/log/dotenv.rb +12 -0
  62. data/lib/log_struct/log/error.rb +58 -46
  63. data/lib/log_struct/log/good_job/enqueue.rb +126 -0
  64. data/lib/log_struct/log/good_job/error.rb +151 -0
  65. data/lib/log_struct/log/good_job/finish.rb +136 -0
  66. data/lib/log_struct/log/good_job/log.rb +131 -0
  67. data/lib/log_struct/log/good_job/schedule.rb +136 -0
  68. data/lib/log_struct/log/good_job/start.rb +136 -0
  69. data/lib/log_struct/log/good_job.rb +40 -141
  70. data/lib/log_struct/log/interfaces/additional_data_field.rb +1 -17
  71. data/lib/log_struct/log/interfaces/common_fields.rb +1 -39
  72. data/lib/log_struct/log/interfaces/public_common_fields.rb +4 -0
  73. data/lib/log_struct/log/interfaces/request_fields.rb +1 -33
  74. data/lib/log_struct/log/plain.rb +59 -34
  75. data/lib/log_struct/log/puma/shutdown.rb +80 -0
  76. data/lib/log_struct/log/puma/start.rb +120 -0
  77. data/lib/log_struct/log/puma.rb +10 -0
  78. data/lib/log_struct/log/request.rb +132 -48
  79. data/lib/log_struct/log/security/blocked_host.rb +141 -0
  80. data/lib/log_struct/log/security/csrf_violation.rb +131 -0
  81. data/lib/log_struct/log/security/ip_spoof.rb +141 -0
  82. data/lib/log_struct/log/security.rb +40 -70
  83. data/lib/log_struct/log/shared/add_request_fields.rb +1 -26
  84. data/lib/log_struct/log/shared/merge_additional_data_fields.rb +1 -25
  85. data/lib/log_struct/log/shared/serialize_common.rb +1 -33
  86. data/lib/log_struct/log/shared/serialize_common_public.rb +44 -0
  87. data/lib/log_struct/log/shrine/delete.rb +85 -0
  88. data/lib/log_struct/log/shrine/download.rb +90 -0
  89. data/lib/log_struct/log/shrine/exist.rb +90 -0
  90. data/lib/log_struct/log/shrine/metadata.rb +90 -0
  91. data/lib/log_struct/log/shrine/upload.rb +105 -0
  92. data/lib/log_struct/log/shrine.rb +10 -67
  93. data/lib/log_struct/log/sidekiq.rb +65 -26
  94. data/lib/log_struct/log/sql.rb +113 -106
  95. data/lib/log_struct/log.rb +31 -32
  96. data/lib/log_struct/multi_error_reporter.rb +80 -22
  97. data/lib/log_struct/param_filters.rb +50 -7
  98. data/lib/log_struct/rails_boot_banner_silencer.rb +123 -0
  99. data/lib/log_struct/railtie.rb +71 -0
  100. data/lib/log_struct/semantic_logger/formatter.rb +4 -2
  101. data/lib/log_struct/semantic_logger/setup.rb +34 -18
  102. data/lib/log_struct/shared/interfaces/additional_data_field.rb +22 -0
  103. data/lib/log_struct/shared/interfaces/common_fields.rb +39 -0
  104. data/lib/log_struct/shared/interfaces/public_common_fields.rb +29 -0
  105. data/lib/log_struct/shared/interfaces/request_fields.rb +39 -0
  106. data/lib/log_struct/shared/shared/add_request_fields.rb +28 -0
  107. data/lib/log_struct/shared/shared/merge_additional_data_fields.rb +27 -0
  108. data/lib/log_struct/shared/shared/serialize_common.rb +58 -0
  109. data/lib/log_struct/version.rb +1 -1
  110. data/lib/log_struct.rb +22 -4
  111. data/logstruct.gemspec +3 -0
  112. metadata +108 -5
  113. data/lib/log_struct/log/interfaces/message_field.rb +0 -20
  114. data/lib/log_struct/log_keys.rb +0 -102
@@ -0,0 +1,482 @@
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 = false
143
+ begin
144
+ server_mode = ::LogStruct.instance_variable_defined?(:@server_mode) && ::LogStruct.instance_variable_get(:@server_mode)
145
+ rescue
146
+ server_mode = false
147
+ end
148
+ if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env.test? && !server_mode
149
+ raise e
150
+ else
151
+ LogStruct.handle_exception(e, source: Source::Puma)
152
+ end
153
+ end
154
+
155
+ # No stdout interception
156
+
157
+ sig { void }
158
+ def state_reset!
159
+ STATE[:boot_emitted] = false
160
+ STATE[:shutdown_emitted] = false
161
+ STATE[:started_emitted] = false
162
+ STATE[:handler_pending_started] = false
163
+ STATE[:start_info] = {
164
+ mode: nil,
165
+ puma_version: nil,
166
+ puma_codename: nil,
167
+ ruby_version: nil,
168
+ min_threads: nil,
169
+ max_threads: nil,
170
+ environment: nil,
171
+ pid: nil,
172
+ listening: []
173
+ }
174
+ end
175
+
176
+ sig { params(line: String).returns(T::Boolean) }
177
+ def process_line(line)
178
+ l = line.to_s.strip
179
+ return false if l.empty?
180
+
181
+ # Suppress non-JSON rails banners
182
+ return true if l.start_with?("=> ")
183
+
184
+ # Ignore boot line
185
+ return true if l.start_with?("=> Booting Puma")
186
+
187
+ if l.start_with?("Puma starting in ")
188
+ # Example: Puma starting in single mode...
189
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:mode] = l.sub("Puma starting in ", "").sub(" mode...", "")
190
+ return true
191
+ end
192
+
193
+ if (m = l.match(/^(?:\*\s*)?Puma version: (\S+)(?:.*"([^\"]+)")?/))
194
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:puma_version] = m[1]
195
+ if m[2]
196
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:puma_codename] = m[2]
197
+ end
198
+ return true
199
+ end
200
+
201
+ if (m = l.match(/^\* Ruby version: (.+)$/))
202
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:ruby_version] = m[1]
203
+ return true
204
+ end
205
+
206
+ if (m = l.match(/^(?:\*\s*)?Min threads: (\d+)/))
207
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:min_threads] = m[1].to_i
208
+ return true
209
+ end
210
+
211
+ if (m = l.match(/^(?:\*\s*)?Max threads: (\d+)/))
212
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:max_threads] = m[1].to_i
213
+ return true
214
+ end
215
+
216
+ if (m = l.match(/^(?:\*\s*)?Environment: (\S+)/))
217
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:environment] = m[1]
218
+ return true
219
+ end
220
+
221
+ if (m = l.match(/^(?:\*\s*)?PID:\s+(\d+)/))
222
+ T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:pid] = m[1].to_i
223
+ return true
224
+ end
225
+
226
+ if (m = l.match(/^\*?\s*Listening on (.+)$/))
227
+ si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
228
+ list = T.cast(si[:listening], T::Array[T.untyped])
229
+ address = T.must(m[1])
230
+ list << address unless list.include?(address)
231
+ # Emit started when we see the first listening address
232
+ if !STATE[:started_emitted]
233
+ emit_started!
234
+ STATE[:started_emitted] = true
235
+ end
236
+ return true
237
+ end
238
+
239
+ if l == "Use Ctrl-C to stop"
240
+ si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
241
+ # Fallback: if no listening address captured yet, infer from ARGV
242
+ if T.cast(si[:listening], T::Array[T.untyped]).empty?
243
+ begin
244
+ port = T.let(nil, T.untyped)
245
+ ARGV.each_with_index do |arg, idx|
246
+ if arg == "-p" || arg == "--port"
247
+ port = ARGV[idx + 1]
248
+ break
249
+ elsif arg.start_with?("--port=")
250
+ port = arg.split("=", 2)[1]
251
+ break
252
+ end
253
+ end
254
+ if port
255
+ si[:listening] << "tcp://127.0.0.1:#{port}"
256
+ end
257
+ rescue => e
258
+ handle_integration_error(e)
259
+ end
260
+ end
261
+ if !STATE[:started_emitted]
262
+ emit_started!
263
+ STATE[:started_emitted] = true
264
+ end
265
+ return false
266
+ end
267
+
268
+ if l.start_with?("- Gracefully stopping")
269
+ emit_shutdown!(l)
270
+ return true
271
+ end
272
+
273
+ if l.start_with?("=== puma shutdown:")
274
+ emit_shutdown!(l)
275
+ return true
276
+ end
277
+
278
+ if l == "- Goodbye!"
279
+ # Swallow
280
+ return true
281
+ end
282
+
283
+ if l == "Exiting"
284
+ emit_shutdown!(l)
285
+ return true
286
+ end
287
+
288
+ false
289
+ end
290
+
291
+ sig { void }
292
+ def emit_boot_if_needed!
293
+ # Intentionally no-op: we no longer emit a boot log
294
+ STATE[:boot_emitted] = true
295
+ end
296
+
297
+ # No server hooks; rely on parsing only
298
+
299
+ sig { void }
300
+ def emit_started!
301
+ si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
302
+ log = Log::Puma::Start.new(
303
+ mode: T.cast(si[:mode], T.nilable(String)),
304
+ puma_version: T.cast(si[:puma_version], T.nilable(String)),
305
+ puma_codename: T.cast(si[:puma_codename], T.nilable(String)),
306
+ ruby_version: T.cast(si[:ruby_version], T.nilable(String)),
307
+ min_threads: T.cast(si[:min_threads], T.nilable(Integer)),
308
+ max_threads: T.cast(si[:max_threads], T.nilable(Integer)),
309
+ environment: T.cast(si[:environment], T.nilable(String)),
310
+ process_id: T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:pid] || Process.pid,
311
+ listening_addresses: T.cast(T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:listening], T::Array[String]),
312
+ level: Level::Info,
313
+ timestamp: Time.now
314
+ )
315
+ LogStruct.info(log)
316
+ STATE[:handler_pending_started] = false
317
+ # Only use LogStruct; SemanticLogger routes to STDOUT in test
318
+ end
319
+
320
+ sig { params(_message: String).void }
321
+ def emit_shutdown!(_message)
322
+ return if STATE[:shutdown_emitted]
323
+ STATE[:shutdown_emitted] = true
324
+ log = Log::Puma::Shutdown.new(
325
+ process_id: T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:pid] || Process.pid,
326
+ level: Level::Info,
327
+ timestamp: Time.now
328
+ )
329
+ LogStruct.info(log)
330
+ # Only use LogStruct; SemanticLogger routes to STDOUT in test
331
+ # Let SemanticLogger appender write to STDOUT
332
+ end
333
+ end
334
+
335
+ # STDOUT interception is handled globally via StdoutFilter; keep Puma patches minimal
336
+
337
+ # Patch Puma::LogWriter to intercept log writes
338
+ module LogWriterPatch
339
+ extend T::Sig
340
+
341
+ sig { params(msg: String).returns(T.untyped) }
342
+ def log(msg)
343
+ consumed = ::LogStruct::Integrations::Puma.process_line(msg)
344
+ super unless consumed
345
+ end
346
+
347
+ sig { params(msg: String).returns(T.untyped) }
348
+ def write(msg)
349
+ any_consumed = T.let(false, T::Boolean)
350
+ msg.to_s.each_line do |l|
351
+ any_consumed = true if ::LogStruct::Integrations::Puma.process_line(l)
352
+ end
353
+ super unless any_consumed
354
+ end
355
+
356
+ sig { params(msg: String).returns(T.untyped) }
357
+ def <<(msg)
358
+ any_consumed = T.let(false, T::Boolean)
359
+ msg.to_s.each_line do |l|
360
+ any_consumed = true if ::LogStruct::Integrations::Puma.process_line(l)
361
+ end
362
+ super unless any_consumed
363
+ end
364
+
365
+ sig { params(msg: String).returns(T.untyped) }
366
+ def puts(msg)
367
+ consumed = ::LogStruct::Integrations::Puma.process_line(msg)
368
+ if consumed
369
+ # attempt to suppress; only forward if not consumed
370
+ return nil
371
+ end
372
+ if ::Kernel.instance_variables.include?(:@stdout)
373
+ io = T.unsafe(::Kernel.instance_variable_get(:@stdout))
374
+ return io.puts(msg)
375
+ end
376
+ super
377
+ end
378
+
379
+ sig { params(msg: String).returns(T.untyped) }
380
+ def info(msg)
381
+ consumed = ::LogStruct::Integrations::Puma.process_line(msg)
382
+ super unless consumed
383
+ end
384
+ end
385
+
386
+ # Patch Puma::Events as a fallback for some versions where Events handles output
387
+ module EventsPatch
388
+ extend T::Sig
389
+
390
+ sig { params(str: String).returns(T.untyped) }
391
+ def log(str)
392
+ consumed = ::LogStruct::Integrations::Puma.process_line(str)
393
+ super unless consumed
394
+ end
395
+ end
396
+
397
+ # Hook Rack::Handler::Puma.run to emit structured started/shutdown
398
+ module RackHandlerPatch
399
+ extend T::Sig
400
+
401
+ sig do
402
+ params(
403
+ app: T.untyped,
404
+ args: T.untyped,
405
+ block: T.nilable(T.proc.returns(T.untyped))
406
+ ).returns(T.untyped)
407
+ end
408
+ def run(app, *args, &block)
409
+ rest = args
410
+ options = T.let({}, T::Hash[T.untyped, T.untyped])
411
+ rest.each do |value|
412
+ next unless value.is_a?(Hash)
413
+ options.merge!(value)
414
+ end
415
+
416
+ begin
417
+ si = T.cast(::LogStruct::Integrations::Puma::STATE[:start_info], T::Hash[Symbol, T.untyped])
418
+ si[:mode] ||= "single"
419
+ si[:environment] ||= ((defined?(::Rails) && ::Rails.respond_to?(:env)) ? ::Rails.env : nil)
420
+ si[:pid] ||= Process.pid
421
+ si[:listening] ||= []
422
+ port = T.let(nil, T.untyped)
423
+ host = T.let(nil, T.untyped)
424
+ if options.respond_to?(:[])
425
+ port = options[:Port] || options["Port"] || options[:port] || options["port"]
426
+ host = options[:Host] || options["Host"] || options[:host] || options["host"]
427
+ end
428
+ if port
429
+ list = T.cast(si[:listening], T::Array[T.untyped])
430
+ list.clear
431
+ h = (host && host != "0.0.0.0") ? host : "127.0.0.1"
432
+ list << "tcp://#{h}:#{port}"
433
+ end
434
+ state = ::LogStruct::Integrations::Puma::STATE
435
+ state[:handler_pending_started] = true unless state[:started_emitted]
436
+ rescue => e
437
+ ::LogStruct::Integrations::Puma.handle_integration_error(e)
438
+ end
439
+
440
+ begin
441
+ Kernel.at_exit do
442
+ unless ::LogStruct::Integrations::Puma::STATE[:shutdown_emitted]
443
+ ::LogStruct::Integrations::Puma.emit_shutdown!("Exiting")
444
+ ::LogStruct::Integrations::Puma::STATE[:shutdown_emitted] = true
445
+ end
446
+ rescue => e
447
+ ::LogStruct::Integrations::Puma.handle_integration_error(e)
448
+ end
449
+ rescue => e
450
+ ::LogStruct::Integrations::Puma.handle_integration_error(e)
451
+ end
452
+
453
+ begin
454
+ result = super(app, **options, &block)
455
+ ensure
456
+ state = ::LogStruct::Integrations::Puma::STATE
457
+ if state[:handler_pending_started] && !state[:started_emitted]
458
+ begin
459
+ ::LogStruct::Integrations::Puma.emit_started!
460
+ state[:started_emitted] = true
461
+ rescue => e
462
+ ::LogStruct::Integrations::Puma.handle_integration_error(e)
463
+ ensure
464
+ state[:handler_pending_started] = false
465
+ end
466
+ end
467
+ end
468
+
469
+ result
470
+ end
471
+ end
472
+
473
+ # (No Launcher patch)
474
+
475
+ # No Server patch
476
+
477
+ # No InterceptorIO
478
+
479
+ # Removed EventsInitPatch and CLIPatch to avoid version-specific conflicts
480
+ end
481
+ end
482
+ 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