activesupport-json_logging 1.0.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b542fc320d19a1789f47805250847291ca671676834f66f1b99257d0311706ef
4
+ data.tar.gz: 8dac946f3ee55c6b22ff4d7996e12c6b6ea220434c7ba4514a7495ce251c7504
5
+ SHA512:
6
+ metadata.gz: 82f446cdb9ddc23c91ce99df51f90fd4d6cc2f8eecdaa45276bb8ff8aa074cb1ef07bde42b1ce3e2fc907e34e2e235a544cd54d36a1da5016a0fcf621b8b397b
7
+ data.tar.gz: 21cc1fe28c94ae215a2293fe5f934bc9f139bba4affd9431b0aa41f0c0620e30ab456c0c43bd4b834e61d4e89a6d2a5876c3fdc47d2ca21b2d4163f4a3327578
data/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 (2024-10-31)
4
+
5
+ First stable release.
6
+
7
+ - feat: single-line JSON format compatible with cloud logging services (GCP, AWS, Azure)
8
+ - feat: native tagged logging with `logger.tagged("TAG")` API compatible with Rails
9
+ - feat: thread-safe context via `JsonLogging.with_context` for per-thread fields
10
+ - feat: smart message parsing for hashes, JSON strings, and plain strings
11
+ - feat: inherit all ActiveSupport::Logger features (silence, local_level, etc.)
12
+ - feat: BroadcastLogger compatibility for Rails 7.1+ automatic wrapping
13
+ - feat: timestamp precision in microseconds (iso8601 with 6 decimals)
14
+ - feat: Rails ParameterFilter integration for automatic sensitive data filtering
15
+ - feat: input sanitization removing control characters and truncating long strings
16
+ - feat: sensitive key pattern matching fallback when ParameterFilter unavailable
17
+ - feat: depth and size limits for nested structures to prevent log bloat
18
+ - feat: single-line JSON output to prevent log injection via newlines
19
+ - feat: graceful error handling with fallback entries on serialization errors
20
+ - feat: Rails 6.0, 6.1, 7.0, 7.1, 7.2, 8.0 support
21
+ - feat: IsolatedExecutionState for thread/Fiber isolation (Rails 7.1+)
22
+ - feat: backward compatible fallback to Thread.current for Rails 6-7.0
23
+ - feat: kwargs support in logger initialization for Rails 7+
24
+ - perf: ~0.006ms per log entry overhead (250-400% vs plain text, typical for JSON)
25
+ - perf: memory efficient with ~3KB per entry and zero retained memory
26
+ - feat: performance benchmarks with memory profiling included
27
+ - test: 93.78% code coverage with comprehensive RSpec suite
28
+ - test: BroadcastLogger integration tests
29
+ - test: IsolatedExecutionState thread safety tests
30
+ - test: Appraisals configured for multi-version testing (Rails 6-8)
31
+ - test: GitHub Actions CI workflow
32
+ - docs: complete README with installation, usage, and API docs
33
+ - docs: Rails environment configuration examples (development, production, test)
34
+ - docs: Lograge integration with third-party logger configurations
35
+ - docs: Puma integration example
36
+ - docs: security best practices and ParameterFilter guide
37
+ - docs: inherited Rails logger features documentation
38
+
39
+ ---
40
+
41
+ ## 0.1.0
42
+
43
+ - feat: initial JSON formatter and logger implementation
44
+ - test: basic RSpec test coverage
data/LICENSE.md ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+
data/README.md ADDED
@@ -0,0 +1,541 @@
1
+ # activesupport-json_logging
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/activesupport-json_logging.svg)](https://badge.fury.io/rb/activesupport-json_logging) [![Test Status](https://github.com/amkisko/activesupport-json_logging.rb/actions/workflows/ci.yml/badge.svg)](https://github.com/amkisko/activesupport-json_logging.rb/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/amkisko/activesupport-json_logging.rb/graph/badge.svg?token=881AFPL643)](https://codecov.io/gh/amkisko/activesupport-json_logging.rb)
4
+
5
+ Structured JSON logging for Rails and ActiveSupport with a safe, single-line formatter.
6
+ No dependencies beyond Rails and Activesupport.
7
+ Supports Rails versions from 6 to 8.
8
+
9
+ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
10
+
11
+ ## Installation
12
+
13
+ Add to your Gemfile:
14
+
15
+ ```ruby
16
+ gem "activesupport-json_logging"
17
+ ```
18
+
19
+ Run bundler:
20
+
21
+ ```bash
22
+ bundle install
23
+ ```
24
+
25
+ Update configuration:
26
+
27
+ ```ruby
28
+ Rails.application.configure do
29
+ config.logger = JsonLogging.new(Logger.new(STDOUT))
30
+ end
31
+ ```
32
+
33
+ ### Development: Using from Local Repository
34
+
35
+ When developing the gem or testing changes in your application, you can point your Gemfile to a local path:
36
+
37
+ ```ruby
38
+ # In your application's Gemfile
39
+ gem "activesupport-json_logging", path: "../activesupport-json_logging.rb"
40
+ ```
41
+
42
+ Then run:
43
+
44
+ ```bash
45
+ bundle install
46
+ ```
47
+
48
+ **Note:** When using `path:` in your Gemfile, Bundler will use the local gem directly. Changes you make to the gem code will be immediately available in your application without needing to rebuild or reinstall the gem. This is ideal for development and testing.
49
+
50
+ ## What you get
51
+
52
+ - `JsonLogging.new(logger)` - Wraps any standard Logger object to provide JSON formatting (similar to `ActiveSupport::TaggedLogging.new`)
53
+ - `JsonLogging.logger(*args)` - Convenience method that creates an `ActiveSupport::Logger` and wraps it
54
+ - Safe JSON serialization that never raises from the formatter
55
+ - `JsonLogging.with_context` to attach contextual fields per-thread
56
+ - Smart message parsing (handles hashes, JSON strings, plain strings, and Exception objects)
57
+ - Native `tagged` method support - use it just like Rails' tagged logger
58
+ - Automatic Rails integration via Railtie (auto-requires the gem in Rails apps)
59
+
60
+ ## Basic usage
61
+
62
+ ```ruby
63
+ # Wrap any standard logger
64
+ logger = JsonLogging.new(Logger.new(STDOUT))
65
+ logger.info("Booted")
66
+
67
+ # Or use the convenience method
68
+ logger = JsonLogging.logger($stdout)
69
+ logger.info("Booted")
70
+
71
+ # Tagged logging - works just like Rails.logger.tagged
72
+ logger.tagged("REQUEST", request_id) do
73
+ logger.info("Processing request")
74
+ end
75
+
76
+ # Tagged logging without block (returns new logger with tags)
77
+ logger.tagged("BCX").info("Stuff")
78
+ logger.tagged("BCX", "Jason").info("Stuff")
79
+ logger.tagged("BCX").tagged("Jason").info("Stuff")
80
+
81
+ # Add context
82
+ JsonLogging.with_context(user_id: 123) do
83
+ logger.warn({event: "slow_query", duration_ms: 250})
84
+ end
85
+
86
+ # Log exceptions (automatically formatted with class, message, and backtrace)
87
+ begin
88
+ raise StandardError.new("Something went wrong")
89
+ rescue => e
90
+ logger.error(e) # Exception is automatically parsed and formatted
91
+ end
92
+ ```
93
+
94
+ ## Rails configuration
95
+
96
+ This gem does **not** automatically configure your Rails app. You set it up manually in initializers or environment configs.
97
+
98
+ **Note:**
99
+ - In Rails apps, the gem is automatically required via Railtie, so you typically don't need to manually `require "json_logging"` in initializers (though it's harmless if you do).
100
+ - In Rails 7.1+, Rails automatically wraps your logger in `ActiveSupport::BroadcastLogger` to enable writing to multiple destinations (e.g., STDOUT and file simultaneously). This works seamlessly with our logger - your JSON logger will be wrapped and all method calls will delegate correctly. No special handling needed.
101
+ - In Rails 7.1+, tag storage uses `ActiveSupport::IsolatedExecutionState` for improved thread/Fiber safety.
102
+
103
+ ### Basic setup
104
+
105
+ Create `config/initializers/json_logging.rb`:
106
+
107
+ ```ruby
108
+ # Note: In Rails apps, the gem is automatically required via Railtie,
109
+ # so the require below is optional but harmless
110
+ # require "json_logging"
111
+
112
+ Rails.application.configure do
113
+ # Build JSON logger
114
+ base_logger = ActiveSupport::Logger.new($stdout)
115
+ json_logger = JsonLogging.new(base_logger)
116
+
117
+ # Set as Rails logger
118
+ config.logger = json_logger
119
+
120
+ # Optional: set log tags for request_id, etc.
121
+ config.log_tags = [:request_id, :remote_ip]
122
+
123
+ # Set component loggers
124
+ config.active_record.logger = json_logger
125
+ config.action_view.logger = json_logger
126
+ config.action_mailer.logger = json_logger
127
+ config.active_job.logger = json_logger
128
+ end
129
+ ```
130
+
131
+ ### Environment-specific examples
132
+
133
+ #### Development (`config/environments/development.rb`)
134
+
135
+ ```ruby
136
+ Rails.application.configure do
137
+ config.log_level = ENV["DEBUG"].present? ? :debug : :info
138
+
139
+ # Set up JSON logging
140
+ base_logger = ActiveSupport::Logger.new($stdout)
141
+ base_logger.level = config.log_level
142
+ json_logger = JsonLogging.new(base_logger)
143
+ config.logger = json_logger
144
+ config.log_tags = [:request_id]
145
+
146
+ # Set component loggers
147
+ config.active_record.logger = json_logger
148
+ config.action_view.logger = json_logger
149
+ config.action_mailer.logger = json_logger
150
+ config.active_job.logger = json_logger
151
+
152
+ # Disable verbose enqueue logs to reduce noise
153
+ config.active_job.verbose_enqueue_logs = false
154
+
155
+ # ... rest of your development config
156
+ end
157
+ ```
158
+
159
+ #### Production (`config/environments/production.rb`)
160
+
161
+ ```ruby
162
+ Rails.application.configure do
163
+ config.log_level = :info
164
+ config.log_tags = [:request_id]
165
+
166
+ # Set up JSON logging
167
+ logdev = Rails.root.join("log", "production.log")
168
+ base_logger = ActiveSupport::Logger.new(logdev)
169
+ base_logger.level = config.log_level
170
+ json_logger = JsonLogging.new(base_logger)
171
+ config.logger = json_logger
172
+
173
+ # Set component loggers
174
+ config.active_record.logger = json_logger
175
+ config.action_view.logger = json_logger
176
+ config.action_mailer.logger = json_logger
177
+ config.active_job.logger = json_logger
178
+
179
+ # ... rest of your production config
180
+ end
181
+ ```
182
+
183
+ #### Test (`config/environments/test.rb`)
184
+
185
+ ```ruby
186
+ Rails.application.configure do
187
+ # Set log level to fatal to reduce noise during tests
188
+ config.log_level = ENV["DEBUG"].present? ? :debug : :fatal
189
+
190
+ # Optionally use JSON logger in tests too
191
+ if ENV["JSON_LOGS"] == "true"
192
+ base_logger = ActiveSupport::Logger.new($stdout)
193
+ base_logger.level = config.log_level
194
+ json_logger = JsonLogging.new(base_logger)
195
+ config.logger = json_logger
196
+ end
197
+
198
+ # ... rest of your test config
199
+ end
200
+ ```
201
+
202
+ ### Lograge integration
203
+
204
+ If you use Lograge, configure it to feed raw hashes and let this gem handle JSON formatting. This example shows a complete setup including all Rails component loggers and common third-party libraries:
205
+
206
+ ```ruby
207
+ # config/initializers/lograge.rb
208
+ # Note: require is optional in Rails apps (auto-loaded via Railtie)
209
+ # require "json_logging"
210
+
211
+ Rails.application.configure do
212
+ # Configure Lograge
213
+ config.lograge.enabled = true
214
+ # Use Raw formatter so we pass a Hash to our JSON logger and avoid double serialization
215
+ config.lograge.formatter = Lograge::Formatters::Raw.new
216
+ config.lograge.keep_original_rails_log = ENV["DEBUG"] ? true : false
217
+
218
+ # Merge additional context into Lograge output
219
+ config.lograge.custom_options = ->(_event) { JsonLogging.additional_context }
220
+
221
+ # Build unified JSON logger
222
+ logdev = Rails.env.production? ? Rails.root.join("log", "#{Rails.env}.log") : $stdout
223
+ base_logger = ActiveSupport::Logger.new(logdev)
224
+ base_logger.level = config.log_level
225
+ json_logger = JsonLogging.new(base_logger)
226
+
227
+ # Set the main Rails logger
228
+ config.logger = json_logger
229
+ config.log_tags = [:request_id, :remote_ip]
230
+
231
+ # Override Rails.logger to ensure it uses our formatter
232
+ Rails.logger = json_logger
233
+
234
+ # Set all Rails component loggers to use the same tagged logger
235
+ config.active_record.logger = json_logger
236
+ config.action_view.logger = json_logger
237
+ config.action_mailer.logger = json_logger
238
+ config.active_job.logger = json_logger
239
+
240
+ # Configure third-party library loggers (if gems are present)
241
+ OmniAuth.config.logger = json_logger if defined?(OmniAuth)
242
+ Sidekiq.logger = json_logger if defined?(Sidekiq)
243
+ Shrine.logger = json_logger if defined?(Shrine)
244
+ Sentry.configuration.sdk_logger = json_logger if defined?(Sentry)
245
+ Dotenv::Rails.logger = json_logger if defined?(Dotenv::Rails)
246
+ Webpacker.logger = json_logger if defined?(Webpacker)
247
+
248
+ # Disable verbose enqueue logs to reduce noise
249
+ config.active_job.verbose_enqueue_logs = false
250
+
251
+ # Optional: Customize log tags based on request
252
+ # config.log_tags = [
253
+ # :request_id,
254
+ # ->(request) { request.remote_ip },
255
+ # ->(request) { request.subdomain }
256
+ # ]
257
+ end
258
+ ```
259
+
260
+ ### Puma integration
261
+
262
+ To make Puma output JSON lines, configure it in `config/puma.rb`:
263
+
264
+ ```ruby
265
+ # config/puma.rb
266
+ # Note: require is optional in Rails apps (auto-loaded via Railtie)
267
+ # require "json_logging"
268
+
269
+ # ... puma config ...
270
+
271
+ log_formatter do |message|
272
+ formatter = JsonLogging::Formatter.new(tags: ["Puma"])
273
+ formatter.call(nil, Time.current, nil, message).strip
274
+ end
275
+
276
+ # Optional: handle low-level errors
277
+ lowlevel_error_handler do |e|
278
+ # Your error reporting logic here
279
+ [
280
+ 500,
281
+ {"Content-Type" => "application/json"},
282
+ [{error: "Critical error has occurred"}.to_json]
283
+ ]
284
+ end
285
+ ```
286
+
287
+ ## API
288
+
289
+ ### JsonLogging.logger(*args, **kwargs)
290
+
291
+ Returns an `ActiveSupport::Logger` that has already been wrapped with JSON logging concern. Convenience method for creating a logger and wrapping it in one call.
292
+
293
+ ```ruby
294
+ logger = JsonLogging.logger($stdout)
295
+ logger.info("message")
296
+ ```
297
+
298
+ ### JsonLogging.new(logger)
299
+
300
+ Wraps any standard Logger object to provide JSON formatting capabilities. Similar to `ActiveSupport::TaggedLogging.new`.
301
+
302
+ ```ruby
303
+ # Wrap any standard logger
304
+ logger = JsonLogging.new(Logger.new(STDOUT))
305
+ logger.info("message")
306
+ logger.info({event: "test", value: 123}) # Hashes are merged into payload
307
+
308
+ # Log exceptions (automatically formatted with class, message, and backtrace)
309
+ begin
310
+ raise StandardError.new("Error message")
311
+ rescue => e
312
+ logger.error(e) # Exception parsed and formatted automatically
313
+ end
314
+
315
+ # Tagged logging with block
316
+ logger.tagged("REQUEST", request_id) do
317
+ logger.info("tagged message")
318
+ end
319
+
320
+ # Tagged logging without block (returns new logger with tags)
321
+ logger.tagged("BCX").info("Stuff")
322
+ logger.tagged("BCX", "Jason").info("Stuff")
323
+ logger.tagged("BCX").tagged("Jason").info("Stuff")
324
+ ```
325
+
326
+ **Wrapping Compatibility:** You can wrap loggers that have already been wrapped with `ActiveSupport::TaggedLogging`:
327
+
328
+ ```ruby
329
+ # Wrap a TaggedLogging logger - works perfectly
330
+ tagged_logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
331
+ json_logger = JsonLogging.new(tagged_logger)
332
+ json_logger.tagged("TEST") { json_logger.info("message") } # Tags appear in JSON context
333
+ ```
334
+
335
+ **Note:** If you wrap a `JsonLogging` logger with `ActiveSupport::TaggedLogging`, the TaggedLogging's text-based tags will appear as part of the message string in the JSON output, not as structured tags in the context. For best results, wrap loggers with `JsonLogging` last.
336
+
337
+ ### JsonLogging::JsonLogger
338
+
339
+ `JsonLogging::JsonLogger` is a class that extends `ActiveSupport::Logger` directly. While still fully functional, the recommended approach is to use `JsonLogging.new` to wrap any logger, as it provides more flexibility and works with any Logger implementation (including loggers already wrapped with `ActiveSupport::TaggedLogging`).
340
+
341
+ ```ruby
342
+ # Direct usage (still supported)
343
+ logger = JsonLogging::JsonLogger.new($stdout)
344
+ logger.info("message")
345
+
346
+ # Recommended: wrap any logger
347
+ logger = JsonLogging.new(ActiveSupport::Logger.new($stdout))
348
+ logger.info("message")
349
+ ```
350
+
351
+ ### JsonLogging::Formatter
352
+
353
+ A standalone formatter that can be used independently (e.g., in Puma's `log_formatter`). Supports adding tags via the constructor.
354
+
355
+ ```ruby
356
+ # Basic usage without tags
357
+ formatter = JsonLogging::Formatter.new
358
+ formatter.call("INFO", Time.now, nil, "message")
359
+
360
+ # With tags (useful for Puma or other standalone use cases)
361
+ formatter = JsonLogging::Formatter.new(tags: ["Puma"])
362
+ formatter.call("INFO", Time.now, nil, "message") # Output includes "Puma" tag in context
363
+
364
+ # Multiple tags
365
+ formatter = JsonLogging::Formatter.new(tags: ["Puma", "Worker"])
366
+ formatter.call("INFO", Time.now, nil, "message") # Output includes both tags
367
+ ```
368
+
369
+ **Note:** When used with a logger (via `JsonLogging.new`), the logger uses `FormatterWithTags` which automatically includes tags from the logger's tagged context. Use `Formatter` directly only when you need a standalone formatter without a logger instance.
370
+
371
+ ### JsonLogging.with_context
372
+
373
+ Add thread-local context that appears in all log entries within the block:
374
+
375
+ ```ruby
376
+ JsonLogging.with_context(user_id: 123, request_id: "abc") do
377
+ logger.info("message") # Will include user_id and request_id in context
378
+ end
379
+ ```
380
+
381
+ ### JsonLogging.additional_context
382
+
383
+ Returns the current thread-local context when called without arguments, or sets a transformer when called with a block or assigned a proc.
384
+
385
+ **Getting context:**
386
+ ```ruby
387
+ JsonLogging.with_context(user_id: 5) do
388
+ JsonLogging.additional_context # => {user_id: 5}
389
+ end
390
+ ```
391
+
392
+ **Setting a transformer:**
393
+ You can customize how `additional_context` is built by setting a transformer. This is useful for adding default fields, computed values, or filtering context. Supports both block and assignment syntax. Note: keys are automatically stringified, so no key transformation is needed.
394
+
395
+ ```ruby
396
+ # Using a block (recommended)
397
+ JsonLogging.additional_context do |context|
398
+ context.merge(
399
+ environment: Rails.env,
400
+ hostname: Socket.gethostname,
401
+ app_version: MyApp::VERSION
402
+ )
403
+ end
404
+
405
+ # Using assignment with a proc
406
+ JsonLogging.additional_context = ->(context) do
407
+ context.merge(environment: Rails.env, hostname: Socket.gethostname)
408
+ end
409
+
410
+ # Add computed values based on current request
411
+ JsonLogging.additional_context do |context|
412
+ context.merge(
413
+ request_id: Current.request_id,
414
+ user_agent: Current.user_agent,
415
+ ip_address: Current.ip_address
416
+ )
417
+ end
418
+
419
+ # Filter out nil values
420
+ JsonLogging.additional_context do |context|
421
+ context.compact
422
+ end
423
+ ```
424
+
425
+ The transformer receives the current thread-local context hash and should return a hash. If the transformer raises an error, the base context will be returned to avoid breaking logging.
426
+
427
+ **Note:** The transformer is called every time a log entry is created, so keep it lightweight to avoid performance issues.
428
+
429
+ ### Inherited Rails Logger Features
430
+
431
+ Since `JsonLogging::JsonLogger` extends `ActiveSupport::Logger`, it inherits all standard Rails logger features:
432
+
433
+ #### Silencing Logs
434
+
435
+ Temporarily silence logs below a certain severity level:
436
+
437
+ ```ruby
438
+ logger.silence(Logger::ERROR) do
439
+ logger.debug("This won't be logged")
440
+ logger.info("This won't be logged")
441
+ logger.warn("This won't be logged")
442
+ logger.error("This WILL be logged")
443
+ end
444
+ ```
445
+
446
+ #### Thread-Local Log Levels (Rails 7.1+)
447
+
448
+ Set a log level that only affects the current thread/Fiber:
449
+
450
+ ```ruby
451
+ # Set thread-local level
452
+ logger.local_level = :debug
453
+
454
+ # Only this thread will log at debug level
455
+ logger.debug("Debug message") # Will be logged
456
+
457
+ # Other threads still use the global level
458
+ ```
459
+
460
+ This is useful for:
461
+ - Debugging specific requests without changing global log level
462
+ - Temporary verbose logging in background jobs
463
+ - Per-request log level changes
464
+
465
+ **Note:** `local_level` is available in Rails 7.1+. In Rails 6-7.0, only the global `level` is available.
466
+
467
+ #### Standard Logger Methods
468
+
469
+ All standard Ruby Logger and ActiveSupport::Logger methods work:
470
+
471
+ ```ruby
472
+ logger.level = Logger::WARN # Set log level
473
+ logger.level # Get current level
474
+ logger.debug? # Check if debug level is enabled
475
+ logger.info? # Check if info level is enabled
476
+ logger.close # Close the logger
477
+ logger.reopen # Reopen the logger (if supported by logdev)
478
+ ```
479
+
480
+ ## Features
481
+
482
+ - **Native tagged logging**: Use `logger.tagged("TAG")` just like Rails' tagged logger
483
+ - **Smart message parsing**: Automatically handles hashes, JSON strings, plain strings, and Exception objects (with class, message, and backtrace)
484
+ - **Thread-safe context**: `JsonLogging.with_context` works across threads
485
+ - **Rails 7.1+ thread/Fiber safety**: Uses `ActiveSupport::IsolatedExecutionState` for improved concurrency
486
+ - **Never raises**: Formatter and logger methods handle errors gracefully with fallback entries
487
+ - **Single-line JSON**: Each log entry is a single line, safe for log aggregation tools
488
+
489
+ ## Security & privacy
490
+
491
+ - **Rails ParameterFilter integration**: Automatically uses `Rails.application.config.filter_parameters` to filter sensitive data (passwords, tokens, etc.). This includes encrypted attributes automatically. See [Rails parameter filtering guide](https://thoughtbot.com/blog/parameter-filtering).
492
+ - **Input sanitization**: Removes control characters, truncates long strings, and limits structure depth/size:
493
+ - Maximum string length: 10,000 characters (truncated with `...[truncated]` suffix)
494
+ - Maximum context hash size: 50 keys (additional keys are truncated)
495
+ - Maximum nesting depth: 10 levels (deeper structures return `{"error" => "max_depth_exceeded"}`)
496
+ - Maximum backtrace lines: 20 lines per exception
497
+ - **Single-line JSON**: Emits single-line JSON to avoid log injection via newlines
498
+ - **Never fails**: Formatter and logger never raise; fallback to safe entries on serialization errors
499
+ - **Sensitive key detection**: Falls back to pattern matching when Rails ParameterFilter isn't available
500
+ - **You control context**: You decide what goes into context via `JsonLogging.with_context`; avoid sensitive data
501
+
502
+ ### Configuring sensitive parameter filtering
503
+
504
+ This gem automatically uses Rails' `config.filter_parameters` when available. Configure it in `config/initializers/filter_parameter_logging.rb`:
505
+
506
+ ```ruby
507
+ Rails.application.config.filter_parameters += [
508
+ :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
509
+ ]
510
+ ```
511
+
512
+ The gem will automatically filter these from all log entries, including context data. Encrypted attributes (using Rails 7+ `encrypts`) are automatically filtered as well.
513
+
514
+ ## Development
515
+
516
+ ```bash
517
+ # Install dependencies
518
+ bundle install
519
+ bundle exec appraisal install
520
+
521
+ # Run tests for current Rails version
522
+ bundle exec rspec
523
+
524
+ # Run tests for all Rails versions (6.0, 6.1, 7.0, 7.1, 7.2, 8.0)
525
+ bin/appraisals
526
+
527
+ # Run tests for specific Rails version
528
+ bin/appraisals rails-7.0
529
+
530
+ # Run tests for multiple versions
531
+ bin/appraisals rails-7.0 rails-8.0
532
+
533
+ # Or use appraisal directly
534
+ bundle exec appraisal rails-7.0 rspec
535
+
536
+ bundle exec standardrb --fix
537
+ ```
538
+
539
+ ## License
540
+
541
+ MIT
@@ -0,0 +1,8 @@
1
+ require "json_logging"
2
+
3
+ module JsonLogging
4
+ class Railtie < ::Rails::Railtie
5
+ # This Railtie ensures json_logging is automatically required when Rails loads
6
+ # Without this, users would need to manually require "json_logging" in initializers
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # Auto-require file for activesupport-json_logging gem
2
+ # This file is automatically loaded by Bundler when the gem is required
3
+ require "json_logging"
4
+
5
+ # Ensure Railtie is loaded so Rails auto-discovers it
6
+ require "activesupport/json_logging/railtie" if defined?(Rails)
7
+
@@ -0,0 +1,36 @@
1
+ module JsonLogging
2
+ class Formatter < ::Logger::Formatter
3
+ def initialize(tags: [])
4
+ super()
5
+ @tags = Array(tags)
6
+ end
7
+
8
+ attr_reader :tags
9
+
10
+ def call(severity, timestamp, progname, msg)
11
+ timestamp_str = Helpers.normalize_timestamp(timestamp)
12
+ payload = PayloadBuilder.build_base_payload(msg, severity: severity, timestamp: timestamp_str)
13
+ payload = PayloadBuilder.merge_context(payload, additional_context: JsonLogging.additional_context.compact, tags: @tags)
14
+
15
+ "#{payload.compact.to_json}\n"
16
+ rescue => e
17
+ build_fallback_output(severity, timestamp, msg, e)
18
+ end
19
+
20
+ private
21
+
22
+ def build_fallback_output(severity, timestamp, msg, error)
23
+ timestamp_str = Helpers.normalize_timestamp(timestamp)
24
+ fallback_payload = {
25
+ timestamp: timestamp_str,
26
+ severity: severity,
27
+ message: Helpers.safe_string(msg),
28
+ formatter_error: {
29
+ class: Sanitizer.sanitize_string(error.class.name),
30
+ message: Sanitizer.sanitize_string(Helpers.safe_string(error.message))
31
+ }
32
+ }
33
+ "#{fallback_payload.compact.to_json}\n"
34
+ end
35
+ end
36
+ end