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 +4 -4
- data/README.md +32 -5
- data/lib/logbrew/http_client_tracing.rb +32 -27
- data/lib/logbrew/queue_carrier.rb +56 -0
- data/lib/logbrew/rails.rb +4 -2
- data/lib/logbrew/rails_integration.rb +194 -0
- data/lib/logbrew/sidekiq.rb +16 -44
- data/lib/logbrew/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ed4c09b597bf3b6c6d2a9426c378c1ef86c3ad32b141cdbf8782d0477ba41d6
|
|
4
|
+
data.tar.gz: aae266ff513452aa9adc3e48d53ff117aa9ea80bbd6d5dd3ed35da05db2cbb9c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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.
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
-
)
|
|
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
|
data/lib/logbrew/sidekiq.rb
CHANGED
|
@@ -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 :
|
|
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?(
|
|
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[
|
|
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 =
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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/version.rb
CHANGED
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.
|
|
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-
|
|
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
|