logister-ruby 0.4.2 → 0.5.0

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: a71c622e00652d3d7cc881b852987bb0c16ce383456e8c6f1aaacb4d195bbca1
4
+ data.tar.gz: 3ce350703b176ce69a72bc96a2a860f29d615a5132ee242c0a0b39fbdb1bb54d
5
5
  SHA512:
6
- metadata.gz: a5380bdc6bc4c7b4c1e4cb9e2c0d9d4cc4825f87d3517b57c22db7e63fff519071e46e9ad3555d3035819f4e1a64819d26305a1646f18cb32c02977cff925da2
7
- data.tar.gz: b3d7c3a5932f560be7ec3b1dc1a88a2319799e2b166e91d8a6d6084e8d6d7eaf262c047c5d7f9ba3e095d44dc857d1ed701605ced120a2253a168d3568cc5687
6
+ metadata.gz: e62e74979448480d5d383c08b0b53f0f3e39789aa4ee050d85debe6a9a5c4b922779a5866fe4d48632bc96f3346c8b69799b63d92f2bd5c4152e866037d16fe0
7
+ data.tar.gz: ae516f75badb5bc30013410e9c540fb8066a63edd2c637ae320d26dc01016e660178f83a0da8b456b0b0efedd850af34bd6d6703b9693cd16d4c4878cf35b36a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.5.0 - 2026-09-24
4
+
5
+ - Propagate W3C request context through Rack and Rails, including logs, errors, transactions, and server spans.
6
+ - Add immutable request handles and exact-origin outbound header helpers; preserve incoming trace flags and isolate concurrent request scopes.
7
+
3
8
  ## v0.4.2 - 2026-09-16
4
9
 
5
10
  - 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/).
@@ -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
@@ -25,6 +25,14 @@ module Logister
25
25
  trim_collection!(breadcrumbs, max_breadcrumbs)
26
26
  end
27
27
 
28
+ def trace_context
29
+ request_scope[:trace_context]
30
+ end
31
+
32
+ def trace_context=(value)
33
+ request_scope[:trace_context] = value
34
+ end
35
+
28
36
  def breadcrumbs
29
37
  request_scope[:breadcrumbs].dup
30
38
  end
@@ -12,8 +12,14 @@ module Logister
12
12
 
13
13
  def call(env)
14
14
  Logister::ContextStore.reset_request_scope!
15
+ Logister::ContextStore.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
+ env["logister.trace_context"] = Logister::ContextStore.trace_context
15
19
  started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
- @app.call(env)
20
+ status, headers, body = @app.call(env)
21
+ headers = headers.merge("x-request-id" => Logister::ContextStore.trace_context.request_id)
22
+ [status, headers, body]
17
23
  rescue StandardError => e
18
24
  request = ActionDispatch::Request.new(env)
19
25
  request_context = build_request_context(request, env, error: e, started_at: started_at)
@@ -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.0'
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
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.0
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-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -109,6 +109,7 @@ files:
109
109
  - lib/logister/reporting_scope.rb
110
110
  - lib/logister/request_subscriber.rb
111
111
  - lib/logister/sql_subscriber.rb
112
+ - lib/logister/trace_context.rb
112
113
  - lib/logister/version.rb
113
114
  - logister-ruby.gemspec
114
115
  homepage: https://github.com/taimoorq/logister-ruby