semantic_logger 4.17.0 → 5.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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +42 -81
  3. data/Rakefile +8 -1
  4. data/lib/semantic_logger/appender/async.rb +86 -173
  5. data/lib/semantic_logger/appender/cloudwatch_logs.rb +4 -4
  6. data/lib/semantic_logger/appender/elasticsearch.rb +6 -182
  7. data/lib/semantic_logger/appender/elasticsearch_base.rb +212 -0
  8. data/lib/semantic_logger/appender/elasticsearch_http.rb +2 -2
  9. data/lib/semantic_logger/appender/file.rb +20 -4
  10. data/lib/semantic_logger/appender/graylog.rb +2 -2
  11. data/lib/semantic_logger/appender/honeybadger_insights.rb +1 -1
  12. data/lib/semantic_logger/appender/http.rb +27 -2
  13. data/lib/semantic_logger/appender/io.rb +8 -4
  14. data/lib/semantic_logger/appender/kafka.rb +2 -2
  15. data/lib/semantic_logger/appender/loki.rb +2 -4
  16. data/lib/semantic_logger/appender/mongodb.rb +3 -6
  17. data/lib/semantic_logger/appender/new_relic_logs.rb +12 -2
  18. data/lib/semantic_logger/appender/open_telemetry.rb +25 -11
  19. data/lib/semantic_logger/appender/opensearch.rb +35 -0
  20. data/lib/semantic_logger/appender/rabbitmq.rb +3 -3
  21. data/lib/semantic_logger/appender/sentry_ruby.rb +8 -3
  22. data/lib/semantic_logger/appender/splunk.rb +2 -2
  23. data/lib/semantic_logger/appender/splunk_http.rb +3 -3
  24. data/lib/semantic_logger/appender/syslog.rb +5 -4
  25. data/lib/semantic_logger/appender/tcp.rb +2 -2
  26. data/lib/semantic_logger/appender/udp.rb +2 -2
  27. data/lib/semantic_logger/appender/wrapper.rb +4 -4
  28. data/lib/semantic_logger/appender.rb +30 -19
  29. data/lib/semantic_logger/appenders.rb +26 -5
  30. data/lib/semantic_logger/base.rb +113 -21
  31. data/lib/semantic_logger/concerns/compatibility.rb +2 -2
  32. data/lib/semantic_logger/core_ext/process.rb +34 -0
  33. data/lib/semantic_logger/formatters/base.rb +46 -7
  34. data/lib/semantic_logger/formatters/color.rb +6 -3
  35. data/lib/semantic_logger/formatters/default.rb +6 -4
  36. data/lib/semantic_logger/formatters/ecs.rb +151 -0
  37. data/lib/semantic_logger/formatters/fluentd.rb +15 -4
  38. data/lib/semantic_logger/formatters/json.rb +6 -1
  39. data/lib/semantic_logger/formatters/logfmt.rb +2 -2
  40. data/lib/semantic_logger/formatters/loki.rb +4 -4
  41. data/lib/semantic_logger/formatters/new_relic_logs.rb +4 -6
  42. data/lib/semantic_logger/formatters/open_telemetry.rb +40 -2
  43. data/lib/semantic_logger/formatters/pattern.rb +235 -0
  44. data/lib/semantic_logger/formatters/raw.rb +2 -2
  45. data/lib/semantic_logger/formatters/signalfx.rb +2 -2
  46. data/lib/semantic_logger/formatters/syslog.rb +14 -3
  47. data/lib/semantic_logger/formatters/syslog_cee.rb +1 -1
  48. data/lib/semantic_logger/formatters.rb +2 -0
  49. data/lib/semantic_logger/log.rb +23 -4
  50. data/lib/semantic_logger/logger.rb +2 -2
  51. data/lib/semantic_logger/metric/new_relic.rb +2 -2
  52. data/lib/semantic_logger/metric/signalfx.rb +2 -2
  53. data/lib/semantic_logger/metric/statsd.rb +2 -2
  54. data/lib/semantic_logger/processor.rb +22 -1
  55. data/lib/semantic_logger/queue_processor.rb +369 -0
  56. data/lib/semantic_logger/reporters/minitest.rb +4 -4
  57. data/lib/semantic_logger/semantic_logger.rb +103 -11
  58. data/lib/semantic_logger/subscriber.rb +15 -2
  59. data/lib/semantic_logger/sync_processor.rb +25 -3
  60. data/lib/semantic_logger/test/capture_log_events.rb +2 -2
  61. data/lib/semantic_logger/test/minitest.rb +8 -4
  62. data/lib/semantic_logger/test/rspec.rb +249 -0
  63. data/lib/semantic_logger/utils.rb +83 -4
  64. data/lib/semantic_logger/version.rb +1 -1
  65. data/lib/semantic_logger.rb +9 -0
  66. metadata +15 -6
  67. data/lib/semantic_logger/appender/async_batch.rb +0 -93
@@ -0,0 +1,249 @@
1
+ require "rspec/expectations"
2
+
3
+ module SemanticLogger
4
+ module Test
5
+ # RSpec matchers and helpers for asserting on Semantic Logger events.
6
+ #
7
+ # These mirror the Minitest helpers in SemanticLogger::Test::Minitest.
8
+ #
9
+ # Enable them once, in spec_helper.rb:
10
+ #
11
+ # require "semantic_logger/test/rspec"
12
+ #
13
+ # RSpec.configure do |config|
14
+ # config.include SemanticLogger::Test::RSpec
15
+ # end
16
+ #
17
+ # Capture the events emitted whilst running a block, then assert on them:
18
+ #
19
+ # events = capture_semantic_logger_events { User.new.enable! }
20
+ # expect(events.first).to be_a_semantic_logger_event(level: :info, message: "User enabled")
21
+ #
22
+ # Or assert directly that a block logs a matching event:
23
+ #
24
+ # expect { User.new.enable! }.to(
25
+ # log_semantic_logger_event(level: :info, message: "User enabled")
26
+ # )
27
+ module RSpec
28
+ # Attributes that may be asserted on a single log event. The keys map
29
+ # directly onto SemanticLogger::Log attributes, except for the
30
+ # *_includes variants which assert a partial (substring / subset) match.
31
+ EVENT_ATTRIBUTES = %i[
32
+ level name message thread_name tags named_tags context
33
+ metric metric_amount dimensions level_index duration time
34
+ exception backtrace payload
35
+ ].freeze
36
+
37
+ # Matches a single SemanticLogger::Log against a set of expectations.
38
+ #
39
+ # An expected value may be:
40
+ # * a Class - the actual value must be a kind_of that class
41
+ # * :nil - the actual value must be nil
42
+ # * any value - compared with ==
43
+ class EventMatcher
44
+ include ::RSpec::Matchers::Composable
45
+
46
+ def initialize(message_includes: nil, payload_includes: nil, exception_includes: nil, **expected)
47
+ unknown = expected.keys - EVENT_ATTRIBUTES
48
+ raise ArgumentError, "Unknown log event attribute(s): #{unknown.join(', ')}" unless unknown.empty?
49
+
50
+ @expected = expected
51
+ @message_includes = message_includes
52
+ @payload_includes = payload_includes
53
+ @exception_includes = exception_includes
54
+ end
55
+
56
+ def matches?(event)
57
+ @event = event
58
+ return failed("no log event") unless event
59
+
60
+ @expected.each_pair do |name, expected|
61
+ actual = event.public_send(name)
62
+ return failed(mismatch(name, expected, actual)) unless attribute_matches?(expected, actual)
63
+ end
64
+
65
+ return failed(includes_failure(:message, @message_includes)) unless message_includes_matches?
66
+ return failed(includes_failure(:payload, @payload_includes)) unless payload_includes_matches?
67
+ return failed(includes_failure(:exception, @exception_includes)) unless exception_includes_matches?
68
+
69
+ true
70
+ end
71
+
72
+ def description
73
+ "be a semantic logger event matching #{full_expectations.inspect}"
74
+ end
75
+
76
+ def failure_message
77
+ "expected log event to #{description}, but #{@failure}.\n" \
78
+ "Log event: #{event_inspect}"
79
+ end
80
+
81
+ def failure_message_when_negated
82
+ "expected log event not to #{description}.\nLog event: #{event_inspect}"
83
+ end
84
+
85
+ private
86
+
87
+ def failed(reason)
88
+ @failure = reason
89
+ false
90
+ end
91
+
92
+ def attribute_matches?(expected, actual)
93
+ case expected
94
+ when :nil
95
+ actual.nil?
96
+ when Class
97
+ actual.is_a?(expected)
98
+ else
99
+ expected == actual
100
+ end
101
+ end
102
+
103
+ def message_includes_matches?
104
+ return true unless @message_includes
105
+
106
+ @event.message&.include?(@message_includes)
107
+ end
108
+
109
+ def payload_includes_matches?
110
+ return true unless @payload_includes
111
+
112
+ payload = @event.payload || {}
113
+ @payload_includes.all? { |key, value| payload[key] == value }
114
+ end
115
+
116
+ def exception_includes_matches?
117
+ return true unless @exception_includes
118
+
119
+ exception = @event.exception
120
+ return false unless exception
121
+
122
+ @exception_includes.all? { |key, value| exception.public_send(key) == value }
123
+ end
124
+
125
+ def mismatch(name, expected, actual)
126
+ "#{name} was #{actual.inspect} (expected #{expected.inspect})"
127
+ end
128
+
129
+ def includes_failure(name, expected)
130
+ "#{name} did not include #{expected.inspect}"
131
+ end
132
+
133
+ def full_expectations
134
+ @expected.merge(
135
+ {
136
+ message_includes: @message_includes,
137
+ payload_includes: @payload_includes,
138
+ exception_includes: @exception_includes
139
+ }.compact
140
+ )
141
+ end
142
+
143
+ def event_inspect
144
+ @event.respond_to?(:to_h) ? @event.to_h.inspect : @event.inspect
145
+ end
146
+ end
147
+
148
+ # Matches a block, asserting that it emits at least one log event that
149
+ # matches the supplied expectations.
150
+ class LogEventMatcher
151
+ include ::RSpec::Matchers::Composable
152
+
153
+ def initialize(capture, on:, expected:)
154
+ @capture = capture
155
+ @on = on
156
+ @matcher = EventMatcher.new(**expected)
157
+ end
158
+
159
+ def matches?(block)
160
+ @events = @capture.call(@on, &block)
161
+ @events.any? { |event| @matcher.matches?(event) }
162
+ end
163
+
164
+ def supports_block_expectations?
165
+ true
166
+ end
167
+
168
+ def description
169
+ "log a semantic logger event matching #{@matcher.description}"
170
+ end
171
+
172
+ def failure_message
173
+ "expected the block to #{description}.\n" \
174
+ "Captured #{@events.size} event(s):\n#{captured_inspect}"
175
+ end
176
+
177
+ def failure_message_when_negated
178
+ "expected the block not to #{description}, but it did.\n" \
179
+ "Captured #{@events.size} event(s):\n#{captured_inspect}"
180
+ end
181
+
182
+ private
183
+
184
+ def captured_inspect
185
+ @events.map { |event| " #{event.to_h.inspect}" }.join("\n")
186
+ end
187
+ end
188
+
189
+ # Returns [Array<SemanticLogger::Log>] the log events captured whilst
190
+ # running the supplied block.
191
+ #
192
+ # Notes:
193
+ # - All log events are captured regardless of the global default log level.
194
+ # - Pass a class to capture only events logged through that class's logger.
195
+ # Otherwise every log event in the process is captured for the duration
196
+ # of the block.
197
+ def capture_semantic_logger_events(klass = nil, silence: :trace, &block)
198
+ logger = SemanticLogger::Test::CaptureLogEvents.new
199
+
200
+ if klass
201
+ allow(klass).to receive(:logger).and_return(logger)
202
+ block.call
203
+ elsif silence
204
+ SemanticLogger.silence(silence) do
205
+ stub_processor(logger, &block)
206
+ end
207
+ else
208
+ stub_processor(logger, &block)
209
+ end
210
+
211
+ logger.events
212
+ end
213
+
214
+ # Matcher for a single captured log event.
215
+ #
216
+ # expect(events.first).to be_a_semantic_logger_event(level: :info, message: "Hi")
217
+ def be_a_semantic_logger_event(**expected)
218
+ EventMatcher.new(**expected)
219
+ end
220
+
221
+ # Composable alias, for use inside other matchers:
222
+ #
223
+ # expect(events).to include(a_semantic_logger_event(message: "Hi"))
224
+ def a_semantic_logger_event(**expected)
225
+ EventMatcher.new(**expected)
226
+ end
227
+
228
+ # Block matcher asserting that the block logs a matching event.
229
+ #
230
+ # expect { User.new.enable! }.to(
231
+ # log_semantic_logger_event(level: :info, message: "User enabled")
232
+ # )
233
+ #
234
+ # Pass `on:` to capture only one class's events.
235
+ def log_semantic_logger_event(on: nil, **expected)
236
+ LogEventMatcher.new(method(:capture_semantic_logger_events), on: on, expected: expected)
237
+ end
238
+
239
+ private
240
+
241
+ def stub_processor(logger)
242
+ allow(SemanticLogger::Logger).to receive(:processor).and_return(logger)
243
+ yield
244
+ ensure
245
+ allow(SemanticLogger::Logger).to receive(:processor).and_call_original
246
+ end
247
+ end
248
+ end
249
+ end
@@ -1,15 +1,28 @@
1
+ require "json"
2
+
1
3
  module SemanticLogger
2
4
  # Internal-use only utility functions for Semantic Logger.
3
5
  # Not intended for public use.
4
6
  module Utils
5
7
  def self.constantize_symbol(symbol, namespace = "SemanticLogger::Appender")
6
8
  klass = "#{namespace}::#{camelize(symbol.to_s)}"
7
- begin
8
- Object.const_get(klass)
9
- rescue NameError
9
+ constant =
10
+ begin
11
+ Object.const_get(klass)
12
+ rescue NameError
13
+ raise(ArgumentError,
14
+ "Could not convert symbol: #{symbol.inspect} to a class in: #{namespace}. Looking for: #{klass}")
15
+ end
16
+
17
+ # The resolved constant is instantiated by the caller, so ensure it is
18
+ # actually a class within the expected namespace rather than some other
19
+ # constant that happens to share the name.
20
+ unless constant.is_a?(Class)
10
21
  raise(ArgumentError,
11
- "Could not convert symbol: #{symbol.inspect} to a class in: #{namespace}. Looking for: #{klass}")
22
+ "Could not convert symbol: #{symbol.inspect} to a class in: #{namespace}. #{klass} is not a class.")
12
23
  end
24
+
25
+ constant
13
26
  end
14
27
 
15
28
  # Borrow from Rails, when not running Rails
@@ -76,5 +89,71 @@ module SemanticLogger
76
89
  def self.strip_path?(path)
77
90
  strip_paths.any? { |exclude| path.start_with?(exclude) }
78
91
  end
92
+
93
+ # Serializes the value to JSON, repairing invalid UTF-8 only when necessary.
94
+ #
95
+ # Non UTF-8 data appears in well under 1% of log events, so it is wasteful to
96
+ # walk and reallocate the entire structure (see .encode_utf8) on every call.
97
+ # Instead this attempts `.to_json` directly and only falls back to cleansing
98
+ # when serialization fails because of an encoding problem.
99
+ #
100
+ # The exception raised for non UTF-8 data depends on the json gem version:
101
+ # older versions raise Encoding::UndefinedConversionError (an EncodingError),
102
+ # newer versions wrap it as JSON::GeneratorError, so both are rescued. The
103
+ # retry is attempted only once: if it still fails (for example a
104
+ # JSON::GeneratorError caused by something other than encoding, such as NaN),
105
+ # the error propagates unchanged rather than being swallowed.
106
+ def self.to_json(value)
107
+ value.to_json
108
+ rescue JSON::GeneratorError, EncodingError
109
+ encode_utf8(value).to_json
110
+ end
111
+
112
+ # Returns a copy of the supplied value with every String converted to valid UTF-8.
113
+ #
114
+ # Recurses through Hash and Array structures, cleansing both keys and values.
115
+ # Strings that are already valid UTF-8 are returned unchanged (the common case),
116
+ # so the fast path allocates nothing. Any other value (Symbol, Numeric, Time, nil,
117
+ # ...) is returned as-is.
118
+ #
119
+ # Used by .to_json on the rare failing path, and directly by formatters that
120
+ # serialize per value or emit to a non-JSON sink (where a single `.to_json`
121
+ # rescue boundary cannot catch an intermediate failure).
122
+ def self.encode_utf8(value)
123
+ case value
124
+ when String
125
+ encode_utf8_string(value)
126
+ when Hash
127
+ value.each_with_object({}) do |(key, val), hash|
128
+ hash[encode_utf8(key)] = encode_utf8(val)
129
+ end
130
+ when Array
131
+ value.map { |element| encode_utf8(element) }
132
+ else
133
+ value
134
+ end
135
+ end
136
+
137
+ # Options used when transcoding a string to UTF-8.
138
+ # Invalid byte sequences and characters that cannot be represented in UTF-8 are
139
+ # dropped rather than substituted, matching the preference in issue #180.
140
+ ENCODE_UTF8_OPTIONS = {invalid: :replace, undef: :replace, replace: "".freeze}.freeze
141
+
142
+ # Returns the string converted to valid UTF-8, dropping any invalid bytes.
143
+ def self.encode_utf8_string(string)
144
+ return string if string.encoding == Encoding::UTF_8 && string.valid_encoding?
145
+
146
+ if string.encoding == Encoding::UTF_8
147
+ # Correctly tagged as UTF-8 but contains invalid byte sequences.
148
+ string.scrub("")
149
+ else
150
+ # Different encoding (e.g. ASCII-8BIT / Latin-1): transcode into UTF-8.
151
+ string.encode(Encoding::UTF_8, **ENCODE_UTF8_OPTIONS)
152
+ end
153
+ rescue EncodingError
154
+ # Last resort for encodings without a converter to UTF-8: reinterpret the
155
+ # raw bytes as UTF-8 and drop anything invalid. Logging must never raise.
156
+ string.dup.force_encoding(Encoding::UTF_8).scrub("")
157
+ end
79
158
  end
80
159
  end
@@ -1,3 +1,3 @@
1
1
  module SemanticLogger
2
- VERSION = "4.17.0".freeze
2
+ VERSION = "5.0.0".freeze
3
3
  end
@@ -12,12 +12,21 @@ require "semantic_logger/loggable"
12
12
  require "semantic_logger/concerns/compatibility"
13
13
  require "semantic_logger/appender"
14
14
  require "semantic_logger/appenders"
15
+ require "semantic_logger/queue_processor"
15
16
  require "semantic_logger/processor"
16
17
  require "semantic_logger/sync_processor"
17
18
  require "semantic_logger/logger"
18
19
  require "semantic_logger/debug_as_trace_logger"
19
20
  require "semantic_logger/semantic_logger"
20
21
 
22
+ # Automatically reopen appenders in the child process after a fork.
23
+ # Enabled by default; opt out with `SemanticLogger.reopen_on_fork = false`.
24
+ # Skipped on platforms without `Process._fork` (e.g. JRuby), which cannot fork.
25
+ if Process.respond_to?(:_fork)
26
+ require "semantic_logger/core_ext/process"
27
+ Process.singleton_class.prepend(SemanticLogger::CoreExt::Process)
28
+ end
29
+
21
30
  # Flush all appenders at exit, waiting for outstanding messages on the queue
22
31
  # to be written first.
23
32
  at_exit do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: semantic_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.17.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
@@ -23,6 +23,9 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '1.0'
26
+ description: Semantic Logger is a high-performance, asynchronous structured logging
27
+ framework for Ruby & Rails. It logs to multiple destinations via a background thread,
28
+ preserving structured (semantic) payloads.
26
29
  executables: []
27
30
  extensions: []
28
31
  extra_rdoc_files: []
@@ -34,10 +37,10 @@ files:
34
37
  - lib/semantic_logger/ansi_colors.rb
35
38
  - lib/semantic_logger/appender.rb
36
39
  - lib/semantic_logger/appender/async.rb
37
- - lib/semantic_logger/appender/async_batch.rb
38
40
  - lib/semantic_logger/appender/bugsnag.rb
39
41
  - lib/semantic_logger/appender/cloudwatch_logs.rb
40
42
  - lib/semantic_logger/appender/elasticsearch.rb
43
+ - lib/semantic_logger/appender/elasticsearch_base.rb
41
44
  - lib/semantic_logger/appender/elasticsearch_http.rb
42
45
  - lib/semantic_logger/appender/file.rb
43
46
  - lib/semantic_logger/appender/graylog.rb
@@ -51,6 +54,7 @@ files:
51
54
  - lib/semantic_logger/appender/new_relic.rb
52
55
  - lib/semantic_logger/appender/new_relic_logs.rb
53
56
  - lib/semantic_logger/appender/open_telemetry.rb
57
+ - lib/semantic_logger/appender/opensearch.rb
54
58
  - lib/semantic_logger/appender/rabbitmq.rb
55
59
  - lib/semantic_logger/appender/sentry.rb
56
60
  - lib/semantic_logger/appender/sentry_ruby.rb
@@ -63,12 +67,14 @@ files:
63
67
  - lib/semantic_logger/appenders.rb
64
68
  - lib/semantic_logger/base.rb
65
69
  - lib/semantic_logger/concerns/compatibility.rb
70
+ - lib/semantic_logger/core_ext/process.rb
66
71
  - lib/semantic_logger/core_ext/thread.rb
67
72
  - lib/semantic_logger/debug_as_trace_logger.rb
68
73
  - lib/semantic_logger/formatters.rb
69
74
  - lib/semantic_logger/formatters/base.rb
70
75
  - lib/semantic_logger/formatters/color.rb
71
76
  - lib/semantic_logger/formatters/default.rb
77
+ - lib/semantic_logger/formatters/ecs.rb
72
78
  - lib/semantic_logger/formatters/fluentd.rb
73
79
  - lib/semantic_logger/formatters/json.rb
74
80
  - lib/semantic_logger/formatters/logfmt.rb
@@ -76,6 +82,7 @@ files:
76
82
  - lib/semantic_logger/formatters/new_relic_logs.rb
77
83
  - lib/semantic_logger/formatters/one_line.rb
78
84
  - lib/semantic_logger/formatters/open_telemetry.rb
85
+ - lib/semantic_logger/formatters/pattern.rb
79
86
  - lib/semantic_logger/formatters/raw.rb
80
87
  - lib/semantic_logger/formatters/signalfx.rb
81
88
  - lib/semantic_logger/formatters/syslog.rb
@@ -89,6 +96,7 @@ files:
89
96
  - lib/semantic_logger/metric/signalfx.rb
90
97
  - lib/semantic_logger/metric/statsd.rb
91
98
  - lib/semantic_logger/processor.rb
99
+ - lib/semantic_logger/queue_processor.rb
92
100
  - lib/semantic_logger/reporters/minitest.rb
93
101
  - lib/semantic_logger/semantic_logger.rb
94
102
  - lib/semantic_logger/subscriber.rb
@@ -96,6 +104,7 @@ files:
96
104
  - lib/semantic_logger/sync_processor.rb
97
105
  - lib/semantic_logger/test/capture_log_events.rb
98
106
  - lib/semantic_logger/test/minitest.rb
107
+ - lib/semantic_logger/test/rspec.rb
99
108
  - lib/semantic_logger/utils.rb
100
109
  - lib/semantic_logger/version.rb
101
110
  homepage: https://logger.rocketjob.io
@@ -103,8 +112,9 @@ licenses:
103
112
  - Apache-2.0
104
113
  metadata:
105
114
  bug_tracker_uri: https://github.com/reidmorrison/semantic_logger/issues
115
+ changelog_uri: https://github.com/reidmorrison/semantic_logger/releases
106
116
  documentation_uri: https://logger.rocketjob.io
107
- source_code_uri: https://github.com/reidmorrison/semantic_logger/tree/v4.17.0
117
+ source_code_uri: https://github.com/reidmorrison/semantic_logger/tree/v5.0.0
108
118
  rubygems_mfa_required: 'true'
109
119
  rdoc_options: []
110
120
  require_paths:
@@ -113,7 +123,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
113
123
  requirements:
114
124
  - - ">="
115
125
  - !ruby/object:Gem::Version
116
- version: '2.7'
126
+ version: '3.2'
117
127
  required_rubygems_version: !ruby/object:Gem::Requirement
118
128
  requirements:
119
129
  - - ">="
@@ -122,6 +132,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
132
  requirements: []
123
133
  rubygems_version: 3.6.9
124
134
  specification_version: 4
125
- summary: Feature rich logging framework, and replacement for existing Ruby & Rails
126
- loggers.
135
+ summary: High-performance, asynchronous structured logging framework for Ruby & Rails.
127
136
  test_files: []
@@ -1,93 +0,0 @@
1
- module SemanticLogger
2
- module Appender
3
- # Log asynchronously in batches using a separate thread.
4
- #
5
- # Log messages are grouped up and only logged when:
6
- # * The number of queued messages is exceeded.
7
- # * Or, the appropriate amount of time has passed since the last batch was sent.
8
- class AsyncBatch < Async
9
- attr_accessor :batch_size, :batch_seconds
10
- attr_reader :signal
11
-
12
- # Batching Appender proxy for appenders that support batches.
13
- #
14
- # Parameters:
15
- # batch_size: [Integer]
16
- # Maximum number of messages to batch up before sending.
17
- # Default: 300
18
- #
19
- # batch_seconds: [Integer]
20
- # Maximum number of seconds between sending batches.
21
- # Default: 5
22
- #
23
- # See SemanticLogger::Appender::Async for other paramaters
24
- #
25
- # Note:
26
- # * `lag_check_interval` is not applicable to batches, since the first message of every batch
27
- # is the oldest and is always checked to see if the lag interval has been exceeded.
28
- def initialize(appender:,
29
- max_queue_size: 10_000,
30
- lag_threshold_s: 30,
31
- batch_size: 300,
32
- batch_seconds: 5)
33
- @batch_size = batch_size
34
- @batch_seconds = batch_seconds
35
- @signal = Concurrent::Event.new
36
- super(
37
- appender: appender,
38
- max_queue_size: max_queue_size,
39
- lag_threshold_s: lag_threshold_s
40
- )
41
-
42
- return if appender.respond_to?(:batch)
43
-
44
- raise(ArgumentError, "#{appender.class.name} does not support batching. It must implement #batch")
45
- end
46
-
47
- # Add log message for processing.
48
- def log(log)
49
- result = super
50
- # Wake up the processing thread since the number of queued messages has been exceeded.
51
- signal.set if queue.size >= batch_size
52
- result
53
- end
54
-
55
- private
56
-
57
- # Separate thread for batching up log messages before writing.
58
- def process_messages
59
- loop do
60
- # Wait for batch interval or number of messages to be exceeded.
61
- signal.wait(batch_seconds)
62
-
63
- logs = []
64
- messages = []
65
- first = true
66
- message_count = queue.length
67
- message_count.times do
68
- # Queue#pop(true) raises an exception when there are no more messages, which is considered expensive.
69
- message = queue.pop
70
- if message.is_a?(Log)
71
- logs << message
72
- if first
73
- check_lag(message)
74
- first = false
75
- end
76
- else
77
- messages << message
78
- end
79
- end
80
- appender.batch(logs) if logs.size.positive?
81
- messages.each { |message| process_message(message) }
82
- signal.reset unless queue.size >= batch_size
83
- end
84
- end
85
-
86
- def submit_request(command)
87
- # Wake up the processing thread to process this command immediately.
88
- signal.set
89
- super
90
- end
91
- end
92
- end
93
- end