logister-ruby 0.4.0 → 0.4.2

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: ff89324842289ec0925e84131ee9442d4dfed5589b86deb1bb56498e55eb5977
4
- data.tar.gz: f262d7ab245a8c5703fb28873e65d4ee11194be7146833f656fd74d85434f2ff
3
+ metadata.gz: 381dffab29b34b6beffe54e105fcf8dc5151f59bde1e44cfbba1749d21bff763
4
+ data.tar.gz: 5fe2ef7923ebeb4aa440d4aff35b246b2d407da0fe11dd26e26633e491105657
5
5
  SHA512:
6
- metadata.gz: 28a1b3621688f111349b37c9996d530855d07081c17ce4525e33426b2ff30f1c714f8871555925dd7c11ec78ff09ad0566295552c54d05d141c28b944fe2f383
7
- data.tar.gz: c262d8b152f55680dc7ad02d19733089b59415f4a853cf33d5e8ffcade2851a69d4ada2963a7e05e4af8ca758e08340288213ea5e90da68d68c1b6baaf768108
6
+ metadata.gz: a5380bdc6bc4c7b4c1e4cb9e2c0d9d4cc4825f87d3517b57c22db7e63fff519071e46e9ad3555d3035819f4e1a64819d26305a1646f18cb32c02977cff925da2
7
+ data.tar.gz: b3d7c3a5932f560be7ec3b1dc1a88a2319799e2b166e91d8a6d6084e8d6d7eaf262c047c5d7f9ba3e095d44dc857d1ed701605ced120a2253a168d3568cc5687
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.4.2 - 2026-09-16
4
+
5
+ - Added process-local delivery counters and an optional recursion-safe observer for queued, acknowledged, unconfirmed, queue-full, and retry outcomes without exporting event contents.
6
+ - Clarified that a drained queue or successful `flush` does not prove every event was acknowledged; timed-out requests may already have reached the server.
7
+ - Removed exception messages from terminal event-publish warnings to avoid exposing transport details.
8
+ - Bounded async shutdown even when the queue is full; retained the worker until it finishes and report incomplete shutdown honestly.
9
+
10
+ ## v0.4.1 - 2026-09-11
11
+
12
+ - Constrained JSON to the compatible 2.x line while supported ActiveSupport 8 releases use positional JSON.parse options; verified real Rails request decoding.
13
+ - Restored explicit publication dispatch and immutable-tag recovery after successful current-main CI. Weekly dependency checks cannot publish releases.
14
+
3
15
  ## v0.4.0 - 2026-08-08
4
16
 
5
17
  - Added stable UUID assignment before synchronous or asynchronous delivery so retries preserve one logical event identity.
data/README.md CHANGED
@@ -162,6 +162,24 @@ safe against a Logister server that supports the batch endpoint. Older servers a
162
162
  detected automatically and receive the same stable events through the single-event
163
163
  endpoint. Call `Logister.flush` before a short-lived process exits.
164
164
 
165
+ ### Delivery outcomes
166
+
167
+ `flush` waits for queued and in-flight attempts to finish; it does not guarantee
168
+ server acknowledgement. Inspect `Logister.delivery_stats` for process-local
169
+ `queued_events`, `acknowledged_events`, `unconfirmed_events`, `queue_full_events`,
170
+ `retry_attempts`, and `pending_events`. An unconfirmed event may already have
171
+ reached the server when its response timed out. Counts are not durable and reset
172
+ with the client/process; they cover events, not deployment reports.
173
+
174
+ For local delivery diagnostics, set `config.delivery_observer` to a fast callback
175
+ accepting `{ outcome:, count:, reason: }`. It receives no payload, identifiers,
176
+ credentials, or exception messages. Reporting is suppressed inside the callback
177
+ to prevent feedback loops; callback failure cannot change delivery. Write these
178
+ diagnostics to a local log or counter rather than publishing them through Logister.
179
+ `shutdown` waits at most one second for the worker and returns false if attempts
180
+ are still running. It stops accepting asynchronous events and does not kill an
181
+ in-flight request or block while adding a sentinel to a full queue.
182
+
165
183
  Every HTTP attempt applies `timeout_seconds` to connect, read, and write operations.
166
184
  Retryable responses honor `Retry-After` when present, cap any individual wait at
167
185
  `max_retry_delay`, and add bounded positive jitter controlled by `retry_jitter`.
@@ -427,3 +445,22 @@ Verify both release surfaces before calling a release complete:
427
445
  curl -fsSL https://rubygems.org/api/v2/rubygems/logister-ruby/versions/X.Y.Z.json | jq '{number,ruby_version,sha}'
428
446
  gh release view vX.Y.Z
429
447
  ```
448
+
449
+
450
+ ### Coordinated release preparation
451
+
452
+ For a coordinated ecosystem release, keep the version-changing PR unmerged until
453
+ the final agreed Rails PR has been published and its deployment verified. Recheck
454
+ the upstream contract/workflow pin against that final backend commit before merge.
455
+ Successful source CI, a tag, or a release-impact dispatch alone is not backend readiness.
456
+ After independent review, merging the new version runs CI, creates an immutable tag,
457
+ and explicitly dispatches publication. A tag without a package remains incomplete.
458
+
459
+ To recover an existing reviewed tag, dispatch the publisher workflow from `main`
460
+ with `-f tag=vX.Y.Z` (Python uses `publish.yml`; other SDKs use `release.yml`). The
461
+ workflow checks out that exact tag, proves it belongs to main, and verifies public
462
+ package identity before creating the GitHub Release. Never move a consumed tag.
463
+
464
+ Weekly CI audits/tests current dependencies and cannot trigger automatic publication.
465
+ Dependabot groups compatible minor/patch updates; major toolchain migrations keep
466
+ separate PRs. Pin Actions to full commits and retain supported runtime floors.
@@ -32,9 +32,13 @@ module Logister
32
32
  @queue = SizedQueue.new(@configuration.queue_size)
33
33
  @worker = nil
34
34
  @running = false
35
+ @stopping = false
35
36
  @pending_mutex = Mutex.new
36
37
  @pending_condition = ConditionVariable.new
37
38
  @pending_count = 0
39
+ @delivery_mutex = Mutex.new
40
+ @delivery_counts = { queued_events: 0, acknowledged_events: 0, unconfirmed_events: 0,
41
+ queue_full_events: 0, retry_attempts: 0 }
38
42
 
39
43
  # Cache values that are static for the lifetime of this client so we
40
44
  # don't allocate on every send_request call.
@@ -48,6 +52,7 @@ module Logister
48
52
  end
49
53
 
50
54
  def publish(payload)
55
+ return false if ReportingScope.suppressed?
51
56
  return false unless ready?
52
57
 
53
58
  payload = with_stable_uuid(payload)
@@ -83,25 +88,40 @@ module Logister
83
88
  def shutdown
84
89
  return true unless @configuration.async
85
90
 
86
- @running = false
87
- begin
88
- @queue.push(nil)
89
- rescue StandardError
90
- nil
91
+ worker = @worker_mutex.synchronize do
92
+ @stopping = true
93
+ begin
94
+ @queue.push(nil, true)
95
+ rescue ThreadError
96
+ # A full queue will be drained by the worker, which also checks the
97
+ # stopping flag. Never block the caller waiting to enqueue a sentinel.
98
+ end
99
+ @worker
91
100
  end
92
- @worker&.join(1)
93
- @worker = nil
94
- true
101
+ worker&.join(1)
102
+ !worker&.alive?
103
+ end
104
+
105
+ # Process-local counters, never an assertion of exactly-once server receipt.
106
+ def delivery_stats
107
+ @delivery_mutex.synchronize { @delivery_counts.dup }
108
+ .merge(pending_events: @pending_mutex.synchronize { @pending_count })
95
109
  end
96
110
 
97
111
  private
98
112
 
99
113
  def enqueue(payload)
100
- increment_pending
101
- @queue.push(payload, true)
114
+ @worker_mutex.synchronize do
115
+ return false if @stopping
116
+
117
+ increment_pending
118
+ @queue.push(payload, true)
119
+ end
120
+ record_delivery(:queued_events, 1)
102
121
  true
103
122
  rescue ThreadError
104
123
  complete_pending(1)
124
+ record_delivery(:queue_full_events, 1, reason: 'queue_full')
105
125
  @configuration.logger.warn('logister queue full; dropping event')
106
126
  false
107
127
  end
@@ -111,6 +131,7 @@ module Logister
111
131
  return if @running && @worker&.alive?
112
132
 
113
133
  @worker_mutex.synchronize do
134
+ return if @stopping
114
135
  return if @running && @worker&.alive?
115
136
 
116
137
  @running = true
@@ -144,7 +165,7 @@ module Logister
144
165
 
145
166
  publish_batch_sync(batch)
146
167
  complete_pending(batch.length)
147
- break if stop_after_batch
168
+ break if stop_after_batch || (@stopping && @queue.empty?)
148
169
  end
149
170
  rescue StandardError => e
150
171
  @configuration.logger.warn("logister worker crashed: #{e.class} #{e.message}")
@@ -158,14 +179,22 @@ module Logister
158
179
  attempts = 0
159
180
  begin
160
181
  attempts += 1
161
- send_request(payload)
182
+ delivered = send_request(payload)
183
+ unless delivered
184
+ record_delivery(:unconfirmed_events, 1, reason: 'no_acknowledgment')
185
+ return false
186
+ end
187
+ record_delivery(:acknowledged_events, 1)
188
+ true
162
189
  rescue StandardError => e
163
190
  if attempts <= @configuration.max_retries && retryable_error?(e)
191
+ record_delivery(:retry_attempts, 1, reason: failure_reason(e))
164
192
  sleep(retry_delay(e, attempts))
165
193
  retry
166
194
  end
167
195
 
168
- @configuration.logger.warn("logister publish failed: #{e.class} #{e.message}")
196
+ record_delivery(:unconfirmed_events, 1, reason: failure_reason(e))
197
+ @configuration.logger.warn("logister publish failed: #{e.class}")
169
198
  false
170
199
  end
171
200
  end
@@ -174,7 +203,13 @@ module Logister
174
203
  attempts = 0
175
204
  begin
176
205
  attempts += 1
177
- send_batch_request(payloads)
206
+ delivered = send_batch_request(payloads)
207
+ unless delivered
208
+ record_delivery(:unconfirmed_events, payloads.length, reason: 'no_acknowledgment')
209
+ return false
210
+ end
211
+ record_delivery(:acknowledged_events, payloads.length)
212
+ true
178
213
  rescue UnsupportedBatchEndpoint
179
214
  payloads.map { |payload| publish_sync(payload) }.all?
180
215
  rescue RequestError => e
@@ -185,19 +220,23 @@ module Logister
185
220
  return first_half_delivered && second_half_delivered
186
221
  end
187
222
  if attempts <= @configuration.max_retries && retryable_error?(e)
223
+ record_delivery(:retry_attempts, 1, reason: failure_reason(e))
188
224
  sleep(retry_delay(e, attempts))
189
225
  retry
190
226
  end
191
227
 
192
- @configuration.logger.warn("logister batch publish failed: #{e.class} #{e.message}")
228
+ record_delivery(:unconfirmed_events, payloads.length, reason: failure_reason(e))
229
+ @configuration.logger.warn("logister batch publish failed: #{e.class}")
193
230
  false
194
231
  rescue StandardError => e
195
232
  if attempts <= @configuration.max_retries && retryable_error?(e)
233
+ record_delivery(:retry_attempts, 1, reason: failure_reason(e))
196
234
  sleep(retry_delay(e, attempts))
197
235
  retry
198
236
  end
199
237
 
200
- @configuration.logger.warn("logister batch publish failed: #{e.class} #{e.message}")
238
+ record_delivery(:unconfirmed_events, payloads.length, reason: failure_reason(e))
239
+ @configuration.logger.warn("logister batch publish failed: #{e.class}")
201
240
  false
202
241
  end
203
242
  end
@@ -290,6 +329,26 @@ module Logister
290
329
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
291
330
  end
292
331
 
332
+ def failure_reason(error)
333
+ return "http_#{error.status}" if error.is_a?(RequestError)
334
+ return 'timeout' if error.is_a?(Timeout::Error)
335
+
336
+ 'transport_error'
337
+ end
338
+
339
+ def record_delivery(counter, count, reason: nil)
340
+ @delivery_mutex.synchronize { @delivery_counts[counter] += count }
341
+ observer = @configuration.delivery_observer
342
+ return unless observer
343
+
344
+ ReportingScope.suppress do
345
+ observer.call({ outcome: counter.to_s, count: count, reason: reason }.compact.freeze)
346
+ end
347
+ rescue StandardError
348
+ # Counters survive observer failure; never report this through telemetry.
349
+ nil
350
+ end
351
+
293
352
  def with_stable_uuid(payload)
294
353
  attributes = payload.to_h.dup
295
354
  key = attributes.keys.any? { |candidate| candidate.is_a?(String) } ? 'uuid' : :uuid
@@ -10,7 +10,7 @@ module Logister
10
10
  :capture_db_metrics, :db_metric_min_duration_ms, :db_metric_sample_rate,
11
11
  :feature_flags_resolver, :dependency_resolver, :anonymize_ip,
12
12
  :max_breadcrumbs, :max_dependencies, :capture_request_spans,
13
- :capture_sql_breadcrumbs, :sql_breadcrumb_min_duration_ms
13
+ :capture_sql_breadcrumbs, :sql_breadcrumb_min_duration_ms, :delivery_observer
14
14
  attr_writer :deployment_endpoint, :batch_endpoint
15
15
 
16
16
  def initialize
@@ -32,6 +32,7 @@ module Logister
32
32
  @ignore_environments = []
33
33
  @ignore_paths = []
34
34
  @before_notify = nil
35
+ @delivery_observer = nil
35
36
 
36
37
  @async = true
37
38
  @queue_size = 1000
@@ -298,6 +298,10 @@ module Logister
298
298
  @client.flush(timeout: timeout)
299
299
  end
300
300
 
301
+ def delivery_stats
302
+ @client.delivery_stats
303
+ end
304
+
301
305
  def shutdown
302
306
  @client.shutdown
303
307
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Logister
4
- VERSION = '0.4.0'
4
+ VERSION = '0.4.2'
5
5
  end
data/lib/logister.rb CHANGED
@@ -64,6 +64,10 @@ module Logister
64
64
  reporter.flush(timeout: timeout)
65
65
  end
66
66
 
67
+ def delivery_stats
68
+ reporter.delivery_stats
69
+ end
70
+
67
71
  def shutdown
68
72
  reporter.shutdown
69
73
  end
@@ -25,6 +25,8 @@ Gem::Specification.new do |spec|
25
25
  spec.require_paths = ['lib']
26
26
 
27
27
  spec.add_dependency 'activesupport', '>= 8.0.5', '< 9'
28
+ # ActiveSupport 8 passes JSON.parse options positionally; JSON 3 removed that API.
29
+ spec.add_dependency 'json', '>= 2', '< 3'
28
30
  spec.add_development_dependency 'actionpack', '>= 8.0.5', '< 9'
29
31
  spec.add_development_dependency 'rake', '>= 13.0'
30
32
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logister-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Logister
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2026-09-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -29,6 +29,26 @@ dependencies:
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '9'
32
+ - !ruby/object:Gem::Dependency
33
+ name: json
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '2'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '3'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '2'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '3'
32
52
  - !ruby/object:Gem::Dependency
33
53
  name: actionpack
34
54
  requirement: !ruby/object:Gem::Requirement