Skip to content

Commit 1040b36

Browse files
fix: Handle Self field in X-Amzn-Trace-Id header (#1958)
fix: Parse X-Amzn-Trace-Id header fields individually by name Replace regex-based parsing of the X-Amzn-Trace-Id header with direct extraction of named fields (Root, Parent, Sampled, Self). This approach is less brittle and order-independent, fixing issues when AWS load balancers add the `Self` field to headers that already contain `Root`. Co-authored-by: Ariel Valentin <arielvalentin@users.noreply.github.com>
1 parent 66b4667 commit 1040b36

File tree

2 files changed

+75
-6
lines changed

2 files changed

+75
-6
lines changed

propagator/xray/lib/opentelemetry/propagator/xray/text_map_propagator.rb

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,23 @@ module XRay
1818
# Propagates context in carriers in the xray single header format
1919
class TextMapPropagator
2020
XRAY_CONTEXT_KEY = 'X-Amzn-Trace-Id'
21-
XRAY_CONTEXT_REGEX = /\ARoot=(?<trace_id>([a-z0-9\-]{35}))(?:;Parent=(?<span_id>([a-z0-9]{16})))?(?:;Sampled=(?<sampling_state>[01d](?![0-9a-f])))?(?:;(?<trace_state>.*))?\Z/ # rubocop:disable Lint/MixedRegexpCaptureTypes
2221
SAMPLED_VALUES = %w[1 d].freeze
2322
FIELDS = [XRAY_CONTEXT_KEY].freeze
2423

25-
private_constant :XRAY_CONTEXT_KEY, :XRAY_CONTEXT_REGEX, :SAMPLED_VALUES, :FIELDS
24+
# Header parsing constants
25+
KV_PAIR_DELIMITER = ';'
26+
KEY_AND_VALUE_DELIMITER = '='
27+
TRACE_ID_KEY = 'Root'
28+
PARENT_ID_KEY = 'Parent'
29+
SAMPLED_FLAG_KEY = 'Sampled'
30+
TRACE_ID_LENGTH = 35
31+
SPAN_ID_LENGTH = 16
32+
VALID_SAMPLED_VALUES = %w[0 1 d].freeze
33+
34+
private_constant :XRAY_CONTEXT_KEY, :SAMPLED_VALUES, :FIELDS,
35+
:KV_PAIR_DELIMITER, :KEY_AND_VALUE_DELIMITER,
36+
:TRACE_ID_KEY, :PARENT_ID_KEY, :SAMPLED_FLAG_KEY,
37+
:TRACE_ID_LENGTH, :SPAN_ID_LENGTH, :VALID_SAMPLED_VALUES
2638

2739
# Extract trace context from the supplied carrier.
2840
# If extraction fails, the original context will be returned
@@ -90,11 +102,56 @@ def inject(carrier, context: Context.current, setter: Context::Propagation.text_
90102
private
91103

92104
def parse_header(header)
93-
return nil unless (match = header.match(XRAY_CONTEXT_REGEX))
94-
return nil unless match['trace_id']
95-
return nil unless match['span_id']
105+
trace_id = nil
106+
span_id = nil
107+
sampling_state = nil
108+
trace_state_parts = []
109+
110+
header.split(KV_PAIR_DELIMITER).each do |pair|
111+
# Split only on first '=' to handle values that might contain '='
112+
key, value = pair.split(KEY_AND_VALUE_DELIMITER, 2)
113+
next unless key && value
114+
115+
case key
116+
when TRACE_ID_KEY
117+
trace_id = value if valid_trace_id?(value)
118+
when PARENT_ID_KEY
119+
span_id = value if valid_span_id?(value)
120+
when SAMPLED_FLAG_KEY
121+
sampling_state = value if valid_sampling_state?(value)
122+
when 'Self'
123+
# Ignore Self field added by load balancers
124+
next
125+
else
126+
# Collect other fields as potential tracestate
127+
trace_state_parts << pair
128+
end
129+
end
130+
131+
return nil unless trace_id && span_id
132+
133+
{
134+
'trace_id' => trace_id,
135+
'span_id' => span_id,
136+
'sampling_state' => sampling_state,
137+
'trace_state' => trace_state_parts.empty? ? nil : trace_state_parts.join(KV_PAIR_DELIMITER)
138+
}
139+
end
140+
141+
def valid_trace_id?(value)
142+
return false unless value.length == TRACE_ID_LENGTH
143+
return false unless value.start_with?('1-')
144+
return false unless value[10] == '-'
145+
146+
true
147+
end
148+
149+
def valid_span_id?(value)
150+
value.length == SPAN_ID_LENGTH && value.match?(/\A[a-f0-9]+\z/)
151+
end
96152

97-
match
153+
def valid_sampling_state?(value)
154+
VALID_SAMPLED_VALUES.include?(value)
98155
end
99156

100157
# Convert an id from a hex encoded string to byte array. Assumes the input id has already been

propagator/xray/test/text_map_propagator_test.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@
5454
_(extracted_context).must_be(:remote?)
5555
end
5656

57+
it 'extracts context with self field' do
58+
parent_context = OpenTelemetry::Context.empty
59+
carrier = { 'X-Amzn-Trace-Id' => 'Self=1-696f97a4-68ce1b2e0232080e03cce4f7;Root=1-80f198ea-e56343ba864fe8b2a57d3eff;Parent=e457b5a2e4d86bd1' }
60+
61+
context = propagator.extract(carrier, context: parent_context)
62+
extracted_context = OpenTelemetry::Trace.current_span(context).context
63+
64+
_(extracted_context.hex_trace_id).must_equal('80f198eae56343ba864fe8b2a57d3eff')
65+
_(extracted_context.hex_span_id).must_equal('e457b5a2e4d86bd1')
66+
_(extracted_context).must_be(:remote?)
67+
end
68+
5769
it 'converts debug flag to sampled' do
5870
parent_context = OpenTelemetry::Context.empty
5971
carrier = { 'X-Amzn-Trace-Id' => 'Root=1-80f198ea-e56343ba864fe8b2a57d3eff;Parent=e457b5a2e4d86bd1;Sampled=d' }

0 commit comments

Comments
 (0)