logbrew-sdk 0.1.7 → 0.1.9

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: f086729efb0a0a98821ef3bce8d96b71066b202beff5656917d958e4ca119479
4
- data.tar.gz: 7c5eafb18cda7d001dc8c72c9646852e857a46bd4e06508abeda5a23365489a1
3
+ metadata.gz: 95bb6223a8ef074b3c8dd0973fe99e36734849c1b47a19f1c1ed88655888d71b
4
+ data.tar.gz: 4ac8224ffe4e2fae6f4262b80a3119f2574982a6c0f384c529c77c74163340fc
5
5
  SHA512:
6
- metadata.gz: 583a7cdc7ccc4ea1435a31454c0d90cea390a7b9f37e1322c3f39bc84bf0b2c731aaa08abff5384675871f62082fd8fc98c5c2c7284ad89a1fb6c9b1221982e0
7
- data.tar.gz: 5093fb1d5ea5540302c6a99c8d6fa7ce1b66142a55010c7d867e1f0539d1fe57525270486c59e58acd687a68bb51e44aaa62f480a3c119681648d9a876d70fbb
6
+ metadata.gz: f90b3564b39c4546b8a34b424b1f59e8ca5de6afaba35529e511a688712e770c92c04deb547fd7e370bfa1590688be7ce3b11ca5f04ccd7d11ecb022f37ff401
7
+ data.tar.gz: 3d47121b9f61ed19bbeda0e9389f3daa5137c34b88256ea9b5d3f7e47dd28b4d4c36fc7f94bf0c9e2172be0b65f72235e5e96e2923c113811e0b758158498f18
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, and ActiveJob capture; standard-library `Net::HTTP` 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.7"
26
+ gem "logbrew-sdk", "~> 0.1.9"
27
27
  ```
28
28
 
29
29
  ```bash
@@ -53,7 +53,19 @@ 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
+ The explicit Rails capture settings are:
57
69
 
58
70
  | Environment variable | Default | Purpose |
59
71
  | --- | --- | --- |
@@ -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
@@ -80,6 +78,9 @@ module LogBrew
80
78
  if runtime.configuration.enabled?
81
79
  LogBrew::Rails.const_get(:RequestOperations).install(::ActiveSupport::Notifications)
82
80
  end
81
+ ::ActiveSupport.on_load(:active_job) do
82
+ prepend LogBrew::Rails::ActiveJobExtension unless ancestors.include?(LogBrew::Rails::ActiveJobExtension)
83
+ end
83
84
  middleware = application.config.middleware
84
85
  if defined?(::ActionDispatch::ShowExceptions)
85
86
  middleware.insert_after(
@@ -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,173 @@ module LogBrew
395
398
  end
396
399
  end
397
400
 
401
+ # One privacy-bounded producer or worker operation for ActiveJob.
402
+ class ActiveJobOperation
403
+ attr_reader :context
404
+
405
+ def self.create(runtime, job, kind, carrier: nil)
406
+ client = runtime&.client
407
+ return if client.nil?
408
+
409
+ new(runtime, client, job, kind, carrier)
410
+ rescue StandardError => error
411
+ runtime&.report_error("active_job_capture", error)
412
+ nil
413
+ end
414
+
415
+ def initialize(runtime, client, job, kind, carrier)
416
+ @runtime = runtime
417
+ @client = client
418
+ @kind = kind
419
+ @context = kind == :enqueue ? OperationTracing.child_context : QueueCarrier.child_context(QueueCarrier.read(carrier))
420
+ @job_class = bounded_identifier(job.class.name, "ActiveJob")
421
+ @adapter = bounded_identifier(job.class.respond_to?(:queue_adapter_name) ? job.class.queue_adapter_name : nil, "active_job")
422
+ @retry_count = normalized_retry_count(job.respond_to?(:executions) ? job.executions : nil)
423
+ @queue_wait_ms = kind == :perform ? QueueCarrier.queue_wait_ms(QueueCarrier.read(carrier)) : nil
424
+ @started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
425
+ @timestamp = Time.now.utc.iso8601(6)
426
+ @finished = false
427
+ end
428
+
429
+ def around
430
+ result = Trace.with_context(@context) { yield }
431
+ finish
432
+ result
433
+ rescue Exception => error # rubocop:disable Lint/RescueException
434
+ finish(error)
435
+ raise
436
+ end
437
+
438
+ def finish(error = nil)
439
+ return if @finished
440
+
441
+ @finished = true
442
+ options = {
443
+ timestamp: @timestamp,
444
+ source: "rails.active_job",
445
+ system: @adapter,
446
+ operation: @kind.to_s,
447
+ metadata: operation_metadata
448
+ }
449
+ OperationTracing.capture_span(@client, "queue", "active_job.#{@kind}", @context, @started_at, options, error)
450
+ rescue StandardError => capture_error
451
+ @runtime.report_error("active_job_capture", capture_error)
452
+ end
453
+
454
+ def capture_terminal_issue(error)
455
+ return unless error.is_a?(StandardError)
456
+
457
+ metadata = operation_metadata.merge(
458
+ "source" => "rails.active_job",
459
+ "handled" => false,
460
+ "mechanism" => "rails.active_job",
461
+ "issueGroupingKey" => grouping_key(error),
462
+ "issueGroupingSource" => "exception_type_job_file"
463
+ )
464
+ attributes = IssueDiagnostics.from_exception(
465
+ error,
466
+ title: IssueDiagnostics.safe_exception_type(error),
467
+ message: @runtime.configuration.capture_exception_messages? ? error.message : nil,
468
+ mechanism_type: "rails.active_job",
469
+ handled: false,
470
+ metadata: metadata
471
+ )
472
+ Trace.with_context(@context) do
473
+ @client.issue("ruby_active_job_issue_#{@context.span_id}", Time.now.utc.iso8601, attributes)
474
+ end
475
+ rescue StandardError => capture_error
476
+ @runtime.report_error("active_job_capture", capture_error)
477
+ end
478
+
479
+ private
480
+
481
+ def operation_metadata
482
+ @runtime.metadata.merge("activeJob.class" => @job_class).tap do |metadata|
483
+ metadata["retryCount"] = @retry_count unless @retry_count.nil?
484
+ metadata["queueWaitMs"] = @queue_wait_ms unless @queue_wait_ms.nil?
485
+ end
486
+ end
487
+
488
+ def grouping_key(error)
489
+ frame = IssueDiagnostics.stack_frames_from_exception(error).first
490
+ file = frame.nil? ? "" : frame.fetch("filename")
491
+ values = [IssueDiagnostics.safe_exception_type(error), @job_class, file]
492
+ "rails-active-job-#{Digest::SHA256.hexdigest(values.join("\n"))}"
493
+ end
494
+
495
+ def bounded_identifier(value, fallback)
496
+ text = value.to_s
497
+ text.match?(/\A[A-Za-z_][A-Za-z0-9_:.-]{0,254}\z/) ? text : fallback
498
+ end
499
+
500
+ def normalized_retry_count(value)
501
+ [[value, 0].max, 1_000].min if value.is_a?(Integer)
502
+ end
503
+ end
504
+ private_constant :ActiveJobOperation
505
+
506
+ # Adapter-neutral ActiveJob tracing installed through the Rails lazy-load hook.
507
+ module ActiveJobExtension
508
+ def enqueue(options = {})
509
+ operation = ActiveJobOperation.create(LogBrew::Rails.runtime, self, :enqueue)
510
+ return super if operation.nil?
511
+
512
+ previous = @logbrew_enqueue_operation
513
+ @logbrew_enqueue_operation = operation
514
+ operation.around { super }
515
+ ensure
516
+ @logbrew_enqueue_operation = previous unless operation.nil?
517
+ end
518
+
519
+ def serialize
520
+ payload = super
521
+ operation = @logbrew_enqueue_operation
522
+ return payload if operation.nil?
523
+
524
+ begin
525
+ payload[QueueCarrier::KEY] = QueueCarrier.create(operation.context)
526
+ rescue StandardError => error
527
+ LogBrew::Rails.runtime&.report_error("active_job_capture", error)
528
+ end
529
+ payload
530
+ end
531
+
532
+ def deserialize(payload)
533
+ result = super
534
+ begin
535
+ @logbrew_queue_carrier = QueueCarrier.read(payload[QueueCarrier::KEY]) if payload.is_a?(Hash)
536
+ rescue StandardError => error
537
+ LogBrew::Rails.runtime&.report_error("active_job_capture", error)
538
+ end
539
+ result
540
+ end
541
+
542
+ def perform_now
543
+ operation = ActiveJobOperation.create(LogBrew::Rails.runtime, self, :perform, carrier: @logbrew_queue_carrier)
544
+ return super if operation.nil?
545
+
546
+ previous = @logbrew_perform_operation
547
+ @logbrew_perform_operation = operation
548
+ begin
549
+ Trace.with_context(operation.context) { super }
550
+ rescue Exception => error # rubocop:disable Lint/RescueException
551
+ operation.finish(error)
552
+ operation.capture_terminal_issue(error)
553
+ raise
554
+ ensure
555
+ unless operation.nil?
556
+ operation.finish
557
+ @logbrew_perform_operation = previous
558
+ end
559
+ end
560
+ end
561
+
562
+ def _perform_job
563
+ operation = @logbrew_perform_operation
564
+ operation.nil? ? super : operation.around { super }
565
+ end
566
+ end
567
+
398
568
  # Buffers only the slowest request-local framework operations without raw
399
569
  # SQL, cache keys, absolute paths, or exception messages.
400
570
  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}",
data/lib/logbrew/trace.rb CHANGED
@@ -240,7 +240,7 @@ module LogBrew
240
240
 
241
241
  private
242
242
 
243
- def capture_request_span(env, status_code, elapsed_ms, status)
243
+ def capture_request_span(env, status_code, elapsed_ms, status, timestamp)
244
244
  context = rack_trace_context(env)
245
245
  attributes = {
246
246
  name: request_name(env),
@@ -252,31 +252,21 @@ module LogBrew
252
252
  }
253
253
  attributes[:parentSpanId] = context.parent_span_id if context && context.parent_span_id
254
254
 
255
- @client.span(next_event_id("span"), logbrew_timestamp, attributes)
255
+ @client.span(next_event_id("span"), timestamp, attributes)
256
256
  end
257
257
 
258
258
  def trace_id(env)
259
- context = rack_trace_context(env)
260
- return context.trace_id if context
261
-
262
- super
259
+ rack_trace_context(env)&.trace_id || super
263
260
  end
264
261
 
265
262
  def span_id(env)
266
- context = rack_trace_context(env)
267
- return context.span_id if context
268
-
269
- super
263
+ rack_trace_context(env)&.span_id || super
270
264
  end
271
265
 
272
266
  def request_metadata(env, status_code)
273
267
  Trace.add_metadata(super, rack_trace_context(env))
274
268
  end
275
269
 
276
- def exception_metadata(env, error)
277
- Trace.add_metadata(super, rack_trace_context(env))
278
- end
279
-
280
270
  def rack_trace_context(env)
281
271
  trace = env["logbrew.trace"] if env.respond_to?(:[])
282
272
  trace.is_a?(TraceContext) ? trace : Trace.current
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LogBrew
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.9"
5
5
  end
data/lib/logbrew.rb CHANGED
@@ -45,9 +45,9 @@ module LogBrew
45
45
  metadata.each_with_object({}) { |(key, value), copied| copied[key.to_s] = value if primitive_metadata_value?(value) }
46
46
  end
47
47
 
48
- def logbrew_timestamp
48
+ def logbrew_timestamp(precision = nil)
49
49
  timestamp = @timestamp_provider.respond_to?(:call) ? @timestamp_provider.call : Time.now
50
- timestamp.respond_to?(:iso8601) ? timestamp.iso8601 : timestamp.to_s
50
+ timestamp.respond_to?(:iso8601) ? timestamp.iso8601(*[precision].compact) : timestamp.to_s
51
51
  end
52
52
 
53
53
  def capture_safely
@@ -412,36 +412,42 @@ module LogBrew
412
412
 
413
413
  def call(env)
414
414
  started_at = monotonic_time
415
- begin
416
- response = @app.call(env)
415
+ started_timestamp, timestamp_error = request_timestamp
416
+ response = begin
417
+ @app.call(env)
417
418
  rescue StandardError => error
418
- status_code = exception_status_code(error)
419
- capture_safely do
420
- capture_exception_issue(env, error) if status_code >= 500
421
- capture_request_span(env, status_code, duration_ms(started_at), status_code >= 500 ? "error" : "ok")
422
- flush_if_configured
423
- end
419
+ capture_request(env, exception_status_code(error), started_at, started_timestamp, timestamp_error, error)
424
420
  raise
425
421
  end
422
+ capture_request(env, rack_status(response), started_at, started_timestamp, timestamp_error)
423
+ response
424
+ end
425
+
426
+ private
426
427
 
427
- status_code = rack_status(response)
428
+ def capture_request(env, status_code, started_at, timestamp, timestamp_error, error = nil)
428
429
  capture_safely do
429
- capture_request_span(env, status_code, duration_ms(started_at), status_code >= 500 ? "error" : "ok")
430
+ raise timestamp_error unless timestamp_error.nil?
431
+ capture_exception_issue(env, error, logbrew_timestamp(6)) if error && status_code >= 500
432
+ capture_request_span(env, status_code, duration_ms(started_at), status_code >= 500 ? "error" : "ok", timestamp)
430
433
  flush_if_configured
431
434
  end
432
- response
433
435
  end
434
436
 
435
- private
437
+ def request_timestamp
438
+ [logbrew_timestamp(6), nil]
439
+ rescue StandardError => error
440
+ [nil, error]
441
+ end
436
442
 
437
443
  def exception_status_code(_error)
438
444
  500
439
445
  end
440
446
 
441
- def capture_request_span(env, status_code, elapsed_ms, status)
447
+ def capture_request_span(env, status_code, elapsed_ms, status, timestamp)
442
448
  @client.span(
443
449
  next_event_id("span"),
444
- logbrew_timestamp,
450
+ timestamp,
445
451
  name: request_name(env),
446
452
  traceId: trace_id(env),
447
453
  spanId: span_id(env),
@@ -451,7 +457,7 @@ module LogBrew
451
457
  )
452
458
  end
453
459
 
454
- def capture_exception_issue(env, error)
460
+ def capture_exception_issue(env, error, timestamp)
455
461
  attributes = IssueDiagnostics.from_exception(
456
462
  error,
457
463
  message: @include_exception_message ? error.message : nil,
@@ -459,7 +465,7 @@ module LogBrew
459
465
  handled: false,
460
466
  metadata: exception_metadata(env, error)
461
467
  )
462
- @client.issue(next_event_id("issue"), logbrew_timestamp, attributes)
468
+ @client.issue(next_event_id("issue"), timestamp, attributes)
463
469
  end
464
470
 
465
471
  def next_event_id(kind)
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.7
4
+ version: 0.1.9
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