From 360a4adfc8ac3a1a3a186735fd88c2ea688f2af3 Mon Sep 17 00:00:00 2001 From: Priyanka Dhingra Date: Fri, 31 Jul 2026 23:24:53 +0000 Subject: [PATCH] fix(java-wrapper): link spans to invocation X-Ray trace via traceHeader system property On managed Java 17+ Lambda runtimes the per-invocation X-Ray trace context is exposed via the com.amazonaws.xray.traceHeader system property rather than the _X_AMZN_TRACE_ID environment variable the OTel Lambda wrapper reads. As a result the SDK wrapper's spans were assigned a fresh X-Ray-compatible trace id (from AwsOtelTracerProviderConfigurer) that was disconnected from the invocation's X-Ray trace, so downstream segments (e.g. S3) never appeared in the invocation trace and soak trace validation failed. Add an AutoConfigurationCustomizerProvider that wraps the configured propagators: event-carrier extraction still takes precedence, but when it yields no valid parent the wrapper falls back to the traceHeader system property and presents it to the already-configured X-Ray propagator as a synthetic X-Amzn-Trace-Id. The property is read per invocation and no new runtime dependency is introduced. Ships only in the wrapper extension jar, so the javaagent layer is unaffected. --- java/build.gradle.kts | 26 ++- ...xtAutoConfigurationCustomizerProvider.java | 118 ++++++++++++ ...re.spi.AutoConfigurationCustomizerProvider | 1 + ...toConfigurationCustomizerProviderTest.java | 175 ++++++++++++++++++ 4 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 java/src/main/java/software/amazon/opentelemetry/lambda/AwsLambdaXrayContextAutoConfigurationCustomizerProvider.java create mode 100644 java/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider create mode 100644 java/src/test/java/software/amazon/opentelemetry/lambda/AwsLambdaXrayContextAutoConfigurationCustomizerProviderTest.java diff --git a/java/build.gradle.kts b/java/build.gradle.kts index 92d7729e6..5eaceda59 100644 --- a/java/build.gradle.kts +++ b/java/build.gradle.kts @@ -7,6 +7,10 @@ plugins { base.archivesBaseName = "aws-otel-lambda-java-extensions" group = "software.amazon.opentelemetry.lambda" +val openTelemetryVersion = "1.32.0" +val openTelemetryAlphaVersion = "1.32.0-alpha" +val junitVersion = "5.10.1" + repositories { mavenCentral() mavenLocal() @@ -28,12 +32,26 @@ val javaagentDependency by configurations.creating { } dependencies { - compileOnly(platform("io.opentelemetry:opentelemetry-bom:1.32.0")) - compileOnly(platform("io.opentelemetry:opentelemetry-bom-alpha:1.32.0-alpha")) + compileOnly(platform("io.opentelemetry:opentelemetry-bom:$openTelemetryVersion")) + compileOnly(platform("io.opentelemetry:opentelemetry-bom-alpha:$openTelemetryAlphaVersion")) + // opentelemetry-api and opentelemetry-context are already provided by the wrapper layer at + // runtime, so they are compileOnly here. + compileOnly("io.opentelemetry:opentelemetry-api") + compileOnly("io.opentelemetry:opentelemetry-context") // Already included in wrapper so compileOnly compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") compileOnly("io.opentelemetry:opentelemetry-sdk-extension-aws") javaagentDependency("software.amazon.opentelemetry:aws-opentelemetry-agent:1.32.0-adot-lambda1") + + testImplementation(platform("io.opentelemetry:opentelemetry-bom:$openTelemetryVersion")) + testImplementation("io.opentelemetry:opentelemetry-api") + testImplementation("io.opentelemetry:opentelemetry-context") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + // Supplies the X-Ray propagator the wrapper layer configures at runtime; needed here only to + // exercise the fallback path in tests. + testImplementation( + "io.opentelemetry.contrib:opentelemetry-aws-xray-propagator:$openTelemetryAlphaVersion") + testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") } tasks.register("download") { @@ -44,3 +62,7 @@ tasks.register("download") { tasks.named("build") { dependsOn("download") } + +tasks.test { + useJUnitPlatform() +} diff --git a/java/src/main/java/software/amazon/opentelemetry/lambda/AwsLambdaXrayContextAutoConfigurationCustomizerProvider.java b/java/src/main/java/software/amazon/opentelemetry/lambda/AwsLambdaXrayContextAutoConfigurationCustomizerProvider.java new file mode 100644 index 000000000..dee78929d --- /dev/null +++ b/java/src/main/java/software/amazon/opentelemetry/lambda/AwsLambdaXrayContextAutoConfigurationCustomizerProvider.java @@ -0,0 +1,118 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.lambda; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import java.util.Collection; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; + +/** + * Links spans created by the OTel Lambda SDK "wrapper" layer to the X-Ray trace of the enclosing + * Lambda invocation. + * + *

On the managed Java 17+ Lambda runtimes the per-invocation X-Ray trace header is exposed via + * the {@code com.amazonaws.xray.traceHeader} system property rather than the {@code + * _X_AMZN_TRACE_ID} environment variable that older OTel Lambda instrumentation reads. Without this + * customizer the wrapper root span gets a fresh (X-Ray-compatible) trace id from {@link + * software.amazon.opentelemetry.lambda.AwsOtelTracerProviderConfigurer}, so downstream spans land + * in a trace that is disconnected from the invocation's X-Ray trace. + * + *

This customizer wraps every configured propagator. During {@code extract} it first runs the + * normal propagation from the event carrier (for example API Gateway headers). If that yields a + * valid remote parent it is preserved. Otherwise it reads the X-Ray trace-header system property + * and presents it to the same configured propagators as a synthetic {@code X-Amzn-Trace-Id} header, + * delegating the actual parsing to the already-configured X-Ray propagator. This adds no new + * runtime dependency: the wrapper layer supplies and (by default) configures the X-Ray propagator + * via {@code OTEL_PROPAGATORS}. + * + *

This extension jar is only packaged into the wrapper layer, so the javaagent layer (which + * wires X-Ray propagation through bytecode instrumentation) is unaffected. + */ +public final class AwsLambdaXrayContextAutoConfigurationCustomizerProvider + implements AutoConfigurationCustomizerProvider { + + static final String TRACE_HEADER_PROPERTY = "com.amazonaws.xray.traceHeader"; + private static final String TRACE_HEADER_KEY = "x-amzn-trace-id"; + + @Override + public void customize(AutoConfigurationCustomizer autoConfiguration) { + autoConfiguration.addPropagatorCustomizer( + (propagator, config) -> new LambdaXraySystemPropertyPropagator(propagator)); + } + + static final class LambdaXraySystemPropertyPropagator implements TextMapPropagator { + private static final TextMapGetter> MAP_GETTER = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + if (carrier == null) { + return null; + } + return carrier.get(key.toLowerCase(Locale.ROOT)); + } + }; + + private final TextMapPropagator delegate; + + LambdaXraySystemPropertyPropagator(TextMapPropagator delegate) { + this.delegate = delegate; + } + + @Override + public Collection fields() { + return delegate.fields(); + } + + @Override + public void inject(Context context, C carrier, TextMapSetter setter) { + delegate.inject(context, carrier, setter); + } + + @Override + public Context extract(Context context, C carrier, TextMapGetter getter) { + Context base = context == null ? Context.root() : context; + Context extracted = delegate.extract(base, carrier, getter); + + // Preserve an already-valid parent extracted from the event carrier (e.g. API Gateway + // headers) so explicit upstream propagation always wins. + if (Span.fromContext(extracted).getSpanContext().isValid()) { + return extracted; + } + + // Java 17+ managed Lambda runtimes place the per-invocation X-Ray header here. + String traceHeader = System.getProperty(TRACE_HEADER_PROPERTY); + if (traceHeader == null || traceHeader.trim().isEmpty()) { + return extracted; + } + + return delegate.extract( + extracted, Collections.singletonMap(TRACE_HEADER_KEY, traceHeader), MAP_GETTER); + } + } +} diff --git a/java/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider b/java/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider new file mode 100644 index 000000000..12235320d --- /dev/null +++ b/java/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider @@ -0,0 +1 @@ +software.amazon.opentelemetry.lambda.AwsLambdaXrayContextAutoConfigurationCustomizerProvider diff --git a/java/src/test/java/software/amazon/opentelemetry/lambda/AwsLambdaXrayContextAutoConfigurationCustomizerProviderTest.java b/java/src/test/java/software/amazon/opentelemetry/lambda/AwsLambdaXrayContextAutoConfigurationCustomizerProviderTest.java new file mode 100644 index 000000000..acdc91cbf --- /dev/null +++ b/java/src/test/java/software/amazon/opentelemetry/lambda/AwsLambdaXrayContextAutoConfigurationCustomizerProviderTest.java @@ -0,0 +1,175 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.lambda; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import software.amazon.opentelemetry.lambda.AwsLambdaXrayContextAutoConfigurationCustomizerProvider.LambdaXraySystemPropertyPropagator; + +class AwsLambdaXrayContextAutoConfigurationCustomizerProviderTest { + + private static final String XRAY_HEADER_KEY = "X-Amzn-Trace-Id"; + + // Trace context that the Lambda runtime exposes via the system property. + private static final String PROPERTY_HEADER = + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1"; + private static final String PROPERTY_TRACE_ID = "8a3c60f7d188f8fa79d48a391a778fa6"; + private static final String PROPERTY_SPAN_ID = "53995c3f42cd8ad8"; + + // A different, valid trace context that arrives on the event carrier (e.g. API Gateway headers). + private static final String EVENT_HEADER = + "Root=1-11111111-222222222222222222222222;Parent=3333333333333333;Sampled=1"; + private static final String EVENT_TRACE_ID = "11111111222222222222222222222222"; + + // A second distinct system-property value, used to prove the property is re-read per extraction. + private static final String SECOND_PROPERTY_HEADER = + "Root=1-44444444-555555555555555555555555;Parent=6666666666666666;Sampled=1"; + private static final String SECOND_PROPERTY_TRACE_ID = "44444444555555555555555555555555"; + + private static final TextMapGetter> EXACT_KEY_GETTER = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier == null ? null : carrier.get(key); + } + }; + + @AfterEach + void clearTraceHeader() { + System.clearProperty( + AwsLambdaXrayContextAutoConfigurationCustomizerProvider.TRACE_HEADER_PROPERTY); + } + + private static TextMapPropagator xrayFallbackPropagator() { + return new LambdaXraySystemPropertyPropagator(AwsXrayPropagator.getInstance()); + } + + private static SpanContext extract(TextMapPropagator propagator, Map carrier) { + Context result = propagator.extract(Context.root(), carrier, EXACT_KEY_GETTER); + return Span.fromContext(result).getSpanContext(); + } + + @Test + void emptyHeadersWithValidProperty_extractsPropertyContext() { + System.setProperty( + AwsLambdaXrayContextAutoConfigurationCustomizerProvider.TRACE_HEADER_PROPERTY, + PROPERTY_HEADER); + + SpanContext spanContext = extract(xrayFallbackPropagator(), Collections.emptyMap()); + + assertTrue(spanContext.isValid(), "expected a valid parent from the system property"); + assertEquals(PROPERTY_TRACE_ID, spanContext.getTraceId()); + assertEquals(PROPERTY_SPAN_ID, spanContext.getSpanId()); + assertTrue(spanContext.isSampled()); + assertTrue(spanContext.isRemote()); + } + + @Test + void validEventHeaderWithDifferentProperty_eventHeaderWins() { + System.setProperty( + AwsLambdaXrayContextAutoConfigurationCustomizerProvider.TRACE_HEADER_PROPERTY, + PROPERTY_HEADER); + + Map carrier = Collections.singletonMap(XRAY_HEADER_KEY, EVENT_HEADER); + SpanContext spanContext = extract(xrayFallbackPropagator(), carrier); + + assertTrue(spanContext.isValid()); + assertEquals(EVENT_TRACE_ID, spanContext.getTraceId(), "event header must take precedence"); + } + + @Test + void missingProperty_behaviorUnchanged() { + System.clearProperty( + AwsLambdaXrayContextAutoConfigurationCustomizerProvider.TRACE_HEADER_PROPERTY); + + SpanContext spanContext = extract(xrayFallbackPropagator(), Collections.emptyMap()); + + assertFalse(spanContext.isValid(), "no header and no property must yield no parent"); + } + + @Test + void malformedProperty_noExceptionAndNoParent() { + System.setProperty( + AwsLambdaXrayContextAutoConfigurationCustomizerProvider.TRACE_HEADER_PROPERTY, + "this-is-not-a-valid-xray-trace-header"); + + SpanContext spanContext = extract(xrayFallbackPropagator(), Collections.emptyMap()); + + assertFalse(spanContext.isValid(), "a malformed property must not produce a valid parent"); + } + + @Test + void propertyChangesBetweenExtractions_eachExtractionReadsCurrentValue() { + TextMapPropagator propagator = xrayFallbackPropagator(); + + System.setProperty( + AwsLambdaXrayContextAutoConfigurationCustomizerProvider.TRACE_HEADER_PROPERTY, + PROPERTY_HEADER); + SpanContext first = extract(propagator, Collections.emptyMap()); + + System.setProperty( + AwsLambdaXrayContextAutoConfigurationCustomizerProvider.TRACE_HEADER_PROPERTY, + SECOND_PROPERTY_HEADER); + SpanContext second = extract(propagator, Collections.emptyMap()); + + assertEquals(PROPERTY_TRACE_ID, first.getTraceId()); + assertEquals(SECOND_PROPERTY_TRACE_ID, second.getTraceId()); + } + + @Test + void blankProperty_treatedAsAbsent() { + System.setProperty( + AwsLambdaXrayContextAutoConfigurationCustomizerProvider.TRACE_HEADER_PROPERTY, " "); + + SpanContext spanContext = extract(xrayFallbackPropagator(), Collections.emptyMap()); + + assertFalse(spanContext.isValid(), "a whitespace-only property must be treated as absent"); + } + + @Test + void delegateWithoutXrayPropagator_propertyIgnored() { + System.setProperty( + AwsLambdaXrayContextAutoConfigurationCustomizerProvider.TRACE_HEADER_PROPERTY, + PROPERTY_HEADER); + + // A delegate that does not understand X-Amzn-Trace-Id must leave the property unused, honoring + // an explicit OTEL_PROPAGATORS configuration that excludes xray. + TextMapPropagator propagator = + new LambdaXraySystemPropertyPropagator(W3CTraceContextPropagator.getInstance()); + + SpanContext spanContext = extract(propagator, Collections.emptyMap()); + + assertFalse(spanContext.isValid(), "non-xray delegate must ignore the X-Ray property"); + } +}