Move X-Ray Env Variable propagation to span link instead of parent (#7970)

https://github.com/open-telemetry/opentelemetry-specification/pull/3166

Per discussion in the FAAS SIG, we decided that the AWS X-Ray
environment variable should be moved to a span link to avoid interfering
with the configured propagators.

Also related to #5167.
This commit is contained in:
Tyler Benson 2023-03-16 01:10:09 -07:00 committed by GitHub
parent 4d6ef782b3
commit 083acddb02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 228 additions and 214 deletions

View File

@ -30,14 +30,7 @@ public abstract class ApiGatewayProxyRequest {
private static boolean noHttpPropagationNeeded() {
Collection<String> fields =
GlobalOpenTelemetry.getPropagators().getTextMapPropagator().fields();
return fields.isEmpty() || xrayPropagationFieldsOnly(fields);
}
private static boolean xrayPropagationFieldsOnly(Collection<String> fields) {
// ugly but faster than typical convert-to-set-and-check-contains-only
return (fields.size() == 1)
&& ParentContextExtractor.AWS_TRACE_HEADER_PROPAGATOR_KEY.equalsIgnoreCase(
fields.iterator().next());
return fields.isEmpty();
}
public static ApiGatewayProxyRequest forStream(InputStream source) {

View File

@ -11,6 +11,7 @@ import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.internal.ContextPropagationDebug;
import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;
@ -46,15 +47,25 @@ public class AwsLambdaFunctionInstrumenter {
}
public Context extract(AwsLambdaRequest input) {
return ParentContextExtractor.extract(input.getHeaders(), this);
}
public Context extract(Map<String, String> headers, TextMapGetter<Map<String, String>> getter) {
ContextPropagationDebug.debugContextLeakIfEnabled();
return openTelemetry
.getPropagators()
.getTextMapPropagator()
.extract(Context.root(), headers, getter);
.extract(Context.root(), input.getHeaders(), MapGetter.INSTANCE);
}
private enum MapGetter implements TextMapGetter<Map<String, String>> {
INSTANCE;
@Override
public Iterable<String> keys(Map<String, String> map) {
return map.keySet();
}
@Override
public String get(Map<String, String> map, String s) {
return map.get(s.toLowerCase(Locale.ROOT));
}
}
}

View File

@ -23,6 +23,7 @@ public final class AwsLambdaFunctionInstrumenterFactory {
openTelemetry,
"io.opentelemetry.aws-lambda-core-1.0",
AwsLambdaFunctionInstrumenterFactory::spanName)
.addSpanLinksExtractor(new AwsXrayEnvSpanLinksExtractor())
.addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor())
.buildInstrumenter(SpanKindExtractor.alwaysServer()));
}

View File

@ -0,0 +1,74 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator;
import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksBuilder;
import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksExtractor;
import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
final class AwsXrayEnvSpanLinksExtractor implements SpanLinksExtractor<AwsLambdaRequest> {
private static final String AWS_TRACE_HEADER_ENV_KEY = "_X_AMZN_TRACE_ID";
// lower-case map getter used for extraction
private static final String AWS_TRACE_HEADER_PROPAGATOR_KEY = "x-amzn-trace-id";
private static final Attributes LINK_ATTRIBUTES =
Attributes.of(AttributeKey.stringKey("source"), "x-ray-env");
@Override
public void extract(
SpanLinksBuilder spanLinks,
io.opentelemetry.context.Context parentContext,
AwsLambdaRequest awsLambdaRequest) {
extract(spanLinks);
}
public static void extract(SpanLinksBuilder spanLinks) {
String parentTraceHeader = System.getenv(AWS_TRACE_HEADER_ENV_KEY);
if (parentTraceHeader == null || parentTraceHeader.isEmpty()) {
return;
}
Context xrayContext =
AwsXrayPropagator.getInstance()
.extract(
Context.root(),
Collections.singletonMap(AWS_TRACE_HEADER_PROPAGATOR_KEY, parentTraceHeader),
MapGetter.INSTANCE);
SpanContext envVarSpanCtx = Span.fromContext(xrayContext).getSpanContext();
if (envVarSpanCtx.isValid()) {
spanLinks.addLink(envVarSpanCtx, LINK_ATTRIBUTES);
}
}
private enum MapGetter implements TextMapGetter<Map<String, String>> {
INSTANCE;
@Override
public Iterable<String> keys(Map<String, String> map) {
return map.keySet();
}
@Override
public String get(Map<String, String> map, String s) {
return map.get(s.toLowerCase(Locale.ROOT));
}
}
}

View File

@ -1,81 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal;
import static io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.MapUtils.lowercaseMap;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public final class ParentContextExtractor {
private static final String AWS_TRACE_HEADER_ENV_KEY = "_X_AMZN_TRACE_ID";
static Context extract(Map<String, String> headers, AwsLambdaFunctionInstrumenter instrumenter) {
Context parentContext = null;
String parentTraceHeader = System.getenv(AWS_TRACE_HEADER_ENV_KEY);
if (parentTraceHeader != null) {
parentContext = fromXrayHeader(parentTraceHeader);
}
if (!isValidAndSampled(parentContext)) {
// try http
parentContext = fromHttpHeaders(headers, instrumenter);
}
return parentContext;
}
private static boolean isValidAndSampled(Context context) {
if (context == null) {
return false;
}
Span parentSpan = Span.fromContext(context);
SpanContext parentSpanContext = parentSpan.getSpanContext();
return (parentSpanContext.isValid() && parentSpanContext.isSampled());
}
private static Context fromHttpHeaders(
Map<String, String> headers, AwsLambdaFunctionInstrumenter instrumenter) {
return instrumenter.extract(lowercaseMap(headers), MapGetter.INSTANCE);
}
// lower-case map getter used for extraction
static final String AWS_TRACE_HEADER_PROPAGATOR_KEY = "x-amzn-trace-id";
public static Context fromXrayHeader(String parentHeader) {
return AwsXrayPropagator.getInstance()
.extract(
// see BaseTracer#extract() on why we're using root() here
Context.root(),
Collections.singletonMap(AWS_TRACE_HEADER_PROPAGATOR_KEY, parentHeader),
MapGetter.INSTANCE);
}
private enum MapGetter implements TextMapGetter<Map<String, String>> {
INSTANCE;
@Override
public Iterable<String> keys(Map<String, String> map) {
return map.keySet();
}
@Override
public String get(Map<String, String> map, String s) {
return map.get(s.toLowerCase(Locale.ROOT));
}
}
private ParentContextExtractor() {}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksBuilder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;
import uk.org.webcompere.systemstubs.jupiter.SystemStub;
import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension;
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
@ExtendWith(SystemStubsExtension.class)
class AwsXrayEnvSpanLinksExtractorTest {
private static final Attributes EXPECTED_LINK_ATTRIBUTES =
Attributes.of(AttributeKey.stringKey("source"), "x-ray-env");
@SystemStub final EnvironmentVariables environmentVariables = new EnvironmentVariables();
@Test
void shouldIgnoreIfEnvVarEmpty() {
// given
SpanLinksBuilder spanLinksBuilder = mock(SpanLinksBuilder.class);
environmentVariables.set("_X_AMZN_TRACE_ID", "");
// when
AwsXrayEnvSpanLinksExtractor.extract(spanLinksBuilder);
// then
verifyNoInteractions(spanLinksBuilder);
}
@Test
void shouldLinkAwsParentHeaderIfValidAndNotSampled() {
// given
SpanLinksBuilder spanLinksBuilder = mock(SpanLinksBuilder.class);
environmentVariables.set(
"_X_AMZN_TRACE_ID",
"Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=0");
// when
AwsXrayEnvSpanLinksExtractor.extract(spanLinksBuilder);
// then
ArgumentCaptor<SpanContext> captor = ArgumentCaptor.forClass(SpanContext.class);
verify(spanLinksBuilder).addLink(captor.capture(), eq(EXPECTED_LINK_ATTRIBUTES));
SpanContext spanContext = captor.getValue();
assertThat(spanContext.isValid()).isTrue();
assertThat(spanContext.isSampled()).isFalse();
assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456");
assertThat(spanContext.getTraceId()).isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6");
}
@Test
void shouldLinkAwsParentHeaderIfValidAndSampled() {
// given
SpanLinksBuilder spanLinksBuilder = mock(SpanLinksBuilder.class);
environmentVariables.set(
"_X_AMZN_TRACE_ID",
"Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=1");
// when
AwsXrayEnvSpanLinksExtractor.extract(spanLinksBuilder);
// then
ArgumentCaptor<SpanContext> captor = ArgumentCaptor.forClass(SpanContext.class);
verify(spanLinksBuilder).addLink(captor.capture(), eq(EXPECTED_LINK_ATTRIBUTES));
SpanContext spanContext = captor.getValue();
assertThat(spanContext.isValid()).isTrue();
assertThat(spanContext.isSampled()).isTrue();
assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456");
assertThat(spanContext.getTraceId()).isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6");
}
}

View File

@ -1,113 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.common.collect.ImmutableMap;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.extension.trace.propagation.B3Propagator;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;
import uk.org.webcompere.systemstubs.jupiter.SystemStub;
import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension;
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
@ExtendWith(SystemStubsExtension.class)
class ParentContextExtractorTest {
@SystemStub final EnvironmentVariables environmentVariables = new EnvironmentVariables();
private static final OpenTelemetry OTEL =
OpenTelemetry.propagating(ContextPropagators.create(B3Propagator.injectingSingleHeader()));
private static final AwsLambdaFunctionInstrumenter INSTRUMENTER =
AwsLambdaFunctionInstrumenterFactory.createInstrumenter(OTEL);
@Test
void shouldUseHttpIfAwsParentNotSampled() {
// given
Map<String, String> headers =
ImmutableMap.of(
"X-b3-traceId",
"4fd0b6131f19f39af59518d127b0cafe",
"x-b3-spanid",
"0000000000000123",
"X-B3-Sampled",
"true");
environmentVariables.set(
"_X_AMZN_TRACE_ID",
"Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=0");
// when
Context context = ParentContextExtractor.extract(headers, INSTRUMENTER);
// then
Span span = Span.fromContext(context);
SpanContext spanContext = span.getSpanContext();
assertThat(spanContext.isValid()).isTrue();
assertThat(spanContext.isValid()).isTrue();
assertThat(spanContext.getSpanId()).isEqualTo("0000000000000123");
assertThat(spanContext.getTraceId()).isEqualTo("4fd0b6131f19f39af59518d127b0cafe");
}
@Test
void shouldPreferAwsParentHeaderIfValidAndSampled() {
// given
Map<String, String> headers =
ImmutableMap.of(
"X-b3-traceId",
"4fd0b6131f19f39af59518d127b0cafe",
"x-b3-spanid",
"0000000000000456",
"X-B3-Sampled",
"true");
environmentVariables.set(
"_X_AMZN_TRACE_ID",
"Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=1");
// when
Context context = ParentContextExtractor.extract(headers, INSTRUMENTER);
// then
Span span = Span.fromContext(context);
SpanContext spanContext = span.getSpanContext();
assertThat(spanContext.isValid()).isTrue();
assertThat(spanContext.isValid()).isTrue();
assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456");
assertThat(spanContext.getTraceId()).isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6");
}
@Test
void shouldExtractCaseInsensitiveHeaders() {
// given
Map<String, String> headers =
ImmutableMap.of(
"X-b3-traceId",
"4fd0b6131f19f39af59518d127b0cafe",
"x-b3-spanid",
"0000000000000456",
"X-B3-Sampled",
"true");
// when
Context context = ParentContextExtractor.extract(headers, INSTRUMENTER);
// then
Span span = Span.fromContext(context);
SpanContext spanContext = span.getSpanContext();
assertThat(spanContext.isValid()).isTrue();
assertThat(spanContext.isValid()).isTrue();
assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456");
assertThat(spanContext.getTraceId()).isEqualTo("4fd0b6131f19f39af59518d127b0cafe");
}
}

View File

@ -12,6 +12,8 @@ import static org.mockito.Mockito.when;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.sdk.trace.data.StatusData;
@ -108,8 +110,22 @@ public abstract class AbstractAwsLambdaTest {
span ->
span.hasName("my_function")
.hasKind(SpanKind.SERVER)
.hasTraceId("8a3c60f7d188f8fa79d48a391a778fa6")
.hasParentSpanId("0000000000000456")
.hasLinksSatisfying(
links ->
assertThat(links)
.singleElement()
.satisfies(
link -> {
assertThat(link.getSpanContext().getTraceId())
.isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6");
assertThat(link.getSpanContext().getSpanId())
.isEqualTo("0000000000000456");
assertThat(link.getAttributes())
.isEqualTo(
Attributes.of(
AttributeKey.stringKey("source"),
"x-ray-env"));
}))
.hasAttributesSatisfying(
attrs ->
assertThat(attrs)

View File

@ -9,22 +9,48 @@ import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator;
import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksBuilder;
import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksExtractor;
import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.ParentContextExtractor;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
class SqsMessageSpanLinksExtractor implements SpanLinksExtractor<SQSMessage> {
private static final String AWS_TRACE_HEADER_SQS_ATTRIBUTE_KEY = "AWSTraceHeader";
// lower-case map getter used for extraction
static final String AWS_TRACE_HEADER_PROPAGATOR_KEY = "x-amzn-trace-id";
@Override
public void extract(SpanLinksBuilder spanLinks, Context parentContext, SQSMessage message) {
String parentHeader = message.getAttributes().get(AWS_TRACE_HEADER_SQS_ATTRIBUTE_KEY);
if (parentHeader != null) {
SpanContext parentCtx =
Span.fromContext(ParentContextExtractor.fromXrayHeader(parentHeader)).getSpanContext();
if (parentCtx.isValid()) {
spanLinks.addLink(parentCtx);
Context xrayContext =
AwsXrayPropagator.getInstance()
.extract(
Context.root(), // We don't want the ambient context.
Collections.singletonMap(AWS_TRACE_HEADER_PROPAGATOR_KEY, parentHeader),
MapGetter.INSTANCE);
SpanContext messageSpanCtx = Span.fromContext(xrayContext).getSpanContext();
if (messageSpanCtx.isValid()) {
spanLinks.addLink(messageSpanCtx);
}
}
}
private enum MapGetter implements TextMapGetter<Map<String, String>> {
INSTANCE;
@Override
public Iterable<String> keys(Map<String, String> map) {
return map.keySet();
}
@Override
public String get(Map<String, String> map, String s) {
return map.get(s.toLowerCase(Locale.ROOT));
}
}
}