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 +7 -0
- data/CHANGELOG.md +44 -0
- data/LICENSE.md +23 -0
- data/README.md +541 -0
- data/lib/activesupport/json_logging/railtie.rb +8 -0
- data/lib/activesupport/json_logging.rb +7 -0
- data/lib/json_logging/formatter.rb +36 -0
- data/lib/json_logging/formatter_with_tags.rb +57 -0
- data/lib/json_logging/helpers.rb +17 -0
- data/lib/json_logging/json_logger.rb +31 -0
- data/lib/json_logging/json_logger_extension.rb +177 -0
- data/lib/json_logging/message_parser.rb +32 -0
- data/lib/json_logging/payload_builder.rb +46 -0
- data/lib/json_logging/sanitizer.rb +158 -0
- data/lib/json_logging/version.rb +3 -0
- data/lib/json_logging.rb +124 -0
- metadata +186 -0
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
|
+
[](https://badge.fury.io/rb/activesupport-json_logging) [](https://github.com/amkisko/activesupport-json_logging.rb/actions/workflows/ci.yml) [](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,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
|