1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
|
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2025, by Samuel Williams.
require "securerandom"
module Traces
# A generic representation of the current tracing context.
class Context
# Parse a string representation of a distributed trace.
# @parameter parent [String] The parent trace context.
# @parameter state [Array(String)] Any attached trace state.
def self.parse(parent, state = nil, baggage = nil, **options)
version, trace_id, parent_id, flags = parent.split("-")
if version == "00" && trace_id && parent_id && flags
flags = Integer(flags, 16)
if state.is_a?(String)
state = state.split(",")
end
if state
state = state.map{|item| item.split("=")}.to_h
end
if baggage.is_a?(String)
baggage = baggage.split(",")
end
if baggage
baggage = baggage.map{|item| item.split("=")}.to_h
end
self.new(trace_id, parent_id, flags, state, baggage, **options)
end
end
# Create a local trace context which is likely to be globally unique.
# @parameter flags [Integer] Any trace context flags.
def self.local(flags = 0, **options)
self.new(SecureRandom.hex(16), SecureRandom.hex(8), flags, **options)
end
# Nest a local trace context in an optional parent context.
# @parameter parent [Context] An optional parent context.
def self.nested(parent, flags = 0)
if parent
parent.nested(flags)
else
self.local(flags)
end
end
SAMPLED = 0x01
# Initialize the trace context.
# @parameter trace_id [String] The ID of the whole trace forest.
# @parameter parent_id [String] The ID of this operation as known by the caller (sometimes referred to as the span ID).
# @parameter flags [Integer] An 8-bit field that controls tracing flags such as sampling, trace level, etc.
# @parameter state [Hash] Additional vendor-specific trace identification information.
# @parameter remote [Boolean] Whether this context was created from a distributed trace header.
def initialize(trace_id, parent_id, flags, state = nil, baggage = nil, remote: false)
@trace_id = trace_id
@parent_id = parent_id
@flags = flags
@state = state
@baggage = baggage
@remote = remote
end
# Create a new nested trace context in which spans can be recorded.
def nested(flags = @flags)
Context.new(@trace_id, SecureRandom.hex(8), flags, @state, @baggage, remote: @remote)
end
# The ID of the whole trace forest and is used to uniquely identify a distributed trace through a system. It is represented as a 16-byte array, for example, 4bf92f3577b34da6a3ce929d0e0e4736. All bytes as zero (00000000000000000000000000000000) is considered an invalid value.
attr :trace_id
# The ID of this operation as known by the caller (in some tracing systems, this is known as the span-id, where a span is the execution of a client operation). It is represented as an 8-byte array, for example, 00f067aa0ba902b7. All bytes as zero (0000000000000000) is considered an invalid value.
attr :parent_id
# An 8-bit field that controls tracing flags such as sampling, trace level, etc. These flags are recommendations given by the caller rather than strict rules.
attr :flags
# Provides additional vendor-specific trace identification information across different distributed tracing systems.
attr :state
# Provides additional application-specific trace identification information across different distributed tracing systems.
attr :baggage
# Denotes that the caller may have recorded trace data. When unset, the caller did not record trace data out-of-band.
def sampled?
(@flags & SAMPLED) != 0
end
# Whether this context was created from a distributed trace header.
def remote?
@remote
end
# A string representation of the trace context (excluding trace state).
def to_s
"00-#{@trace_id}-#{@parent_id}-#{@flags.to_s(16)}"
end
# Convert the trace context to a JSON representation, including trace state.
def as_json
{
trace_id: @trace_id,
parent_id: @parent_id,
flags: @flags,
state: @state,
baggage: @baggage,
remote: @remote
}
end
# Convert the trace context to a JSON string.
def to_json(...)
as_json.to_json(...)
end
# Inject the trace context into the headers, including the `"traceparent"`, `"tracestate"`, and `"baggage"` headers.
#
# @parameter headers [Hash] The headers hash to inject the trace context into.
#
# @returns [Hash] The modified headers hash.
def inject(headers)
headers["traceparent"] = self.to_s
if @state and !@state.empty?
headers["tracestate"] = self.state.map{|key, value| "#{key}=#{value}"}.join(",")
end
if @baggage and !@baggage.empty?
headers["baggage"] = self.baggage.map{|key, value| "#{key}=#{value}"}.join(",")
end
return headers
end
# Extract the trace context from the headers.
#
# The `"traceparent"` header is a string representation of the trace context. If it is an Array, the first element is used, otherwise it is used as is.
# The `"tracestate"` header is a string representation of the trace state. If it is a String, it is split on commas before being processed.
# The `"baggage"` header is a string representation of the baggage. If it is a String, it is split on commas before being processed.
#
# @parameter headers [Hash] The headers hash containing trace context.
# @returns [Context | Nil] The extracted trace context, or nil if no valid context found.
# @raises [ArgumentError] If headers is not a Hash or contains malformed trace data.
def self.extract(headers)
if traceparent = headers["traceparent"]
if traceparent.is_a?(Array)
traceparent = traceparent.first
end
if traceparent.empty?
return nil
end
tracestate = headers["tracestate"]
baggage = headers["baggage"]
return self.parse(traceparent, tracestate, baggage, remote: true)
end
end
end
end
|