lumberjack_json_device 1.0.0 → 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09791e96f24532aeb09e536ed838a28e26d08ed47a868f3f31698b1fefd12c29'
4
- data.tar.gz: 4314fc238210e81ffb8743c660bf6cefe2921567e3611dd122b898542fe4578f
3
+ metadata.gz: 81b9e1a335053608bb8428db2f6828acd748c02ec0610c0c064e060ab94fe36c
4
+ data.tar.gz: 9a1bf5cb6cce667cde7837624be8db44826976abb4c07c9568481d89efd38c16
5
5
  SHA512:
6
- metadata.gz: db055ceb22bfca8ebcd2b04454ef2bc4634a48af53d960e64196ff328f06eecee687bab2f8b776a203b3a7f52d3f131e5c2d4a8ff8525930ebfe06010d52ccef
7
- data.tar.gz: bee629eeef6c71b6c19c39ec3d4e863baa796f053879202efb460aec88d6649f9127437bd4672a1dafb7a201e6de36f11eaf1e7cfb3f02768687216e77d808d0
6
+ metadata.gz: 3226afbeeb3c72a0e60b940e26fa47973882d6f5770dff91d0d859087c0399a033c1dd54982da74a03fddd8e8fc7acc2849773b0a20bf362d1c4551314a5ba4e
7
+ data.tar.gz: 2068343690c36690deda5c7c6b91d9c9f1ba8663ad375f28f064ab88d9acdb9ffdb8912b04611c4898363c024bcc01d9e6fca8ab8293a58db3a72992234ff4b7
@@ -0,0 +1,12 @@
1
+ # Dependabot update strategy
2
+ version: 2
3
+ updates:
4
+ - package-ecosystem: bundler
5
+ directory: "/"
6
+ schedule:
7
+ interval: weekly
8
+ allow:
9
+ # Automatically keep all runtime dependencies updated
10
+ - dependency-name: "*"
11
+ dependency-type: "production"
12
+ versioning-strategy: lockfile-only
@@ -0,0 +1,46 @@
1
+ name: Continuous Integration
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - actions-*
8
+ tags:
9
+ - v*
10
+ pull_request:
11
+ branches-ignore:
12
+ - actions-*
13
+ workflow_dispatch:
14
+
15
+ env:
16
+ BUNDLE_CLEAN: "true"
17
+ BUNDLE_PATH: vendor/bundle
18
+ BUNDLE_JOBS: 3
19
+ BUNDLE_RETRY: 3
20
+
21
+ jobs:
22
+ build:
23
+ name: ${{ matrix.ruby }} build
24
+ runs-on: ubuntu-latest
25
+ strategy:
26
+ matrix:
27
+ include:
28
+ - ruby: "ruby"
29
+ standardrb: true
30
+ - ruby: "3.0"
31
+ - ruby: "2.7"
32
+ - ruby: "2.5"
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+ - name: Set up Ruby
36
+ uses: ruby/setup-ruby@v1
37
+ with:
38
+ ruby-version: ${{ matrix.ruby}}
39
+ - name: Install gems
40
+ run: |
41
+ bundle install
42
+ - name: Run Tests
43
+ run: bundle exec rake
44
+ - name: standardrb
45
+ if: matrix.standardrb
46
+ run: bundle exec standardrb
data/.standard.yml ADDED
@@ -0,0 +1,8 @@
1
+ ruby_version: 2.5
2
+
3
+ format: progress
4
+
5
+ ignore:
6
+ - 'spec/**/*':
7
+ - Lint/UselessAssignment
8
+ - Lint/Void
data/CHANGE_LOG.md CHANGED
@@ -1,3 +1,36 @@
1
- # 1.0.0
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
2
3
 
3
- * Initial release
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 2.1.0
8
+
9
+ ### Changed
10
+
11
+ - Tags that contain arrays of hashes are no longer expanded to nested hashes if the hashes in the array use dot nottion in their keys. The hashes in the array will now be included as is in JSON output.
12
+
13
+ ## 2.0.0
14
+
15
+ ### Added
16
+
17
+ - Field mapping for the JSON can now be set to an array where the first element is the key to map and the second element is a callable object that will transform the value.
18
+ - Output can be set to pretty for better display in development environments.
19
+ - Added post_processor option to allow custom processing of the log entry hash before it is written to the output stream.
20
+
21
+ ### Changed
22
+
23
+ - Tag structure is now consistently expanded from dot notation into nested hashes in the `tag` field. Previoulsly this was only done when the template copied tags to the root level of the JSON document.
24
+ - The mapping options now supports setting the value to `false` to exclude a field from the JSON output.
25
+ - Tag mapping can now be set to `"*"` to copy all tags into the root of the JSON document.
26
+
27
+ ### Removed
28
+
29
+ - Remove gem dependency on `multi_json`. The gem now uses the `JSON` code from the Ruby standard library for consistency. `JSON` is also now amoung the fastest JSON libraries available in Ruby so performance is no longer a concern.
30
+ - Removed support for Ruby versions below 2.5.
31
+
32
+ ## 1.0.0
33
+
34
+ ### Added
35
+
36
+ - Initial release
data/README.md CHANGED
@@ -1,13 +1,27 @@
1
1
  # Lumberjack JSON Device
2
2
 
3
- [![Build Status](https://travis-ci.org/bdurand/lumberjack_json_device.svg?branch=master)](https://travis-ci.org/bdurand/lumberjack_json_device)
4
- [![Maintainability](https://api.codeclimate.com/v1/badges/c62cb886b86381560810/maintainability)](https://codeclimate.com/github/bdurand/lumberjack_json_device/maintainability)
3
+ [![Continuous Integration](https://github.com/bdurand/lumberjack_json_device/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/lumberjack_json_device/actions/workflows/continuous_integration.yml)
4
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
+ [![Gem Version](https://badge.fury.io/rb/lumberjack_json_device.svg)](https://badge.fury.io/rb/lumberjack_json_device)
5
6
 
6
- This gem provides a logging device for the [lumberjack](https://github.com/bdurand/lumberjack) gem that will log JSON formatted output to a stream with one JSON document per line. This can be used as part of a log processing pipeline to ship the log to a structured data store or logging service.
7
+ This gem provides a logging device for the [lumberjack](https://github.com/bdurand/lumberjack) gem that outputs JSON formatted log entries to a stream with one JSON document per line. This format is ideal for structured logging pipelines and can be easily consumed by log aggregation services, search engines, and monitoring tools.
7
8
 
8
- ## Destination
9
+ ## Quick Start
9
10
 
10
- You can send the JSON output either to a stream or to another Lumberjack device.
11
+ ```ruby
12
+ require 'lumberjack_json_device'
13
+
14
+ # Create a logger with JSON output to STDOUT
15
+ logger = Lumberjack::Logger.new(Lumberjack::JsonDevice.new(STDOUT))
16
+
17
+ # Log a message with tags
18
+ 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
+
22
+ ## Output Destinations
23
+
24
+ You can send the JSON output to either a stream or to another Lumberjack device.
11
25
 
12
26
  ```ruby
13
27
  # Send to STDOUT
@@ -20,17 +34,23 @@ device = Lumberjack::JsonDevice.new(log_file)
20
34
 
21
35
  ## JSON Structure
22
36
 
23
- By default, the JSON document will map to the `Lumberjack::LogEntry` data structure.
37
+ By default, the JSON document maps to the `Lumberjack::LogEntry` data structure and includes all standard fields:
24
38
 
25
39
  ```json
26
- {"timestamp": "2020-01-02T19:47:45.123455", "severity": "INFO", "progname": "web", "pid": 101, "message": "test", "tags": {"foo": "bar"}}
40
+ {"time": "2020-01-02T19:47:45.123455-0800", "severity": "INFO", "progname": "web", "pid": 101, "message": "test", "tags": {"foo": "bar"}}
27
41
  ```
28
42
 
29
- You can specify a mapping to the device to customize the JSON document data structure. You can map the standard field names (time, severity, progname, pid, message, and tags) to custom field names.
43
+ ### Custom Field Mapping
44
+
45
+ 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:
30
46
 
31
- If you map a field to an array, it will be mapped into a nested hash in the JSON document.
47
+ - **String**: Maps the field to a custom JSON key name
48
+ - **Array**: Creates nested JSON structures
49
+ - **`true`**: Maps the field to the same name as the key
50
+ - **`false`**: Excludes the field from the JSON output
51
+ - **Callable**: Transforms the value using custom logic
32
52
 
33
- Any keys beyond the standard field names will be populated by a tag with the same name. These tags will not be included with the rest of the tags.
53
+ You can map the standard field names (`time`, `severity`, `progname`, `pid`, `message`, and `tags`) as well as extract specific tags by name.
34
54
 
35
55
  ```ruby
36
56
  device = Lumberjack::JsonDevice.new(STDOUT, mapping: {
@@ -39,36 +59,41 @@ device = Lumberjack::JsonDevice.new(STDOUT, mapping: {
39
59
  progname: ["app", "name"],
40
60
  pid: ["app", "pid"],
41
61
  message: "message",
42
- duration: "duration",
62
+ duration: "duration", # Extracts the "duration" tag
43
63
  tags: "tags"
44
64
  })
45
65
  ```
46
66
 
47
67
  ```json
48
- {"timestamp": "2020-01-02T19:47:45.123455", "level": "INFO", "app": {"name": "web", "pid": 101}, "message": "test", "duration": 5, "tags": {"foo": "bar"}}
68
+ {"timestamp": "2020-01-02T19:47:45.123455-0800", "level": "INFO", "app": {"name": "web", "pid": 101}, "message": "test", "duration": 5, "tags": {"foo": "bar"}}
49
69
  ```
50
70
 
51
- If you omit any fields in the mapping, they will not appear in the JSON document.
71
+ ### Excluding Fields
72
+
73
+ If you omit fields from the mapping or set them to `false`, they will not appear in the JSON output:
52
74
 
53
75
  ```ruby
54
76
  device = Lumberjack::JsonDevice.new(STDOUT, mapping: {
55
77
  time: "timestamp",
56
78
  severity: "level",
57
79
  message: "message",
80
+ pid: false # Exclude PID from output
58
81
  })
59
82
  ```
60
83
 
61
84
  ```json
62
- {"timestamp": "2020-01-02T19:47:45.123455", "level": "INFO", "message": "test"}
85
+ {"timestamp": "2020-01-02T19:47:45.123455-0800", "level": "INFO", "message": "test"}
63
86
  ```
64
87
 
65
- You can also provide a block or any object that responds to `call` in a mapping. The block will be called with the value and should emit a hash that will be merged into the JSON document.
88
+ ### Custom Transformations
89
+
90
+ 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:
66
91
 
67
92
  ```ruby
68
93
  device = Lumberjack::JsonDevice.new(STDOUT, mapping: {
69
- time: lambda { |val| (timestamp: val.to_f * 1000).round} },
94
+ time: lambda { |val| {timestamp: (val.to_f * 1000).round} },
70
95
  severity: "level",
71
- message: "test",
96
+ message: "message",
72
97
  })
73
98
  ```
74
99
 
@@ -76,35 +101,157 @@ device = Lumberjack::JsonDevice.new(STDOUT, mapping: {
76
101
  {"timestamp": 1578125375588, "level": "INFO", "message": "test"}
77
102
  ```
78
103
 
79
- Finally, you can specify `true` in the mapping as a short cut to map the field to the same name. If the field name contains periods, it will be mapped to a nested structure.
104
+ ### Shortcut Mapping
105
+
106
+ Use `true` as a shortcut to map a field to the same name:
107
+
108
+ ```ruby
109
+ device = Lumberjack::JsonDevice.new(STDOUT, mapping: {
110
+ time: "timestamp",
111
+ severity: true, # Maps to "severity"
112
+ progname: true, # Maps to "progname"
113
+ pid: false, # Excluded from output
114
+ message: "message",
115
+ tags: true # Maps to "tags"
116
+ })
117
+ ```
118
+
119
+ ### Tag Extraction and Dot Notation
120
+
121
+ You can extract specific tags 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:
80
122
 
81
123
  ```ruby
82
124
  device = Lumberjack::JsonDevice.new(STDOUT, mapping: {
83
125
  "message" => true,
84
- "http.status" => true,
85
- "http.method" => true,
86
- "http.path" => true
126
+ "http.status" => true, # Extracts "http.status" tag
127
+ "http.method" => true, # Extracts "http.method" tag
128
+ "http.path" => true, # Extracts "http.path" tag
129
+ "tags" => true
87
130
  })
88
131
  ```
89
132
 
90
133
  ```json
91
- {"message": "test", "http": {"status": 200, "method": "GET", "path": "/resource"}}
134
+ {"message": "test", "http": {"status": 200, "method": "GET", "path": "/resource"}, "tags": {"other": "values"}}
92
135
  ```
93
136
 
137
+ **Important**: All tags are automatically expanded from dot notation into nested hash structures, not just extracted tags. For example, if you have a tag named `"user.profile.name"`, it will automatically become `{"user": {"profile": {"name": "value"}}}` in the tags section.
138
+
139
+ ### Flattening Tags to Root Level
140
+
141
+ Use `"*"` as the tags mapping value to copy all remaining tags directly to the root level of the JSON document:
142
+
143
+ ```ruby
144
+ device = Lumberjack::JsonDevice.new(STDOUT, mapping: {
145
+ "message" => true,
146
+ "tags" => "*"
147
+ })
148
+ ```
149
+
150
+ ```json
151
+ {"message": "test", "tag1": "value", "tag2": "value"}
152
+ ```
94
153
 
95
154
  ## Data Formatting
96
155
 
97
- The device will have a `Lumberjack::Formatter` that will be used to format objects before serializing them as JSON. You can add additional formatters for classes to the default formatter, or supply a custom one when creating the device.
156
+ 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.
98
157
 
99
158
  ```ruby
100
- device.formatter.add(Exception, LumberjacK::Formatter::InspectFormatter.new)
101
- device.formatter.add(ActiveRecord::Base, LumberjacK::Formatter::IdFormatter.new)
159
+ device.formatter.add(Exception, Lumberjack::Formatter::InspectFormatter.new)
160
+ device.formatter.add(ActiveRecord::Base, Lumberjack::Formatter::IdFormatter.new)
102
161
  ```
103
162
 
104
- You can also specify the `datetime_format` that will be used to serialize Time and DateTime objects.
163
+ ### Dynamic Mapping
164
+
165
+ You can incrementally add field mappings after creating the device using the `map` method:
166
+
167
+ ```ruby
168
+ device.map(duration: "response_time", user_id: ["user", "id"])
169
+ ```
170
+
171
+ ### DateTime Formatting
172
+
173
+ You can specify the `datetime_format` that will be used to serialize Time and DateTime objects:
105
174
 
106
175
  ```ruby
107
176
  device.datetime_format = "%Y-%m-%dT%H:%M:%S.%3N"
108
177
  ```
109
178
 
110
- Log entries with no message will not be written to the log.
179
+ ### Post Processing
180
+
181
+ 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
+
183
+ ```ruby
184
+ # Filter out sensitive elements using Rails parameter filter
185
+ param_filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
186
+ device = Lumberjack::JsonDevice.new(STDOUT, post_processor: ->(data) { param_filter.filter(data) }
187
+ ```
188
+
189
+ Note that all hash keys will be strings.If the post processor does not return a hash, it will be ignored.
190
+
191
+ ### Pretty Printing
192
+
193
+ For development or debugging, you can format the JSON output with indentation and newlines by setting the `pretty` option to `true`:
194
+
195
+ ```ruby
196
+ device = Lumberjack::JsonDevice.new(STDOUT, pretty: true)
197
+ ```
198
+
199
+ 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:
200
+
201
+ ```ruby
202
+ device.pretty? # => true or false
203
+ ```
204
+
205
+ ### Empty Messages
206
+
207
+ Log entries with empty or nil messages will not be written to the output.
208
+
209
+ ## Configuration Options
210
+
211
+ The `JsonDevice` constructor accepts the following options:
212
+
213
+ - **`mapping`**: Hash defining how log fields should be mapped to JSON (default: maps all standard fields)
214
+ - **`formatter`**: Custom `Lumberjack::Formatter` instance for formatting values before JSON serialization
215
+ - **`datetime_format`**: String format for Time/DateTime objects (default: `"%Y-%m-%dT%H:%M:%S.%6N%z"`)
216
+ - **`post_processor`**: Callable that receives and can modify the final hash before JSON serialization
217
+ - **`pretty`**: Boolean to enable pretty-printed JSON output (default: `false`)
218
+
219
+ ```ruby
220
+ device = Lumberjack::JsonDevice.new(
221
+ STDOUT,
222
+ mapping: { time: "timestamp", message: true, tags: "*" },
223
+ datetime_format: "%Y-%m-%d %H:%M:%S",
224
+ pretty: true,
225
+ post_processor: lambda { |data| data.merge(app: "myapp") }
226
+ )
227
+ ```
228
+
229
+ ## Installation
230
+
231
+ Add this line to your application's Gemfile:
232
+
233
+ ```ruby
234
+ gem "lumberjack_json_device"
235
+ ```
236
+
237
+ And then execute:
238
+
239
+ ```bash
240
+ bundle install
241
+ ```
242
+
243
+ Or install it yourself as:
244
+
245
+ ```bash
246
+ gem install lumberjack_json_device
247
+ ```
248
+
249
+ ## Contributing
250
+
251
+ Open a pull request on GitHub.
252
+
253
+ Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
254
+
255
+ ## License
256
+
257
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 2.1.0
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'lumberjack'
4
- require 'multi_json'
5
- require 'thread'
3
+ require "lumberjack"
4
+ require "json"
6
5
 
7
6
  module Lumberjack
8
7
  # This Lumberjack device logs output to another device as JSON formatted text with one document per line.
@@ -24,7 +23,6 @@ module Lumberjack
24
23
  #
25
24
  # You can create a nested JSON structure by specifying an array as the JSON key.
26
25
  class JsonDevice < Device
27
-
28
26
  DEFAULT_MAPPING = {
29
27
  time: true,
30
28
  severity: true,
@@ -37,15 +35,32 @@ module Lumberjack
37
35
  DEFAULT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%6N%z"
38
36
 
39
37
  attr_accessor :formatter
38
+ attr_accessor :post_processor
39
+ attr_writer :pretty
40
40
  attr_reader :mapping
41
41
 
42
- def initialize(stream_or_device, mapping: DEFAULT_MAPPING, formatter: nil, datetime_format: nil)
42
+ # @param stream_or_device [IO, Lumberjack::Device] The output stream or Lumberjack device to write
43
+ # the JSON formatted log entries to.
44
+ # @param mapping [Hash] A hash where the key is the log entry field name and the value indicates how
45
+ # to map the field if it exists. If the value is `true`, the field will be mapped to the same name.
46
+ # If the value is an array, it will be mapped to a nested structure that follows the array elements.
47
+ # If the value is a callable object, it will be called with the value and is expected to return
48
+ # a hash that will be merged into the JSON document.
49
+ # If the value is `false`, the field will not be included in the JSON output.
50
+ # @param formatter [Lumberjack::Formatter] An optional formatter to use for formatting the log entry data.
51
+ # @param datetime_format [String] An optional datetime format string to use for formatting the log timestamp.
52
+ # @param post_processor [Proc] An optional callable object that will be called with the log entry hash
53
+ # before it is written to the output stream. This can be used to modify the log entry data
54
+ # before it is serialized to JSON.
55
+ # @param pretty [Boolean] If true, the output will be formatted as pretty JSON with indentation and newlines.
56
+ # The default is false, which writes each log entry as a single line JSON document.
57
+ def initialize(stream_or_device, mapping: DEFAULT_MAPPING, formatter: nil, datetime_format: nil, post_processor: nil, pretty: false)
43
58
  @mutex = Mutex.new
44
59
 
45
- if stream_or_device.is_a?(Device)
46
- @device = stream_or_device
60
+ @device = if stream_or_device.is_a?(Device)
61
+ stream_or_device
47
62
  else
48
- @device = Writer.new(stream_or_device)
63
+ Lumberjack::Device::Writer.new(stream_or_device)
49
64
  end
50
65
 
51
66
  self.mapping = mapping
@@ -57,12 +72,17 @@ module Lumberjack
57
72
  datetime_format = DEFAULT_TIME_FORMAT if datetime_format.nil?
58
73
  end
59
74
  add_datetime_formatter!(datetime_format) unless datetime_format.nil?
75
+
76
+ @post_processor = post_processor
77
+
78
+ @pretty = !!pretty
60
79
  end
61
80
 
62
81
  def write(entry)
63
- return if entry.message.nil? || entry.message == ""
82
+ return if entry.empty?
83
+
64
84
  data = entry_as_json(entry)
65
- json = MultiJson.dump(data)
85
+ json = @pretty ? JSON.pretty_generate(data) : JSON.generate(data)
66
86
  @device.write(json)
67
87
  end
68
88
 
@@ -70,16 +90,31 @@ module Lumberjack
70
90
  @device.flush
71
91
  end
72
92
 
73
- def datetime_format
74
- @datetime_format
75
- end
93
+ attr_reader :datetime_format
76
94
 
77
95
  # Set the datetime format for the log timestamp.
96
+ #
97
+ # @param format [String] The datetime format string to use for formatting the log timestamp.
78
98
  def datetime_format=(format)
79
99
  add_datetime_formatter!(format)
80
100
  end
81
101
 
102
+ # Return true if the output is written in a multi-line pretty format. The default is to write each
103
+ # log entry as a single line JSON document.
104
+ #
105
+ # @return [Boolean]
106
+ def pretty?
107
+ !!@pretty
108
+ end
109
+
82
110
  # Set the mapping for how to map an entry to a JSON object.
111
+ #
112
+ # @param mapping [Hash] A hash where the key is the log entry field name and the value is the JSON key.
113
+ # If the value is `true`, the field will be mapped to the same name
114
+ # If the value is an array, it will be mapped to a nested structure.
115
+ # 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.
116
+ # If the value is `false`, the field will not be included in the JSON output.
117
+ # @return [void]
83
118
  def mapping=(mapping)
84
119
  @mutex.synchronize do
85
120
  keys = {}
@@ -97,48 +132,110 @@ module Lumberjack
97
132
  @pid_key = keys.delete(:pid)
98
133
  @message_key = keys.delete(:message)
99
134
  @tags_key = keys.delete(:tags)
100
- @custom_keys = keys
135
+ @custom_keys = keys.map do |name, key|
136
+ [name.to_s.split("."), key]
137
+ end.to_h
101
138
  @mapping = mapping
102
139
  end
103
- nil
104
140
  end
105
141
 
142
+ # Add a field mapping to the existing mappings.
143
+ #
144
+ # @param field_mapping [Hash] A hash where the key is the log entry field name and the value is the JSON key.
145
+ # If the value is `true`, the field will be mapped to the same name
146
+ # If the value is an array, it will be mapped to a nested structure.
147
+ # 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.
148
+ # If the value is `false`, the field will not be included in the JSON output.
149
+ # @return [void]
106
150
  def map(field_mapping)
107
- new_mapping = {}
108
- field_mapping.each do |key, value|
109
- new_mapping[key.to_sym] = value
110
- end
151
+ new_mapping = field_mapping.transform_keys(&:to_sym)
111
152
  self.mapping = mapping.merge(new_mapping)
112
153
  end
113
154
 
114
155
  # Convert a Lumberjack::LogEntry to a Hash using the specified field mapping.
156
+ #
157
+ # @param entry [Lumberjack::LogEntry] The log entry to convert.
158
+ # @return [Hash] A hash representing the log entry in JSON format.
115
159
  def entry_as_json(entry)
116
160
  data = {}
117
- set_attribute(data, @time_key, entry.time) unless @time_key.nil?
118
- set_attribute(data, @severity_key, entry.severity_label) unless @severity_key.nil?
119
- set_attribute(data, @progname_key, entry.progname) unless @progname_key.nil?
120
- set_attribute(data, @pid_key, entry.pid) unless @pid_key.nil?
121
- set_attribute(data, @message_key, entry.message) unless @message_key.nil?
122
-
123
- tags = entry.tags
124
- if @custom_keys.size > 0
125
- tags = (tags.nil? ? {} : tags.dup)
161
+ set_attribute(data, @time_key, entry.time) if @time_key
162
+ set_attribute(data, @severity_key, entry.severity_label) if @severity_key
163
+ set_attribute(data, @message_key, entry.message) if @message_key
164
+ set_attribute(data, @progname_key, entry.progname) if @progname_key
165
+ set_attribute(data, @pid_key, entry.pid) if @pid_key
166
+
167
+ tags = Lumberjack::Utils.expand_tags(entry.tags) if entry.tags
168
+ extracted_tags = nil
169
+ if @custom_keys.size > 0 && !tags&.empty?
170
+ extracted_tags = []
126
171
  @custom_keys.each do |name, key|
127
- set_attribute(data, key, tags.delete(name.to_s))
172
+ set_attribute(data, key, tag_value(tags, name))
173
+ extracted_tags << name
174
+ end
175
+
176
+ extracted_tags.each do |path|
177
+ tags = deep_remove_tag(tags, path, entry.tags)
128
178
  end
129
179
  end
130
180
 
131
- unless @tags_key.nil?
181
+ if @tags_key
132
182
  tags ||= {}
133
- set_attribute(data, @tags_key, tags)
183
+ if @tags_key == "*"
184
+ data = tags.merge(data) unless tags.empty?
185
+ else
186
+ set_attribute(data, @tags_key, tags)
187
+ end
134
188
  end
135
189
 
136
190
  data = @formatter.format(data) if @formatter
191
+ if @post_processor
192
+ processed_result = @post_processor.call(data)
193
+ data = processed_result if processed_result.is_a?(Hash)
194
+ end
195
+
137
196
  data
138
197
  end
139
198
 
140
199
  private
141
200
 
201
+ def tag_value(tags, name)
202
+ return nil if tags.nil?
203
+ return tags[name] unless name.is_a?(Array)
204
+
205
+ val = tags[name.first]
206
+ return val if name.length == 1
207
+ return nil unless val.is_a?(Hash)
208
+
209
+ tag_value(val, name[1, name.length])
210
+ end
211
+
212
+ def deep_remove_tag(tags, path, original_tags)
213
+ return nil if tags.nil?
214
+
215
+ dup_needed = tags.equal?(original_tags)
216
+ key = path.first
217
+ val = tags[key] if path.length > 1
218
+ unless val.is_a?(Hash)
219
+ if tags.include?(key)
220
+ tags = tags.dup if dup_needed
221
+ tags.delete(key)
222
+ end
223
+ return tags
224
+ end
225
+
226
+ new_val = deep_remove_tag(val, path[1, path.length], original_tags[key])
227
+ if new_val.empty? || !new_val.equal?(val)
228
+ tags = tags.dup if dup_needed
229
+ if new_val.empty?
230
+ tags.delete(key)
231
+ else
232
+ tags[key] = new_val
233
+ end
234
+ end
235
+
236
+ tags
237
+ end
238
+
142
239
  def set_attribute(data, key, value)
143
240
  return if value.nil?
144
241
 
@@ -191,13 +288,12 @@ module Lumberjack
191
288
  hash.merge!(other_hash) do |key, this_val, other_val|
192
289
  if this_val.is_a?(Hash) && other_val.is_a?(Hash)
193
290
  deep_merge!(this_val, other_val, &block)
194
- elsif block_given?
291
+ elsif block
195
292
  block.call(key, this_val, other_val)
196
293
  else
197
294
  other_val
198
295
  end
199
296
  end
200
297
  end
201
-
202
298
  end
203
299
  end
@@ -1,16 +1,16 @@
1
1
  Gem::Specification.new do |spec|
2
- spec.name = 'lumberjack_json_device'
3
- spec.version = File.read(File.expand_path("../VERSION", __FILE__)).strip
4
- spec.authors = ['Brian Durand']
5
- spec.email = ['bbdurand@gmail.com']
2
+ spec.name = "lumberjack_json_device"
3
+ spec.version = File.read(File.join(__dir__, "VERSION")).strip
4
+ spec.authors = ["Brian Durand"]
5
+ spec.email = ["bbdurand@gmail.com"]
6
6
 
7
- spec.summary = "A logging device for the lumberjack gem that writes log entries as JSON documentspec."
7
+ spec.summary = "A logging device for the lumberjack gem that writes log entries as JSON documents for use with structured logging."
8
8
  spec.homepage = "https://github.com/bdurand/lumberjack_json_device"
9
9
  spec.license = "MIT"
10
10
 
11
11
  # Specify which files should be added to the gem when it is released.
12
12
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
13
- ignore_files = %w(
13
+ ignore_files = %w[
14
14
  .gitignore
15
15
  .travis.yml
16
16
  Appraisals
@@ -19,16 +19,14 @@ Gem::Specification.new do |spec|
19
19
  Rakefile
20
20
  gemfiles/
21
21
  spec/
22
- )
23
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
- `git ls-files -z`.split("\x0").reject{ |f| ignore_files.any?{ |path| f.start_with?(path) } }
22
+ ]
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
25
25
  end
26
26
 
27
- spec.require_paths = ['lib']
27
+ spec.require_paths = ["lib"]
28
28
 
29
- spec.add_dependency "lumberjack", ">=1.2"
30
- spec.add_dependency "multi_json"
29
+ spec.required_ruby_version = ">= 2.5"
31
30
 
32
- spec.add_development_dependency("rspec", ["~> 3.0"])
33
- spec.add_development_dependency "rake"
31
+ spec.add_dependency "lumberjack", ">=1.3.3"
34
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lumberjack_json_device
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-20 00:00:00.000000000 Z
11
+ date: 2025-07-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lumberjack
@@ -16,65 +16,26 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.2'
19
+ version: 1.3.3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.2'
27
- - !ruby/object:Gem::Dependency
28
- name: multi_json
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '3.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '3.0'
55
- - !ruby/object:Gem::Dependency
56
- name: rake
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- description:
26
+ version: 1.3.3
27
+ description:
70
28
  email:
71
29
  - bbdurand@gmail.com
72
30
  executables: []
73
31
  extensions: []
74
32
  extra_rdoc_files: []
75
33
  files:
34
+ - ".github/dependabot.yml"
35
+ - ".github/workflows/continuous_integration.yml"
36
+ - ".standard.yml"
76
37
  - CHANGE_LOG.md
77
- - MIT_LICENSE
38
+ - MIT_LICENSE.txt
78
39
  - README.md
79
40
  - VERSION
80
41
  - lib/lumberjack_json_device.rb
@@ -83,7 +44,7 @@ homepage: https://github.com/bdurand/lumberjack_json_device
83
44
  licenses:
84
45
  - MIT
85
46
  metadata: {}
86
- post_install_message:
47
+ post_install_message:
87
48
  rdoc_options: []
88
49
  require_paths:
89
50
  - lib
@@ -91,15 +52,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
91
52
  requirements:
92
53
  - - ">="
93
54
  - !ruby/object:Gem::Version
94
- version: '0'
55
+ version: '2.5'
95
56
  required_rubygems_version: !ruby/object:Gem::Requirement
96
57
  requirements:
97
58
  - - ">="
98
59
  - !ruby/object:Gem::Version
99
60
  version: '0'
100
61
  requirements: []
101
- rubygems_version: 3.0.3
102
- signing_key:
62
+ rubygems_version: 3.4.10
63
+ signing_key:
103
64
  specification_version: 4
104
- summary: A logging device for the lumberjack gem that writes log entries as JSON documentspec.
65
+ summary: A logging device for the lumberjack gem that writes log entries as JSON documents
66
+ for use with structured logging.
105
67
  test_files: []
File without changes