diff --git a/.github/component_owners.yml b/.github/component_owners.yml index ad0c038c5..a58e86088 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -14,6 +14,8 @@ components: - willarmiros aws-xray: - willarmiros + aws-xray-propagator: + - willarmiros consistent-sampling: - oertl - PeterF778 diff --git a/README.md b/README.md index 1997cfd56..99fc777c0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ feature or via instrumentation, this project is hopefully for you. ## Provided Libraries -* [AWS X-Ray Support](./aws-xray/README.md) +* [AWS Resources](./aws-resources/README.md) +* [AWS X-Ray SDK Support](./aws-xray/README.md) +* [AWS X-Ray Propagator](./aws-xray-propagator/README.md) * [Consistent sampling](./consistent-sampling/README.md) * [JFR Streaming](./jfr-streaming/README.md) * [JMX Metric Gatherer](./jmx-metrics/README.md) diff --git a/aws-xray-propagator/README.md b/aws-xray-propagator/README.md new file mode 100644 index 000000000..ee8b156f7 --- /dev/null +++ b/aws-xray-propagator/README.md @@ -0,0 +1,10 @@ +# OpenTelemetry AWS X-Ray Propagator + +This module contains a `TextMapPropagator` implementation compatible with +the [AWS X-Ray Trace Header propagation protocol](https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader). + +## Component owners + +- [William Armiros](https://github.com/willarmiros), AWS + +Learn more about component owners in [component_owners.yml](../.github/component_owners.yml). diff --git a/aws-xray-propagator/build.gradle.kts b/aws-xray-propagator/build.gradle.kts new file mode 100644 index 000000000..435b5509a --- /dev/null +++ b/aws-xray-propagator/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("otel.java-conventions") + + id("otel.publish-conventions") +} + +description = "OpenTelemetry AWS X-Ray Propagator" + +dependencies { + api("io.opentelemetry:opentelemetry-api") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") +} diff --git a/aws-xray-propagator/gradle.properties b/aws-xray-propagator/gradle.properties new file mode 100644 index 000000000..a0402e1e2 --- /dev/null +++ b/aws-xray-propagator/gradle.properties @@ -0,0 +1,2 @@ +# TODO: uncomment when ready to mark as stable +# otel.stable=true diff --git a/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsConfigurablePropagator.java b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsConfigurablePropagator.java new file mode 100644 index 000000000..7027f464e --- /dev/null +++ b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsConfigurablePropagator.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.awsxray.propagator; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; + +/** + * A {@link ConfigurablePropagatorProvider} which allows enabling the {@link AwsXrayPropagator} with + * the propagator name {@code xray}. + */ +public final class AwsConfigurablePropagator implements ConfigurablePropagatorProvider { + @Override + public TextMapPropagator getPropagator(ConfigProperties config) { + return AwsXrayPropagator.getInstance(); + } + + @Override + public String getName() { + return "xray"; + } +} diff --git a/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayPropagator.java b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayPropagator.java new file mode 100644 index 000000000..2ce17875b --- /dev/null +++ b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayPropagator.java @@ -0,0 +1,329 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.awsxray.propagator; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageBuilder; +import io.opentelemetry.api.baggage.BaggageEntry; +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * Implementation of the AWS X-Ray Trace Header propagation protocol. See AWS + * Tracing header spec + * + *

To register the X-Ray propagator together with default propagator when using the SDK: + * + *

{@code
+ * OpenTelemetrySdk.builder()
+ *   .setPropagators(
+ *     ContextPropagators.create(
+ *         TextMapPropagator.composite(
+ *             W3CTraceContextPropagator.getInstance(),
+ *             AWSXrayPropagator.getInstance())))
+ *    .build();
+ * }
+ */ +public final class AwsXrayPropagator implements TextMapPropagator { + + // Visible for testing + static final String TRACE_HEADER_KEY = "X-Amzn-Trace-Id"; + + private static final Logger logger = Logger.getLogger(AwsXrayPropagator.class.getName()); + + private static final char TRACE_HEADER_DELIMITER = ';'; + private static final char KV_DELIMITER = '='; + + private static final String TRACE_ID_KEY = "Root"; + private static final int TRACE_ID_LENGTH = 35; + private static final String TRACE_ID_VERSION = "1"; + private static final char TRACE_ID_DELIMITER = '-'; + private static final int TRACE_ID_DELIMITER_INDEX_1 = 1; + private static final int TRACE_ID_DELIMITER_INDEX_2 = 10; + private static final int TRACE_ID_FIRST_PART_LENGTH = 8; + + private static final String PARENT_ID_KEY = "Parent"; + private static final int PARENT_ID_LENGTH = 16; + + private static final String SAMPLED_FLAG_KEY = "Sampled"; + private static final int SAMPLED_FLAG_LENGTH = 1; + private static final char IS_SAMPLED = '1'; + private static final char NOT_SAMPLED = '0'; + + private static final List FIELDS = Collections.singletonList(TRACE_HEADER_KEY); + + private static final AwsXrayPropagator INSTANCE = new AwsXrayPropagator(); + + private AwsXrayPropagator() { + // singleton + } + + public static AwsXrayPropagator getInstance() { + return INSTANCE; + } + + @Override + public List fields() { + return FIELDS; + } + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) { + if (context == null) { + return; + } + if (setter == null) { + return; + } + + Span span = Span.fromContext(context); + if (!span.getSpanContext().isValid()) { + return; + } + + SpanContext spanContext = span.getSpanContext(); + + String otTraceId = spanContext.getTraceId(); + String xrayTraceId = + TRACE_ID_VERSION + + TRACE_ID_DELIMITER + + otTraceId.substring(0, TRACE_ID_FIRST_PART_LENGTH) + + TRACE_ID_DELIMITER + + otTraceId.substring(TRACE_ID_FIRST_PART_LENGTH); + String parentId = spanContext.getSpanId(); + char samplingFlag = spanContext.isSampled() ? IS_SAMPLED : NOT_SAMPLED; + // TODO: Add OT trace state to the X-Ray trace header + + StringBuilder traceHeader = new StringBuilder(); + traceHeader + .append(TRACE_ID_KEY) + .append(KV_DELIMITER) + .append(xrayTraceId) + .append(TRACE_HEADER_DELIMITER) + .append(PARENT_ID_KEY) + .append(KV_DELIMITER) + .append(parentId) + .append(TRACE_HEADER_DELIMITER) + .append(SAMPLED_FLAG_KEY) + .append(KV_DELIMITER) + .append(samplingFlag); + + Baggage baggage = Baggage.fromContext(context); + // Truncate baggage to 256 chars per X-Ray spec. + baggage.forEach( + new BiConsumer() { + + private int baggageWrittenBytes; + + @Override + public void accept(String key, BaggageEntry entry) { + if (key.equals(TRACE_ID_KEY) + || key.equals(PARENT_ID_KEY) + || key.equals(SAMPLED_FLAG_KEY)) { + return; + } + // Size is key/value pair, excludes delimiter. + int size = key.length() + entry.getValue().length() + 1; + if (baggageWrittenBytes + size > 256) { + return; + } + traceHeader + .append(TRACE_HEADER_DELIMITER) + .append(key) + .append(KV_DELIMITER) + .append(entry.getValue()); + baggageWrittenBytes += size; + } + }); + + setter.set(carrier, TRACE_HEADER_KEY, traceHeader.toString()); + } + + @Override + public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) { + if (context == null) { + return Context.root(); + } + if (getter == null) { + return context; + } + + return getContextFromHeader(context, carrier, getter); + } + + private static Context getContextFromHeader( + Context context, @Nullable C carrier, TextMapGetter getter) { + String traceHeader = getter.get(carrier, TRACE_HEADER_KEY); + if (traceHeader == null || traceHeader.isEmpty()) { + return context; + } + + String traceId = TraceId.getInvalid(); + String spanId = SpanId.getInvalid(); + Boolean isSampled = false; + + BaggageBuilder baggage = null; + int baggageReadBytes = 0; + + int pos = 0; + while (pos < traceHeader.length()) { + int delimiterIndex = traceHeader.indexOf(TRACE_HEADER_DELIMITER, pos); + String part; + if (delimiterIndex >= 0) { + part = traceHeader.substring(pos, delimiterIndex); + pos = delimiterIndex + 1; + } else { + // Last part. + part = traceHeader.substring(pos); + pos = traceHeader.length(); + } + String trimmedPart = part.trim(); + int equalsIndex = trimmedPart.indexOf(KV_DELIMITER); + if (equalsIndex < 0) { + logger.fine("Error parsing X-Ray trace header. Invalid key value pair: " + part); + return context; + } + + String value = trimmedPart.substring(equalsIndex + 1); + + if (trimmedPart.startsWith(TRACE_ID_KEY)) { + traceId = parseTraceId(value); + } else if (trimmedPart.startsWith(PARENT_ID_KEY)) { + spanId = parseSpanId(value); + } else if (trimmedPart.startsWith(SAMPLED_FLAG_KEY)) { + isSampled = parseTraceFlag(value); + } else if (baggageReadBytes + trimmedPart.length() <= 256) { + if (baggage == null) { + baggage = Baggage.builder(); + } + baggage.put(trimmedPart.substring(0, equalsIndex), value); + baggageReadBytes += trimmedPart.length(); + } + } + if (isSampled == null) { + logger.fine( + "Invalid Sampling flag in X-Ray trace header: '" + + TRACE_HEADER_KEY + + "' with value " + + traceHeader + + "'."); + return context; + } + + if (spanId == null || traceId == null) { + logger.finest("Both traceId and spanId are required to extract a valid span context. "); + } + + SpanContext spanContext = + SpanContext.createFromRemoteParent( + StringUtils.padLeft(traceId, TraceId.getLength()), + spanId, + isSampled ? TraceFlags.getSampled() : TraceFlags.getDefault(), + TraceState.getDefault()); + if (spanContext.isValid()) { + context = context.with(Span.wrap(spanContext)); + } + if (baggage != null) { + context = context.with(baggage.build()); + } + return context; + } + + private static String parseTraceId(String xrayTraceId) { + return (xrayTraceId.length() == TRACE_ID_LENGTH + ? parseSpecTraceId(xrayTraceId) + : parseShortTraceId(xrayTraceId)); + } + + private static String parseSpecTraceId(String xrayTraceId) { + + // Check version trace id version + if (!xrayTraceId.startsWith(TRACE_ID_VERSION)) { + return TraceId.getInvalid(); + } + + // Check delimiters + if (xrayTraceId.charAt(TRACE_ID_DELIMITER_INDEX_1) != TRACE_ID_DELIMITER + || xrayTraceId.charAt(TRACE_ID_DELIMITER_INDEX_2) != TRACE_ID_DELIMITER) { + return TraceId.getInvalid(); + } + + String epochPart = + xrayTraceId.substring(TRACE_ID_DELIMITER_INDEX_1 + 1, TRACE_ID_DELIMITER_INDEX_2); + String uniquePart = xrayTraceId.substring(TRACE_ID_DELIMITER_INDEX_2 + 1, TRACE_ID_LENGTH); + + // X-Ray trace id format is 1-{8 digit hex}-{24 digit hex} + return epochPart + uniquePart; + } + + private static String parseShortTraceId(String xrayTraceId) { + if (xrayTraceId.length() > TRACE_ID_LENGTH) { + return TraceId.getInvalid(); + } + + // Check version trace id version + if (!xrayTraceId.startsWith(TRACE_ID_VERSION)) { + return TraceId.getInvalid(); + } + + // Check delimiters + int firstDelimiter = xrayTraceId.indexOf(TRACE_ID_DELIMITER); + // we don't allow the epoch part to be missing completely + int secondDelimiter = xrayTraceId.indexOf(TRACE_ID_DELIMITER, firstDelimiter + 2); + if (firstDelimiter != TRACE_ID_DELIMITER_INDEX_1 + || secondDelimiter == -1 + || secondDelimiter > TRACE_ID_DELIMITER_INDEX_2) { + return TraceId.getInvalid(); + } + + String epochPart = xrayTraceId.substring(firstDelimiter + 1, secondDelimiter); + String uniquePart = xrayTraceId.substring(secondDelimiter + 1, secondDelimiter + 25); + + // X-Ray trace id format is 1-{at most 8 digit hex}-{24 digit hex} + // epoch part can have leading 0s truncated + return epochPart + uniquePart; + } + + private static String parseSpanId(String xrayParentId) { + if (xrayParentId.length() != PARENT_ID_LENGTH) { + return SpanId.getInvalid(); + } + + return xrayParentId; + } + + @Nullable + private static Boolean parseTraceFlag(String xraySampledFlag) { + if (xraySampledFlag.length() != SAMPLED_FLAG_LENGTH) { + // Returning null as there is no invalid trace flag defined. + return null; + } + + char flag = xraySampledFlag.charAt(0); + if (flag == IS_SAMPLED) { + return true; + } else if (flag == NOT_SAMPLED) { + return false; + } else { + return null; + } + } +} diff --git a/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/package-info.java b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/package-info.java new file mode 100644 index 000000000..ee9b359df --- /dev/null +++ b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/package-info.java @@ -0,0 +1,5 @@ +/** OpenTelemetry AWS X-Ray propagator extension. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.contrib.awsxray.propagator; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/aws-xray-propagator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider b/aws-xray-propagator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider new file mode 100644 index 000000000..95ace8d1c --- /dev/null +++ b/aws-xray-propagator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider @@ -0,0 +1 @@ +io.opentelemetry.contrib.awsxray.propagator.AwsConfigurablePropagator diff --git a/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayPropagatorTest.java b/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayPropagatorTest.java new file mode 100644 index 000000000..452c7c1bd --- /dev/null +++ b/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayPropagatorTest.java @@ -0,0 +1,477 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.awsxray.propagator; + +import static io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator.TRACE_HEADER_KEY; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +class AwsXrayPropagatorTest { + + private static final String TRACE_ID = "8a3c60f7d188f8fa79d48a391a778fa6"; + private static final String SPAN_ID = "53995c3f42cd8ad8"; + + private static final TextMapSetter> setter = Map::put; + private static final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Set keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + private final AwsXrayPropagator xrayPropagator = AwsXrayPropagator.getInstance(); + + @Test + void inject_SampledContext() { + Map carrier = new LinkedHashMap<>(); + xrayPropagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()), + Context.current()), + carrier, + setter); + + assertThat(carrier) + .containsEntry( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1"); + } + + @Test + void inject_NotSampledContext() { + Map carrier = new LinkedHashMap<>(); + xrayPropagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()), + carrier, + setter); + + assertThat(carrier) + .containsEntry( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"); + } + + @Test + void inject_WithBaggage() { + Map carrier = new LinkedHashMap<>(); + xrayPropagator.inject( + withSpanContext( + SpanContext.create( + TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()) + .with( + Baggage.builder() + .put("cat", "meow") + .put("dog", "bark") + .put("Root", "ignored") + .put("Parent", "ignored") + .put("Sampled", "ignored") + .build()), + carrier, + setter); + + assertThat(carrier) + .containsEntry( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0;" + + "cat=meow;dog=bark"); + } + + @Test + void inject_WithBaggage_LimitTruncates() { + Map carrier = new LinkedHashMap<>(); + // Limit is 256 characters for all baggage. We add a 254-character key/value pair and a + // 3 character key value pair. + String key1 = Stream.generate(() -> "a").limit(252).collect(Collectors.joining()); + String value1 = "a"; // 252 + 1 (=) + 1 = 254 + + String key2 = "b"; + String value2 = "b"; // 1 + 1 (=) + 1 = 3 + + Baggage baggage = Baggage.builder().put(key1, value1).put(key2, value2).build(); + + xrayPropagator.inject( + withSpanContext( + SpanContext.create( + TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()) + .with(baggage), + carrier, + setter); + + assertThat(carrier) + .containsEntry( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0;" + + key1 + + '=' + + value1); + } + + @Test + void inject_WithTraceState() { + Map carrier = new LinkedHashMap<>(); + xrayPropagator.inject( + withSpanContext( + SpanContext.create( + TRACE_ID, + SPAN_ID, + TraceFlags.getDefault(), + TraceState.builder().put("foo", "bar").build()), + Context.current()), + carrier, + setter); + + // TODO: assert trace state when the propagator supports it, for general key/value pairs we are + // mapping with baggage. + assertThat(carrier) + .containsEntry( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"); + } + + @Test + void inject_nullContext() { + Map carrier = new LinkedHashMap<>(); + xrayPropagator.inject(null, carrier, setter); + assertThat(carrier).isEmpty(); + } + + @Test + void inject_nullSetter() { + Map carrier = new LinkedHashMap<>(); + Context context = + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()); + xrayPropagator.inject(context, carrier, null); + assertThat(carrier).isEmpty(); + } + + @Test + void extract_Nothing() { + // Context remains untouched. + assertThat( + xrayPropagator.extract( + Context.current(), Collections.emptyMap(), getter)) + .isSameAs(Context.current()); + } + + @Test + void extract_SampledContext() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_NotSampledContext() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())); + } + + @Test + void extract_DifferentPartOrder() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Parent=53995c3f42cd8ad8;Sampled=1;Root=1-8a3c60f7-d188f8fa79d48a391a778fa6"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_AdditionalFields() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Foo=Bar"); + + Context context = xrayPropagator.extract(Context.current(), carrier, getter); + assertThat(getSpanContext(context)) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + assertThat(Baggage.fromContext(context).getEntryValue("Foo")).isEqualTo("Bar"); + } + + @Test + void extract_Baggage_LimitTruncates() { + // Limit is 256 characters for all baggage. We add a 254-character key/value pair and a + // 3 character key value pair. + String key1 = Stream.generate(() -> "a").limit(252).collect(Collectors.joining()); + String value1 = "a"; // 252 + 1 (=) + 1 = 254 + + String key2 = "b"; + String value2 = "b"; // 1 + 1 (=) + 1 = 3 + + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;" + + key1 + + '=' + + value1 + + ';' + + key2 + + '=' + + value2); + + Context context = xrayPropagator.extract(Context.current(), carrier, getter); + assertThat(getSpanContext(context)) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + assertThat(Baggage.fromContext(context).getEntryValue(key1)).isEqualTo(value1); + assertThat(Baggage.fromContext(context).getEntryValue(key2)).isNull(); + } + + @Test + void extract_EmptyHeaderValue() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(TRACE_HEADER_KEY, ""); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceId() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=abcdefghijklmnopabcdefghijklmnop;Parent=53995c3f42cd8ad8;Sampled=0"); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceId_Size() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa600;Parent=53995c3f42cd8ad8;Sampled=0"); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=abcdefghijklmnop;Sampled=0"); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId_Size() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad800;Sampled=0"); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidFlags() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled="); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidFlags_Size() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=10220"); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidFlags_NonNumeric() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=a"); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_Invalid_NoSpanId() { + Map invalidHeaders = new LinkedHashMap<>(1); + invalidHeaders.put(TRACE_HEADER_KEY, "Root=1-622422bf-59625fe25708d4660735d8ef"); + + verifyInvalidBehavior(invalidHeaders); + } + + private void verifyInvalidBehavior(Map invalidHeaders) { + Context input = Context.current(); + Context result = xrayPropagator.extract(input, invalidHeaders, getter); + assertThat(result).isSameAs(input); + assertThat(getSpanContext(result)).isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_nullContext() { + assertThat(xrayPropagator.extract(null, Collections.emptyMap(), getter)) + .isSameAs(Context.root()); + } + + @Test + void extract_nullGetter() { + Context context = + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()); + assertThat(xrayPropagator.extract(context, Collections.emptyMap(), null)).isSameAs(context); + } + + @Test + void extract_EpochPart_ZeroedSingleDigit() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-0-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Foo=Bar"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + "00000000d188f8fa79d48a391a778fa6", + SPAN_ID, + TraceFlags.getSampled(), + TraceState.getDefault())); + } + + @Test + void extract_EpochPart_TwoChars() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-1a-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Foo=Bar"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + "0000001ad188f8fa79d48a391a778fa6", + SPAN_ID, + TraceFlags.getSampled(), + TraceState.getDefault())); + } + + @Test + void extract_EpochPart_Zeroed() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-00000000-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Foo=Bar"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + "00000000d188f8fa79d48a391a778fa6", + SPAN_ID, + TraceFlags.getSampled(), + TraceState.getDefault())); + } + + @Test + void extract_InvalidTraceId_EpochPart_TooLong() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f711-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), invalidHeaders, getter))) + .isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_InvalidTraceId_EpochPart_Empty() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, "Root=1--d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), invalidHeaders, getter))) + .isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_InvalidTraceId_EpochPart_Missing() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, "Root=1-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), invalidHeaders, getter))) + .isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_InvalidTraceId_WrongVersion() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=2-1a2a3a4a-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Foo=Bar"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isSameAs(SpanContext.getInvalid()); + } + + private static Context withSpanContext(SpanContext spanContext, Context context) { + return context.with(Span.wrap(spanContext)); + } + + private static SpanContext getSpanContext(Context context) { + return Span.fromContext(context).getSpanContext(); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index d029c02bb..431f7ba95 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -40,6 +40,7 @@ rootProject.name = "opentelemetry-java-contrib" include(":all") include(":aws-resources") include(":aws-xray") +include(":aws-xray-propagator") include(":consistent-sampling") include(":dependencyManagement") include(":example")