Skip to content

Commit 07d188e

Browse files
feat: contribute xray lambda propagator (#1464)
Co-authored-by: Kayla Reopelle <[email protected]>
1 parent fb84ce0 commit 07d188e

File tree

4 files changed

+234
-1
lines changed

4 files changed

+234
-1
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the documentation for the `opentelemetry-api` gem for details.
1313

1414
require_relative 'xray/text_map_propagator'
15+
require_relative 'xray/lambda_text_map_propagator'
1516

1617
module OpenTelemetry
1718
# Namespace for OpenTelemetry propagator extension libraries
@@ -22,7 +23,8 @@ module XRay
2223

2324
DEBUG_CONTEXT_KEY = Context.create_key('xray-debug-key')
2425
TEXT_MAP_PROPAGATOR = TextMapPropagator.new
25-
private_constant :DEBUG_CONTEXT_KEY, :TEXT_MAP_PROPAGATOR
26+
LAMBDA_TEXT_MAP_PROPAGATOR = LambdaTextMapPropagator.new
27+
private_constant :DEBUG_CONTEXT_KEY, :TEXT_MAP_PROPAGATOR, :LAMBDA_TEXT_MAP_PROPAGATOR
2628

2729
# @api private
2830
# Returns a new context with the xray debug flag enabled
@@ -41,6 +43,12 @@ def debug?(context)
4143
def text_map_propagator
4244
TEXT_MAP_PROPAGATOR
4345
end
46+
47+
# Returns a text map propagator that propagates context in the XRay
48+
# format with special handling for Lambda environment.
49+
def lambda_text_map_propagator
50+
LAMBDA_TEXT_MAP_PROPAGATOR
51+
end
4452
end
4553
end
4654
end
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
# OpenTelemetry is an open source observability framework, providing a
8+
# general-purpose API, SDK, and related tools required for the instrumentation
9+
# of cloud-native software, frameworks, and libraries.
10+
#
11+
# The OpenTelemetry module provides global accessors for telemetry objects.
12+
# See the documentation for the `opentelemetry-api` gem for details.
13+
module OpenTelemetry
14+
# Namespace for OpenTelemetry propagator extension libraries
15+
module Propagator
16+
# Namespace for OpenTelemetry XRay propagation
17+
module XRay
18+
# Implementation of AWS X-Ray Trace Header propagation with special handling for
19+
# Lambda's _X_AMZN_TRACE_ID environment variable
20+
class LambdaTextMapPropagator < TextMapPropagator
21+
AWS_TRACE_HEADER_ENV_KEY = '_X_AMZN_TRACE_ID'
22+
23+
# Extract trace context from the supplied carrier or from Lambda environment variable
24+
# If extraction fails, the original context will be returned
25+
#
26+
# @param [Carrier] carrier The carrier to get the header from
27+
# @param [optional Context] context Context to be updated with the trace context
28+
# extracted from the carrier. Defaults to +Context.current+.
29+
# @param [optional Getter] getter If the optional getter is provided, it
30+
# will be used to read the header from the carrier, otherwise the default
31+
# text map getter will be used.
32+
#
33+
# @return [Context] context updated with extracted baggage, or the original context
34+
# if extraction fails
35+
def extract(carrier, context: Context.current, getter: Context::Propagation.text_map_getter)
36+
# Check if the original input context already has a valid span
37+
span_context = Trace.current_span(context).context
38+
# If original context is valid, just return it - do not extract from carrier
39+
return context if span_context.valid?
40+
41+
# First try to extract from the carrier using the standard X-Ray propagator
42+
xray_context = super
43+
44+
# Check if we successfully extracted a context from the carrier
45+
span_context = Trace.current_span(xray_context).context
46+
return xray_context if span_context.valid?
47+
48+
# If not, check for the Lambda environment variable
49+
trace_header = ENV.fetch(AWS_TRACE_HEADER_ENV_KEY, nil)
50+
return xray_context unless trace_header
51+
52+
# Create a carrier with the trace header and extract from it
53+
env_carrier = { XRAY_CONTEXT_KEY => trace_header }
54+
super(env_carrier, context: xray_context, getter: getter)
55+
rescue OpenTelemetry::Error
56+
context
57+
end
58+
end
59+
end
60+
end
61+
end
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
require 'test_helper'
8+
9+
describe OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator do
10+
Span = OpenTelemetry::Trace::Span
11+
SpanContext = OpenTelemetry::Trace::SpanContext
12+
TraceFlags = OpenTelemetry::Trace::TraceFlags
13+
14+
let(:propagator) { OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator.new }
15+
16+
describe('#extract') do
17+
it 'returns the original context when no headers or env vars exist' do
18+
parent_context = OpenTelemetry::Context.empty
19+
carrier = {}
20+
21+
# Ensure environment variable is not set
22+
original_env = ENV.fetch(OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY, nil)
23+
ENV.delete(OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY)
24+
25+
context = propagator.extract(carrier, context: parent_context)
26+
extracted_context = OpenTelemetry::Trace.current_span(context).context
27+
28+
_(extracted_context.valid?).must_equal(false)
29+
_(context).must_equal(parent_context)
30+
31+
# Restore original env value if it existed
32+
ENV[OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY] = original_env if original_env
33+
end
34+
35+
it 'returns existing context when valid and no env var exists' do
36+
# Create a valid context
37+
valid_context = create_context(
38+
trace_id: '80f198eae56343ba864fe8b2a57d3eff',
39+
span_id: 'e457b5a2e4d86bd1',
40+
trace_flags: TraceFlags::SAMPLED
41+
)
42+
43+
carrier = {}
44+
45+
# Ensure environment variable is not set
46+
original_env = ENV.fetch(OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY, nil)
47+
ENV.delete(OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY)
48+
49+
context = propagator.extract(carrier, context: valid_context)
50+
extracted_context = OpenTelemetry::Trace.current_span(context).context
51+
52+
_(extracted_context.hex_trace_id).must_equal('80f198eae56343ba864fe8b2a57d3eff')
53+
_(extracted_context.hex_span_id).must_equal('e457b5a2e4d86bd1')
54+
_(extracted_context.trace_flags).must_equal(TraceFlags::SAMPLED)
55+
_(context).must_equal(valid_context)
56+
57+
# Restore original env value if it existed
58+
ENV[OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY] = original_env if original_env
59+
end
60+
61+
it 'extracts context from environment variable when no headers exist' do
62+
parent_context = OpenTelemetry::Context.empty
63+
carrier = {}
64+
65+
# Set environment variable with trace information
66+
original_env = ENV.fetch(OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY, nil)
67+
ENV[OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY] = 'Root=1-80f198ea-e56343ba864fe8b2a57d3eff;Parent=e457b5a2e4d86bd1;Sampled=1'
68+
69+
context = propagator.extract(carrier, context: parent_context)
70+
extracted_context = OpenTelemetry::Trace.current_span(context).context
71+
72+
_(extracted_context.hex_trace_id).must_equal('80f198eae56343ba864fe8b2a57d3eff')
73+
_(extracted_context.hex_span_id).must_equal('e457b5a2e4d86bd1')
74+
_(extracted_context.trace_flags).must_equal(TraceFlags::SAMPLED)
75+
_(extracted_context).must_be(:remote?)
76+
77+
# Restore original env value if it existed
78+
ENV[OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY] = original_env if original_env
79+
end
80+
81+
it 'prioritizes header over environment variable' do
82+
parent_context = OpenTelemetry::Context.empty
83+
carrier = { 'X-Amzn-Trace-Id' => 'Root=1-90f198ea-f56343ba964fe8b2a67d3eff;Parent=f457b5a2e4d86bd2;Sampled=1' }
84+
85+
# Set environment variable with different trace information
86+
original_env = ENV.fetch(OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY, nil)
87+
ENV[OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY] = 'Root=1-80f198ea-e56343ba864fe8b2a57d3eff;Parent=e457b5a2e4d86bd1;Sampled=1'
88+
89+
context = propagator.extract(carrier, context: parent_context)
90+
extracted_context = OpenTelemetry::Trace.current_span(context).context
91+
92+
# Should use the header, not the environment variable
93+
_(extracted_context.hex_trace_id).must_equal('90f198eaf56343ba964fe8b2a67d3eff')
94+
_(extracted_context.hex_span_id).must_equal('f457b5a2e4d86bd2')
95+
_(extracted_context.trace_flags).must_equal(TraceFlags::SAMPLED)
96+
_(extracted_context).must_be(:remote?)
97+
98+
# Restore original env value if it existed
99+
ENV[OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY] = original_env if original_env
100+
end
101+
102+
it 'handles malformed environment variable gracefully' do
103+
parent_context = OpenTelemetry::Context.empty
104+
carrier = {}
105+
106+
# Set environment variable with malformed trace information
107+
original_env = ENV.fetch(OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY, nil)
108+
ENV[OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY] = 'NotAValidTraceHeader'
109+
110+
context = propagator.extract(carrier, context: parent_context)
111+
112+
# Should return the original context since the env var is invalid
113+
_(context).must_equal(parent_context)
114+
115+
# Restore original env value if it existed
116+
ENV[OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator::AWS_TRACE_HEADER_ENV_KEY] = original_env if original_env
117+
end
118+
119+
it 'uses existing context when valid, even if headers exist' do
120+
# Create a valid context
121+
valid_context = create_context(
122+
trace_id: '80f198eae56343ba864fe8b2a57d3eff',
123+
span_id: 'e457b5a2e4d86bd1',
124+
trace_flags: TraceFlags::SAMPLED
125+
)
126+
127+
# Create a carrier with header
128+
carrier = { 'X-Amzn-Trace-Id' => 'Root=1-90f198ea-f56343ba964fe8b2a67d3eff;Parent=f457b5a2e4d86bd2;Sampled=1' }
129+
130+
context = propagator.extract(carrier, context: valid_context)
131+
extracted_context = OpenTelemetry::Trace.current_span(context).context
132+
133+
# Should use the existing context, not the header
134+
_(extracted_context.hex_trace_id).must_equal('80f198eae56343ba864fe8b2a57d3eff')
135+
_(extracted_context.hex_span_id).must_equal('e457b5a2e4d86bd1')
136+
_(extracted_context.trace_flags).must_equal(TraceFlags::SAMPLED)
137+
_(context).must_equal(valid_context)
138+
end
139+
end
140+
141+
# Helper method to create a context with specified values
142+
def create_context(trace_id:, span_id:, trace_flags: TraceFlags::DEFAULT, xray_debug: false)
143+
span_context = SpanContext.new(
144+
trace_id: Array(trace_id).pack('H*'),
145+
span_id: Array(span_id).pack('H*'),
146+
trace_flags: trace_flags,
147+
remote: false
148+
)
149+
150+
span = Span.new(
151+
span_context: span_context
152+
)
153+
154+
context = OpenTelemetry::Trace.context_with_span(span)
155+
return context unless xray_debug
156+
157+
OpenTelemetry::Propagator::XRay.send(:context_with_debug, context)
158+
end
159+
end

propagator/xray/test/xray_test.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,10 @@
1414
OpenTelemetry::Propagator::XRay::TextMapPropagator
1515
)
1616
end
17+
18+
it 'provides access to lambda text map propagator' do
19+
propagator = OpenTelemetry::Propagator::XRay.lambda_text_map_propagator
20+
_(propagator).must_be_instance_of(OpenTelemetry::Propagator::XRay::LambdaTextMapPropagator)
21+
end
1722
end
1823
end

0 commit comments

Comments
 (0)