logbrew-sdk 0.1.8 → 0.1.10

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: 23bb3971a708e01fb3baa93461beb30dda7ff5a6d1b807a6f21a2c4365b718ee
4
- data.tar.gz: 2254bb2a7317aa32053e43ae2463ba527c8ec4bd9d7147d31a1c085df690857d
3
+ metadata.gz: 8ed4c09b597bf3b6c6d2a9426c378c1ef86c3ad32b141cdbf8782d0477ba41d6
4
+ data.tar.gz: aae266ff513452aa9adc3e48d53ff117aa9ea80bbd6d5dd3ed35da05db2cbb9c
5
5
  SHA512:
6
- metadata.gz: b516708afcb021d131aac74c02b0dae2b97d04875f45ec408ff431a5046d2c12af0e824ddddbd5e9280c934ed2a016615cabb4e9d035174c2da99e4806afbc14
7
- data.tar.gz: '09b94b37beba32a8956b06295ca7ad0b4f16b0de21e1148a127797f1fd61f0c7763dc85b336ee7a8e249dbd38f02e2e878aadb855dc6beb4404584a5ae6b89d4'
6
+ metadata.gz: 8ba3ea392a5a0f56cd7ab4f2c7a23271424149b9fcf8eaa0527b7002403069f9368f08729531b70f4e1c98390a261f544f61f540f95b1f65bfcb0e165ad5df1f
7
+ data.tar.gz: e0d0b0c6f19fd4c14b1ade31e6e23f31f80928e2faeeb34875fc65ad15e1784a6fe5830e59b958f8650b09cc64965f00235e5ea83f35407a828b00f7a93d3798
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <img src="https://raw.githubusercontent.com/LogBrewCo/sdk/main/assets/brand/logbrew-logo-transparent-512.png" alt="LogBrew logo" width="96" height="96">
5
5
  </p>
6
6
 
7
- Public Ruby SDK for building, validating, previewing, and flushing LogBrew event batches, with automatic Rails request, database, cache, view, and error capture; standard-library `Net::HTTP` delivery; opt-in standard-library `Logger` support; and manual Rack helpers.
7
+ Public Ruby SDK for building, validating, previewing, and flushing LogBrew event batches, with automatic Rails request, database, cache, view, error, ActiveJob, and outbound `Net::HTTP` capture; standard-library delivery; opt-in standard-library `Logger` support; and manual Rack helpers.
8
8
 
9
9
  The core package has no runtime gem dependencies. Its automatic integration
10
10
  activates only inside an application that has already loaded Rails.
@@ -23,7 +23,7 @@ needed:
23
23
 
24
24
  ```ruby
25
25
  # Gemfile
26
- gem "logbrew-sdk", "~> 0.1.8"
26
+ gem "logbrew-sdk", "~> 0.1.10"
27
27
  ```
28
28
 
29
29
  ```bash
@@ -53,7 +53,24 @@ frames. Handled Rails reports use `rails.error_reporter` with `handled: true`.
53
53
  It does not record concrete request paths, query strings, request or response
54
54
  bodies, arbitrary headers, authorization values, cookies, user IDs, exception
55
55
  messages, raw backtrace text, source snippets, locals, arguments, or absolute
56
- paths. Exception messages and raw backtrace text are separate opt-ins:
56
+ paths. Exception messages and raw backtrace text are separate opt-ins.
57
+
58
+ ActiveJob needs no extra initializer. Each enqueue and execution emits a
59
+ producer or worker span, including bounded adapter identity, retry count, and
60
+ queue wait. A versioned W3C carrier keeps retries and supported queue adapters
61
+ on one trace. A retryable failure remains span evidence; only an unexpected
62
+ terminal `StandardError` creates an unhandled `rails.active_job` issue with
63
+ sanitized structured frames. The adapter does not capture job IDs, queue names,
64
+ arguments, serialized payloads, exception messages, or raw backtraces by
65
+ default. When ActiveJob uses LogBrew's Sidekiq middleware, the inner carrier
66
+ suppresses duplicate Sidekiq spans and issues.
67
+
68
+ Outbound `Net::HTTP` calls made inside an active Rails request or job trace also
69
+ need no initializer. They propagate one W3C child and record only method,
70
+ normalized host, status, duration, adapter source, sampled state, and exception
71
+ type. Calls outside an active trace remain exact pass-throughs.
72
+
73
+ The explicit Rails capture settings are:
57
74
 
58
75
  | Environment variable | Default | Purpose |
59
76
  | --- | --- | --- |
@@ -415,7 +432,9 @@ end
415
432
 
416
433
  ## Outbound HTTP Tracing
417
434
 
418
- Wrap an app-owned `Net::HTTP` connection explicitly when outbound work should become a child of the active LogBrew trace:
435
+ The enabled Rails integration captures `Net::HTTP` automatically while a
436
+ LogBrew trace is active. Outside Rails, wrap an app-owned connection explicitly
437
+ when outbound work should become a child of the active trace:
419
438
 
420
439
  ```ruby
421
440
  uri = URI("https://service.example/health")
@@ -440,7 +459,15 @@ connection = Faraday.new("https://service.example") do |builder|
440
459
  end
441
460
  ```
442
461
 
443
- Both adapters are literal pass-throughs when `LogBrew::Trace.current` is absent. With an active parent, they propagate one W3C `traceparent`, return the caller-visible header and trace scope to their prior values, and capture one completion span per actual execution. Duplicate wrappers, nested LogBrew HTTP middleware, and SDK delivery are suppressed without process-wide hooks. Net::HTTP start blocks, response streaming, Faraday middleware ordering, responses, and exceptions retain their normal behavior; telemetry capture failures are advisory.
462
+ Both explicit adapters are literal pass-throughs when `LogBrew::Trace.current`
463
+ is absent. With an active parent, explicit adapters and automatic Rails
464
+ `Net::HTTP` capture propagate one W3C `traceparent`, return caller-visible
465
+ header and trace state, and capture one completion span per actual execution.
466
+ Duplicate wrappers, nested LogBrew HTTP middleware, and SDK delivery are
467
+ suppressed. Outside an enabled Rails process, the SDK does not patch
468
+ `Net::HTTP`. Start blocks, response streaming, Faraday middleware ordering,
469
+ responses, and exceptions retain normal behavior; capture failures are
470
+ advisory.
444
471
 
445
472
  Outbound HTTP spans allow only method, normalized host, status code, duration, adapter source, sampled state, and exception type. They never record scheme, port, path, query, fragment, full URL, request or response headers, bodies or sizes, exception messages or stacks, authentication material, cookies, baggage, tracestate, resolved addresses, or arbitrary request options.
446
473
 
@@ -86,35 +86,12 @@ module LogBrew
86
86
  end
87
87
 
88
88
  def request(request, body = nil, &block)
89
- prepared = HttpClientTracing.prepare(
89
+ HttpClientTracing.capture_net_http(
90
+ @http,
91
+ request,
90
92
  client: @client,
91
- source: "net_http",
92
93
  on_capture_error: @on_capture_error
93
- ) do
94
- [request.method, address, HttpClientTracing::NetHttpHeaderSnapshot.new(request)]
95
- end
96
- return @http.request(request, body, &block) unless prepared
97
-
98
- operation, header = prepared
99
- begin
100
- header.inject(operation.traceparent)
101
- rescue StandardError => error
102
- operation.capture_error(error)
103
- HttpClientTracing.reset_header(header, operation)
104
- return @http.request(request, body, &block)
105
- end
106
-
107
- response = nil
108
- begin
109
- response = operation.around { @http.request(request, body, &block) }
110
- rescue StandardError => error
111
- operation.finish(error: error)
112
- raise
113
- ensure
114
- HttpClientTracing.reset_header(header, operation)
115
- end
116
- operation.finish(status_code: HttpClientTracing.read_status(response, :code, operation))
117
- response
94
+ ) { @http.request(request, body, &block) }
118
95
  end
119
96
 
120
97
  def start(*arguments)
@@ -187,6 +164,34 @@ module LogBrew
187
164
  NetHttpTracingClient.new(http, client: client, on_capture_error: on_capture_error)
188
165
  end
189
166
 
167
+ def capture_net_http(http, request, client:, on_capture_error: nil)
168
+ prepared = prepare(client: client, source: "net_http", on_capture_error: on_capture_error) do
169
+ [request.method, http.address, NetHttpHeaderSnapshot.new(request)]
170
+ end
171
+ return yield unless prepared
172
+
173
+ operation, header = prepared
174
+ begin
175
+ header.inject(operation.traceparent)
176
+ rescue StandardError => error
177
+ operation.capture_error(error)
178
+ reset_header(header, operation)
179
+ return yield
180
+ end
181
+
182
+ response = nil
183
+ begin
184
+ response = operation.around { yield }
185
+ rescue StandardError => error
186
+ operation.finish(error: error)
187
+ raise
188
+ ensure
189
+ reset_header(header, operation)
190
+ end
191
+ operation.finish(status_code: read_status(response, :code, operation))
192
+ response
193
+ end
194
+
190
195
  def prepare(client:, source:, on_capture_error: nil)
191
196
  parent = Trace.current
192
197
  return nil unless parent
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LogBrew
4
+ # Bounded W3C carrier shared by explicit and framework-owned queue adapters.
5
+ module QueueCarrier
6
+ KEY = "logbrew".freeze
7
+ VERSION = 1
8
+ MAX_ENQUEUED_AT_MS = 9_007_199_254_740_991
9
+ MAX_QUEUE_WAIT_MS = 604_800_000
10
+
11
+ module_function
12
+
13
+ def create(context, enqueued_at_ms: wall_time_ms)
14
+ {
15
+ "version" => VERSION,
16
+ "traceparent" => Trace.create_headers(context).fetch("traceparent"),
17
+ "enqueuedAtMs" => enqueued_at_ms
18
+ }
19
+ end
20
+
21
+ def read(value)
22
+ keys = %w[enqueuedAtMs traceparent version]
23
+ return unless value.is_a?(Hash) && value.size == keys.length && keys.all? { |key| value.key?(key) }
24
+ return unless value["version"] == VERSION
25
+ return unless value["traceparent"].is_a?(String) && value["traceparent"].bytesize <= 55
26
+ return unless value["enqueuedAtMs"].is_a?(Integer) && value["enqueuedAtMs"].between?(0, MAX_ENQUEUED_AT_MS)
27
+
28
+ Traceparent.parse(value["traceparent"])
29
+ value
30
+ rescue SdkError
31
+ nil
32
+ end
33
+
34
+ def child_context(carrier)
35
+ parsed = carrier && Traceparent.parse(carrier.fetch("traceparent"))
36
+ return Trace.create_root if parsed.nil?
37
+
38
+ Trace.create(
39
+ trace_id: parsed.trace_id,
40
+ span_id: Trace.generate_span_id,
41
+ parent_span_id: parsed.parent_span_id,
42
+ trace_flags: parsed.trace_flags
43
+ )
44
+ end
45
+
46
+ def queue_wait_ms(carrier)
47
+ return if carrier.nil?
48
+
49
+ [[wall_time_ms - carrier.fetch("enqueuedAtMs"), 0].max, MAX_QUEUE_WAIT_MS].min
50
+ end
51
+
52
+ def wall_time_ms
53
+ (Time.now.to_f * 1000.0).floor
54
+ end
55
+ end
56
+ end
data/lib/logbrew/rails.rb CHANGED
@@ -8,8 +8,6 @@ module LogBrew
8
8
  # Process-safe Rails integration installed by RailsRailtie.
9
9
  module Rails
10
10
  class << self
11
- attr_reader :runtime
12
-
13
11
  def install(application:, environment: ENV)
14
12
  @installation_mutex ||= Mutex.new
15
13
  @installation_mutex.synchronize do
@@ -79,6 +77,10 @@ module LogBrew
79
77
  runtime = LogBrew::Rails.install(application: application)
80
78
  if runtime.configuration.enabled?
81
79
  LogBrew::Rails.const_get(:RequestOperations).install(::ActiveSupport::Notifications)
80
+ LogBrew::Rails.const_get(:OutboundHttp).install
81
+ end
82
+ ::ActiveSupport.on_load(:active_job) do
83
+ prepend LogBrew::Rails::ActiveJobExtension unless ancestors.include?(LogBrew::Rails::ActiveJobExtension)
82
84
  end
83
85
  middleware = application.config.middleware
84
86
  if defined?(::ActionDispatch::ShowExceptions)
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../logbrew" unless defined?(LogBrew::Client)
4
+ require_relative "queue_carrier"
4
5
  require "uri"
5
6
 
6
7
  module LogBrew
7
8
  module Rails
9
+ singleton_class.attr_reader :runtime
10
+
8
11
  # Immutable, environment-derived settings for the automatic Rails adapter.
9
12
  class Configuration
10
13
  DEFAULT_ENDPOINT = LogBrew::HttpTransport::DEFAULT_ENDPOINT
@@ -395,6 +398,197 @@ module LogBrew
395
398
  end
396
399
  end
397
400
 
401
+ # Adds bounded child spans to Net::HTTP calls made inside an active Rails trace.
402
+ module OutboundHttp
403
+ module Request
404
+ def request(request, body = nil, &block)
405
+ runtime = LogBrew::Rails.runtime
406
+ client = LogBrew::Trace.current && runtime&.client
407
+ return super unless client
408
+
409
+ LogBrew::HttpClientTracing.capture_net_http(
410
+ self,
411
+ request,
412
+ client: client,
413
+ on_capture_error: ->(error) { runtime.report_error("outbound_http_capture", error) }
414
+ ) { super(request, body, &block) }
415
+ end
416
+ end
417
+
418
+ module_function
419
+
420
+ def install
421
+ Net::HTTP.prepend(Request) unless Net::HTTP.ancestors.include?(Request)
422
+ end
423
+ end
424
+
425
+ # One privacy-bounded producer or worker operation for ActiveJob.
426
+ class ActiveJobOperation
427
+ attr_reader :context
428
+
429
+ def self.create(runtime, job, kind, carrier: nil)
430
+ client = runtime&.client
431
+ return if client.nil?
432
+
433
+ new(runtime, client, job, kind, carrier)
434
+ rescue StandardError => error
435
+ runtime&.report_error("active_job_capture", error)
436
+ nil
437
+ end
438
+
439
+ def initialize(runtime, client, job, kind, carrier)
440
+ @runtime = runtime
441
+ @client = client
442
+ @kind = kind
443
+ @context = kind == :enqueue ? OperationTracing.child_context : QueueCarrier.child_context(QueueCarrier.read(carrier))
444
+ @job_class = bounded_identifier(job.class.name, "ActiveJob")
445
+ @adapter = bounded_identifier(job.class.respond_to?(:queue_adapter_name) ? job.class.queue_adapter_name : nil, "active_job")
446
+ @retry_count = normalized_retry_count(job.respond_to?(:executions) ? job.executions : nil)
447
+ @queue_wait_ms = kind == :perform ? QueueCarrier.queue_wait_ms(QueueCarrier.read(carrier)) : nil
448
+ @started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
449
+ @timestamp = Time.now.utc.iso8601(6)
450
+ @finished = false
451
+ end
452
+
453
+ def around
454
+ result = Trace.with_context(@context) { yield }
455
+ finish
456
+ result
457
+ rescue Exception => error # rubocop:disable Lint/RescueException
458
+ finish(error)
459
+ raise
460
+ end
461
+
462
+ def finish(error = nil)
463
+ return if @finished
464
+
465
+ @finished = true
466
+ options = {
467
+ timestamp: @timestamp,
468
+ source: "rails.active_job",
469
+ system: @adapter,
470
+ operation: @kind.to_s,
471
+ metadata: operation_metadata
472
+ }
473
+ OperationTracing.capture_span(@client, "queue", "active_job.#{@kind}", @context, @started_at, options, error)
474
+ rescue StandardError => capture_error
475
+ @runtime.report_error("active_job_capture", capture_error)
476
+ end
477
+
478
+ def capture_terminal_issue(error)
479
+ return unless error.is_a?(StandardError)
480
+
481
+ metadata = operation_metadata.merge(
482
+ "source" => "rails.active_job",
483
+ "handled" => false,
484
+ "mechanism" => "rails.active_job",
485
+ "issueGroupingKey" => grouping_key(error),
486
+ "issueGroupingSource" => "exception_type_job_file"
487
+ )
488
+ attributes = IssueDiagnostics.from_exception(
489
+ error,
490
+ title: IssueDiagnostics.safe_exception_type(error),
491
+ message: @runtime.configuration.capture_exception_messages? ? error.message : nil,
492
+ mechanism_type: "rails.active_job",
493
+ handled: false,
494
+ metadata: metadata
495
+ )
496
+ Trace.with_context(@context) do
497
+ @client.issue("ruby_active_job_issue_#{@context.span_id}", Time.now.utc.iso8601, attributes)
498
+ end
499
+ rescue StandardError => capture_error
500
+ @runtime.report_error("active_job_capture", capture_error)
501
+ end
502
+
503
+ private
504
+
505
+ def operation_metadata
506
+ @runtime.metadata.merge("activeJob.class" => @job_class).tap do |metadata|
507
+ metadata["retryCount"] = @retry_count unless @retry_count.nil?
508
+ metadata["queueWaitMs"] = @queue_wait_ms unless @queue_wait_ms.nil?
509
+ end
510
+ end
511
+
512
+ def grouping_key(error)
513
+ frame = IssueDiagnostics.stack_frames_from_exception(error).first
514
+ file = frame.nil? ? "" : frame.fetch("filename")
515
+ values = [IssueDiagnostics.safe_exception_type(error), @job_class, file]
516
+ "rails-active-job-#{Digest::SHA256.hexdigest(values.join("\n"))}"
517
+ end
518
+
519
+ def bounded_identifier(value, fallback)
520
+ text = value.to_s
521
+ text.match?(/\A[A-Za-z_][A-Za-z0-9_:.-]{0,254}\z/) ? text : fallback
522
+ end
523
+
524
+ def normalized_retry_count(value)
525
+ [[value, 0].max, 1_000].min if value.is_a?(Integer)
526
+ end
527
+ end
528
+ private_constant :ActiveJobOperation
529
+
530
+ # Adapter-neutral ActiveJob tracing installed through the Rails lazy-load hook.
531
+ module ActiveJobExtension
532
+ def enqueue(options = {})
533
+ operation = ActiveJobOperation.create(LogBrew::Rails.runtime, self, :enqueue)
534
+ return super if operation.nil?
535
+
536
+ previous = @logbrew_enqueue_operation
537
+ @logbrew_enqueue_operation = operation
538
+ operation.around { super }
539
+ ensure
540
+ @logbrew_enqueue_operation = previous unless operation.nil?
541
+ end
542
+
543
+ def serialize
544
+ payload = super
545
+ operation = @logbrew_enqueue_operation
546
+ return payload if operation.nil?
547
+
548
+ begin
549
+ payload[QueueCarrier::KEY] = QueueCarrier.create(operation.context)
550
+ rescue StandardError => error
551
+ LogBrew::Rails.runtime&.report_error("active_job_capture", error)
552
+ end
553
+ payload
554
+ end
555
+
556
+ def deserialize(payload)
557
+ result = super
558
+ begin
559
+ @logbrew_queue_carrier = QueueCarrier.read(payload[QueueCarrier::KEY]) if payload.is_a?(Hash)
560
+ rescue StandardError => error
561
+ LogBrew::Rails.runtime&.report_error("active_job_capture", error)
562
+ end
563
+ result
564
+ end
565
+
566
+ def perform_now
567
+ operation = ActiveJobOperation.create(LogBrew::Rails.runtime, self, :perform, carrier: @logbrew_queue_carrier)
568
+ return super if operation.nil?
569
+
570
+ previous = @logbrew_perform_operation
571
+ @logbrew_perform_operation = operation
572
+ begin
573
+ Trace.with_context(operation.context) { super }
574
+ rescue Exception => error # rubocop:disable Lint/RescueException
575
+ operation.finish(error)
576
+ operation.capture_terminal_issue(error)
577
+ raise
578
+ ensure
579
+ unless operation.nil?
580
+ operation.finish
581
+ @logbrew_perform_operation = previous
582
+ end
583
+ end
584
+ end
585
+
586
+ def _perform_job
587
+ operation = @logbrew_perform_operation
588
+ operation.nil? ? super : operation.around { super }
589
+ end
590
+ end
591
+
398
592
  # Buffers only the slowest request-local framework operations without raw
399
593
  # SQL, cache keys, absolute paths, or exception messages.
400
594
  module RequestOperations
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../logbrew"
4
+ require_relative "queue_carrier"
4
5
 
5
6
  module LogBrew
6
7
  # Explicit Sidekiq middleware for app-owned client and server chains.
7
8
  module Sidekiq
8
- CARRIER_KEY = "logbrew".freeze
9
- CARRIER_VERSION = 1
10
- MAX_QUEUE_WAIT_MS = 604_800_000
11
9
  MAX_RETRY_COUNT = 1_000
12
10
  MAX_REPORTED_FAILURES = 1_024
13
- private_constant :CARRIER_KEY, :CARRIER_VERSION, :MAX_QUEUE_WAIT_MS, :MAX_RETRY_COUNT, :MAX_REPORTED_FAILURES
11
+ private_constant :MAX_RETRY_COUNT, :MAX_REPORTED_FAILURES
14
12
 
15
13
  class TraceOperation
16
14
  attr_reader :context, :traceparent
@@ -197,17 +195,13 @@ module LogBrew
197
195
  def around_client(job)
198
196
  return yield unless capture_enabled?
199
197
  return yield unless job.is_a?(Hash)
200
- return yield if job.key?(CARRIER_KEY)
198
+ return yield if active_job_wrapper?(job) || job.key?(QueueCarrier::KEY)
201
199
 
202
200
  operation = prepare_operation(name: "sidekiq.enqueue", source: "sidekiq.client", continue_current: true)
203
201
  return yield if operation.nil?
204
202
 
205
203
  begin
206
- job[CARRIER_KEY] = {
207
- "version" => CARRIER_VERSION,
208
- "traceparent" => operation.traceparent,
209
- "enqueuedAtMs" => wall_time_ms
210
- }
204
+ job[QueueCarrier::KEY] = QueueCarrier.create(operation.context)
211
205
  rescue StandardError => error
212
206
  report_capture_error(error)
213
207
  return yield
@@ -218,16 +212,17 @@ module LogBrew
218
212
  def around_server(job)
219
213
  return yield unless capture_enabled?
220
214
  return yield unless job.is_a?(Hash)
215
+ return yield if active_job_wrapper?(job)
221
216
 
222
217
  begin
223
- carrier = read_carrier(job[CARRIER_KEY])
218
+ carrier = QueueCarrier.read(job[QueueCarrier::KEY])
224
219
  retry_count = normalized_retry_count(job["retry_count"], job.key?("retry_count"))
225
220
  operation = prepare_operation(
226
221
  name: "sidekiq.perform",
227
222
  source: "sidekiq.server",
228
223
  carrier: carrier,
229
224
  retry_count: retry_count,
230
- queue_wait_ms: queue_wait_ms(carrier)
225
+ queue_wait_ms: QueueCarrier.queue_wait_ms(carrier)
231
226
  )
232
227
  rescue StandardError => error
233
228
  report_capture_error(error)
@@ -328,15 +323,9 @@ module LogBrew
328
323
  end
329
324
 
330
325
  def prepare_operation(name:, source:, carrier: nil, retry_count: nil, queue_wait_ms: nil, continue_current: false)
331
- parsed = carrier && Traceparent.parse(carrier.fetch("traceparent"))
332
- parent = parsed.nil? && continue_current ? Trace.current : parsed
333
- context = if parsed
334
- Trace.create(
335
- trace_id: parsed.trace_id,
336
- span_id: Trace.generate_span_id,
337
- parent_span_id: parsed.parent_span_id,
338
- trace_flags: parsed.trace_flags
339
- )
326
+ parent = continue_current ? Trace.current : nil
327
+ context = if carrier
328
+ QueueCarrier.child_context(carrier)
340
329
  elsif parent
341
330
  Trace.create(
342
331
  trace_id: parent.trace_id,
@@ -360,18 +349,12 @@ module LogBrew
360
349
  nil
361
350
  end
362
351
 
363
- def read_carrier(value)
364
- return nil unless value.is_a?(Hash)
365
- keys = %w[enqueuedAtMs traceparent version]
366
- return nil unless value.size == keys.length && keys.all? { |key| value.key?(key) }
367
- return nil unless value["version"] == CARRIER_VERSION
368
- return nil unless value["traceparent"].is_a?(String) && value["traceparent"].bytesize <= 55
369
- return nil unless value["enqueuedAtMs"].is_a?(Integer) && value["enqueuedAtMs"].between?(0, 9_007_199_254_740_991)
370
-
371
- Traceparent.parse(value["traceparent"])
372
- value
373
- rescue SdkError
374
- nil
352
+ def active_job_wrapper?(job)
353
+ wrapped = job["wrapped"]
354
+ arguments = job["args"]
355
+ payload = arguments.first if arguments.is_a?(Array)
356
+ wrapped.is_a?(String) && payload.is_a?(Hash) && payload["job_class"] == wrapped &&
357
+ !QueueCarrier.read(payload[QueueCarrier::KEY]).nil?
375
358
  end
376
359
 
377
360
  def normalized_retry_count(value, present)
@@ -392,17 +375,6 @@ module LogBrew
392
375
  retry_count >= [limit - 1, 0].max
393
376
  end
394
377
 
395
- def queue_wait_ms(carrier)
396
- return nil if carrier.nil?
397
-
398
- elapsed = wall_time_ms - carrier.fetch("enqueuedAtMs")
399
- [[elapsed, 0].max, MAX_QUEUE_WAIT_MS].min
400
- end
401
-
402
- def wall_time_ms
403
- (Time.now.to_f * 1000.0).floor
404
- end
405
-
406
378
  def capture_span(operation, error)
407
379
  @client.span(
408
380
  "ruby_sidekiq_span_#{operation.context.span_id}",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LogBrew
4
- VERSION = "0.1.8"
4
+ VERSION = "0.1.10"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logbrew-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - LogBrew
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-13 00:00:00.000000000 Z
11
+ date: 2026-08-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Public LogBrew Ruby SDK with typed issue diagnostics, automatic Rails
14
14
  request/error capture, and standard-library delivery.
@@ -38,6 +38,7 @@ files:
38
38
  - lib/logbrew/operation_tracing.rb
39
39
  - lib/logbrew/persistent_event_store.rb
40
40
  - lib/logbrew/product_timeline.rb
41
+ - lib/logbrew/queue_carrier.rb
41
42
  - lib/logbrew/rails.rb
42
43
  - lib/logbrew/rails_integration.rb
43
44
  - lib/logbrew/sidekiq.rb