diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/BinaryFormat.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/BinaryFormat.java
new file mode 100644
index 00000000000..d4cb929b99e
--- /dev/null
+++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/BinaryFormat.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 The gRPC Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 io.grpc.opentelemetry;
+
+
+import io.opentelemetry.api.trace.SpanContext;
+
+/**
+ * This is a helper class for SpanContext propagation on the wire using binary encoding.
+ */
+abstract class BinaryFormat {
+ public byte[] toByteArray(SpanContext spanContext) {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ public SpanContext fromByteArray(byte[] bytes) {
+ throw new UnsupportedOperationException("not implemented");
+ }
+}
diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/BinaryFormatImpl.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/BinaryFormatImpl.java
new file mode 100644
index 00000000000..6f70d789753
--- /dev/null
+++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/BinaryFormatImpl.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2024 The gRPC Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 io.grpc.opentelemetry;
+
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+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 java.util.Arrays;
+
+/**
+ * Binary encoded {@link SpanContext} for context propagation. This is adapted from OpenCensus
+ * binary format.
+ *
+ *
BinaryFormat format:
+ *
+ *
+ * - Binary value: <version_id><version_format>
+ *
- version_id: 1-byte representing the version id.
+ *
- For version_id = 0:
+ *
+ * - version_format: <field><field>
+ *
- field_format: <field_id><field_format>
+ *
- Fields:
+ *
+ * - TraceId: (field_id = 0, len = 16, default = "0000000000000000") -
+ * 16-byte array representing the trace_id.
+ *
- SpanId: (field_id = 1, len = 8, default = "00000000") - 8-byte array
+ * representing the span_id.
+ *
- TraceFlags: (field_id = 2, len = 1, default = "0") - 1-byte array
+ * representing the trace_flags.
+ *
+ * - Fields MUST be encoded using the field id order (smaller to higher).
+ *
- Valid value example:
+ *
+ * - {0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97,
+ * 98, 99, 100, 101, 102, 103, 104, 2, 1}
+ *
- version_id = 0;
+ *
- trace_id = {64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}
+ *
- span_id = {97, 98, 99, 100, 101, 102, 103, 104};
+ *
- trace_flags = {1};
+ *
+ *
+ *
+ */
+final class BinaryFormatImpl extends BinaryFormat {
+ private static final byte VERSION_ID = 0;
+ private static final int VERSION_ID_OFFSET = 0;
+ private static final byte ID_SIZE = 1;
+ private static final byte TRACE_ID_FIELD_ID = 0;
+
+ private static final int TRACE_ID_FIELD_ID_OFFSET = VERSION_ID_OFFSET + ID_SIZE;
+ private static final int TRACE_ID_OFFSET = TRACE_ID_FIELD_ID_OFFSET + ID_SIZE;
+ private static final int TRACE_ID_SIZE = TraceId.getLength() / 2;
+
+ private static final byte SPAN_ID_FIELD_ID = 1;
+ private static final int SPAN_ID_FIELD_ID_OFFSET = TRACE_ID_OFFSET + TRACE_ID_SIZE;
+ private static final int SPAN_ID_OFFSET = SPAN_ID_FIELD_ID_OFFSET + ID_SIZE;
+ private static final int SPAN_ID_SIZE = SpanId.getLength() / 2;
+
+ private static final byte TRACE_FLAG_FIELD_ID = 2;
+ private static final int TRACE_FLAG_FIELD_ID_OFFSET = SPAN_ID_OFFSET + SPAN_ID_SIZE;
+ private static final int TRACE_FLAG_OFFSET = TRACE_FLAG_FIELD_ID_OFFSET + ID_SIZE;
+ private static final int REQUIRED_FORMAT_LENGTH = 3 * ID_SIZE + TRACE_ID_SIZE + SPAN_ID_SIZE;
+ private static final int TRACE_FLAG_SIZE = TraceFlags.getLength() / 2;
+ private static final int ALL_FORMAT_LENGTH = REQUIRED_FORMAT_LENGTH + ID_SIZE + TRACE_FLAG_SIZE;
+
+ private static final BinaryFormatImpl INSTANCE = new BinaryFormatImpl();
+
+ public static BinaryFormatImpl getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public byte[] toByteArray(SpanContext spanContext) {
+ checkNotNull(spanContext, "spanContext");
+ byte[] bytes = new byte[ALL_FORMAT_LENGTH];
+ bytes[VERSION_ID_OFFSET] = VERSION_ID;
+ bytes[TRACE_ID_FIELD_ID_OFFSET] = TRACE_ID_FIELD_ID;
+ System.arraycopy(spanContext.getTraceIdBytes(), 0, bytes, TRACE_ID_OFFSET, TRACE_ID_SIZE);
+ bytes[SPAN_ID_FIELD_ID_OFFSET] = SPAN_ID_FIELD_ID;
+ System.arraycopy(spanContext.getSpanIdBytes(), 0, bytes, SPAN_ID_OFFSET, SPAN_ID_SIZE);
+ bytes[TRACE_FLAG_FIELD_ID_OFFSET] = TRACE_FLAG_FIELD_ID;
+ bytes[TRACE_FLAG_OFFSET] = spanContext.getTraceFlags().asByte();
+ return bytes;
+ }
+
+ @Override
+ public SpanContext fromByteArray(byte[] bytes) {
+ checkNotNull(bytes, "bytes");
+ if (bytes.length == 0 || bytes[0] != VERSION_ID) {
+ throw new IllegalArgumentException("Unsupported version.");
+ }
+ if (bytes.length < REQUIRED_FORMAT_LENGTH) {
+ throw new IllegalArgumentException("Invalid input: truncated");
+ }
+ String traceId;
+ String spanId;
+ TraceFlags traceFlags = TraceFlags.getDefault();
+ int pos = 1;
+ if (bytes[pos] == TRACE_ID_FIELD_ID) {
+ traceId = TraceId.fromBytes(
+ Arrays.copyOfRange(bytes, pos + ID_SIZE, pos + ID_SIZE + TRACE_ID_SIZE));
+ pos += ID_SIZE + TRACE_ID_SIZE;
+ } else {
+ throw new IllegalArgumentException("Invalid input: expected trace ID at offset " + pos);
+ }
+ if (bytes[pos] == SPAN_ID_FIELD_ID) {
+ spanId = SpanId.fromBytes(
+ Arrays.copyOfRange(bytes, pos + ID_SIZE, pos + ID_SIZE + SPAN_ID_SIZE));
+ pos += ID_SIZE + SPAN_ID_SIZE;
+ } else {
+ throw new IllegalArgumentException("Invalid input: expected span ID at offset " + pos);
+ }
+ if (bytes.length > pos && bytes[pos] == TRACE_FLAG_FIELD_ID) {
+ if (bytes.length < ALL_FORMAT_LENGTH) {
+ throw new IllegalArgumentException("Invalid input: truncated");
+ }
+ traceFlags = TraceFlags.fromByte(bytes[pos + ID_SIZE]);
+ }
+ return SpanContext.create(traceId, spanId, traceFlags, TraceState.getDefault());
+ }
+}
diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcTraceBinContextPropagator.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcTraceBinContextPropagator.java
new file mode 100644
index 00000000000..c3017d5170f
--- /dev/null
+++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcTraceBinContextPropagator.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2024 The gRPC Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 io.grpc.opentelemetry;
+
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.BaseEncoding;
+import io.grpc.ExperimentalApi;
+import io.grpc.Metadata;
+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.context.propagation.TextMapPropagator;
+import io.opentelemetry.context.propagation.TextMapSetter;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * A {@link TextMapPropagator} for transmitting "grpc-trace-bin" span context.
+ *
+ * This propagator can transmit the "grpc-trace-bin" context in either binary or Base64-encoded
+ * text format, depending on the capabilities of the provided {@link TextMapGetter} and
+ * {@link TextMapSetter}.
+ *
+ *
If the {@code TextMapGetter} and {@code TextMapSetter} only support text format, Base64
+ * encoding and decoding will be used when communicating with the carrier API. But gRPC uses
+ * it with gRPC's metadata-based getter/setter, and the propagator can directly transmit the binary
+ * header, avoiding the need for Base64 encoding.
+ */
+
+@ExperimentalApi("https://github.com/grpc/grpc-java/issues/11400")
+public final class GrpcTraceBinContextPropagator implements TextMapPropagator {
+ private static final Logger log = Logger.getLogger(GrpcTraceBinContextPropagator.class.getName());
+ public static final String GRPC_TRACE_BIN_HEADER = "grpc-trace-bin";
+ private final BinaryFormat binaryFormat;
+ private static final GrpcTraceBinContextPropagator INSTANCE =
+ new GrpcTraceBinContextPropagator(BinaryFormatImpl.getInstance());
+
+ public static GrpcTraceBinContextPropagator defaultInstance() {
+ return INSTANCE;
+ }
+
+ @VisibleForTesting
+ GrpcTraceBinContextPropagator(BinaryFormat binaryFormat) {
+ this.binaryFormat = checkNotNull(binaryFormat, "binaryFormat");
+ }
+
+ @Override
+ public Collection fields() {
+ return Collections.singleton(GRPC_TRACE_BIN_HEADER);
+ }
+
+ @Override
+ public void inject(Context context, @Nullable C carrier, TextMapSetter setter) {
+ if (context == null || setter == null) {
+ return;
+ }
+ SpanContext spanContext = Span.fromContext(context).getSpanContext();
+ if (!spanContext.isValid()) {
+ return;
+ }
+ try {
+ byte[] b = binaryFormat.toByteArray(spanContext);
+ if (setter instanceof MetadataSetter) {
+ ((MetadataSetter) setter).set((Metadata) carrier, GRPC_TRACE_BIN_HEADER, b);
+ } else {
+ setter.set(carrier, GRPC_TRACE_BIN_HEADER, BaseEncoding.base64().encode(b));
+ }
+ } catch (Exception e) {
+ log.log(Level.FINE, "Set grpc-trace-bin spanContext failed", e);
+ }
+ }
+
+ @Override
+ public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) {
+ if (context == null) {
+ return Context.root();
+ }
+ if (getter == null) {
+ return context;
+ }
+ byte[] b;
+ if (getter instanceof MetadataGetter) {
+ b = ((MetadataGetter) getter).getBinary((Metadata) carrier, GRPC_TRACE_BIN_HEADER);
+ if (b == null) {
+ log.log(Level.FINE, "No grpc-trace-bin present in carrier");
+ return context;
+ }
+ } else {
+ String value = getter.get(carrier, GRPC_TRACE_BIN_HEADER);
+ if (value == null) {
+ log.log(Level.FINE, "No grpc-trace-bin present in carrier");
+ return context;
+ }
+ try {
+ b = BaseEncoding.base64().decode(value);
+ } catch (Exception e) {
+ log.log(Level.FINE, "Base64-decode spanContext bytes failed");
+ return context;
+ }
+ }
+
+ SpanContext spanContext;
+ try {
+ spanContext = binaryFormat.fromByteArray(b);
+ } catch (Exception e) {
+ log.log(Level.FINE, "Failed to parse tracing header", e);
+ return context;
+ }
+ if (!spanContext.isValid()) {
+ return context;
+ }
+ return context.with(Span.wrap(spanContext));
+ }
+}
diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/MetadataGetter.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/MetadataGetter.java
new file mode 100644
index 00000000000..b9110e2d402
--- /dev/null
+++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/MetadataGetter.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 The gRPC Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 io.grpc.opentelemetry;
+
+
+import io.grpc.Metadata;
+import io.opentelemetry.context.propagation.TextMapGetter;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * A TextMapGetter that reads value from gRPC {@link Metadata}. Supports both text and binary
+ * headers. Supporting binary header is an optimization path for GrpcTraceBinContextPropagator
+ * to work around the lack of binary propagator API and thus avoid
+ * base64 (de)encoding when passing data between propagator API interfaces.
+ */
+final class MetadataGetter implements TextMapGetter {
+ private static final Logger logger = Logger.getLogger(MetadataGetter.class.getName());
+
+ private static final MetadataGetter INSTANCE = new MetadataGetter();
+
+ public static MetadataGetter getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public Iterable keys(Metadata carrier) {
+ return carrier.keys();
+ }
+
+ @Nullable
+ @Override
+ public String get(@Nullable Metadata carrier, String key) {
+ if (carrier == null) {
+ logger.log(Level.FINE, "Carrier is null, getting no data");
+ return null;
+ }
+ return carrier.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER));
+ }
+
+ @Nullable
+ public byte[] getBinary(@Nullable Metadata carrier, String key) {
+ if (carrier == null) {
+ logger.log(Level.FINE, "Carrier is null, getting no data");
+ return null;
+ }
+ return carrier.get(Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER));
+ }
+}
diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/MetadataSetter.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/MetadataSetter.java
new file mode 100644
index 00000000000..af5ad1d46e7
--- /dev/null
+++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/MetadataSetter.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 The gRPC Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 io.grpc.opentelemetry;
+
+
+import io.grpc.Metadata;
+import io.opentelemetry.context.propagation.TextMapSetter;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * A {@link TextMapSetter} that sets value to gRPC {@link Metadata}. Supports both text and binary
+ * headers. Supporting binary header is an optimization path for GrpcTraceBinContextPropagator
+ * to work around the lack of binary propagator API and thus avoid
+ * base64 (de)encoding when passing data between propagator API interfaces.
+ */
+final class MetadataSetter implements TextMapSetter {
+ private static final Logger logger = Logger.getLogger(MetadataSetter.class.getName());
+ private static final MetadataSetter INSTANCE = new MetadataSetter();
+
+ public static MetadataSetter getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public void set(@Nullable Metadata carrier, String key, String value) {
+ if (carrier == null) {
+ logger.log(Level.FINE, "Carrier is null, setting no data");
+ return;
+ }
+ assert !key.endsWith("bin");
+ carrier.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), value);
+ }
+
+ void set(@Nullable Metadata carrier, String key, byte[] value) {
+ if (carrier == null) {
+ logger.log(Level.FINE, "Carrier is null, setting no data");
+ return;
+ }
+ assert key.endsWith("bin");
+ if (!key.equals("grpc-trace-bin")) {
+ throw new IllegalArgumentException("Only support 'grpc-trace-bin' binary header");
+ }
+ carrier.put(Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER), value);
+ }
+}
diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/GrpcTraceBinContextPropagatorTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/GrpcTraceBinContextPropagatorTest.java
new file mode 100644
index 00000000000..53a6f125045
--- /dev/null
+++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/GrpcTraceBinContextPropagatorTest.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright 2024 The gRPC Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 io.grpc.opentelemetry;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
+import io.grpc.Metadata;
+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.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class GrpcTraceBinContextPropagatorTest {
+ private static final String TRACE_ID_BASE16 = "e384981d65129fa3e384981d65129fa3";
+ private static final String SPAN_ID_BASE16 = "e384981d65129fa3";
+ private static final String TRACE_HEADER_SAMPLED =
+ "0000" + TRACE_ID_BASE16 + "01" + SPAN_ID_BASE16 + "0201";
+ private static final String TRACE_HEADER_NOT_SAMPLED =
+ "0000" + TRACE_ID_BASE16 + "01" + SPAN_ID_BASE16 + "0200";
+ private final String goldenHeaderEncodedSampled = encode(TRACE_HEADER_SAMPLED);
+ private final String goldenHeaderEncodedNotSampled = encode(TRACE_HEADER_NOT_SAMPLED);
+ private static final TextMapSetter