julewire-semantic_logger 1.0.1 → 1.1.1

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: 56e9c8fb67e4c5a896837a1dfd5f1a81c14aa27a3167dd9e140c7cca706f65d9
4
- data.tar.gz: 3f4ded10ad8caedf602ad81fa88e4587461ddaef3ca5befab5e25eff710a4314
3
+ metadata.gz: f20275d81c92f2b0b9627a99df1efab4498b6b158ae7e164f5201154df6ffd31
4
+ data.tar.gz: 3370e7ab54112ce8f44d58ce21f015afc8814fdc7dd7068d74963d4133b9375b
5
5
  SHA512:
6
- metadata.gz: 60eda983c6c3acd4983ae38d76493a0cbf0a915079abb26ae793880ce12090eec5a16ed82f24ebf724f684fc0dd860d64f7f92c847ca0094845bd9b937faa482
7
- data.tar.gz: 9facbc31acb5355bb8b65abc37fc4e83d3e15e3706efb4749797a02d28ea6d3ddc2972b8a09673eadfad3c75a41c81956305f0cc6ab2584ada30028def9f9187
6
+ metadata.gz: f10f73c0122ec869e93122a415f773047386fcf8fe55d7daefdd9938079d9fcb5250efafdfca4dcb93c9967261facf88814741fdb8f22cc69e1776600ea43402
7
+ data.tar.gz: e53e96ec846b167d7ea3c30c58970795993894ca1c936ab8997e7b2b3c6c3dbc340ce228fe5c1ecb743bf9e9f29a57d12c6cd4e9687c9b928879f522b264e221
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 1.1.1 - 2026-07-26
4
+
5
+ - Test against Semantic Logger 5.1 and refresh the dependency lockset.
6
+
7
+ ## 1.1.0 - 2026-07-20
8
+
9
+ - Support Semantic Logger 4 and 5 async option shapes.
10
+ - Keep destination health and callback failure reporting aligned with core.
11
+ - Require julewire-core 1.1.0.
12
+
3
13
  ## 1.0.1 - 2026-06-25
4
14
 
5
15
  - Report destination callback failures in health, matching core destinations.
@@ -36,6 +36,18 @@ These options are passed to `SemanticLogger::Appender::Async` when
36
36
  | `lag_check_interval:` | `1_000` |
37
37
  | `lag_threshold_s:` | `30` |
38
38
 
39
+ ## Async Queue Options
40
+
41
+ Transport-level async options are passed to the Semantic Logger async proxy
42
+ when the installed Semantic Logger version supports them. Semantic Logger 4.18
43
+ supports queue size and lag options. Semantic Logger 5 also supports
44
+ `non_blocking:`, `dropped_message_report_seconds:`, and `async_max_retries:`.
45
+ Setting a non-default unsupported async option raises a configuration error.
46
+
47
+ `batch: true` uses `SemanticLogger::Appender::AsyncBatch` on Semantic Logger 4
48
+ and `SemanticLogger::Appender::Async` batch mode on Semantic Logger 5. Batch
49
+ mode implies async output.
50
+
39
51
  ## Appender Defaults
40
52
 
41
53
  Unknown transport options are merged into each appender spec. This is useful for
@@ -39,6 +39,12 @@ At least one appender target is required.
39
39
  | `appenders:` | none | Array of appender specs. |
40
40
  | `async:` | `false` | Wrap the sink in `SemanticLogger::Appender::Async`. |
41
41
  | `max_queue_size:` | `10_000` | Async queue size. `-1` means unbounded in Semantic Logger. |
42
+ | `batch:` | `nil` | Use Semantic Logger batch async processing when supported by the appender. `true` implies async output. |
43
+ | `batch_size:` | `300` | Batch size for batch-capable async appenders. |
44
+ | `batch_seconds:` | `5` | Maximum seconds between batch writes. |
45
+ | `non_blocking:` | `false` | Semantic Logger 5+ async drop mode. Raises on older Semantic Logger when set to a non-default value. |
46
+ | `dropped_message_report_seconds:` | `30` | Semantic Logger 5+ dropped-message report interval. Raises on older Semantic Logger when set to a non-default value. |
47
+ | `async_max_retries:` | `100` | Semantic Logger 5+ async worker retry limit. Raises on older Semantic Logger when set to a non-default value. |
42
48
 
43
49
  Unknown transport options are passed to Semantic Logger appender construction.
44
50
 
data/docs/health.md CHANGED
@@ -11,7 +11,7 @@ Destination health includes:
11
11
  - destination `status`
12
12
  - destination write/failure counts
13
13
  - transport write/failure counts
14
- - async queue state
14
+ - async queue state and lag settings
15
15
  - file appender metadata
16
16
  - child appender shape for multi-appender output
17
17
  - lifecycle warnings
@@ -38,6 +38,8 @@ One practical mapping is:
38
38
  | `destinations.*.transport.counts.*` | `julewire_semantic_logger_transport_total{destination,event}` |
39
39
  | `destinations.*.transport.appender.queue_size` | `julewire_semantic_logger_queue_size{destination}` |
40
40
  | `destinations.*.transport.appender.max_queue_size` | `julewire_semantic_logger_queue_capacity{destination}` |
41
+ | `destinations.*.transport.appender.lag_check_interval` | `julewire_semantic_logger_lag_check_interval{destination}` |
42
+ | `destinations.*.transport.appender.lag_threshold_s` | `julewire_semantic_logger_lag_threshold_seconds{destination}` |
41
43
  | `destinations.*.transport.warnings.*` | `julewire_semantic_logger_warning{destination,reason}` |
42
44
 
43
45
  Treat core health paths as inputs, not as a global metrics schema.
data/docs/transport.md CHANGED
@@ -64,21 +64,9 @@ Verified paths:
64
64
  ## Metric Mapping
65
65
 
66
66
  The adapter owns metric names because queue, file, and appender behavior is
67
- transport-specific. One practical mapping is:
68
-
69
- | Health path | Metric name |
70
- | --- | --- |
71
- | `counts.*` | `julewire_runtime_total{event}` |
72
- | `pipeline.counts.*` | `julewire_pipeline_total{event}` |
73
- | `destinations.*.counts.*` | `julewire_destination_total{destination,event}` |
74
- | `destinations.*.last_loss.reason` | `julewire_destination_last_loss{destination,reason}` |
75
- | `destinations.*.transport.counts.*` | `julewire_semantic_logger_transport_total{destination,event}` |
76
- | `destinations.*.transport.appender.queue_size` | `julewire_semantic_logger_queue_size{destination}` |
77
- | `destinations.*.transport.appender.max_queue_size` | `julewire_semantic_logger_queue_capacity{destination}` |
78
- | `destinations.*.transport.warnings.*` | `julewire_semantic_logger_warning{destination,reason}` |
79
-
80
- Extensions should treat core health paths as inputs, not as a global metrics
81
- schema.
67
+ transport-specific. The [health guide](health.md#metric-mapping) is the
68
+ canonical mapping; extensions should treat core health paths as inputs, not as
69
+ a global metrics schema.
82
70
 
83
71
  Observed behavior:
84
72
 
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.executables = []
33
33
  spec.require_paths = ["lib"]
34
34
 
35
- spec.add_dependency "julewire-core", ">= 1.0.1"
35
+ spec.add_dependency "julewire-core", ">= 1.1.0"
36
36
  spec.add_dependency "logger", ">= 1.7"
37
37
  spec.add_dependency "semantic_logger", ">= 4.18"
38
38
  spec.add_dependency "zeitwerk", ">= 2.8.1"
@@ -36,6 +36,8 @@ module Julewire
36
36
  {
37
37
  active: value.active?,
38
38
  capped: value.capped?,
39
+ lag_check_interval: value.lag_check_interval,
40
+ lag_threshold_s: value.lag_threshold_s,
39
41
  max_queue_size: value.max_queue_size,
40
42
  queue_size: value.queue.size,
41
43
  wrapped: call(value.appender)
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module SemanticLogger
5
+ class AsyncOptions
6
+ DEFAULT_MAX_QUEUE_SIZE = 10_000
7
+ DEFAULTS = {
8
+ max_queue_size: DEFAULT_MAX_QUEUE_SIZE,
9
+ lag_check_interval: 1_000,
10
+ lag_threshold_s: 30,
11
+ batch: nil,
12
+ batch_size: 300,
13
+ batch_seconds: 5,
14
+ non_blocking: false,
15
+ dropped_message_report_seconds: 30,
16
+ async_max_retries: 100
17
+ }.freeze
18
+ BATCH_OPTIONS = { batch_size: true, batch_seconds: true }.freeze
19
+ KEYWORD_PARAMETER_KINDS = { key: true, keyreq: true }.freeze
20
+ SUPPORTED_DEFAULTS = DEFAULTS.keys.to_h { [it, true] }.freeze
21
+ ASYNC_APPENDER = ::SemanticLogger::Appender::Async
22
+ ASYNC_APPENDER_ACCEPTS_ANY_KEYWORD =
23
+ ASYNC_APPENDER.instance_method(:initialize).parameters.any? { |kind, _name| kind == :keyrest }
24
+ ASYNC_BATCH_APPENDER = if ::SemanticLogger::Appender.const_defined?(:AsyncBatch, false)
25
+ ::SemanticLogger::Appender.const_get(:AsyncBatch, false)
26
+ end
27
+
28
+ class << self
29
+ def extract(options)
30
+ configured = []
31
+ values = DEFAULTS.to_h do |name, default|
32
+ if options.key?(name)
33
+ configured << name
34
+ [name, options.delete(name)]
35
+ else
36
+ [name, default]
37
+ end
38
+ end
39
+
40
+ new(values: values, configured: configured)
41
+ end
42
+ end
43
+
44
+ def initialize(values:, configured:)
45
+ @values = values
46
+ @configured = configured
47
+ end
48
+
49
+ def async?(base_async)
50
+ base_async || batch_enabled?
51
+ end
52
+
53
+ def max_queue_size
54
+ @values.fetch(:max_queue_size)
55
+ end
56
+
57
+ def build_appender(sink)
58
+ klass = appender_class
59
+ klass.new(appender: sink, **constructor_options(klass))
60
+ end
61
+
62
+ private
63
+
64
+ if ASYNC_APPENDER_ACCEPTS_ANY_KEYWORD
65
+ def appender_class
66
+ ASYNC_APPENDER
67
+ end
68
+ else
69
+ def appender_class
70
+ return ASYNC_APPENDER unless batch_enabled?
71
+
72
+ ASYNC_BATCH_APPENDER
73
+ end
74
+ end
75
+
76
+ def constructor_options(klass)
77
+ supported = supported_options(klass)
78
+ unsupported = unsupported_configured_options(supported)
79
+ unless unsupported.empty?
80
+ raise ArgumentError, "#{klass} does not support async option(s): #{unsupported.join(", ")}"
81
+ end
82
+
83
+ DEFAULTS.each_with_object({}) do |(name, _default), kwargs|
84
+ next unless supported.fetch(name, false)
85
+
86
+ value = @values.fetch(name)
87
+ kwargs[name] = value unless value.nil?
88
+ end
89
+ end
90
+
91
+ def supported_options(klass)
92
+ parameters = klass.instance_method(:initialize).parameters
93
+ return SUPPORTED_DEFAULTS if keyrest_constructor?(parameters)
94
+
95
+ parameters.each_with_object({}) do |(kind, name), supported|
96
+ supported[name] = true if KEYWORD_PARAMETER_KINDS.key?(kind)
97
+ end
98
+ end
99
+
100
+ def keyrest_constructor?(parameters)
101
+ parameters.any? { |kind, _name| kind == :keyrest }
102
+ end
103
+
104
+ def unsupported_configured_options(supported)
105
+ @configured.reject do |name|
106
+ mode_selector?(name) || supported.fetch(name, false) || default_value?(name) || inactive_batch_option?(name)
107
+ end
108
+ end
109
+
110
+ def mode_selector?(name)
111
+ name == :batch
112
+ end
113
+
114
+ def default_value?(name)
115
+ @values.fetch(name) == DEFAULTS.fetch(name)
116
+ end
117
+
118
+ def inactive_batch_option?(name)
119
+ BATCH_OPTIONS.key?(name) && !batch_enabled?
120
+ end
121
+
122
+ def batch_enabled?
123
+ @values.fetch(:batch).equal?(true)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -21,16 +21,16 @@ module Julewire
21
21
  end
22
22
 
23
23
  def emit(record)
24
+ degradation_marker = @health.degradation_marker
24
25
  formatted = false
25
26
  increment(:received)
26
27
  payload = @formatter.call(record)
27
28
  formatted = true
28
29
  @transport.write(encoded_payload(payload), severity: record.fetch(:severity))
29
- record_written
30
+ record_written(degradation_marker)
30
31
  nil
31
32
  rescue StandardError => e
32
33
  record_failure(e, formatted: formatted, record: record)
33
- nil
34
34
  end
35
35
 
36
36
  def flush(*) = call_lifecycle(:flush) { @transport.flush }
@@ -46,7 +46,7 @@ module Julewire
46
46
  def health
47
47
  transport = @transport.health
48
48
  @health.snapshot(
49
- status: status(@health.degraded?, transport),
49
+ status: status(transport),
50
50
  type: "semantic_logger_destination",
51
51
  transport: transport
52
52
  )
@@ -71,14 +71,14 @@ module Julewire
71
71
  @health.increment(name)
72
72
  end
73
73
 
74
- def record_written
75
- @health.increment(:formatted)
76
- @health.increment(:written)
77
- @health.clear_degraded!
74
+ def record_written(degradation_marker)
75
+ increment(:formatted)
76
+ increment(:written)
77
+ @health.clear_degradation_if_unchanged(degradation_marker)
78
78
  end
79
79
 
80
- def record_failure(error, formatted: false, record: nil)
81
- @health.increment(:formatted) if formatted
80
+ def record_failure(error, formatted:, record:)
81
+ increment(:formatted) if formatted
82
82
  @health.record_failure(error, destination: name, phase: :destination, record_metadata: record_metadata(record))
83
83
  notify_failure(error, phase: :destination, record_metadata: record_metadata(record))
84
84
  record_drop(:destination_exception, record)
@@ -90,8 +90,9 @@ module Julewire
90
90
  end
91
91
 
92
92
  def call_lifecycle(action)
93
+ degradation_marker = @health.degradation_marker
93
94
  yield
94
- clear_degraded
95
+ @health.clear_degradation_if_unchanged(degradation_marker)
95
96
  true
96
97
  rescue StandardError => e
97
98
  record_lifecycle_failure(e, action: action)
@@ -126,20 +127,18 @@ module Julewire
126
127
  end
127
128
 
128
129
  def record_metadata(record)
129
- Core::Records::Metadata.call(record) if record
130
- end
131
-
132
- def status(currently_degraded, transport)
133
- transport_status = transport[:status]
134
- return :closed if transport_status == :closed
135
- return :degraded if currently_degraded
136
- return :degraded if transport_status && transport_status != :ok
137
-
138
- :ok
139
- end
140
-
141
- def clear_degraded
142
- @health.clear_degraded!
130
+ Core::Records::Metadata.call(record)
131
+ end
132
+
133
+ def status(transport)
134
+ case transport[:status]
135
+ when :closed
136
+ :closed
137
+ when nil, :ok
138
+ nil
139
+ else
140
+ :degraded
141
+ end
143
142
  end
144
143
  end
145
144
  end
@@ -6,7 +6,7 @@ module Julewire
6
6
  module SemanticLogger
7
7
  class Transport
8
8
  LOGGER_NAME = "julewire"
9
- DEFAULT_MAX_QUEUE_SIZE = 10_000
9
+ DEFAULT_MAX_QUEUE_SIZE = AsyncOptions::DEFAULT_MAX_QUEUE_SIZE
10
10
  LEVEL_MAP = {
11
11
  # SemanticLogger has no unknown level; fatal keeps unknown core records visible.
12
12
  unknown: :fatal
@@ -15,14 +15,11 @@ module Julewire
15
15
 
16
16
  def initialize(**options)
17
17
  @mutex = Mutex.new
18
- @async = options.delete(:async) { false }
19
- @max_queue_size = options.delete(:max_queue_size) { DEFAULT_MAX_QUEUE_SIZE }
20
- @lag_check_interval = options.delete(:lag_check_interval) { 1_000 }
21
- @lag_threshold_s = options.delete(:lag_threshold_s) { 30 }
22
- @write_count = 0
23
- @failure_count = 0
24
- @degraded = false
25
- @closed = false
18
+ @async = options.delete(:async)
19
+ @async_options = AsyncOptions.extract(options)
20
+ @async = @async_options.async?(@async)
21
+ @max_queue_size = @async_options.max_queue_size
22
+ @health = Core::Integration::DestinationHealth.new(counter_keys: %i[writes])
26
23
  @appenders = build_appenders(
27
24
  appenders: options.delete(:appenders),
28
25
  appender: options.delete(:appender),
@@ -35,62 +32,50 @@ module Julewire
35
32
  end
36
33
 
37
34
  def write(value, severity:)
35
+ degradation_marker = @health.degradation_marker
38
36
  log = log_for(value, severity: severity)
39
- @mutex.synchronize do
40
- @write_count += 1
41
- # Synchronous appenders write under our mutex; async appenders own
42
- # queue synchronization and may block on bounded queues.
43
- appender.log(log) unless @async
44
- end
37
+ @health.increment(:writes)
38
+ @mutex.synchronize { appender.log(log) } unless @async
45
39
  appender.log(log) if @async
46
- clear_degraded
47
- nil
48
- rescue StandardError
49
- @mutex.synchronize do
50
- @failure_count += 1
51
- @degraded = true
52
- end
40
+ @health.clear_degradation_if_unchanged(degradation_marker)
41
+ rescue StandardError => e
42
+ @health.record_failure(e)
53
43
  raise
54
44
  end
55
45
 
56
46
  def flush
57
- appender.flush if appender.respond_to?(:flush)
58
- clear_degraded
47
+ degradation_marker = @health.degradation_marker
48
+ appender.flush
49
+ @health.clear_degradation_if_unchanged(degradation_marker)
59
50
  nil
60
51
  end
61
52
 
62
53
  def close
63
- appender.close if appender.respond_to?(:close)
64
- @mutex.synchronize { @closed = true }
54
+ appender.close
55
+ @closed = true
65
56
  nil
66
57
  end
67
58
 
68
59
  def reopen
60
+ degradation_marker = @health.degradation_marker
69
61
  appender.reopen if appender.respond_to?(:reopen)
70
- @mutex.synchronize do
71
- @closed = false
72
- @degraded = false
73
- end
62
+ @closed = false
63
+ @health.clear_degradation_if_unchanged(degradation_marker)
74
64
  nil
75
65
  end
76
66
 
77
67
  def after_fork! = reopen
78
68
 
79
69
  def health
80
- counts = @mutex.synchronize do
81
- {
82
- closed: @closed,
83
- degraded: @degraded,
84
- failures: @failure_count,
85
- writes: @write_count
86
- }
87
- end
70
+ snapshot = @health.snapshot(status: status)
71
+ counts = snapshot.fetch(:counts)
88
72
 
89
73
  {
90
74
  type: "semantic_logger",
91
- status: status(counts),
75
+ status: snapshot.fetch(:status),
92
76
  async: @async,
93
77
  warnings: lifecycle_warnings,
78
+ last_failure: snapshot[:last_failure],
94
79
  counts: {
95
80
  writes: counts.fetch(:writes),
96
81
  failures: counts.fetch(:failures)
@@ -140,12 +125,7 @@ module Julewire
140
125
  def build_transport_appender(sink)
141
126
  return sink unless @async
142
127
 
143
- ::SemanticLogger::Appender::Async.new(
144
- appender: sink,
145
- lag_check_interval: @lag_check_interval,
146
- lag_threshold_s: @lag_threshold_s,
147
- max_queue_size: @max_queue_size
148
- )
128
+ @async_options.build_appender(sink)
149
129
  end
150
130
 
151
131
  def log_for(value, severity:)
@@ -159,23 +139,20 @@ module Julewire
159
139
  end
160
140
 
161
141
  def semantic_level(value)
162
- level = value.is_a?(Symbol) ? value : value.to_s.downcase.to_sym
163
- level = LEVEL_MAP.fetch(level, level)
142
+ level = LEVEL_MAP.fetch(value, value)
164
143
  return level if LEVEL_SET.key?(level)
165
144
 
166
145
  :info
167
146
  end
168
147
 
169
- def status(counts)
170
- return :closed if counts.fetch(:closed)
171
- return :degraded if appender.is_a?(::SemanticLogger::Appender::Async) && !appender.active?
172
- return :degraded if counts.fetch(:degraded)
148
+ def status
149
+ return :closed if @closed
173
150
 
174
- :ok
151
+ :degraded if async_appender_inactive?
175
152
  end
176
153
 
177
- def clear_degraded
178
- @mutex.synchronize { @degraded = false }
154
+ def async_appender_inactive?
155
+ @async && !appender.active?
179
156
  end
180
157
 
181
158
  def lifecycle_warnings
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Julewire
4
4
  module SemanticLogger
5
- VERSION = "1.0.1"
5
+ VERSION = "1.1.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: julewire-semantic_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Grebennik
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 1.0.1
18
+ version: 1.1.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 1.0.1
25
+ version: 1.1.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: logger
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -84,6 +84,7 @@ files:
84
84
  - lib/julewire-semantic_logger.rb
85
85
  - lib/julewire/semantic_logger.rb
86
86
  - lib/julewire/semantic_logger/appender_health.rb
87
+ - lib/julewire/semantic_logger/async_options.rb
87
88
  - lib/julewire/semantic_logger/destination.rb
88
89
  - lib/julewire/semantic_logger/exact_formatter.rb
89
90
  - lib/julewire/semantic_logger/lifecycle_warnings.rb
@@ -111,7 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
112
  - !ruby/object:Gem::Version
112
113
  version: '0'
113
114
  requirements: []
114
- rubygems_version: 4.0.14
115
+ rubygems_version: 4.0.16
115
116
  specification_version: 4
116
117
  summary: Semantic Logger transport adapter for Julewire.
117
118
  test_files: []