logister-ruby 0.4.2 → 0.5.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: 381dffab29b34b6beffe54e105fcf8dc5151f59bde1e44cfbba1749d21bff763
4
- data.tar.gz: 5fe2ef7923ebeb4aa440d4aff35b246b2d407da0fe11dd26e26633e491105657
3
+ metadata.gz: 831c1daef24ddf24e6b74e58b22abf58280881bc1b6db683819198e5f6808c03
4
+ data.tar.gz: 1682771b953d6db5f323b33013c97618945bf3ff95b1c3117ae513c09e565d4c
5
5
  SHA512:
6
- metadata.gz: a5380bdc6bc4c7b4c1e4cb9e2c0d9d4cc4825f87d3517b57c22db7e63fff519071e46e9ad3555d3035819f4e1a64819d26305a1646f18cb32c02977cff925da2
7
- data.tar.gz: b3d7c3a5932f560be7ec3b1dc1a88a2319799e2b166e91d8a6d6084e8d6d7eaf262c047c5d7f9ba3e095d44dc857d1ed701605ced120a2253a168d3568cc5687
6
+ metadata.gz: 5580094db13b735ec710752d10633d646f4975ebc48cc4720ea224af883e75c353f9afd724f4e7d41dddf32169498a4fea6f3c2bb1fb6bba0272cb72826ec094
7
+ data.tar.gz: bae736ef8c2fff8e37b87fe10b21d5ce9162917c5ae24d63aa58c52396f0f0f8b74101a782840d86aba98eb90d1a6b4ebe73c39bc76b61dc6aaaeb58c5771856
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.5.1 - 2026-09-26
4
+
5
+ - Restore the enclosing request or job scope after inline and nested Active Job executions, including failures, preserving request tracing, breadcrumbs, and dependencies while keeping job telemetry isolated.
6
+ - Capture request identity before invoking the Rack app so response headers stay stable if ambient context changes; clear request summaries using that captured identity during cleanup.
7
+
8
+ ## v0.5.0 - 2026-09-24
9
+
10
+ - Propagate W3C request context through Rack and Rails, including logs, errors, transactions, and server spans.
11
+ - Add immutable request handles and exact-origin outbound header helpers; preserve incoming trace flags and isolate concurrent request scopes.
12
+
3
13
  ## v0.4.2 - 2026-09-16
4
14
 
5
15
  - Added process-local delivery counters and an optional recursion-safe observer for queued, acknowledged, unconfirmed, queue-full, and retry outcomes without exporting event contents.
data/README.md CHANGED
@@ -464,3 +464,37 @@ package identity before creating the GitHub Release. Never move a consumed tag.
464
464
  Weekly CI audits/tests current dependencies and cannot trigger automatic publication.
465
465
  Dependabot groups compatible minor/patch updates; major toolchain migrations keep
466
466
  separate PRs. Pin Actions to full commits and retain supported runtime floors.
467
+
468
+ ## Request correlation (0.5.0+)
469
+
470
+ The Rack/Rails integration accepts strict W3C version 00 `traceparent` headers,
471
+ creates a local server span, and applies its identity to automatic and manual
472
+ telemetry during the request. It preserves the incoming trace flags. Each request
473
+ has its own scope, cleared even when the request raises.
474
+
475
+ For an outbound call, retain a child handle and apply headers only to the intended
476
+ origin. Disable automatic redirects, or regenerate and validate headers for every
477
+ hop. Keep telemetry and token-issuer calls outside this instrumentation.
478
+
479
+ ```ruby
480
+ trace = Logister.outbound_trace_context
481
+ headers = trace.headers_for("https://api.example.test/orders", allowed_origins: ["https://api.example.test"])
482
+ # Pass headers to your HTTP client with automatic redirects disabled.
483
+ # When that request fails:
484
+ Logister.report_error(StandardError.new("Order request failed"), context: trace.to_h)
485
+ ```
486
+
487
+ `Logister.current_trace_context` returns the immutable active request handle.
488
+ Background jobs require explicitly supplied context; there is no automatic queue
489
+ propagation in this release.
490
+
491
+ A linked-project lookup also requires Logister 3.7+, the instance flag
492
+ `LOGISTER_CROSS_PROJECT_CORRELATIONS=true`, and explicit project/environment
493
+ connections under Settings → Integrations → Connected projects. Enable related
494
+ requests on both projects. A connection never grants project access.
495
+
496
+ Use the returned request handle when reporting a handled HTTP failure later.
497
+ Do not attach the most recent request to an unrelated crash or OS diagnostic.
498
+ Configure each app's own `release` and `environment`; mobile and backend releases
499
+ are independent. The backend shows exact identifier evidence and retention gaps.
500
+ See the [request correlation guide](https://logister.org/docs/request-correlation/).
@@ -15,24 +15,23 @@ module Logister
15
15
 
16
16
  included do
17
17
  around_perform do |job, block|
18
- Logister::ContextStore.reset_request_scope!
19
- Logister.add_breadcrumb(
20
- category: "job",
21
- message: "Starting #{job.class.name}",
22
- data: { queue: job.queue_name, jobId: job.job_id }
23
- )
24
- started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
25
-
26
- begin
27
- block.call
28
- rescue StandardError => error
29
- Logister.report_error(
30
- error,
31
- context: Logister::ActiveJobReporter.build_job_error_context(job, started_at: started_at)
18
+ Logister::ContextStore.with_request_scope do
19
+ Logister.add_breadcrumb(
20
+ category: "job",
21
+ message: "Starting #{job.class.name}",
22
+ data: { queue: job.queue_name, jobId: job.job_id }
32
23
  )
33
- raise
34
- ensure
35
- Logister::ContextStore.reset_request_scope!
24
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
25
+
26
+ begin
27
+ block.call
28
+ rescue StandardError => error
29
+ Logister.report_error(
30
+ error,
31
+ context: Logister::ActiveJobReporter.build_job_error_context(job, started_at: started_at)
32
+ )
33
+ raise
34
+ end
36
35
  end
37
36
  end
38
37
  end
@@ -12,7 +12,7 @@ module Logister
12
12
  {
13
13
  runtime: {
14
14
  rubyVersion: RUBY_VERSION,
15
- railsVersion: defined?(Rails) ? Rails.version : nil,
15
+ railsVersion: defined?(Rails) && Rails.respond_to?(:version) ? Rails.version : nil,
16
16
  rackVersion: defined?(Rack) ? Rack.release : nil,
17
17
  platform: RUBY_PLATFORM
18
18
  }.compact
@@ -44,6 +44,9 @@ module Logister
44
44
  end
45
45
 
46
46
  def trace_context(headers:, env:)
47
+ current = Logister::ContextStore.trace_context
48
+ return current.to_h.merge(trace: {traceId: current.trace_id, spanId: current.span_id, requestId: current.request_id}) if current
49
+
47
50
  traceparent = header_value(headers, "Traceparent")
48
51
  b3_trace_id = header_value(headers, "X-B3-Traceid")
49
52
  b3_span_id = header_value(headers, "X-B3-Spanid")
@@ -196,15 +199,9 @@ module Logister
196
199
  def parse_traceparent(traceparent)
197
200
  return [ nil, nil, nil ] if traceparent.to_s.empty?
198
201
 
199
- parts = traceparent.to_s.split("-")
200
- return [ nil, nil, nil ] unless parts.size == 4
201
-
202
- trace_id = parts[1].to_s
203
- span_id = parts[2].to_s
204
- flags = parts[3].to_s
205
- sampled = flags.end_with?("01")
206
-
207
- [ trace_id.presence, span_id.presence, sampled ]
202
+ parsed = Logister::TraceContext.parse(traceparent)
203
+ return [nil, nil, nil] unless parsed
204
+ [parsed[:trace_id], parsed[:parent_span_id], (parsed[:flags].to_i(16) & 1) == 1]
208
205
  rescue StandardError
209
206
  [ nil, nil, nil ]
210
207
  end
@@ -12,6 +12,16 @@ module Logister
12
12
  }
13
13
  end
14
14
 
15
+ # Jobs may run inline inside a request or another job. Keep the complete
16
+ # caller scope intact while collecting this execution's telemetry separately.
17
+ def with_request_scope
18
+ previous_scope = Thread.current[REQUEST_SCOPE_KEY]
19
+ reset_request_scope!
20
+ yield
21
+ ensure
22
+ Thread.current[REQUEST_SCOPE_KEY] = previous_scope
23
+ end
24
+
15
25
  def add_breadcrumb(category:, message:, data: {}, level: "info", timestamp: Time.now.utc.iso8601)
16
26
  scope = request_scope
17
27
  breadcrumbs = scope[:breadcrumbs]
@@ -25,6 +35,14 @@ module Logister
25
35
  trim_collection!(breadcrumbs, max_breadcrumbs)
26
36
  end
27
37
 
38
+ def trace_context
39
+ request_scope[:trace_context]
40
+ end
41
+
42
+ def trace_context=(value)
43
+ request_scope[:trace_context] = value
44
+ end
45
+
28
46
  def breadcrumbs
29
47
  request_scope[:breadcrumbs].dup
30
48
  end
@@ -12,8 +12,15 @@ module Logister
12
12
 
13
13
  def call(env)
14
14
  Logister::ContextStore.reset_request_scope!
15
+ trace_context = Logister::TraceContext.from_headers(
16
+ traceparent: env["HTTP_TRACEPARENT"], request_id: env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
17
+ )
18
+ Logister::ContextStore.trace_context = trace_context
19
+ env["logister.trace_context"] = trace_context
15
20
  started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
- @app.call(env)
21
+ status, headers, body = @app.call(env)
22
+ headers = headers.merge("x-request-id" => trace_context.request_id)
23
+ [status, headers, body]
17
24
  rescue StandardError => e
18
25
  request = ActionDispatch::Request.new(env)
19
26
  request_context = build_request_context(request, env, error: e, started_at: started_at)
@@ -24,6 +31,7 @@ module Logister
24
31
  )
25
32
  raise
26
33
  ensure
34
+ Logister::ContextStore.clear_request_summary(trace_context&.request_id)
27
35
  request_id = env["action_dispatch.request_id"]
28
36
  Logister::ContextStore.clear_request_summary(request_id)
29
37
  Logister::ContextStore.reset_request_scope!
@@ -146,7 +146,10 @@ module Logister
146
146
 
147
147
  started_at ||= Time.now.utc - (duration_ms.to_f / 1000.0)
148
148
  span_id ||= SecureRandom.hex(8)
149
- trace_id ||= span_id
149
+ active = Logister::ContextStore.trace_context
150
+ trace_id ||= active&.trace_id || SecureRandom.hex(16)
151
+ request_id ||= active&.request_id
152
+ parent_span_id ||= active&.span_id unless span_id == active&.span_id
150
153
 
151
154
  payload = build_payload(
152
155
  event_type: 'span',
@@ -357,7 +360,7 @@ module Logister
357
360
  # Merge static config context last so caller-supplied keys are not
358
361
  # overwritten, then merge the static values. The static_context Hash
359
362
  # is frozen and reused — only the new outer Hash is allocated.
360
- context: @static_context.merge(context)
363
+ context: @static_context.merge(Logister::ContextStore.trace_context&.to_h || {}).merge(context)
361
364
  }
362
365
  end
363
366
 
@@ -8,8 +8,8 @@ module Logister
8
8
  def install!
9
9
  return if @installed
10
10
 
11
- ActiveSupport::Notifications.subscribe("process_action.action_controller") do |_name, _started, _finished, _id, payload|
12
- handle_process_action(payload)
11
+ ActiveSupport::Notifications.subscribe("process_action.action_controller") do |_name, started, finished, _id, payload|
12
+ handle_process_action(payload.merge(duration: (finished - started) * 1000.0, started_at: started))
13
13
  end
14
14
 
15
15
  ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, started, finished, _id, payload|
@@ -25,7 +25,7 @@ module Logister
25
25
  return if Logister.reporting_suppressed?
26
26
  return unless payload.is_a?(Hash)
27
27
 
28
- request_id = payload[:request_id].to_s.presence
28
+ request_id = Logister::ContextStore.trace_context&.request_id || payload[:request_id].to_s.presence || payload[:request]&.request_id
29
29
  return unless request_id
30
30
 
31
31
  Logister::ContextStore.store_request_summary(
@@ -65,13 +65,16 @@ module Logister
65
65
  return unless duration_ms&.positive?
66
66
 
67
67
  route = "#{payload[:controller]}##{payload[:action]}"
68
- started_at = Time.now.utc - (duration_ms / 1000.0)
68
+ started_at = payload[:started_at] || Time.now.utc - (duration_ms / 1000.0)
69
+ trace = Logister::ContextStore.trace_context || Logister::TraceContext.from_headers(request_id: request_id)
69
70
 
70
71
  Logister.report_span(
71
72
  name: route,
72
73
  kind: "server",
73
74
  status: payload[:status].to_i >= 500 ? "error" : "ok",
74
- trace_id: request_id,
75
+ trace_id: trace.trace_id,
76
+ span_id: trace.span_id,
77
+ parent_span_id: trace.parent_span_id,
75
78
  request_id: request_id,
76
79
  duration_ms: duration_ms,
77
80
  started_at: started_at,
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "uri"
5
+
6
+ module Logister
7
+ # Immutable request identity. Capturing this value lets a caller report a failed
8
+ # request later without borrowing another concurrent request's identity.
9
+ class TraceContext
10
+ attr_reader :trace_id, :span_id, :parent_span_id, :request_id, :flags
11
+
12
+ def initialize(trace_id: SecureRandom.hex(16), span_id: SecureRandom.hex(8), parent_span_id: nil, request_id: SecureRandom.uuid, flags: "01")
13
+ @trace_id, @span_id, @parent_span_id, @request_id, @flags = [trace_id, span_id, parent_span_id, request_id, flags].map { |value| value&.dup&.freeze }
14
+ freeze
15
+ end
16
+
17
+ def self.parse(value)
18
+ match = /\A00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})\z/.match(value.to_s)
19
+ return unless match && match[1] != "0" * 32 && match[2] != "0" * 16
20
+ {trace_id: match[1], parent_span_id: match[2], flags: match[3]}
21
+ end
22
+
23
+ def self.from_headers(traceparent: nil, request_id: nil)
24
+ request_id = nil unless request_id.is_a?(String) && request_id.match?(/\A[A-Za-z0-9._:-]{1,200}\z/)
25
+ new(**(parse(traceparent) || {}), request_id: request_id || SecureRandom.uuid)
26
+ end
27
+
28
+ def to_h
29
+ {trace_id: trace_id, span_id: span_id, parent_span_id: parent_span_id, request_id: request_id}.compact
30
+ end
31
+
32
+ def child
33
+ self.class.new(trace_id: trace_id, parent_span_id: span_id, request_id: request_id, flags: flags)
34
+ end
35
+
36
+ def traceparent = "00-#{trace_id}-#{span_id}-#{flags}"
37
+
38
+ # Apply only to the exact destination origin. HTTP clients must call this on
39
+ # each redirect hop, or disable redirects; never forward the returned headers.
40
+ def headers_for(url, allowed_origins:)
41
+ return {} unless self.class.parse(traceparent) && request_id.is_a?(String) && request_id.match?(/\A[A-Za-z0-9._:-]{1,200}\z/)
42
+ origin = self.class.origin(url)
43
+ return {} unless origin && allowed_origins.any? { |allowed| self.class.origin(allowed) == origin }
44
+ {"traceparent" => traceparent, "x-request-id" => request_id}
45
+ end
46
+
47
+ def self.origin(url)
48
+ uri = URI.parse(url.to_s)
49
+ return unless %w[http https].include?(uri.scheme) && uri.host && !uri.userinfo
50
+ [uri.scheme, uri.host.downcase, uri.port]
51
+ rescue URI::InvalidURIError
52
+ nil
53
+ end
54
+ end
55
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Logister
4
- VERSION = '0.4.2'
4
+ VERSION = '0.5.1'
5
5
  end
data/lib/logister.rb CHANGED
@@ -5,12 +5,23 @@ require_relative 'logister/client'
5
5
  require_relative 'logister/reporter'
6
6
  require_relative 'logister/context_helpers'
7
7
  require_relative 'logister/context_store'
8
+ require_relative 'logister/trace_context'
8
9
  require_relative 'logister/middleware'
9
10
  require_relative 'logister/sql_subscriber'
10
11
  require_relative 'logister/request_subscriber'
11
12
 
12
13
  module Logister
13
14
  class << self
15
+ def current_trace_context
16
+ ContextStore.trace_context
17
+ end
18
+
19
+ # The returned child is the handle for this outbound attempt. Supply its
20
+ # to_h as context when reporting an error after the request scope has ended.
21
+ def outbound_trace_context
22
+ (current_trace_context || TraceContext.new).child
23
+ end
24
+
14
25
  def configuration
15
26
  @configuration ||= Configuration.new
16
27
  end
@@ -28,5 +28,6 @@ Gem::Specification.new do |spec|
28
28
  # ActiveSupport 8 passes JSON.parse options positionally; JSON 3 removed that API.
29
29
  spec.add_dependency 'json', '>= 2', '< 3'
30
30
  spec.add_development_dependency 'actionpack', '>= 8.0.5', '< 9'
31
+ spec.add_development_dependency 'activejob', '>= 8.0.5', '< 9'
31
32
  spec.add_development_dependency 'rake', '>= 13.0'
32
33
  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.2
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Logister
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-09-16 00:00:00.000000000 Z
10
+ date: 2026-09-26 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -69,6 +69,26 @@ dependencies:
69
69
  - - "<"
70
70
  - !ruby/object:Gem::Version
71
71
  version: '9'
72
+ - !ruby/object:Gem::Dependency
73
+ name: activejob
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: 8.0.5
79
+ - - "<"
80
+ - !ruby/object:Gem::Version
81
+ version: '9'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 8.0.5
89
+ - - "<"
90
+ - !ruby/object:Gem::Version
91
+ version: '9'
72
92
  - !ruby/object:Gem::Dependency
73
93
  name: rake
74
94
  requirement: !ruby/object:Gem::Requirement
@@ -109,6 +129,7 @@ files:
109
129
  - lib/logister/reporting_scope.rb
110
130
  - lib/logister/request_subscriber.rb
111
131
  - lib/logister/sql_subscriber.rb
132
+ - lib/logister/trace_context.rb
112
133
  - lib/logister/version.rb
113
134
  - logister-ruby.gemspec
114
135
  homepage: https://github.com/taimoorq/logister-ruby