lumberjack_ecs_device 1.0.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edb8e82b2c9f2ddd629c9efddf43f63ac4f5b984b754a9466c5d714e22727f0b
4
- data.tar.gz: 52bd32275d8e55f1e03cef9aa661e19fcf4de2fe5296c277dcf161cb523a1258
3
+ metadata.gz: 8ff36214ed38d35a87ea9a68e0eb234ca73b6b91421fbfa6d7e79f98e21464cf
4
+ data.tar.gz: 6c604f95f79e8aca48fc552c78200c9feb2ba402e13274adaf8490e920c6c060
5
5
  SHA512:
6
- metadata.gz: 2f0a44ee035ae0053a1d18927051f57d470dbb12b913a18c9e363897ad1148cd74f3d7dd62929377438e9d0657596338ef11b0f32d0951959c8b72e8c7346a29
7
- data.tar.gz: 3ce536acc0ba277dc0d193bc5fa1dd8fb0bf779cc53f9235b6f749d9842a11388d8b5eb03f8c53b7125d1d5f4400f6ea3fa5d3919e606800d64eda6b144129a7
6
+ metadata.gz: 022e7016df06cb1ae4a5b342cfde6905a1303a4ecaa585282c2ec62f089347e9dae11677ff7d75941975ee66f37fcc8493dc1ad17bfedc66d78589b9eaf3daa7
7
+ data.tar.gz: 715eea4f6041827a06adf890a6ffbdfae6596815e000ff6ffad16f63593cdd477f2afcb8d467a8a6debdf930c903b5dbada1f4520da1a8f5eba0d0cea54db376
data/CHANGE_LOG.md CHANGED
@@ -1,3 +1,22 @@
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.0.0
8
+
9
+ ### Changed
10
+
11
+ - Requires lumberjack >= 2.0
12
+ - **Breaking Change** Renamed `tags` to `attributes` in the ECS mapping.
13
+
14
+ ### Removed
15
+
16
+ - Removed support for Ruby < 2.7.
17
+
18
+ ## 1.0.0
19
+
20
+ ### Added
21
+
22
+ - Initial release
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Lumberjack ECS Device
2
2
 
3
- ![Continuous Integration](https://github.com/bdurand/lumberjack_ecs_device/workflows/Continuous%20Integration/badge.svg)
4
- [![Maintainability](https://api.codeclimate.com/v1/badges/97e98dc4d3d2565a3208/maintainability)](https://codeclimate.com/github/bdurand/lumberjack_ecs_device/maintainability)
3
+ [![Continuous Integration](https://github.com/bdurand/lumberjack_ecs_device/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/lumberjack_ecs_device/actions/workflows/continuous_integration.yml)
5
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_ecs_device.svg)](https://badge.fury.io/rb/lumberjack_ecs_device)
6
6
 
7
7
  This gem provides a logging device that produces JSON output that matches the standard fields defined for the [Elastic Common Schema](https://www.elastic.co/guide/en/ecs/current/ecs-reference.html). This allows logs to be sent seamlessly to Kibana or other servers that expect this format.
8
8
 
@@ -36,3 +36,31 @@ logger.tag("http.request.method" => request.method, "url.full" => request.url) d
36
36
  )
37
37
  end
38
38
  ```
39
+
40
+ ## Installation
41
+
42
+ Add this line to your application's Gemfile:
43
+
44
+ ```ruby
45
+ gem "lumberjack_ecs_device"
46
+ ```
47
+
48
+ And then execute:
49
+ ```bash
50
+ $ bundle
51
+ ```
52
+
53
+ Or install it yourself as:
54
+ ```bash
55
+ $ gem install lumberjack_ecs_device
56
+ ```
57
+
58
+ ## Contributing
59
+
60
+ Open a pull request on GitHub.
61
+
62
+ Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
63
+
64
+ ## License
65
+
66
+ 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.0.0
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lumberjack_json_device"
4
+
5
+ # Lumberjack is a simple, powerful, and fast logging library for Ruby.
6
+ module Lumberjack
7
+ # This Lumberjack device logs output to another device as JSON formatted text that maps fields
8
+ # to the standard ECS JSON format.
9
+ #
10
+ # See https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html
11
+ class EcsDevice < JsonDevice
12
+ VERSION = ::File.read(::File.join(__dir__, "..", "..", "VERSION")).strip.freeze
13
+
14
+ DeviceRegistry.add(:ecs, self)
15
+
16
+ # Mixin module that provides helper methods for formatting exception objects into hash format
17
+ # compatible with ECS error field structure.
18
+ module ExceptionHash
19
+ protected
20
+
21
+ def exception_hash(exception, device)
22
+ hash = {"type" => exception.class.name}
23
+ hash["message"] = exception.message unless exception.message.nil?
24
+ trace = exception.backtrace
25
+ if trace && device&.respond_to?(:backtrace_cleaner) && device.backtrace_cleaner
26
+ trace = device.backtrace_cleaner.call(trace)
27
+ end
28
+ hash["stack_trace"] = trace if trace
29
+ hash
30
+ end
31
+ end
32
+
33
+ # Formatter to format a messge as an error if it is an exception.
34
+ class MessageExceptionFormatter
35
+ include ExceptionHash
36
+
37
+ def initialize(device = nil)
38
+ @device = device
39
+ end
40
+
41
+ # Formats a message object into a hash with a "message" field and optionally an "error" field
42
+ # if the object is an exception.
43
+ #
44
+ # @param object [Object] The object to format (usually a message or exception)
45
+ # @return [Hash] A hash containing the formatted message and optional error information
46
+ def call(object)
47
+ if object.is_a?(Exception)
48
+ {
49
+ "message" => object.inspect,
50
+ "error" => exception_hash(object, @device)
51
+ }
52
+ elsif object.is_a?(Hash)
53
+ {"message" => object}
54
+ elsif object.nil?
55
+ {"message" => nil}
56
+ else
57
+ message = object.to_s
58
+ max_message_length = @device.max_message_length
59
+ if max_message_length && message.length > max_message_length
60
+ message = message[0, max_message_length]
61
+ end
62
+ {"message" => message}
63
+ end
64
+ end
65
+ end
66
+
67
+ # Formatter to remove empty attribute values and expand the error tag if it is an exception.
68
+ class EcsTagsFormatter
69
+ include ExceptionHash
70
+
71
+ def initialize(device = nil)
72
+ @device = device
73
+ end
74
+
75
+ # Processes tags by removing empty values and converting exceptions to ECS error format.
76
+ # Also handles nested field names using dot notation.
77
+ #
78
+ # @param tags [Hash] The tags hash to process
79
+ # @return [Hash] A processed hash with empty values removed and exceptions converted
80
+ def call(tags)
81
+ copy = {}
82
+ tags.each do |name, value|
83
+ value = remove_empty_values(value)
84
+ next if value.nil?
85
+
86
+ if name == "error" && value.is_a?(Exception)
87
+ copy[name] = exception_hash(value, @device)
88
+ elsif name.include?(".")
89
+ names = name.split(".")
90
+ next_value_in_hash(copy, names, value)
91
+ else
92
+ copy[name] = value
93
+ end
94
+ end
95
+ copy
96
+ end
97
+
98
+ private
99
+
100
+ def remove_empty_values(value)
101
+ if value.is_a?(String)
102
+ value unless value.empty?
103
+ elsif value.is_a?(Hash)
104
+ new_hash = {}
105
+ value.each do |key, val|
106
+ val = remove_empty_values(val)
107
+ new_hash[key] = val unless val.nil?
108
+ end
109
+ new_hash unless new_hash.empty?
110
+ elsif value.is_a?(Array)
111
+ new_array = value.collect { |val| remove_empty_values(val) }
112
+ new_array unless new_array.empty?
113
+ else
114
+ value
115
+ end
116
+ end
117
+
118
+ def next_value_in_hash(hash, keys, value)
119
+ key = keys.first
120
+ if keys.size == 1
121
+ hash[key] = value
122
+ else
123
+ current = hash[key]
124
+ unless current.is_a?(Hash)
125
+ current = {}
126
+ hash[key] = current
127
+ end
128
+ next_value_in_hash(current, keys[1, keys.size], value)
129
+ end
130
+ end
131
+ end
132
+
133
+ # Formatter that converts duration values to ECS event.duration format with proper units.
134
+ class DurationFormatter
135
+ def initialize(multiplier)
136
+ @multiplier = multiplier
137
+ end
138
+
139
+ # Formats a numeric duration value by applying a multiplier and wrapping it in ECS event structure.
140
+ #
141
+ # @param value [Numeric, Object] The duration value to format
142
+ # @return [Hash] A hash containing the formatted duration in ECS event.duration format
143
+ def call(value)
144
+ if value.is_a?(Numeric)
145
+ value = (value.to_f * @multiplier).round
146
+ end
147
+ {"event" => {"duration" => value}}
148
+ end
149
+ end
150
+
151
+ # The default timestamp format used for ECS @timestamp field.
152
+ # Format: ISO 8601 with microseconds and UTC timezone (e.g., "2023-12-01T14:30:45.123456Z")
153
+ ECS_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%6NZ"
154
+
155
+ # You can specify a backtrace cleaner that will be called with exception backtraces before they
156
+ # are added to the payload. You can use this to remove superfluous lines, compress line length, etc.
157
+ # One use for it is to keep stack traces clean and prevent them from overflowing the limit on
158
+ # the payload size for an individual log entry.
159
+ attr_accessor :backtrace_cleaner
160
+
161
+ # You can specify a limit on the message size. Messages over this size will be split into multiple
162
+ # log entries to prevent overflowing the limit on message size which makes the log entries unparseable.
163
+ attr_accessor :max_message_length
164
+
165
+ def initialize(stream_or_device, backtrace_cleaner: nil, max_message_length: nil, datetime_format: ECS_TIMESTAMP_FORMAT)
166
+ super(output: stream_or_device, mapping: ecs_mapping, datetime_format: datetime_format)
167
+ self.backtrace_cleaner = backtrace_cleaner
168
+ self.max_message_length = max_message_length
169
+ @utc_timestamps = !!datetime_format.match(/[^%]Z/)
170
+ end
171
+
172
+ # Converts a log entry to JSON format, ensuring timestamps are in UTC if required.
173
+ #
174
+ # @param entry [Lumberjack::LogEntry] The log entry to convert
175
+ # @return [String] The JSON representation of the log entry
176
+ def entry_as_json(entry)
177
+ original_time = entry.time
178
+ begin
179
+ entry.time = entry.time.utc if @utc_timestamps && !entry.time.utc?
180
+ super
181
+ ensure
182
+ entry.time = original_time
183
+ end
184
+ end
185
+
186
+ private
187
+
188
+ def ecs_mapping
189
+ {
190
+ time: "@timestamp",
191
+ severity: ["log", "level"],
192
+ progname: ["process", "name"],
193
+ pid: ["process", "pid"],
194
+ message: MessageExceptionFormatter.new(self),
195
+ duration: DurationFormatter.new(1_000_000_000),
196
+ duration_ms: DurationFormatter.new(1_000_000),
197
+ duration_micros: DurationFormatter.new(1_000),
198
+ duration_ns: ["event", "duration"],
199
+ attributes: EcsTagsFormatter.new(self)
200
+ }
201
+ end
202
+ end
203
+ end
@@ -1,175 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "lumberjack_json_device"
4
-
5
- module Lumberjack
6
- # This Lumberjack device logs output to another device as JSON formatted text that maps fields
7
- # to the standard ECS JSON format.
8
- #
9
- # See https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html
10
- class EcsDevice < JsonDevice
11
- module ExceptionHash
12
- protected
13
-
14
- def exception_hash(exception, device)
15
- hash = {"type" => exception.class.name}
16
- hash["message"] = exception.message unless exception.message.nil?
17
- trace = exception.backtrace
18
- if trace && device && device.respond_to?(:backtrace_cleaner) && device.backtrace_cleaner
19
- trace = device.backtrace_cleaner.call(trace)
20
- end
21
- hash["stack_trace"] = trace if trace
22
- hash
23
- end
24
- end
25
-
26
- # Formatter to format a messge as an error if it is an exception.
27
- class MessageExceptionFormatter
28
- include ExceptionHash
29
-
30
- def initialize(device = nil)
31
- @device = device
32
- end
33
-
34
- def call(object)
35
- if object.is_a?(Exception)
36
- {
37
- "message" => object.inspect,
38
- "error" => exception_hash(object, @device)
39
- }
40
- elsif object.is_a?(Hash)
41
- {"message" => object}
42
- elsif object.nil?
43
- {"message" => nil}
44
- else
45
- message = object.to_s
46
- max_message_length = @device.max_message_length
47
- if max_message_length && message.length > max_message_length
48
- message = message[0, max_message_length]
49
- end
50
- {"message" => message}
51
- end
52
- end
53
- end
54
-
55
- # Formatter to remove empty tag values and expand the error tag if it is an exception.
56
- class EcsTagsFormatter
57
- include ExceptionHash
58
-
59
- def initialize(device = nil)
60
- @device = device
61
- end
62
-
63
- def call(tags)
64
- copy = {}
65
- tags.each do |name, value|
66
- value = remove_empty_values(value)
67
- next if value.nil?
68
-
69
- if name == "error" && value.is_a?(Exception)
70
- copy[name] = exception_hash(value, @device)
71
- elsif name.include?(".")
72
- names = name.split(".")
73
- next_value_in_hash(copy, names, value)
74
- else
75
- copy[name] = value
76
- end
77
- end
78
- copy
79
- end
80
-
81
- private
82
-
83
- def remove_empty_values(value)
84
- if value.is_a?(String)
85
- value unless value.empty?
86
- elsif value.is_a?(Hash)
87
- new_hash = {}
88
- value.each do |key, val|
89
- val = remove_empty_values(val)
90
- new_hash[key] = val unless val.nil?
91
- end
92
- new_hash unless new_hash.empty?
93
- elsif value.is_a?(Array)
94
- new_array = value.collect { |val| remove_empty_values(val) }
95
- new_array unless new_array.empty?
96
- else
97
- value
98
- end
99
- end
100
-
101
- def next_value_in_hash(hash, keys, value)
102
- key = keys.first
103
- if keys.size == 1
104
- hash[key] = value
105
- else
106
- current = hash[key]
107
- unless current.is_a?(Hash)
108
- current = {}
109
- hash[key] = current
110
- end
111
- next_value_in_hash(current, keys[1, keys.size], value)
112
- end
113
- end
114
- end
115
-
116
- class DurationFormatter
117
- def initialize(multiplier)
118
- @multiplier = multiplier
119
- end
120
-
121
- def call(value)
122
- if value.is_a?(Numeric)
123
- value = (value.to_f * @multiplier).round
124
- end
125
- {"event" => {"duration" => value}}
126
- end
127
- end
128
-
129
- ECS_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%6NZ"
130
-
131
- # You can specify a backtrace cleaner that will be called with exception backtraces before they
132
- # are added to the payload. You can use this to remove superfluous lines, compress line length, etc.
133
- # One use for it is to keep stack traces clean and prevent them from overflowing the limit on
134
- # the payload size for an individual log entry.
135
- attr_accessor :backtrace_cleaner
136
-
137
- # You can specify a limit on the message size. Messages over this size will be split into multiple
138
- # log entries to prevent overflowing the limit on message size which makes the log entries unparseable.
139
- attr_accessor :max_message_length
140
-
141
- def initialize(stream_or_device, backtrace_cleaner: nil, max_message_length: nil, datetime_format: ECS_TIMESTAMP_FORMAT)
142
- super(stream_or_device, mapping: ecs_mapping, datetime_format: datetime_format)
143
- self.backtrace_cleaner = backtrace_cleaner
144
- self.max_message_length = max_message_length
145
- @utc_timestamps = !!datetime_format.match(/[^%]Z/)
146
- end
147
-
148
- def entry_as_json(entry)
149
- original_time = entry.time
150
- begin
151
- entry.time = entry.time.utc if @utc_timestamps && !entry.time.utc?
152
- super
153
- ensure
154
- entry.time = original_time
155
- end
156
- end
157
-
158
- private
159
-
160
- def ecs_mapping
161
- {
162
- time: "@timestamp",
163
- severity: ["log", "level"],
164
- progname: ["process", "name"],
165
- pid: ["process", "pid"],
166
- message: MessageExceptionFormatter.new(self),
167
- duration: DurationFormatter.new(1_000_000_000),
168
- duration_ms: DurationFormatter.new(1_000_000),
169
- duration_micros: DurationFormatter.new(1_000),
170
- duration_ns: ["event", "duration"],
171
- tags: EcsTagsFormatter.new(self)
172
- }
173
- end
174
- end
175
- end
3
+ require_relative "lumberjack/ecs_device"
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "lumberjack_ecs_device"
3
- spec.version = File.read(File.expand_path("../VERSION", __FILE__)).strip
3
+ spec.version = File.read(File.join(__dir__, "VERSION")).strip
4
4
  spec.authors = ["Brian Durand"]
5
5
  spec.email = ["bbdurand@gmail.com"]
6
6
 
@@ -8,6 +8,12 @@ Gem::Specification.new do |spec|
8
8
  spec.homepage = "https://github.com/bdurand/lumberjack_ecs_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[
@@ -24,5 +30,9 @@ Gem::Specification.new do |spec|
24
30
 
25
31
  spec.require_paths = ["lib"]
26
32
 
27
- spec.add_dependency "lumberjack_json_device", ">=1.0"
33
+ spec.required_ruby_version = ">= 2.7"
34
+
35
+ spec.add_dependency "lumberjack_json_device", ">=3.0"
36
+
37
+ spec.add_development_dependency "bundler"
28
38
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lumberjack_ecs_device
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.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: 2021-01-27 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: lumberjack_json_device
@@ -16,15 +15,28 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '1.0'
18
+ version: '3.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: '1.0'
27
- description:
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: bundler
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
28
40
  email:
29
41
  - bbdurand@gmail.com
30
42
  executables: []
@@ -32,16 +44,19 @@ extensions: []
32
44
  extra_rdoc_files: []
33
45
  files:
34
46
  - CHANGE_LOG.md
35
- - MIT_LICENSE
47
+ - MIT_LICENSE.txt
36
48
  - README.md
37
49
  - VERSION
50
+ - lib/lumberjack/ecs_device.rb
38
51
  - lib/lumberjack_ecs_device.rb
39
52
  - lumberjack_ecs_device.gemspec
40
53
  homepage: https://github.com/bdurand/lumberjack_ecs_device
41
54
  licenses:
42
55
  - MIT
43
- metadata: {}
44
- post_install_message:
56
+ metadata:
57
+ homepage_uri: https://github.com/bdurand/lumberjack_ecs_device
58
+ source_code_uri: https://github.com/bdurand/lumberjack_ecs_device
59
+ changelog_uri: https://github.com/bdurand/lumberjack_ecs_device/blob/main/CHANGE_LOG.md
45
60
  rdoc_options: []
46
61
  require_paths:
47
62
  - lib
@@ -49,15 +64,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
49
64
  requirements:
50
65
  - - ">="
51
66
  - !ruby/object:Gem::Version
52
- version: '0'
67
+ version: '2.7'
53
68
  required_rubygems_version: !ruby/object:Gem::Requirement
54
69
  requirements:
55
70
  - - ">="
56
71
  - !ruby/object:Gem::Version
57
72
  version: '0'
58
73
  requirements: []
59
- rubygems_version: 3.0.3
60
- signing_key:
74
+ rubygems_version: 3.6.9
61
75
  specification_version: 4
62
76
  summary: A logging device for formatting logs in Elastic Container Schema (ECS) format
63
77
  for integration with Kibana.
File without changes