Module: IuguLogger::TraceContext

Defined in:
lib/iugu_logger/trace_context.rb

Overview

Trace context extractor — W3C / OpenTelemetry / Datadog / legacy header chain.

Builds a => trace_id_32hex, ‘span_id’ => span_id_16hex, ‘parent_id’ => parent_span_id_16hex, ‘source’ => <origin>, ‘sampled’ => bool Hash compatible with the canonical ‘trace.*` ECS+OTEL fields. Values (`id`/`span_id`) come straight from OpenTelemetry when the SDK is active, so the ECS names already carry the OTEL identifiers.

‘source` records WHERE the context came from (opentelemetry / datadog / w3c / request_id) and `sampled` mirrors the trace-flags sampling decision when the source exposes it.

‘parent_id` is backfilled from an inbound W3C `traceparent` when a live tracer continued an upstream trace — present means the trace was continued from another service, nil means it was rooted in this call.

‘otel` is a diagnostic added by TraceContext.extract: when the OpenTelemetry SDK is bundled but never configured (so trace context silently fell back to a header/request-id source), it is set to “not_configured” so the misconfiguration is queryable in the log stream.

Source priority (first non-nil wins):

1. OpenTelemetry::Trace.current_span — when otel-api is loaded
   (Ruby >= 2.6 in practice; the gem itself enforces this)
2. Datadog::Tracing.active_trace    — when ddtrace gem is loaded
   (works on Ruby 2.4 with ddtrace ~> 0.45)
3. W3C `traceparent` header         — manual parse, version-agnostic
4. Legacy iugu `X-Request-Id`       — padded to 32 hex (correlation only,
   not a real trace ID; better than nothing for platform Ruby 2.4)

Returns nil when no source has trace context — Logger emits without trace.* block (it is optional in the schema).

Spec: IUGU_LOGGING_STANDARD.md §6.2.2

Constant Summary collapse

TRACEPARENT_REGEX =
/\A(\d{2})-([a-f0-9]{32})-([a-f0-9]{16})-([a-f0-9]{2})\z/.freeze
EMPTY_SPAN_ID =
'0000000000000000'

Class Method Summary collapse

Class Method Details

.annotate_otel_health(result) ⇒ Object

Adds ‘otel: “not_configured”` when the OpenTelemetry SDK is bundled but no SDK tracer provider is active (i.e. `OpenTelemetry::SDK.configure` never ran). That state means the trace context above came from a fallback source, not from a live span — almost always a boot misconfiguration worth surfacing. Stays silent (no field) when:

- OTEL SDK isn't bundled at all (Datadog / platform Ruby 2.4 apps), or
- the SDK is active (`status == 'active'`).

When OTEL is misconfigured but there was no trace context whatsoever, still returns a hash carrying only the diagnostic so it isn’t lost.



97
98
99
100
101
102
103
104
# File 'lib/iugu_logger/trace_context.rb', line 97

def annotate_otel_health(result)
  status = otel_status
  return result if status.nil? || status == 'active'

  result ||= {}
  result['otel'] = status
  result
end

.backfill_parent_id(result, rack_env) ⇒ Object

A live tracer (OTEL/Datadog) creates the local span, but its SpanContext doesn’t expose the parent — only ‘trace_id`/`span_id`. When the request arrived with a W3C `traceparent` whose trace_id matches the active trace, that header’s span_id IS the upstream caller’s span (our parent), so we backfill ‘parent_id` with it. The net effect:

- parent_id present → trace was CONTINUED from another service;
- parent_id nil     → trace was ROOTED in this call.

Skipped for the ‘w3c` fallback source (there the header span_id is already reported as `span_id`, so it has no distinct local parent) and for `request_id` (no real trace).



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/iugu_logger/trace_context.rb', line 70

def backfill_parent_id(result, rack_env)
  return result if result.nil? || rack_env.nil?
  return result unless %w[opentelemetry datadog].include?(result['source'])
  return result unless result['parent_id'].nil?

  header = rack_env['HTTP_TRACEPARENT'] || rack_env['traceparent']
  return result if header.nil? || header.to_s.empty?

  match = TRACEPARENT_REGEX.match(header.to_s.strip)
  return result if match.nil?

  _, header_trace_id, header_span_id, _flags = match.captures
  return result unless header_trace_id == result['id']

  result['parent_id'] = header_span_id
  result
end

.extract(rack_env: nil) ⇒ Hash?

Main entry point — tries each source in priority order.

Parameters:

  • rack_env (Hash, nil) (defaults to: nil)

    Rack env hash (for header sources). Pass ‘request.env` from a controller, or nil if not in a request.

Returns:

  • (Hash, nil)

    =>, ‘span_id’ =>, ‘parent_id’ => or nil



50
51
52
53
54
55
56
57
58
# File 'lib/iugu_logger/trace_context.rb', line 50

def extract(rack_env: nil)
  result = from_opentelemetry ||
           from_datadog ||
           from_w3c_header(rack_env) ||
           from_legacy_header(rack_env)

  result = backfill_parent_id(result, rack_env)
  annotate_otel_health(result)
end

.from_datadogObject



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/iugu_logger/trace_context.rb', line 144

def from_datadog
  return nil unless defined?(::Datadog::Tracing)

  trace = ::Datadog::Tracing.active_trace
  return nil if trace.nil?

  span = ::Datadog::Tracing.active_span
  span_id = span.respond_to?(:id) ? span.id : nil

  {
    'id'        => format('%032x', trace.id),
    'span_id'   => span_id ? format('%016x', span_id) : EMPTY_SPAN_ID,
    'parent_id' => nil,
    'source'    => 'datadog'
  }
rescue StandardError
  nil
end

.from_legacy_header(rack_env) ⇒ Object

Last-resort fallback: derive a deterministic 32-hex trace_id from the iugu legacy X-Request-Id (or Rails action_dispatch.request_id). NOT a real W3C trace ID — only good for correlating logs within a single service. Used by platform/Ruby 2.4 where OTEL is unavailable.



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/iugu_logger/trace_context.rb', line 188

def from_legacy_header(rack_env)
  return nil if rack_env.nil?

  raw = rack_env['HTTP_X_REQUEST_ID'] ||
        rack_env['action_dispatch.request_id'] ||
        rack_env['HTTP_REQUEST_ID']
  return nil if raw.nil? || raw.to_s.empty?

  hex = raw.to_s.gsub(/[^a-f0-9]/i, '').downcase[0, 32].to_s
  return nil if hex.empty?

  hex = hex.ljust(32, '0')
  return nil if hex == ('0' * 32)

  {
    'id'        => hex,
    'span_id'   => EMPTY_SPAN_ID,
    'parent_id' => nil,
    'source'    => 'request_id'
  }
end

.from_opentelemetryObject



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/iugu_logger/trace_context.rb', line 118

def from_opentelemetry
  return nil unless defined?(::OpenTelemetry::Trace)

  span = ::OpenTelemetry::Trace.current_span
  return nil if span.nil?

  ctx = span.context
  return nil if ctx.nil?
  return nil unless ctx.respond_to?(:valid?) && ctx.valid?

  flags   = ctx.respond_to?(:trace_flags) ? ctx.trace_flags : nil
  sampled = flags.respond_to?(:sampled?) ? flags.sampled? : nil

  trace = {
    'id'        => ctx.hex_trace_id,
    'span_id'   => ctx.hex_span_id,
    'parent_id' => nil,
    'source'    => 'opentelemetry'
  }
  trace['sampled'] = sampled unless sampled.nil?
  trace
rescue StandardError
  # Defensive: never let a tracing-lib oddity break logging
  nil
end

.from_w3c_header(rack_env) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/iugu_logger/trace_context.rb', line 163

def from_w3c_header(rack_env)
  return nil if rack_env.nil?

  header = rack_env['HTTP_TRACEPARENT'] || rack_env['traceparent']
  return nil if header.nil? || header.empty?

  match = TRACEPARENT_REGEX.match(header.to_s.strip)
  return nil if match.nil?

  _, trace_id, span_id, flags = match.captures
  trace = {
    'id'        => trace_id,
    'span_id'   => span_id,
    'parent_id' => nil,
    'source'    => 'w3c'
  }
  # traceparent flags: bit 0 (0x01) is the sampled flag (W3C §3.3.1).
  trace['sampled'] = (flags.to_i(16) & 0x01) == 1 if flags
  trace
end

.otel_statusString?

Returns ‘active’ / ‘not_configured’ when the OTEL SDK is bundled, nil when it isn’t (we don’t opine on apps that never shipped OpenTelemetry).

Returns:

  • (String, nil)

    ‘active’ / ‘not_configured’ when the OTEL SDK is bundled, nil when it isn’t (we don’t opine on apps that never shipped OpenTelemetry).



109
110
111
112
113
114
115
116
# File 'lib/iugu_logger/trace_context.rb', line 109

def otel_status
  return nil unless defined?(::OpenTelemetry::SDK)

  provider_class = ::OpenTelemetry.tracer_provider.class.name.to_s
  provider_class.start_with?('OpenTelemetry::SDK') ? 'active' : 'not_configured'
rescue StandardError
  nil
end