activesupport-json_logging 1.2.0 → 1.2.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.
@@ -0,0 +1,206 @@
1
+ module JsonLogging
2
+ module Sanitizer
3
+ module StructuredHash
4
+ JsonableResult = Struct.new(:tree, :owned, :leaf_count, keyword_init: true)
5
+
6
+ module_function
7
+
8
+ def structured_log_hash?(hash, seen = nil)
9
+ seen ||= Set.new.compare_by_identity
10
+ return false unless seen.add?(hash)
11
+
12
+ result = hash.all? { |_key, value| structured_log_value?(value, seen) }
13
+ seen.delete(hash)
14
+ result
15
+ end
16
+
17
+ def structured_log_value?(value, seen = nil)
18
+ case value
19
+ when String, Numeric, TrueClass, FalseClass, NilClass
20
+ Sanitizer.primitive_log_value?(value)
21
+ when Hash
22
+ structured_log_hash?(value, seen)
23
+ when Array
24
+ structured_log_array?(value, seen)
25
+ else
26
+ false
27
+ end
28
+ end
29
+
30
+ def structured_log_array?(array, seen = nil)
31
+ array.all? { |value| structured_log_value?(value, seen) }
32
+ end
33
+
34
+ def sanitize(hash, depth: 0)
35
+ return {"error" => "max_depth_exceeded"} if depth > Sanitizer::MAX_DEPTH
36
+
37
+ filter = Sanitizer.rails_parameter_filter
38
+ if filter
39
+ filter.filter(stringify(hash))
40
+ else
41
+ sanitize_without_filter(hash, depth: depth)
42
+ end
43
+ end
44
+
45
+ def sanitize_without_filter(hash, depth: 0)
46
+ jsonable = jsonable_tree(hash, depth: depth)
47
+ if jsonable.owned
48
+ stringify(jsonable.tree)
49
+ else
50
+ jsonable.tree.dup
51
+ end
52
+ end
53
+
54
+ def jsonable_tree(hash, depth: 0, seen: nil, parent_key: nil, omit_keys: nil, count_leaves: false, leaf_counter: nil)
55
+ seen ||= Set.new.compare_by_identity
56
+ leaf_counter = [0] if count_leaves && leaf_counter.nil?
57
+ if depth > Sanitizer::MAX_DEPTH
58
+ return JsonableResult.new(tree: {"error" => "max_depth_exceeded"}, owned: true, leaf_count: leaf_counter&.[](0))
59
+ end
60
+ unless seen.add?(hash)
61
+ return JsonableResult.new(tree: {"error" => "cyclic_reference"}, owned: true, leaf_count: leaf_counter&.[](0))
62
+ end
63
+
64
+ entries = []
65
+ owned = false
66
+
67
+ hash.each do |key, value|
68
+ entry = process_jsonable_entry(
69
+ key,
70
+ value,
71
+ depth: depth,
72
+ seen: seen,
73
+ parent_key: parent_key,
74
+ omit_keys: omit_keys,
75
+ leaf_counter: leaf_counter
76
+ )
77
+ next unless entry
78
+
79
+ key_string, encoded_value, pair_owned = entry
80
+ owned ||= pair_owned
81
+ entries << [key_string, encoded_value, pair_owned]
82
+ end
83
+
84
+ seen.delete(hash)
85
+ leaf_count = leaf_counter&.[](0)
86
+ unless owned
87
+ tree = omit_root_keys(hash, omit_keys, depth: depth)
88
+ return JsonableResult.new(tree: tree, owned: false, leaf_count: leaf_count)
89
+ end
90
+
91
+ result = entries.each_with_object({}) do |(key_string, encoded_value, _pair_owned), object|
92
+ object[key_string] = encoded_value
93
+ end
94
+ JsonableResult.new(tree: result, owned: true, leaf_count: leaf_count)
95
+ end
96
+
97
+ def jsonable_value(value, depth:, seen:, parent_key:, leaf_counter: nil)
98
+ case value
99
+ when String
100
+ leaf_counter[0] += 1 if leaf_counter
101
+ sanitized = Sanitizer.sanitize_string(value)
102
+ JsonableResult.new(tree: sanitized, owned: sanitized != value)
103
+ when Hash
104
+ jsonable_tree(value, depth: depth + 1, seen: seen, parent_key: parent_key, leaf_counter: leaf_counter)
105
+ when Array
106
+ jsonable_array(value, depth: depth, seen: seen, parent_key: parent_key, leaf_counter: leaf_counter)
107
+ else
108
+ leaf_counter[0] += 1 if leaf_counter
109
+ JsonableResult.new(tree: value, owned: false)
110
+ end
111
+ end
112
+
113
+ def jsonable_array(array, depth:, seen:, parent_key:, leaf_counter: nil)
114
+ owned = false
115
+ result = nil
116
+
117
+ array.each_with_index do |entry, index|
118
+ child = jsonable_value(entry, depth: depth, seen: seen, parent_key: parent_key, leaf_counter: leaf_counter)
119
+ next unless child.owned
120
+
121
+ owned = true
122
+ result ||= array.dup
123
+ result[index] = child.tree
124
+ end
125
+
126
+ JsonableResult.new(tree: owned ? result : array, owned: owned)
127
+ end
128
+
129
+ def process_jsonable_entry(key, value, depth:, seen:, parent_key:, omit_keys:, leaf_counter:)
130
+ key_string = key.to_s
131
+ if root_key_omitted?(depth, key_string, omit_keys)
132
+ count_leaves_in_value(value, leaf_counter, depth: depth, seen: seen) if leaf_counter
133
+ return
134
+ end
135
+
136
+ if Sanitizer::SENSITIVE_KEY_PATTERNS.match?(key_string)
137
+ return [Sanitizer.sensitive_filtered_key_name(key_string), "[FILTERED]", true]
138
+ end
139
+
140
+ nested_parent = parent_key ? "#{parent_key}.#{key_string}" : key_string
141
+ child = jsonable_value(
142
+ value,
143
+ depth: depth,
144
+ seen: seen,
145
+ parent_key: nested_parent,
146
+ leaf_counter: leaf_counter
147
+ )
148
+ encoded_value = child.owned ? child.tree : value
149
+ [key_string, encoded_value, child.owned]
150
+ end
151
+
152
+ def root_key_omitted?(depth, key_string, omit_keys)
153
+ depth.zero? && omit_keys&.include?(key_string)
154
+ end
155
+
156
+ def omit_root_keys(hash, omit_keys, depth:)
157
+ return hash unless depth.zero? && omit_keys&.any?
158
+
159
+ hash.each_with_object({}) do |(key, value), object|
160
+ object[key] = value unless omit_keys.include?(key.to_s)
161
+ end
162
+ end
163
+
164
+ def count_leaves_in_value(value, leaf_counter, depth:, seen:)
165
+ case value
166
+ when Hash
167
+ return unless seen.add?(value)
168
+
169
+ value.each_value do |entry|
170
+ count_leaves_in_value(entry, leaf_counter, depth: depth + 1, seen: seen)
171
+ end
172
+ seen.delete(value)
173
+ when Array
174
+ value.each { |entry| count_leaves_in_value(entry, leaf_counter, depth: depth, seen: seen) }
175
+ else
176
+ leaf_counter[0] += 1
177
+ end
178
+ end
179
+
180
+ def stringify(hash)
181
+ hash.each_with_object({}) do |(key, value), result|
182
+ result[key.to_s] = stringify_value(value)
183
+ end
184
+ end
185
+
186
+ def stringify_value(value)
187
+ case value
188
+ when Hash
189
+ stringify(value)
190
+ when Array
191
+ value.map { |entry| stringify_value(entry) }
192
+ when String
193
+ Sanitizer.sanitize_string(value)
194
+ else
195
+ value
196
+ end
197
+ end
198
+
199
+ def stringify_keys_copy(hash)
200
+ return hash if hash.keys.all? { |key| key.is_a?(String) }
201
+
202
+ hash.transform_keys(&:to_s)
203
+ end
204
+ end
205
+ end
206
+ end
@@ -1,3 +1,3 @@
1
1
  module JsonLogging
2
- VERSION = "1.2.0"
2
+ VERSION = "1.2.2"
3
3
  end
data/lib/json_logging.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "json_logging/helpers"
7
7
  require_relative "json_logging/sanitizer"
8
8
  require_relative "json_logging/message_parser"
9
9
  require_relative "json_logging/payload_builder"
10
+ require_relative "json_logging/line_encoder"
10
11
  require_relative "json_logging/formatter"
11
12
  require_relative "json_logging/formatter_with_tags"
12
13
  require_relative "json_logging/json_logger_extension"
@@ -14,13 +15,28 @@ require_relative "json_logging/json_logger"
14
15
 
15
16
  module JsonLogging
16
17
  THREAD_CONTEXT_KEY = :__json_logging_context
18
+ SANITIZED_CONTEXT_CACHE_KEY = :__json_logging_sanitized_context
19
+ @additional_context_warned = Set.new
20
+ @additional_context_transformer = nil
21
+
22
+ def self.context_storage
23
+ if defined?(ActiveSupport::IsolatedExecutionState)
24
+ ActiveSupport::IsolatedExecutionState
25
+ else
26
+ Thread.current
27
+ end
28
+ end
29
+ private_class_method :context_storage
17
30
 
18
31
  def self.with_context(extra_context)
19
- original = Thread.current[THREAD_CONTEXT_KEY]
20
- Thread.current[THREAD_CONTEXT_KEY] = (original || {}).merge(safe_hash(extra_context))
32
+ storage = context_storage
33
+ original = storage[THREAD_CONTEXT_KEY]
34
+ storage[THREAD_CONTEXT_KEY] = (original || {}).merge(safe_hash(extra_context))
35
+ invalidate_sanitized_context_cache(storage)
21
36
  yield
22
37
  ensure
23
- Thread.current[THREAD_CONTEXT_KEY] = original
38
+ storage[THREAD_CONTEXT_KEY] = original
39
+ invalidate_sanitized_context_cache(storage)
24
40
  end
25
41
 
26
42
  # Returns the current thread-local context when called without arguments,
@@ -42,8 +58,9 @@ module JsonLogging
42
58
  end
43
59
 
44
60
  begin
45
- base_context = (Thread.current[THREAD_CONTEXT_KEY] || {}).dup
46
- rescue
61
+ base_context = (context_storage[THREAD_CONTEXT_KEY] || {}).dup
62
+ rescue => e
63
+ warn_additional_context_once(:dup, "thread context dup failed", e)
47
64
  base_context = {}
48
65
  end
49
66
 
@@ -51,7 +68,8 @@ module JsonLogging
51
68
  if transformer.is_a?(Proc)
52
69
  begin
53
70
  transformer.call(base_context)
54
- rescue
71
+ rescue => e
72
+ warn_additional_context_once(:transformer, "additional_context transformer failed", e)
55
73
  base_context
56
74
  end
57
75
  else
@@ -61,6 +79,26 @@ module JsonLogging
61
79
 
62
80
  def self.additional_context=(proc_or_block)
63
81
  @additional_context_transformer = proc_or_block
82
+ invalidate_sanitized_context_cache(context_storage)
83
+ end
84
+
85
+ def self.additional_context_for_payload
86
+ if @additional_context_transformer.is_a?(Proc)
87
+ return sanitized_context_from(compact_context(additional_context))
88
+ end
89
+
90
+ storage = context_storage
91
+ cached = storage[SANITIZED_CONTEXT_CACHE_KEY]
92
+ return cached if cached
93
+
94
+ raw_context = storage[THREAD_CONTEXT_KEY]
95
+ if raw_context.nil? || (raw_context.is_a?(Hash) && raw_context.empty?)
96
+ return storage[SANITIZED_CONTEXT_CACHE_KEY] = {}.freeze
97
+ end
98
+
99
+ sanitized = sanitized_context_from(compact_context(raw_context))
100
+ storage[SANITIZED_CONTEXT_CACHE_KEY] = sanitized.freeze
101
+ sanitized
64
102
  end
65
103
 
66
104
  def self.safe_hash(obj)
@@ -69,6 +107,34 @@ module JsonLogging
69
107
  {}
70
108
  end
71
109
 
110
+ def self.warn_additional_context_once(key, message, error)
111
+ return if @additional_context_warned.include?(key)
112
+
113
+ @additional_context_warned.add(key)
114
+ warn "[activesupport-json_logging] #{message} (#{error.class}: #{error.message})"
115
+ end
116
+ private_class_method :warn_additional_context_once
117
+
118
+ def self.compact_context(context)
119
+ return {} unless context.is_a?(Hash)
120
+ return {} if context.empty?
121
+
122
+ context.compact
123
+ end
124
+ private_class_method :compact_context
125
+
126
+ def self.sanitized_context_from(compacted_context)
127
+ return {} if compacted_context.empty?
128
+
129
+ Sanitizer.sanitize_hash(compacted_context)
130
+ end
131
+ private_class_method :sanitized_context_from
132
+
133
+ def self.invalidate_sanitized_context_cache(storage)
134
+ storage[SANITIZED_CONTEXT_CACHE_KEY] = nil
135
+ end
136
+ private_class_method :invalidate_sanitized_context_cache
137
+
72
138
  # Returns an `ActiveSupport::Logger` that has already been wrapped with JSON logging concern.
73
139
  #
74
140
  # @param *args Arguments passed to ActiveSupport::Logger.new
@@ -97,6 +163,17 @@ module JsonLogging
97
163
  # logger.tagged("BCX") { logger.info("Stuff") } # Logs with tags
98
164
  # logger.tagged("BCX").info("Stuff") # Logs with tags (non-block form)
99
165
  def self.new(logger)
166
+ logger = prepare_logger_clone(logger)
167
+ logger.extend(JsonLoggerExtension)
168
+ formatter_with_tags = FormatterWithTags.new(logger)
169
+ logger.instance_variable_set(:@formatter_with_tags, formatter_with_tags)
170
+ # Custom formatters are not supported: JSON output requires FormatterWithTags.
171
+ logger.formatter = formatter_with_tags
172
+
173
+ logger
174
+ end
175
+
176
+ def self.prepare_logger_clone(logger)
100
177
  logger = logger.clone
101
178
  if logger.formatter
102
179
  logger.formatter = logger.formatter.clone
@@ -114,11 +191,7 @@ module JsonLogging
114
191
  end
115
192
  end
116
193
 
117
- logger.extend(JsonLoggerExtension)
118
- formatter_with_tags = FormatterWithTags.new(logger)
119
- logger.instance_variable_set(:@formatter_with_tags, formatter_with_tags)
120
- logger.formatter = formatter_with_tags
121
-
122
194
  logger
123
195
  end
196
+ private_class_method :prepare_logger_clone
124
197
  end
@@ -0,0 +1,3 @@
1
+ module JsonLogging
2
+ VERSION: String
3
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activesupport-json_logging
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-11-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -64,6 +63,20 @@ dependencies:
64
63
  - - "~>"
65
64
  - !ruby/object:Gem::Version
66
65
  version: '3'
66
+ - !ruby/object:Gem::Dependency
67
+ name: polyrun
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 2.2.0
73
+ type: :development
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 2.2.0
67
80
  - !ruby/object:Gem::Dependency
68
81
  name: webmock
69
82
  requirement: !ruby/object:Gem::Requirement
@@ -93,61 +106,131 @@ dependencies:
93
106
  - !ruby/object:Gem::Version
94
107
  version: '13'
95
108
  - !ruby/object:Gem::Dependency
96
- name: simplecov
109
+ name: rspec_junit_formatter
97
110
  requirement: !ruby/object:Gem::Requirement
98
111
  requirements:
99
112
  - - "~>"
100
113
  - !ruby/object:Gem::Version
101
- version: '0.22'
114
+ version: '0.6'
102
115
  type: :development
103
116
  prerelease: false
104
117
  version_requirements: !ruby/object:Gem::Requirement
105
118
  requirements:
106
119
  - - "~>"
107
120
  - !ruby/object:Gem::Version
108
- version: '0.22'
121
+ version: '0.6'
109
122
  - !ruby/object:Gem::Dependency
110
- name: rspec_junit_formatter
123
+ name: standard
111
124
  requirement: !ruby/object:Gem::Requirement
112
125
  requirements:
113
126
  - - "~>"
114
127
  - !ruby/object:Gem::Version
115
- version: '0.6'
128
+ version: '1.52'
116
129
  type: :development
117
130
  prerelease: false
118
131
  version_requirements: !ruby/object:Gem::Requirement
119
132
  requirements:
120
133
  - - "~>"
121
134
  - !ruby/object:Gem::Version
122
- version: '0.6'
135
+ version: '1.52'
123
136
  - !ruby/object:Gem::Dependency
124
- name: simplecov-cobertura
137
+ name: standard-custom
125
138
  requirement: !ruby/object:Gem::Requirement
126
139
  requirements:
127
140
  - - "~>"
128
141
  - !ruby/object:Gem::Version
129
- version: '3'
142
+ version: '1.0'
130
143
  type: :development
131
144
  prerelease: false
132
145
  version_requirements: !ruby/object:Gem::Requirement
133
146
  requirements:
134
147
  - - "~>"
135
148
  - !ruby/object:Gem::Version
136
- version: '3'
149
+ version: '1.0'
137
150
  - !ruby/object:Gem::Dependency
138
- name: standard
151
+ name: standard-performance
139
152
  requirement: !ruby/object:Gem::Requirement
140
153
  requirements:
141
154
  - - "~>"
142
155
  - !ruby/object:Gem::Version
143
- version: '1'
156
+ version: '1.8'
144
157
  type: :development
145
158
  prerelease: false
146
159
  version_requirements: !ruby/object:Gem::Requirement
147
160
  requirements:
148
161
  - - "~>"
149
162
  - !ruby/object:Gem::Version
150
- version: '1'
163
+ version: '1.8'
164
+ - !ruby/object:Gem::Dependency
165
+ name: standard-rails
166
+ requirement: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - "~>"
169
+ - !ruby/object:Gem::Version
170
+ version: '1.5'
171
+ type: :development
172
+ prerelease: false
173
+ version_requirements: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - "~>"
176
+ - !ruby/object:Gem::Version
177
+ version: '1.5'
178
+ - !ruby/object:Gem::Dependency
179
+ name: standard-rspec
180
+ requirement: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - "~>"
183
+ - !ruby/object:Gem::Version
184
+ version: '0.3'
185
+ type: :development
186
+ prerelease: false
187
+ version_requirements: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - "~>"
190
+ - !ruby/object:Gem::Version
191
+ version: '0.3'
192
+ - !ruby/object:Gem::Dependency
193
+ name: rubocop-rails
194
+ requirement: !ruby/object:Gem::Requirement
195
+ requirements:
196
+ - - "~>"
197
+ - !ruby/object:Gem::Version
198
+ version: '2.33'
199
+ type: :development
200
+ prerelease: false
201
+ version_requirements: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - "~>"
204
+ - !ruby/object:Gem::Version
205
+ version: '2.33'
206
+ - !ruby/object:Gem::Dependency
207
+ name: rubocop-rspec
208
+ requirement: !ruby/object:Gem::Requirement
209
+ requirements:
210
+ - - "~>"
211
+ - !ruby/object:Gem::Version
212
+ version: '3.8'
213
+ type: :development
214
+ prerelease: false
215
+ version_requirements: !ruby/object:Gem::Requirement
216
+ requirements:
217
+ - - "~>"
218
+ - !ruby/object:Gem::Version
219
+ version: '3.8'
220
+ - !ruby/object:Gem::Dependency
221
+ name: rubocop-thread_safety
222
+ requirement: !ruby/object:Gem::Requirement
223
+ requirements:
224
+ - - "~>"
225
+ - !ruby/object:Gem::Version
226
+ version: '0.7'
227
+ type: :development
228
+ prerelease: false
229
+ version_requirements: !ruby/object:Gem::Requirement
230
+ requirements:
231
+ - - "~>"
232
+ - !ruby/object:Gem::Version
233
+ version: '0.7'
151
234
  - !ruby/object:Gem::Dependency
152
235
  name: appraisal
153
236
  requirement: !ruby/object:Gem::Requirement
@@ -190,6 +273,48 @@ dependencies:
190
273
  - - "~>"
191
274
  - !ruby/object:Gem::Version
192
275
  version: '3'
276
+ - !ruby/object:Gem::Dependency
277
+ name: benchmark
278
+ requirement: !ruby/object:Gem::Requirement
279
+ requirements:
280
+ - - "~>"
281
+ - !ruby/object:Gem::Version
282
+ version: '0.5'
283
+ type: :development
284
+ prerelease: false
285
+ version_requirements: !ruby/object:Gem::Requirement
286
+ requirements:
287
+ - - "~>"
288
+ - !ruby/object:Gem::Version
289
+ version: '0.5'
290
+ - !ruby/object:Gem::Dependency
291
+ name: benchmark-ips
292
+ requirement: !ruby/object:Gem::Requirement
293
+ requirements:
294
+ - - "~>"
295
+ - !ruby/object:Gem::Version
296
+ version: '2'
297
+ type: :development
298
+ prerelease: false
299
+ version_requirements: !ruby/object:Gem::Requirement
300
+ requirements:
301
+ - - "~>"
302
+ - !ruby/object:Gem::Version
303
+ version: '2'
304
+ - !ruby/object:Gem::Dependency
305
+ name: stackprof
306
+ requirement: !ruby/object:Gem::Requirement
307
+ requirements:
308
+ - - "~>"
309
+ - !ruby/object:Gem::Version
310
+ version: '0.2'
311
+ type: :development
312
+ prerelease: false
313
+ version_requirements: !ruby/object:Gem::Requirement
314
+ requirements:
315
+ - - "~>"
316
+ - !ruby/object:Gem::Version
317
+ version: '0.2'
193
318
  description: Lightweight JSON logger and formatter integrating with Rails/ActiveSupport.
194
319
  No extra deps beyond Rails/Activesupport. Compatible with Rails 6–8.
195
320
  email:
@@ -209,10 +334,15 @@ files:
209
334
  - lib/json_logging/helpers.rb
210
335
  - lib/json_logging/json_logger.rb
211
336
  - lib/json_logging/json_logger_extension.rb
337
+ - lib/json_logging/line_encoder.rb
212
338
  - lib/json_logging/message_parser.rb
213
339
  - lib/json_logging/payload_builder.rb
214
340
  - lib/json_logging/sanitizer.rb
341
+ - lib/json_logging/severity.rb
342
+ - lib/json_logging/structured_hash_json_encoder.rb
343
+ - lib/json_logging/structured_hash_sanitizer.rb
215
344
  - lib/json_logging/version.rb
345
+ - sig/json_logging.rbs
216
346
  homepage: https://github.com/amkisko/activesupport-json_logging.rb
217
347
  licenses:
218
348
  - MIT
@@ -220,7 +350,6 @@ metadata:
220
350
  source_code_uri: https://github.com/amkisko/activesupport-json_logging.rb
221
351
  changelog_uri: https://github.com/amkisko/activesupport-json_logging.rb/blob/main/CHANGELOG.md
222
352
  bug_tracker_uri: https://github.com/amkisko/activesupport-json_logging.rb/issues
223
- post_install_message:
224
353
  rdoc_options: []
225
354
  require_paths:
226
355
  - lib
@@ -235,8 +364,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
235
364
  - !ruby/object:Gem::Version
236
365
  version: '0'
237
366
  requirements: []
238
- rubygems_version: 3.5.9
239
- signing_key:
367
+ rubygems_version: 4.0.3
240
368
  specification_version: 4
241
369
  summary: Structured JSON logging for Rails/ActiveSupport with safe, single-line entries.
242
370
  test_files: []