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