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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -1
- data/README.md +23 -3
- data/lib/log_struct/boot_buffer.rb +28 -0
- data/lib/log_struct/builders/active_job.rb +84 -0
- data/lib/log_struct/concerns/configuration.rb +178 -15
- data/lib/log_struct/concerns/error_handling.rb +3 -7
- data/lib/log_struct/config_struct/filters.rb +18 -0
- data/lib/log_struct/config_struct/integrations.rb +8 -12
- data/lib/log_struct/configuration.rb +13 -0
- data/lib/log_struct/enums/event.rb +13 -0
- data/lib/log_struct/enums/log_field.rb +154 -0
- data/lib/log_struct/enums/source.rb +4 -1
- data/lib/log_struct/formatter.rb +29 -17
- data/lib/log_struct/integrations/action_mailer/error_handling.rb +3 -11
- data/lib/log_struct/integrations/action_mailer/event_logging.rb +22 -12
- data/lib/log_struct/integrations/active_job/log_subscriber.rb +52 -48
- data/lib/log_struct/integrations/active_model_serializers.rb +8 -14
- data/lib/log_struct/integrations/active_record.rb +35 -5
- data/lib/log_struct/integrations/active_storage.rb +59 -20
- data/lib/log_struct/integrations/ahoy.rb +2 -1
- data/lib/log_struct/integrations/carrierwave.rb +13 -16
- data/lib/log_struct/integrations/dotenv.rb +278 -0
- data/lib/log_struct/integrations/good_job/log_subscriber.rb +86 -136
- data/lib/log_struct/integrations/good_job/logger.rb +8 -10
- data/lib/log_struct/integrations/good_job.rb +5 -7
- data/lib/log_struct/integrations/host_authorization.rb +25 -4
- data/lib/log_struct/integrations/lograge.rb +20 -14
- data/lib/log_struct/integrations/puma.rb +477 -0
- data/lib/log_struct/integrations/rack_error_handler/middleware.rb +11 -18
- data/lib/log_struct/integrations/shrine.rb +44 -19
- data/lib/log_struct/integrations/sorbet.rb +48 -0
- data/lib/log_struct/integrations.rb +21 -0
- data/lib/log_struct/log/action_mailer/delivered.rb +99 -0
- data/lib/log_struct/log/action_mailer/delivery.rb +99 -0
- data/lib/log_struct/log/action_mailer.rb +30 -45
- data/lib/log_struct/log/active_job/enqueue.rb +125 -0
- data/lib/log_struct/log/active_job/finish.rb +130 -0
- data/lib/log_struct/log/active_job/schedule.rb +125 -0
- data/lib/log_struct/log/active_job/start.rb +130 -0
- data/lib/log_struct/log/active_job.rb +41 -54
- data/lib/log_struct/log/active_model_serializers.rb +72 -33
- data/lib/log_struct/log/active_storage/delete.rb +87 -0
- data/lib/log_struct/log/active_storage/download.rb +103 -0
- data/lib/log_struct/log/active_storage/exist.rb +93 -0
- data/lib/log_struct/log/active_storage/metadata.rb +93 -0
- data/lib/log_struct/log/active_storage/stream.rb +93 -0
- data/lib/log_struct/log/active_storage/upload.rb +118 -0
- data/lib/log_struct/log/active_storage/url.rb +93 -0
- data/lib/log_struct/log/active_storage.rb +32 -68
- data/lib/log_struct/log/ahoy.rb +67 -33
- data/lib/log_struct/log/carrierwave/delete.rb +115 -0
- data/lib/log_struct/log/carrierwave/download.rb +131 -0
- data/lib/log_struct/log/carrierwave/upload.rb +141 -0
- data/lib/log_struct/log/carrierwave.rb +37 -72
- data/lib/log_struct/log/dotenv/load.rb +76 -0
- data/lib/log_struct/log/dotenv/restore.rb +76 -0
- data/lib/log_struct/log/dotenv/save.rb +76 -0
- data/lib/log_struct/log/dotenv/update.rb +76 -0
- data/lib/log_struct/log/dotenv.rb +12 -0
- data/lib/log_struct/log/error.rb +58 -47
- data/lib/log_struct/log/good_job/enqueue.rb +126 -0
- data/lib/log_struct/log/good_job/error.rb +151 -0
- data/lib/log_struct/log/good_job/finish.rb +136 -0
- data/lib/log_struct/log/good_job/log.rb +131 -0
- data/lib/log_struct/log/good_job/schedule.rb +136 -0
- data/lib/log_struct/log/good_job/start.rb +136 -0
- data/lib/log_struct/log/good_job.rb +40 -141
- data/lib/log_struct/log/interfaces/additional_data_field.rb +1 -17
- data/lib/log_struct/log/interfaces/common_fields.rb +1 -39
- data/lib/log_struct/log/interfaces/public_common_fields.rb +1 -28
- data/lib/log_struct/log/interfaces/request_fields.rb +1 -33
- data/lib/log_struct/log/plain.rb +59 -34
- data/lib/log_struct/log/puma/shutdown.rb +80 -0
- data/lib/log_struct/log/puma/start.rb +120 -0
- data/lib/log_struct/log/puma.rb +10 -0
- data/lib/log_struct/log/request.rb +132 -48
- data/lib/log_struct/log/security/blocked_host.rb +141 -0
- data/lib/log_struct/log/security/csrf_violation.rb +131 -0
- data/lib/log_struct/log/security/ip_spoof.rb +141 -0
- data/lib/log_struct/log/security.rb +40 -70
- data/lib/log_struct/log/shared/add_request_fields.rb +1 -26
- data/lib/log_struct/log/shared/merge_additional_data_fields.rb +1 -22
- data/lib/log_struct/log/shared/serialize_common.rb +1 -33
- data/lib/log_struct/log/shared/serialize_common_public.rb +9 -9
- data/lib/log_struct/log/shrine/delete.rb +85 -0
- data/lib/log_struct/log/shrine/download.rb +90 -0
- data/lib/log_struct/log/shrine/exist.rb +90 -0
- data/lib/log_struct/log/shrine/metadata.rb +90 -0
- data/lib/log_struct/log/shrine/upload.rb +105 -0
- data/lib/log_struct/log/shrine.rb +10 -67
- data/lib/log_struct/log/sidekiq.rb +65 -26
- data/lib/log_struct/log/sql.rb +113 -106
- data/lib/log_struct/log.rb +29 -36
- data/lib/log_struct/multi_error_reporter.rb +80 -22
- data/lib/log_struct/param_filters.rb +50 -7
- data/lib/log_struct/rails_boot_banner_silencer.rb +116 -0
- data/lib/log_struct/railtie.rb +67 -0
- data/lib/log_struct/semantic_logger/formatter.rb +4 -2
- data/lib/log_struct/semantic_logger/setup.rb +34 -18
- data/lib/log_struct/shared/interfaces/additional_data_field.rb +22 -0
- data/lib/log_struct/shared/interfaces/common_fields.rb +39 -0
- data/lib/log_struct/shared/interfaces/public_common_fields.rb +29 -0
- data/lib/log_struct/shared/interfaces/request_fields.rb +39 -0
- data/lib/log_struct/shared/shared/add_request_fields.rb +28 -0
- data/lib/log_struct/shared/shared/merge_additional_data_fields.rb +27 -0
- data/lib/log_struct/shared/shared/serialize_common.rb +58 -0
- data/lib/log_struct/version.rb +1 -1
- data/lib/log_struct.rb +36 -4
- data/logstruct.gemspec +2 -1
- metadata +78 -9
- data/lib/log_struct/log/interfaces/message_field.rb +0 -20
- 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
|
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 =
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|