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
-
.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).
-
.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`.
-
.extract(rack_env: nil) ⇒ Hash?
Main entry point — tries each source in priority order.
- .from_datadog ⇒ Object
-
.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).
- .from_opentelemetry ⇒ Object
- .from_w3c_header(rack_env) ⇒ Object
-
.otel_status ⇒ String?
‘active’ / ‘not_configured’ when the OTEL SDK is bundled, nil when it isn’t (we don’t opine on apps that never shipped OpenTelemetry).
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.
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_datadog ⇒ Object
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_opentelemetry ⇒ Object
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_status ⇒ String?
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).
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 |