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
@@ -38,7 +38,7 @@ module LogStruct
38
38
  # Extract key information from the event
39
39
  event_name = event.name.sub(/\.active_storage$/, "")
40
40
  service_name = event.payload[:service]
41
- duration = event.duration
41
+ duration_ms = event.duration
42
42
 
43
43
  # Map service events to log event types
44
44
  event_type = case event_name
@@ -65,26 +65,65 @@ module LogStruct
65
65
  end
66
66
 
67
67
  # Map the event name to an operation
68
- operation = event_name.sub(/^service_/, "").to_sym
68
+ event_name.sub(/^service_/, "").to_sym
69
69
 
70
- # Create structured log event specific to ActiveStorage
71
- log_data = Log::ActiveStorage.new(
72
- event: event_type,
73
- operation: operation,
74
- storage: service_name.to_s,
75
- file_id: event.payload[:key].to_s,
76
- checksum: event.payload[:checksum].to_s,
77
- duration: duration,
78
- # Add other fields where available
79
- metadata: event.payload[:metadata],
80
- exist: event.payload[:exist],
81
- url: event.payload[:url],
82
- filename: event.payload[:filename],
83
- mime_type: event.payload[:content_type],
84
- size: event.payload[:byte_size],
85
- prefix: event.payload[:prefix],
86
- range: event.payload[:range]
87
- )
70
+ # Create structured log event using generated classes
71
+ log_data = case event_type
72
+ when Event::Upload
73
+ Log::ActiveStorage::Upload.new(
74
+ storage: service_name.to_s,
75
+ file_id: event.payload[:key]&.to_s,
76
+ checksum: event.payload[:checksum]&.to_s,
77
+ duration_ms: duration_ms,
78
+ metadata: event.payload[:metadata],
79
+ filename: event.payload[:filename],
80
+ mime_type: event.payload[:content_type],
81
+ size: event.payload[:byte_size]
82
+ )
83
+ when Event::Download
84
+ Log::ActiveStorage::Download.new(
85
+ storage: service_name.to_s,
86
+ file_id: event.payload[:key]&.to_s,
87
+ filename: event.payload[:filename],
88
+ range: event.payload[:range],
89
+ duration_ms: duration_ms
90
+ )
91
+ when Event::Delete
92
+ Log::ActiveStorage::Delete.new(
93
+ storage: service_name.to_s,
94
+ file_id: event.payload[:key]&.to_s
95
+ )
96
+ when Event::Metadata
97
+ Log::ActiveStorage::Metadata.new(
98
+ storage: service_name.to_s,
99
+ file_id: event.payload[:key]&.to_s,
100
+ metadata: event.payload[:metadata]
101
+ )
102
+ when Event::Exist
103
+ Log::ActiveStorage::Exist.new(
104
+ storage: service_name.to_s,
105
+ file_id: event.payload[:key]&.to_s,
106
+ exist: event.payload[:exist]
107
+ )
108
+ when Event::Stream
109
+ Log::ActiveStorage::Stream.new(
110
+ storage: service_name.to_s,
111
+ file_id: event.payload[:key]&.to_s,
112
+ prefix: event.payload[:prefix]
113
+ )
114
+ when Event::Url
115
+ Log::ActiveStorage::Url.new(
116
+ storage: service_name.to_s,
117
+ file_id: event.payload[:key]&.to_s,
118
+ url: event.payload[:url]
119
+ )
120
+ else
121
+ Log::ActiveStorage::Metadata.new(
122
+ storage: service_name.to_s,
123
+ file_id: event.payload[:key]&.to_s,
124
+ metadata: event.payload[:metadata]
125
+ )
126
+ end
88
127
 
89
128
  # Log structured data
90
129
  LogStruct.info(log_data)
@@ -27,7 +27,8 @@ module LogStruct
27
27
  data[:properties] = properties if properties
28
28
  LogStruct.info(
29
29
  LogStruct::Log::Ahoy.new(
30
- ahoy_event: T.let(name, T.nilable(String)),
30
+ message: "ahoy.track",
31
+ ahoy_event: T.must(T.let(name, T.nilable(String))),
31
32
  properties: T.let(
32
33
  properties && properties.transform_keys { |k| k.to_sym },
33
34
  T.nilable(T::Hash[Symbol, T.untyped])
@@ -52,18 +52,16 @@ module LogStruct
52
52
  }
53
53
 
54
54
  # Log the store operation with structured data
55
- log_data = Log::CarrierWave.new(
56
- source: Source::CarrierWave,
57
- event: Event::Upload,
58
- duration: duration * 1000.0, # Convert to ms
59
- model: model.class.name,
60
- uploader: self.class.name,
55
+ log_data = Log::CarrierWave::Upload.new(
61
56
  storage: storage.class.name,
62
- mount_point: mounted_as.to_s,
57
+ file_id: identifier,
63
58
  filename: file.filename,
64
59
  mime_type: file.content_type,
65
60
  size: file_size,
66
- file_id: identifier,
61
+ duration_ms: (duration * 1000.0).to_f,
62
+ uploader: self.class.name,
63
+ model: model.class.name,
64
+ mount_point: mounted_as.to_s,
67
65
  additional_data: {
68
66
  version: version_name.to_s,
69
67
  store_path: store_path,
@@ -78,25 +76,24 @@ module LogStruct
78
76
  # Log file retrieve operations
79
77
  sig { params(identifier: T.untyped, args: T.untyped).returns(T.untyped) }
80
78
  def retrieve_from_store!(identifier, *args)
81
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
79
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
82
80
  result = super
83
- duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
81
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
84
82
 
85
83
  # Extract file information if available
86
84
  file_size = file.size if file&.respond_to?(:size)
87
85
 
88
86
  # Log the retrieve operation with structured data
89
- log_data = Log::CarrierWave.new(
90
- source: Source::CarrierWave,
91
- event: Event::Download,
92
- duration: duration * 1000.0, # Convert to ms
93
- uploader: self.class.name,
87
+ log_data = Log::CarrierWave::Download.new(
94
88
  storage: storage.class.name,
95
- mount_point: mounted_as.to_s,
96
89
  file_id: identifier,
97
90
  filename: file&.filename,
98
91
  mime_type: file&.content_type,
99
92
  size: file_size,
93
+ # No duration field on Download event schema
94
+ uploader: self.class.name,
95
+ model: model.class.name,
96
+ mount_point: mounted_as.to_s,
100
97
  additional_data: {
101
98
  version: version_name.to_s
102
99
  }
@@ -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