lumberjack_json_device 2.2.0 → 3.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 +4 -4
- data/.github/workflows/continuous_integration.yml +4 -3
- data/.standard.yml +1 -1
- data/CHANGE_LOG.md +40 -2
- data/README.md +128 -86
- data/VERSION +1 -1
- data/lib/lumberjack/json_device.rb +382 -0
- data/lib/lumberjack_json_device.rb +1 -281
- data/lumberjack_json_device.gemspec +8 -2
- metadata +11 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 93f11eda1a4055d1cc3b6ee2b9577e28f0a26ae937f88e49bc5c07aa0c1b327d
|
4
|
+
data.tar.gz: 96d500179158ea397ed2f52a3a096520125350cc673e5de5dbe352203b607d3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 00773c4aa83e49275b69fa0d400d8c17783008aa013a0b84b881acd868ab9e3ce6f39e5fc183b8d1ce2584c7e306363cfb7aa60d9045edacd100c66821601d85
|
7
|
+
data.tar.gz: 396d28c5697ded1ef621a959287c356b71a2e33942510cc72c88d32bd41e77c548d0d359df852e41a7ebeff1b60b839c092954e654a7dbbd6b2bc322b207eb1b
|
@@ -5,8 +5,6 @@ on:
|
|
5
5
|
branches:
|
6
6
|
- main
|
7
7
|
- actions-*
|
8
|
-
tags:
|
9
|
-
- v*
|
10
8
|
pull_request:
|
11
9
|
branches-ignore:
|
12
10
|
- actions-*
|
@@ -27,9 +25,9 @@ jobs:
|
|
27
25
|
include:
|
28
26
|
- ruby: "ruby"
|
29
27
|
standardrb: true
|
28
|
+
yard: true
|
30
29
|
- ruby: "3.0"
|
31
30
|
- ruby: "2.7"
|
32
|
-
- ruby: "2.5"
|
33
31
|
steps:
|
34
32
|
- uses: actions/checkout@v4
|
35
33
|
- name: Set up Ruby
|
@@ -44,3 +42,6 @@ jobs:
|
|
44
42
|
- name: standardrb
|
45
43
|
if: matrix.standardrb
|
46
44
|
run: bundle exec standardrb
|
45
|
+
- name: yard
|
46
|
+
if: matrix.yard
|
47
|
+
run: bundle exec yard --fail-on-warning
|
data/.standard.yml
CHANGED
data/CHANGE_LOG.md
CHANGED
@@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
+
## 3.0.0
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
- Support for Lumberjack 2.0.
|
12
|
+
- **Breaking Change** The constructor now takes an options hash rather than keyword arguments.
|
13
|
+
- Added the `:output` key to the constructor options hash to specify the output stream. This argument can take either a stream or a file path.
|
14
|
+
- Added `:utc` option to force timestamps to be in UTC.
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
|
18
|
+
- **Breaking Change** Tags are now called attributes and are put in the `"attributes"` JSON field by default. If you want to keep the old behavior, you will need to set the mapping to:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
{
|
22
|
+
time: true,
|
23
|
+
severity: true,
|
24
|
+
progname: true,
|
25
|
+
pid: true,
|
26
|
+
message: true,
|
27
|
+
attributes: ["tags"]
|
28
|
+
}
|
29
|
+
```
|
30
|
+
|
31
|
+
### Deprecated
|
32
|
+
|
33
|
+
- Deprecated passing in the output stream as the first positional argument. The output stream should now be specified using the `:output` key in the options hash.
|
34
|
+
|
35
|
+
### Removed
|
36
|
+
|
37
|
+
- Support for Ruby versions less than 2.7
|
38
|
+
|
39
|
+
## 2.2.1
|
40
|
+
|
41
|
+
## Changed
|
42
|
+
|
43
|
+
- Added error handling for JSON serialization. This ensures that logs can still be written to even if an error occurs during serialization.
|
44
|
+
|
7
45
|
## 2.2.0
|
8
46
|
|
9
47
|
### Changed
|
@@ -29,9 +67,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
29
67
|
|
30
68
|
### Changed
|
31
69
|
|
32
|
-
- Tag structure is now consistently expanded from dot notation into nested hashes in the `
|
70
|
+
- Tag structure is now consistently expanded from dot notation into nested hashes in the `attribute` field. Previoulsly this was only done when the template copied attributes to the root level of the JSON document.
|
33
71
|
- The mapping options now supports setting the value to `false` to exclude a field from the JSON output.
|
34
|
-
- Tag mapping can now be set to `"*"` to copy all
|
72
|
+
- Tag mapping can now be set to `"*"` to copy all attributes into the root of the JSON document.
|
35
73
|
|
36
74
|
### Removed
|
37
75
|
|
data/README.md
CHANGED
@@ -4,43 +4,49 @@
|
|
4
4
|
[](https://github.com/testdouble/standard)
|
5
5
|
[](https://badge.fury.io/rb/lumberjack_json_device)
|
6
6
|
|
7
|
-
This gem provides a logging device for the [lumberjack](https://github.com/bdurand/lumberjack) gem that outputs
|
7
|
+
This gem provides a logging device for the [lumberjack](https://github.com/bdurand/lumberjack) gem that outputs [JSONL](https://jsonlines.org/) formatted log entries to a stream. This format with one JSON document per line is ideal for structured logging pipelines and can be easily consumed by log aggregation services, search engines, and monitoring tools.
|
8
8
|
|
9
|
-
##
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
### Quick Start
|
10
12
|
|
11
13
|
```ruby
|
12
14
|
require 'lumberjack_json_device'
|
13
15
|
|
14
16
|
# Create a logger with JSON output to STDOUT
|
15
|
-
logger = Lumberjack::Logger.new(Lumberjack::JsonDevice.new(STDOUT))
|
17
|
+
logger = Lumberjack::Logger.new(Lumberjack::JsonDevice.new(output: STDOUT))
|
16
18
|
|
17
|
-
# Log a message with
|
19
|
+
# Log a message with attributes
|
18
20
|
logger.info("User logged in", user_id: 123, session_id: "abc")
|
19
|
-
# Output: {"time":"2020-01-02T19:47:45.123455-0800","severity":"INFO","progname":null,"pid":12345,"message":"User logged in","tags":{"user_id":123,"session_id":"abc"}}
|
20
21
|
```
|
21
22
|
|
22
|
-
|
23
|
+
This will output JSON like:
|
24
|
+
|
25
|
+
```
|
26
|
+
{ "time":"2020-01-02T19:47:45.123456-0800","severity":"INFO","progname":null,"pid":12345,"message":"User logged in","attributes":{ "user_id":123,"session_id":"abc" } }
|
27
|
+
```
|
28
|
+
|
29
|
+
### Output Destinations
|
23
30
|
|
24
31
|
You can send the JSON output to either a stream or to another Lumberjack device.
|
25
32
|
|
26
33
|
```ruby
|
27
|
-
# Send to STDOUT
|
28
|
-
device = Lumberjack::JsonDevice.new(STDOUT)
|
34
|
+
# Send to stream (STDOUT is the default)
|
35
|
+
device = Lumberjack::JsonDevice.new(output: STDOUT)
|
29
36
|
|
30
|
-
# Send to
|
31
|
-
|
32
|
-
device = Lumberjack::JsonDevice.new(log_file)
|
37
|
+
# Send to a log file
|
38
|
+
device = Lumberjack::JsonDevice.new(output: "/var/log/app.log")
|
33
39
|
```
|
34
40
|
|
35
|
-
|
41
|
+
### JSON Structure
|
36
42
|
|
37
43
|
By default, the JSON document maps to the `Lumberjack::LogEntry` data structure and includes all standard fields:
|
38
44
|
|
39
|
-
```
|
40
|
-
{"time": "2020-01-02T19:47:45.
|
45
|
+
```
|
46
|
+
{ "time": "2020-01-02T19:47:45.123456-0800", "severity": "INFO", "progname": "web", "pid": 101, "message": "test", "attributes": { "foo": "bar" } }
|
41
47
|
```
|
42
48
|
|
43
|
-
|
49
|
+
#### Custom Field Mapping
|
44
50
|
|
45
51
|
You can customize the JSON document structure by providing a mapping that specifies how log entry fields should be transformed. The mapping supports several different value types:
|
46
52
|
|
@@ -50,117 +56,146 @@ You can customize the JSON document structure by providing a mapping that specif
|
|
50
56
|
- **`false`**: Excludes the field from the JSON output
|
51
57
|
- **Callable**: Transforms the value using custom logic
|
52
58
|
|
53
|
-
You can map the standard field names (`time`, `severity`, `progname`, `pid`, `message`, and `
|
59
|
+
You can map the standard field names (`time`, `severity`, `progname`, `pid`, `message`, and `attributes`) as well as extract specific attributes by name.
|
54
60
|
|
55
61
|
```ruby
|
56
|
-
device = Lumberjack::JsonDevice.new(
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
62
|
+
device = Lumberjack::JsonDevice.new(
|
63
|
+
output: STDOUT,
|
64
|
+
mapping: {
|
65
|
+
time: "timestamp",
|
66
|
+
severity: "level",
|
67
|
+
progname: ["app", "name"],
|
68
|
+
pid: ["app", "pid"],
|
69
|
+
message: "message",
|
70
|
+
duration: "duration", # Extracts the "duration" attribute
|
71
|
+
attributes: "attributes"
|
72
|
+
}
|
73
|
+
)
|
65
74
|
```
|
66
75
|
|
67
|
-
|
68
|
-
|
76
|
+
Example output:
|
77
|
+
|
78
|
+
```
|
79
|
+
{ "timestamp": "2020-01-02T19:47:45.123456-0800", "level": "INFO", "app": { "name": "web", "pid": 101 }, "message": "test", "duration": 5, "attributes": { "foo": "bar" } }
|
69
80
|
```
|
70
81
|
|
71
|
-
|
82
|
+
#### Excluding Fields
|
72
83
|
|
73
84
|
If you omit fields from the mapping or set them to `false`, they will not appear in the JSON output:
|
74
85
|
|
75
86
|
```ruby
|
76
|
-
device = Lumberjack::JsonDevice.new(
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
87
|
+
device = Lumberjack::JsonDevice.new(
|
88
|
+
output: STDOUT,
|
89
|
+
mapping: {
|
90
|
+
time: "timestamp",
|
91
|
+
severity: "level",
|
92
|
+
message: "message",
|
93
|
+
pid: false # Exclude PID from output
|
94
|
+
}
|
95
|
+
)
|
82
96
|
```
|
83
97
|
|
84
|
-
|
85
|
-
|
98
|
+
Example output:
|
99
|
+
|
100
|
+
```
|
101
|
+
{ "timestamp": "2020-01-02T19:47:45.123456-0800", "level": "INFO", "message": "test" }
|
86
102
|
```
|
87
103
|
|
88
|
-
|
104
|
+
#### Custom Transformations
|
89
105
|
|
90
106
|
You can provide a callable object (proc, lambda, or any object responding to `call`) to transform field values. The callable receives the original value and should return a hash that will be merged into the JSON document:
|
91
107
|
|
92
108
|
```ruby
|
93
|
-
device = Lumberjack::JsonDevice.new(
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
109
|
+
device = Lumberjack::JsonDevice.new(
|
110
|
+
output: STDOUT,
|
111
|
+
mapping: {
|
112
|
+
time: lambda { |val| { timestamp: (val.to_f * 1000).round } },
|
113
|
+
severity: "level",
|
114
|
+
message: "message"
|
115
|
+
}
|
116
|
+
)
|
98
117
|
```
|
99
118
|
|
100
|
-
|
101
|
-
|
119
|
+
Example output:
|
120
|
+
|
121
|
+
```
|
122
|
+
{ "timestamp": 1578125375588, "level": "INFO", "message": "test" }
|
102
123
|
```
|
103
124
|
|
104
|
-
|
125
|
+
#### Shortcut Mapping
|
105
126
|
|
106
127
|
Use `true` as a shortcut to map a field to the same name:
|
107
128
|
|
108
129
|
```ruby
|
109
|
-
device = Lumberjack::JsonDevice.new(
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
130
|
+
device = Lumberjack::JsonDevice.new(
|
131
|
+
output: STDOUT,
|
132
|
+
mapping: {
|
133
|
+
time: "timestamp",
|
134
|
+
severity: true, # Maps to "severity"
|
135
|
+
progname: true, # Maps to "progname"
|
136
|
+
pid: false, # Excluded from output
|
137
|
+
message: "message",
|
138
|
+
attributes: true # Maps to "attributes"
|
139
|
+
}
|
140
|
+
)
|
117
141
|
```
|
118
142
|
|
119
|
-
|
143
|
+
#### Tag Extraction and Dot Notation
|
120
144
|
|
121
|
-
You can extract specific
|
145
|
+
You can extract specific attributes from the log entry and map them to custom locations in the JSON. Tags with dot notation in their names are automatically expanded into nested structures:
|
122
146
|
|
123
147
|
```ruby
|
124
|
-
device = Lumberjack::JsonDevice.new(
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
148
|
+
device = Lumberjack::JsonDevice.new(
|
149
|
+
output: STDOUT,
|
150
|
+
mapping: {
|
151
|
+
message: true,
|
152
|
+
"http.status": true, # Extracts "http.status" attribute
|
153
|
+
"http.method": true, # Extracts "http.method" attribute
|
154
|
+
"http.path": true, # Extracts "http.path" attribute
|
155
|
+
attributes: true
|
156
|
+
}
|
157
|
+
)
|
131
158
|
```
|
132
159
|
|
133
|
-
|
134
|
-
|
160
|
+
Example output:
|
161
|
+
|
162
|
+
```
|
163
|
+
{ "message": "test", "http": { "status": 200, "method": "GET", "path": "/resource" }, "attributes": { "other": "values" } }
|
135
164
|
```
|
136
165
|
|
137
|
-
**Important**: All
|
166
|
+
**Important**: All attributes are automatically expanded from dot notation into nested hash structures, not just extracted attributes. For example, if you have an attribute named `"user.profile.name"`, it will automatically become `{"user": {"profile": {"name": "value"}}}` in the attributes section.
|
138
167
|
|
139
|
-
|
168
|
+
#### Flattening Tags to Root Level
|
140
169
|
|
141
|
-
Use `"*"` as the
|
170
|
+
Use `"*"` as the attributes mapping value to copy all remaining attributes directly to the root level of the JSON document:
|
142
171
|
|
143
172
|
```ruby
|
144
|
-
device = Lumberjack::JsonDevice.new(
|
145
|
-
|
146
|
-
|
147
|
-
|
173
|
+
device = Lumberjack::JsonDevice.new(
|
174
|
+
output: STDOUT,
|
175
|
+
mapping: {
|
176
|
+
message: true,
|
177
|
+
attributes: "*"
|
178
|
+
}
|
179
|
+
)
|
148
180
|
```
|
149
181
|
|
150
|
-
|
151
|
-
|
182
|
+
Example output:
|
183
|
+
|
184
|
+
```
|
185
|
+
{ "message": "test", "attribute1": "value", "attribute2": "value" }
|
152
186
|
```
|
153
187
|
|
154
|
-
|
188
|
+
### Data Formatting
|
155
189
|
|
156
190
|
The device includes a `Lumberjack::Formatter` that formats objects before serializing them as JSON. You can add custom formatters for specific classes or supply your own formatter when creating the device.
|
157
191
|
|
158
192
|
```ruby
|
159
|
-
device.formatter.add(Exception,
|
160
|
-
device.formatter.add(ActiveRecord::Base,
|
193
|
+
device.formatter.add(Exception, :inspect)
|
194
|
+
device.formatter.add(ActiveRecord::Base, :id)
|
195
|
+
device.formatter.add("User") { |user| user.username }
|
161
196
|
```
|
162
197
|
|
163
|
-
|
198
|
+
#### Dynamic Mapping
|
164
199
|
|
165
200
|
You can incrementally add field mappings after creating the device using the `map` method:
|
166
201
|
|
@@ -168,7 +203,7 @@ You can incrementally add field mappings after creating the device using the `ma
|
|
168
203
|
device.map(duration: "response_time", user_id: ["user", "id"])
|
169
204
|
```
|
170
205
|
|
171
|
-
|
206
|
+
#### DateTime Formatting
|
172
207
|
|
173
208
|
You can specify the `datetime_format` that will be used to serialize Time and DateTime objects:
|
174
209
|
|
@@ -176,24 +211,29 @@ You can specify the `datetime_format` that will be used to serialize Time and Da
|
|
176
211
|
device.datetime_format = "%Y-%m-%dT%H:%M:%S.%3N"
|
177
212
|
```
|
178
213
|
|
179
|
-
|
214
|
+
The default format is [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) with millisecond precision.
|
215
|
+
|
216
|
+
#### Post Processing
|
180
217
|
|
181
218
|
You can provide a post processor that will be called on the hash before it is serialized to JSON. This allows you to modify any aspect of the log entry:
|
182
219
|
|
183
220
|
```ruby
|
184
221
|
# Filter out sensitive elements using Rails parameter filter
|
185
222
|
param_filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
|
186
|
-
device = Lumberjack::JsonDevice.new(
|
223
|
+
device = Lumberjack::JsonDevice.new(
|
224
|
+
output: STDOUT,
|
225
|
+
post_processor: ->(data) { param_filter.filter(data) }
|
226
|
+
)
|
187
227
|
```
|
188
228
|
|
189
229
|
Note that all hash keys will be strings and the values will be JSON-safe. If the post processor does not return a hash, it will be ignored.
|
190
230
|
|
191
|
-
|
231
|
+
#### Pretty Printing
|
192
232
|
|
193
233
|
For development or debugging, you can format the JSON output with indentation and newlines by setting the `pretty` option to `true`:
|
194
234
|
|
195
235
|
```ruby
|
196
|
-
device = Lumberjack::JsonDevice.new(STDOUT, pretty: true)
|
236
|
+
device = Lumberjack::JsonDevice.new(output: STDOUT, pretty: true)
|
197
237
|
```
|
198
238
|
|
199
239
|
This will format each log entry as multi-line JSON instead of single-line output. You can check if pretty formatting is enabled using the `pretty?` method:
|
@@ -202,24 +242,26 @@ This will format each log entry as multi-line JSON instead of single-line output
|
|
202
242
|
device.pretty? # => true or false
|
203
243
|
```
|
204
244
|
|
205
|
-
|
245
|
+
#### Empty Messages
|
206
246
|
|
207
247
|
Log entries with empty or nil messages will not be written to the output.
|
208
248
|
|
209
|
-
|
249
|
+
### Configuration Options
|
210
250
|
|
211
251
|
The `JsonDevice` constructor accepts the following options:
|
212
252
|
|
253
|
+
- **`output`**: The output stream, file path, or Lumberjack device to write to (default: STDOUT)
|
213
254
|
- **`mapping`**: Hash defining how log fields should be mapped to JSON (default: maps all standard fields)
|
214
255
|
- **`formatter`**: Custom `Lumberjack::Formatter` instance for formatting values before JSON serialization
|
215
256
|
- **`datetime_format`**: String format for Time/DateTime objects (default: `"%Y-%m-%dT%H:%M:%S.%6N%z"`)
|
216
257
|
- **`post_processor`**: Callable that receives and can modify the final hash before JSON serialization
|
217
258
|
- **`pretty`**: Boolean to enable pretty-printed JSON output (default: `false`)
|
259
|
+
- **`utc`**: Boolean to force timestamps to UTC before formatting (default: `false`)
|
218
260
|
|
219
261
|
```ruby
|
220
262
|
device = Lumberjack::JsonDevice.new(
|
221
|
-
STDOUT,
|
222
|
-
mapping: { time: "timestamp", message: true,
|
263
|
+
output: STDOUT,
|
264
|
+
mapping: { time: "timestamp", message: true, attributes: "*" },
|
223
265
|
datetime_format: "%Y-%m-%d %H:%M:%S",
|
224
266
|
pretty: true,
|
225
267
|
post_processor: lambda { |data| data.merge(app: "myapp") }
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.0.0
|
@@ -0,0 +1,382 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "lumberjack"
|
4
|
+
require "json"
|
5
|
+
require "time"
|
6
|
+
|
7
|
+
# Lumberjack is a simple, powerful, and fast logging library for Ruby that
|
8
|
+
# provides a consistent interface for logging across different output streams.
|
9
|
+
module Lumberjack
|
10
|
+
# This Lumberjack device logs output to another device as JSON formatted text with one document per line.
|
11
|
+
# This format (JSONL) is ideal for structured logging pipelines and can be easily consumed by log
|
12
|
+
# aggregation services, search engines, and monitoring tools.
|
13
|
+
#
|
14
|
+
# The device supports flexible field mapping to customize the JSON structure, datetime formatting,
|
15
|
+
# post-processing, and pretty printing for development use.
|
16
|
+
#
|
17
|
+
# @example Basic usage
|
18
|
+
# device = Lumberjack::JsonDevice.new(output: STDOUT)
|
19
|
+
# logger = Lumberjack::Logger.new(device)
|
20
|
+
# logger.info("User logged in", user_id: 123)
|
21
|
+
#
|
22
|
+
# @example Custom field mapping
|
23
|
+
# device = Lumberjack::JsonDevice.new(
|
24
|
+
# output: STDOUT,
|
25
|
+
# mapping: {
|
26
|
+
# time: "timestamp",
|
27
|
+
# severity: "level",
|
28
|
+
# message: true,
|
29
|
+
# attributes: "*"
|
30
|
+
# }
|
31
|
+
# )
|
32
|
+
#
|
33
|
+
# The mapping parameter can be used to define the JSON data structure. To define the structure pass in a
|
34
|
+
# hash with key indicating the log entry field and the value indicating the JSON document key.
|
35
|
+
#
|
36
|
+
# The standard entry fields are mapped with the following keys:
|
37
|
+
#
|
38
|
+
# * :time
|
39
|
+
# * :severity
|
40
|
+
# * :progname
|
41
|
+
# * :pid
|
42
|
+
# * :message
|
43
|
+
# * :attributes
|
44
|
+
#
|
45
|
+
# Any additional keys will be pulled from the attributes. If any of the standard keys are missing or have a nil
|
46
|
+
# mapping, the entry field will not be included in the JSON output.
|
47
|
+
#
|
48
|
+
# You can create a nested JSON structure by specifying an array as the JSON key.
|
49
|
+
class JsonDevice < Device
|
50
|
+
VERSION = File.read(File.join(__dir__, "..", "..", "VERSION")).strip.freeze
|
51
|
+
|
52
|
+
# Default mapping for standard log entry fields to JSON keys.
|
53
|
+
DEFAULT_MAPPING = {
|
54
|
+
time: true,
|
55
|
+
severity: true,
|
56
|
+
message: true,
|
57
|
+
progname: true,
|
58
|
+
pid: true,
|
59
|
+
attributes: true
|
60
|
+
}.freeze
|
61
|
+
|
62
|
+
# Default ISO 8601 datetime format with microsecond precision and timezone offset.
|
63
|
+
DEFAULT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%6N%z"
|
64
|
+
|
65
|
+
# Classes that can be serialized directly to JSON without transformation.
|
66
|
+
JSON_NATIVE_CLASSES = [String, NilClass, Numeric, TrueClass, FalseClass].freeze
|
67
|
+
private_constant :JSON_NATIVE_CLASSES
|
68
|
+
|
69
|
+
# Valid options that can be passed to the JsonDevice constructor.
|
70
|
+
JSON_OPTIONS = [:output, :mapping, :formatter, :datetime_format, :post_processor, :pretty, :utc].freeze
|
71
|
+
private_constant :JSON_OPTIONS
|
72
|
+
|
73
|
+
# Register the JsonDevice with the device registry for easier instantiation.
|
74
|
+
DeviceRegistry.add(:json, self)
|
75
|
+
|
76
|
+
# @!attribute [rw] formatter
|
77
|
+
# @return [Lumberjack::Formatter] The formatter used to format log entry values before JSON serialization.
|
78
|
+
attr_accessor :formatter
|
79
|
+
|
80
|
+
# @!attribute [rw] post_processor
|
81
|
+
# @return [Proc, nil] A callable object that can modify the log entry hash before JSON serialization.
|
82
|
+
attr_accessor :post_processor
|
83
|
+
|
84
|
+
# @!attribute [w] pretty
|
85
|
+
# @param value [Boolean] Whether to enable pretty-printed JSON output.
|
86
|
+
attr_writer :pretty
|
87
|
+
|
88
|
+
# @!attribute [r] mapping
|
89
|
+
# @return [Hash] The current field mapping configuration.
|
90
|
+
attr_reader :mapping
|
91
|
+
|
92
|
+
# Create a new JsonDevice instance.
|
93
|
+
#
|
94
|
+
# @param options [Hash<Symbol, Object>] The options for the JSON device.
|
95
|
+
# @param deprecated_options [Hash<Symbol, Object>] The device options for the JSON device if the output
|
96
|
+
# stream or device is specified in the first argument. This is deprecated behavior for backward
|
97
|
+
# compatibility with version 2.x.
|
98
|
+
# @option options [IO, Lumberjack::Device, Symbol, String, Pathname, nil] :output The output stream or
|
99
|
+
# Lumberjack device to write the JSON formatted log entries to. If this is a string or Pathname,
|
100
|
+
# then the output will be written to that file path. The values :stdout and :stderr can be used
|
101
|
+
# to write to STDOUT and STDERR respectively. Defaults to STDOUT.
|
102
|
+
# @option options [Hash] :mapping A hash where the key is the log entry field name and the value indicates how
|
103
|
+
# to map the field if it exists. If the value is `true`, the field will be mapped to the same name.
|
104
|
+
# If the value is a String, the field will be mapped to that key name.
|
105
|
+
# If the value is an Array, it will be mapped to a nested structure that follows the array elements.
|
106
|
+
# If the value is a callable object, it will be called with the value and is expected to return
|
107
|
+
# a hash that will be merged into the JSON document.
|
108
|
+
# If the value is `false` or `nil`, the field will not be included in the JSON output.
|
109
|
+
# Special value `"*"` for `:attributes` will flatten all remaining attributes to the root level.
|
110
|
+
# @option options [Lumberjack::Formatter] :formatter An optional formatter to use for formatting the log entry data.
|
111
|
+
# @option options [String] :datetime_format An optional datetime format string to use for formatting the log timestamp.
|
112
|
+
# Defaults to ISO 8601 format with microsecond precision.
|
113
|
+
# @option options [Proc] :post_processor An optional callable object that will be called with the log entry hash
|
114
|
+
# before it is written to the output stream. This can be used to modify the log entry data
|
115
|
+
# before it is serialized to JSON. The callable should return a Hash or the result will be ignored.
|
116
|
+
# @option options [Boolean] :pretty If true, the output will be formatted as pretty JSON with indentation and newlines.
|
117
|
+
# The default is false, which writes each log entry as a single line JSON document.
|
118
|
+
# @option options [Boolean] :utc If true, all times will be converted to UTC before formatting.
|
119
|
+
def initialize(options = {}, deprecated_options = nil)
|
120
|
+
unless options.is_a?(Hash)
|
121
|
+
Lumberjack::Utils.deprecated(:new, "Passing a stream or device as the first argument is no longer supported and will be removed in version 3.1; specify the output stream in the :output key of the options hash.") do
|
122
|
+
options = (deprecated_options || {}).merge(output: options)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
@mutex = Mutex.new
|
127
|
+
|
128
|
+
stream_options = options.dup
|
129
|
+
JSON_OPTIONS.each { |key| stream_options.delete(key) }
|
130
|
+
@output = output_stream(options[:output], stream_options)
|
131
|
+
|
132
|
+
self.mapping = options.fetch(:mapping, DEFAULT_MAPPING)
|
133
|
+
|
134
|
+
@force_utc = options.fetch(:utc, false)
|
135
|
+
@formatter = default_formatter
|
136
|
+
self.datetime_format = options.fetch(:datetime_format, DEFAULT_TIME_FORMAT)
|
137
|
+
@formatter.include(options[:formatter]) if options[:formatter]
|
138
|
+
|
139
|
+
@post_processor = options[:post_processor]
|
140
|
+
|
141
|
+
@pretty = !!options[:pretty]
|
142
|
+
end
|
143
|
+
|
144
|
+
# Write a log entry to the output stream as JSON.
|
145
|
+
# Each entry is written as a single line JSON document (JSONL format) unless pretty printing is enabled.
|
146
|
+
# Empty log entries (nil or empty message) are ignored.
|
147
|
+
#
|
148
|
+
# @param entry [Lumberjack::LogEntry] The log entry to write.
|
149
|
+
# @return [void]
|
150
|
+
def write(entry)
|
151
|
+
return if entry.empty?
|
152
|
+
|
153
|
+
data = entry_as_json(entry)
|
154
|
+
json = @pretty ? JSON.pretty_generate(data) : JSON.generate(data)
|
155
|
+
@output.write("#{json}\n")
|
156
|
+
end
|
157
|
+
|
158
|
+
# Get the underlying device from the output stream.
|
159
|
+
#
|
160
|
+
# @return [Object] The underlying device.
|
161
|
+
def dev
|
162
|
+
@output.dev
|
163
|
+
end
|
164
|
+
|
165
|
+
# Flush the output stream.
|
166
|
+
#
|
167
|
+
# @return [void]
|
168
|
+
def flush
|
169
|
+
@output.flush
|
170
|
+
end
|
171
|
+
|
172
|
+
# @!attribute [r] datetime_format
|
173
|
+
# @return [String] The current datetime format string.
|
174
|
+
attr_reader :datetime_format
|
175
|
+
|
176
|
+
# Set the datetime format for the log timestamp.
|
177
|
+
#
|
178
|
+
# @param format [String] The datetime format string to use for formatting the log timestamp.
|
179
|
+
def datetime_format=(format)
|
180
|
+
@datetime_format = format
|
181
|
+
fmttr = time_formatter(datetime_format: format, force_utc: @force_utc)
|
182
|
+
@formatter.add(Time, fmttr)
|
183
|
+
@formatter.add(DateTime, fmttr)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Return true if the output is written in a multi-line pretty format. The default is to write each
|
187
|
+
# log entry as a single line JSON document.
|
188
|
+
#
|
189
|
+
# @return [Boolean]
|
190
|
+
def pretty?
|
191
|
+
!!@pretty
|
192
|
+
end
|
193
|
+
|
194
|
+
# Set the mapping for how to map an entry to a JSON object.
|
195
|
+
#
|
196
|
+
# @param mapping [Hash] A hash where the key is the log entry field name and the value is the JSON key.
|
197
|
+
# If the value is `true`, the field will be mapped to the same name
|
198
|
+
# If the value is an array, it will be mapped to a nested structure.
|
199
|
+
# If the value is a callable object, it will be called with the value and should return a hash that will be merged into the JSON document.
|
200
|
+
# If the value is `false`, the field will not be included in the JSON output.
|
201
|
+
# @return [void]
|
202
|
+
def mapping=(mapping)
|
203
|
+
@mutex.synchronize do
|
204
|
+
keys = {}
|
205
|
+
mapping.each do |key, value|
|
206
|
+
if value == true
|
207
|
+
value = key.to_s.split(".")
|
208
|
+
value = value.first if value.size == 1
|
209
|
+
end
|
210
|
+
keys[key.to_sym] = value if value
|
211
|
+
end
|
212
|
+
|
213
|
+
@time_key = keys.delete(:time)
|
214
|
+
@severity_key = keys.delete(:severity)
|
215
|
+
@message_key = keys.delete(:message)
|
216
|
+
@progname_key = keys.delete(:progname)
|
217
|
+
@pid_key = keys.delete(:pid)
|
218
|
+
@attributes_key = keys.delete(:attributes)
|
219
|
+
@custom_keys = keys.map do |name, key|
|
220
|
+
[name.to_s.split("."), key]
|
221
|
+
end.to_h
|
222
|
+
|
223
|
+
@mapping = mapping
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Add a field mapping to the existing mappings.
|
228
|
+
#
|
229
|
+
# @param field_mapping [Hash] A hash where the key is the log entry field name and the value is the JSON key.
|
230
|
+
# If the value is `true`, the field will be mapped to the same name
|
231
|
+
# If the value is an array, it will be mapped to a nested structure.
|
232
|
+
# If the value is a callable object, it will be called with the value and should return a hash that will be merged into the JSON document.
|
233
|
+
# If the value is `false`, the field will not be included in the JSON output.
|
234
|
+
# @return [void]
|
235
|
+
def map(field_mapping)
|
236
|
+
new_mapping = field_mapping.transform_keys(&:to_sym)
|
237
|
+
self.mapping = mapping.merge(new_mapping)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Convert a Lumberjack::LogEntry to a Hash using the specified field mapping.
|
241
|
+
#
|
242
|
+
# @param entry [Lumberjack::LogEntry] The log entry to convert.
|
243
|
+
# @return [Hash] A hash representing the log entry in JSON format.
|
244
|
+
def entry_as_json(entry)
|
245
|
+
data = {}
|
246
|
+
set_attribute(data, @time_key, entry.time) if @time_key
|
247
|
+
set_attribute(data, @severity_key, entry.severity_label) if @severity_key
|
248
|
+
set_attribute(data, @message_key, json_safe(entry.message)) if @message_key
|
249
|
+
set_attribute(data, @progname_key, json_safe(entry.progname)) if @progname_key && entry.progname
|
250
|
+
set_attribute(data, @pid_key, entry.pid) if @pid_key
|
251
|
+
|
252
|
+
attributes = entry.attributes.transform_values { |value| json_safe(value) } if entry.attributes
|
253
|
+
|
254
|
+
if @custom_keys.size > 0 && attributes && !attributes&.empty?
|
255
|
+
@custom_keys.each do |name, key|
|
256
|
+
name = name.is_a?(Array) ? name.join(".") : name.to_s
|
257
|
+
value = attributes.delete(name)
|
258
|
+
next if value.nil?
|
259
|
+
|
260
|
+
value = Lumberjack::Utils.expand_attributes(value) if value.is_a?(Hash)
|
261
|
+
set_attribute(data, key, value)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
if @attributes_key && !attributes&.empty?
|
266
|
+
attributes = Lumberjack::Utils.expand_attributes(attributes)
|
267
|
+
if @attributes_key == "*"
|
268
|
+
attributes.each { |k, v| data[k] = v unless data.include?(k) }
|
269
|
+
else
|
270
|
+
set_attribute(data, @attributes_key, attributes)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
data = @formatter.format(data) if @formatter
|
275
|
+
if @post_processor
|
276
|
+
processed_result = @post_processor.call(data)
|
277
|
+
data = processed_result if processed_result.is_a?(Hash)
|
278
|
+
end
|
279
|
+
|
280
|
+
data
|
281
|
+
end
|
282
|
+
|
283
|
+
private
|
284
|
+
|
285
|
+
def output_stream(output, options)
|
286
|
+
output ||= $stdout
|
287
|
+
|
288
|
+
if output.is_a?(Lumberjack::Device)
|
289
|
+
output
|
290
|
+
elsif output.is_a?(String) || (defined?(Pathname) && output.is_a?(Pathname))
|
291
|
+
options = options.slice(:binmode, :autoflush, :shift_age, :shift_size, :shift_period_suffix)
|
292
|
+
Lumberjack::Device::LogFile.new(output, options)
|
293
|
+
else
|
294
|
+
if output == :stdout
|
295
|
+
output = $stdout
|
296
|
+
elsif output == :stderr
|
297
|
+
output = $stderr
|
298
|
+
end
|
299
|
+
options = options.slice(:binmode, :autoflush)
|
300
|
+
Lumberjack::Device::Writer.new(output, options)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def default_formatter
|
305
|
+
Lumberjack::Formatter.build do |formatter|
|
306
|
+
formatter.add(::Enumerable, Lumberjack::Formatter::StructuredFormatter.new(formatter))
|
307
|
+
formatter.add(::Object) { |value| json_safe(value) }
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def time_formatter(datetime_format: nil, force_utc: false)
|
312
|
+
lambda do |time|
|
313
|
+
time = time.utc if force_utc && !time.utc?
|
314
|
+
datetime_format ? time.strftime(datetime_format) : time
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def set_attribute(data, key, value)
|
319
|
+
return if value.nil?
|
320
|
+
|
321
|
+
key = key.split(".") if key.is_a?(String) && key.include?(".")
|
322
|
+
|
323
|
+
if key.is_a?(Array)
|
324
|
+
unless key.empty?
|
325
|
+
if key.size == 1
|
326
|
+
data[key.first] = value
|
327
|
+
else
|
328
|
+
data[key.first] ||= {}
|
329
|
+
set_attribute(data[key.first], key[1, key.size], value)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
elsif key.respond_to?(:call)
|
333
|
+
hash = key.call(value)
|
334
|
+
if hash.is_a?(Hash)
|
335
|
+
deep_merge!(data, hash)
|
336
|
+
end
|
337
|
+
else
|
338
|
+
data[key.to_s] = value unless key.nil?
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def deep_merge!(hash, other_hash, &block)
|
343
|
+
other_hash = other_hash.transform_keys(&:to_s)
|
344
|
+
hash.merge!(other_hash) do |key, this_val, other_val|
|
345
|
+
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
|
346
|
+
deep_merge!(this_val, other_val, &block)
|
347
|
+
elsif block
|
348
|
+
block.call(key, this_val, other_val)
|
349
|
+
else
|
350
|
+
other_val
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def json_safe(value, seen = nil)
|
356
|
+
return value if JSON_NATIVE_CLASSES.include?(value.class)
|
357
|
+
return nil if seen&.include?(value.object_id)
|
358
|
+
|
359
|
+
# Check if the as_json method is defined and takes no parameters
|
360
|
+
as_json_arity = value.method(:as_json).arity if !value.nil? && value.respond_to?(:as_json)
|
361
|
+
|
362
|
+
if as_json_arity == 0 || as_json_arity == -1
|
363
|
+
value.as_json
|
364
|
+
elsif !value.is_a?(Enumerable)
|
365
|
+
value
|
366
|
+
else
|
367
|
+
seen ||= Set.new
|
368
|
+
seen << value.object_id
|
369
|
+
if value.is_a?(Hash)
|
370
|
+
value.transform_values { |v| json_safe(v, seen) }
|
371
|
+
else
|
372
|
+
value.collect { |v| json_safe(v, seen) }
|
373
|
+
end
|
374
|
+
end
|
375
|
+
rescue SystemStackError, StandardError => e
|
376
|
+
error_message = e.class.name
|
377
|
+
error_message = "#{error_message} #{e.message}" if e.message && e.message != ""
|
378
|
+
warn("<Error serializing #{value.class} to JSON: #{error_message}>")
|
379
|
+
"<Error serializing #{value.class} to JSON: #{error_message}>"
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
@@ -1,283 +1,3 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require "json"
|
5
|
-
require "time"
|
6
|
-
|
7
|
-
module Lumberjack
|
8
|
-
# This Lumberjack device logs output to another device as JSON formatted text with one document per line.
|
9
|
-
#
|
10
|
-
# The mapping parameter can be used to define the JSON data structure. To define the structure pass in a
|
11
|
-
# hash with key indicating the log entry field and the value indicating the JSON document key.
|
12
|
-
#
|
13
|
-
# The standard entry fields are mapped with the following keys:
|
14
|
-
#
|
15
|
-
# * :time
|
16
|
-
# * :severity
|
17
|
-
# * :progname
|
18
|
-
# * :pid
|
19
|
-
# * :message
|
20
|
-
# * :tags
|
21
|
-
#
|
22
|
-
# Any additional keys will be pulled from the tags. If any of the standard keys are missing or have a nil
|
23
|
-
# mapping, the entry field will not be included in the JSON output.
|
24
|
-
#
|
25
|
-
# You can create a nested JSON structure by specifying an array as the JSON key.
|
26
|
-
class JsonDevice < Device
|
27
|
-
DEFAULT_MAPPING = {
|
28
|
-
time: true,
|
29
|
-
severity: true,
|
30
|
-
progname: true,
|
31
|
-
pid: true,
|
32
|
-
message: true,
|
33
|
-
tags: true
|
34
|
-
}.freeze
|
35
|
-
|
36
|
-
DEFAULT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%6N%z"
|
37
|
-
|
38
|
-
attr_accessor :formatter
|
39
|
-
attr_accessor :post_processor
|
40
|
-
attr_writer :pretty
|
41
|
-
attr_reader :mapping
|
42
|
-
|
43
|
-
# @param stream_or_device [IO, Lumberjack::Device] The output stream or Lumberjack device to write
|
44
|
-
# the JSON formatted log entries to.
|
45
|
-
# @param mapping [Hash] A hash where the key is the log entry field name and the value indicates how
|
46
|
-
# to map the field if it exists. If the value is `true`, the field will be mapped to the same name.
|
47
|
-
# If the value is an array, it will be mapped to a nested structure that follows the array elements.
|
48
|
-
# If the value is a callable object, it will be called with the value and is expected to return
|
49
|
-
# a hash that will be merged into the JSON document.
|
50
|
-
# If the value is `false`, the field will not be included in the JSON output.
|
51
|
-
# @param formatter [Lumberjack::Formatter] An optional formatter to use for formatting the log entry data.
|
52
|
-
# @param datetime_format [String] An optional datetime format string to use for formatting the log timestamp.
|
53
|
-
# @param post_processor [Proc] An optional callable object that will be called with the log entry hash
|
54
|
-
# before it is written to the output stream. This can be used to modify the log entry data
|
55
|
-
# before it is serialized to JSON.
|
56
|
-
# @param pretty [Boolean] If true, the output will be formatted as pretty JSON with indentation and newlines.
|
57
|
-
# The default is false, which writes each log entry as a single line JSON document.
|
58
|
-
def initialize(stream_or_device, mapping: DEFAULT_MAPPING, formatter: nil, datetime_format: nil, post_processor: nil, pretty: false)
|
59
|
-
@mutex = Mutex.new
|
60
|
-
|
61
|
-
@device = if stream_or_device.is_a?(Device)
|
62
|
-
stream_or_device
|
63
|
-
else
|
64
|
-
Lumberjack::Device::Writer.new(stream_or_device)
|
65
|
-
end
|
66
|
-
|
67
|
-
self.mapping = mapping
|
68
|
-
|
69
|
-
if formatter
|
70
|
-
@formatter = formatter
|
71
|
-
else
|
72
|
-
@formatter = default_formatter
|
73
|
-
datetime_format = DEFAULT_TIME_FORMAT if datetime_format.nil?
|
74
|
-
end
|
75
|
-
add_datetime_formatter!(datetime_format) unless datetime_format.nil?
|
76
|
-
|
77
|
-
@post_processor = post_processor
|
78
|
-
|
79
|
-
@pretty = !!pretty
|
80
|
-
end
|
81
|
-
|
82
|
-
def write(entry)
|
83
|
-
return if entry.empty?
|
84
|
-
|
85
|
-
data = entry_as_json(entry)
|
86
|
-
json = @pretty ? JSON.pretty_generate(data) : JSON.generate(data)
|
87
|
-
@device.write(json)
|
88
|
-
end
|
89
|
-
|
90
|
-
def flush
|
91
|
-
@device.flush
|
92
|
-
end
|
93
|
-
|
94
|
-
attr_reader :datetime_format
|
95
|
-
|
96
|
-
# Set the datetime format for the log timestamp.
|
97
|
-
#
|
98
|
-
# @param format [String] The datetime format string to use for formatting the log timestamp.
|
99
|
-
def datetime_format=(format)
|
100
|
-
add_datetime_formatter!(format)
|
101
|
-
end
|
102
|
-
|
103
|
-
# Return true if the output is written in a multi-line pretty format. The default is to write each
|
104
|
-
# log entry as a single line JSON document.
|
105
|
-
#
|
106
|
-
# @return [Boolean]
|
107
|
-
def pretty?
|
108
|
-
!!@pretty
|
109
|
-
end
|
110
|
-
|
111
|
-
# Set the mapping for how to map an entry to a JSON object.
|
112
|
-
#
|
113
|
-
# @param mapping [Hash] A hash where the key is the log entry field name and the value is the JSON key.
|
114
|
-
# If the value is `true`, the field will be mapped to the same name
|
115
|
-
# If the value is an array, it will be mapped to a nested structure.
|
116
|
-
# If the value is a callable object, it will be called with the value and should return a hash that will be merged into the JSON document.
|
117
|
-
# If the value is `false`, the field will not be included in the JSON output.
|
118
|
-
# @return [void]
|
119
|
-
def mapping=(mapping)
|
120
|
-
@mutex.synchronize do
|
121
|
-
keys = {}
|
122
|
-
mapping.each do |key, value|
|
123
|
-
if value == true
|
124
|
-
value = key.to_s.split(".")
|
125
|
-
value = value.first if value.size == 1
|
126
|
-
end
|
127
|
-
keys[key.to_sym] = value if value
|
128
|
-
end
|
129
|
-
|
130
|
-
@time_key = keys.delete(:time)
|
131
|
-
@severity_key = keys.delete(:severity)
|
132
|
-
@progname_key = keys.delete(:progname)
|
133
|
-
@pid_key = keys.delete(:pid)
|
134
|
-
@message_key = keys.delete(:message)
|
135
|
-
@tags_key = keys.delete(:tags)
|
136
|
-
@custom_keys = keys.map do |name, key|
|
137
|
-
[name.to_s.split("."), key]
|
138
|
-
end.to_h
|
139
|
-
@mapping = mapping
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
# Add a field mapping to the existing mappings.
|
144
|
-
#
|
145
|
-
# @param field_mapping [Hash] A hash where the key is the log entry field name and the value is the JSON key.
|
146
|
-
# If the value is `true`, the field will be mapped to the same name
|
147
|
-
# If the value is an array, it will be mapped to a nested structure.
|
148
|
-
# If the value is a callable object, it will be called with the value and should return a hash that will be merged into the JSON document.
|
149
|
-
# If the value is `false`, the field will not be included in the JSON output.
|
150
|
-
# @return [void]
|
151
|
-
def map(field_mapping)
|
152
|
-
new_mapping = field_mapping.transform_keys(&:to_sym)
|
153
|
-
self.mapping = mapping.merge(new_mapping)
|
154
|
-
end
|
155
|
-
|
156
|
-
# Convert a Lumberjack::LogEntry to a Hash using the specified field mapping.
|
157
|
-
#
|
158
|
-
# @param entry [Lumberjack::LogEntry] The log entry to convert.
|
159
|
-
# @return [Hash] A hash representing the log entry in JSON format.
|
160
|
-
def entry_as_json(entry)
|
161
|
-
data = {}
|
162
|
-
set_attribute(data, @time_key, entry.time) if @time_key
|
163
|
-
set_attribute(data, @severity_key, entry.severity_label) if @severity_key
|
164
|
-
set_attribute(data, @message_key, json_safe(entry.message)) if @message_key
|
165
|
-
set_attribute(data, @progname_key, json_safe(entry.progname)) if @progname_key && entry.progname
|
166
|
-
set_attribute(data, @pid_key, entry.pid) if @pid_key
|
167
|
-
|
168
|
-
tags = entry.tags.transform_values { |value| json_safe(value) } if entry.tags
|
169
|
-
|
170
|
-
extracted_tags = nil
|
171
|
-
if @custom_keys.size > 0 && !tags&.empty?
|
172
|
-
extracted_tags = []
|
173
|
-
@custom_keys.each do |name, key|
|
174
|
-
name = name.is_a?(Array) ? name.join(".") : name.to_s
|
175
|
-
value = tags.delete(name)
|
176
|
-
next if value.nil?
|
177
|
-
|
178
|
-
value = Lumberjack::Utils.expand_tags(value) if value.is_a?(Hash)
|
179
|
-
set_attribute(data, key, value)
|
180
|
-
extracted_tags << name
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
if @tags_key && !tags&.empty?
|
185
|
-
tags = Lumberjack::Utils.expand_tags(tags)
|
186
|
-
if @tags_key == "*"
|
187
|
-
tags.each { |k, v| data[k] = v unless data.include?(k) }
|
188
|
-
else
|
189
|
-
set_attribute(data, @tags_key, tags)
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
data = @formatter.format(data) if @formatter
|
194
|
-
if @post_processor
|
195
|
-
processed_result = @post_processor.call(data)
|
196
|
-
data = processed_result if processed_result.is_a?(Hash)
|
197
|
-
end
|
198
|
-
|
199
|
-
data
|
200
|
-
end
|
201
|
-
|
202
|
-
private
|
203
|
-
|
204
|
-
def set_attribute(data, key, value)
|
205
|
-
return if value.nil?
|
206
|
-
|
207
|
-
if (value.is_a?(Time) || value.is_a?(DateTime)) && @time_formatter
|
208
|
-
value = @time_formatter.call(value)
|
209
|
-
end
|
210
|
-
|
211
|
-
key = key.split(".") if key.is_a?(String) && key.include?(".")
|
212
|
-
|
213
|
-
if key.is_a?(Array)
|
214
|
-
unless key.empty?
|
215
|
-
if key.size == 1
|
216
|
-
data[key.first] = value
|
217
|
-
else
|
218
|
-
data[key.first] ||= {}
|
219
|
-
set_attribute(data[key.first], key[1, key.size], value)
|
220
|
-
end
|
221
|
-
end
|
222
|
-
elsif key.respond_to?(:call)
|
223
|
-
hash = key.call(value)
|
224
|
-
if hash.is_a?(Hash)
|
225
|
-
deep_merge!(data, Lumberjack::Tags.stringify_keys(hash))
|
226
|
-
end
|
227
|
-
else
|
228
|
-
data[key.to_s] = value unless key.nil?
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
|
-
def default_formatter
|
233
|
-
formatter = Formatter.new.clear
|
234
|
-
object_formatter = Lumberjack::Formatter::ObjectFormatter.new
|
235
|
-
formatter.add(String, object_formatter)
|
236
|
-
formatter.add(Object, object_formatter)
|
237
|
-
formatter.add(Enumerable, Formatter::StructuredFormatter.new(formatter))
|
238
|
-
formatter
|
239
|
-
end
|
240
|
-
|
241
|
-
def add_datetime_formatter!(datetime_format)
|
242
|
-
if datetime_format
|
243
|
-
@datetime_format = datetime_format
|
244
|
-
time_formatter = Lumberjack::Formatter::DateTimeFormatter.new(datetime_format)
|
245
|
-
formatter.add(Time, time_formatter)
|
246
|
-
formatter.add(Date, time_formatter)
|
247
|
-
else
|
248
|
-
@datetime_format = nil
|
249
|
-
formatter.remove(Time)
|
250
|
-
formatter.remove(Date)
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
def deep_merge!(hash, other_hash, &block)
|
255
|
-
hash.merge!(other_hash) do |key, this_val, other_val|
|
256
|
-
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
|
257
|
-
deep_merge!(this_val, other_val, &block)
|
258
|
-
elsif block
|
259
|
-
block.call(key, this_val, other_val)
|
260
|
-
else
|
261
|
-
other_val
|
262
|
-
end
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
def json_safe(value)
|
267
|
-
return nil if value.nil?
|
268
|
-
|
269
|
-
# Check if the as_json method is defined takes no parameters
|
270
|
-
as_json_arity = value.method(:as_json).arity if value.respond_to?(:as_json)
|
271
|
-
|
272
|
-
if as_json_arity == 0 || as_json_arity == -1
|
273
|
-
value.as_json
|
274
|
-
elsif value.is_a?(Hash)
|
275
|
-
value.transform_values { |v| json_safe(v) }
|
276
|
-
elsif value.is_a?(Enumerable)
|
277
|
-
value.collect { |v| json_safe(v) }
|
278
|
-
else
|
279
|
-
value
|
280
|
-
end
|
281
|
-
end
|
282
|
-
end
|
283
|
-
end
|
3
|
+
require_relative "lumberjack/json_device"
|
@@ -8,6 +8,12 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.homepage = "https://github.com/bdurand/lumberjack_json_device"
|
9
9
|
spec.license = "MIT"
|
10
10
|
|
11
|
+
spec.metadata = {
|
12
|
+
"homepage_uri" => spec.homepage,
|
13
|
+
"source_code_uri" => spec.homepage,
|
14
|
+
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGE_LOG.md"
|
15
|
+
}
|
16
|
+
|
11
17
|
# Specify which files should be added to the gem when it is released.
|
12
18
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
13
19
|
ignore_files = %w[
|
@@ -26,7 +32,7 @@ Gem::Specification.new do |spec|
|
|
26
32
|
|
27
33
|
spec.require_paths = ["lib"]
|
28
34
|
|
29
|
-
spec.required_ruby_version = ">= 2.
|
35
|
+
spec.required_ruby_version = ">= 2.7"
|
30
36
|
|
31
|
-
spec.add_dependency "lumberjack", ">=
|
37
|
+
spec.add_dependency "lumberjack", ">=2.0"
|
32
38
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lumberjack_json_device
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brian Durand
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date: 2025-
|
10
|
+
date: 2025-10-20 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: lumberjack
|
@@ -16,15 +15,14 @@ dependencies:
|
|
16
15
|
requirements:
|
17
16
|
- - ">="
|
18
17
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
18
|
+
version: '2.0'
|
20
19
|
type: :runtime
|
21
20
|
prerelease: false
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
23
22
|
requirements:
|
24
23
|
- - ">="
|
25
24
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
27
|
-
description:
|
25
|
+
version: '2.0'
|
28
26
|
email:
|
29
27
|
- bbdurand@gmail.com
|
30
28
|
executables: []
|
@@ -38,13 +36,16 @@ files:
|
|
38
36
|
- MIT_LICENSE.txt
|
39
37
|
- README.md
|
40
38
|
- VERSION
|
39
|
+
- lib/lumberjack/json_device.rb
|
41
40
|
- lib/lumberjack_json_device.rb
|
42
41
|
- lumberjack_json_device.gemspec
|
43
42
|
homepage: https://github.com/bdurand/lumberjack_json_device
|
44
43
|
licenses:
|
45
44
|
- MIT
|
46
|
-
metadata:
|
47
|
-
|
45
|
+
metadata:
|
46
|
+
homepage_uri: https://github.com/bdurand/lumberjack_json_device
|
47
|
+
source_code_uri: https://github.com/bdurand/lumberjack_json_device
|
48
|
+
changelog_uri: https://github.com/bdurand/lumberjack_json_device/blob/main/CHANGE_LOG.md
|
48
49
|
rdoc_options: []
|
49
50
|
require_paths:
|
50
51
|
- lib
|
@@ -52,15 +53,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
53
|
requirements:
|
53
54
|
- - ">="
|
54
55
|
- !ruby/object:Gem::Version
|
55
|
-
version: '2.
|
56
|
+
version: '2.7'
|
56
57
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
58
|
requirements:
|
58
59
|
- - ">="
|
59
60
|
- !ruby/object:Gem::Version
|
60
61
|
version: '0'
|
61
62
|
requirements: []
|
62
|
-
rubygems_version: 3.
|
63
|
-
signing_key:
|
63
|
+
rubygems_version: 3.6.2
|
64
64
|
specification_version: 4
|
65
65
|
summary: A logging device for the lumberjack gem that writes log entries as JSON documents
|
66
66
|
for use with structured logging.
|