Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions opentelemetry/src/main/java/io/grpc/opentelemetry/BinaryFormat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* 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.grpc.Metadata;
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.
*
* <p>BinaryFormat format:
*
* <ul>
* <li>Binary value: &lt;version_id&gt;&lt;version_format&gt;
* <li>version_id: 1-byte representing the version id.
* <li>For version_id = 0:
* <ul>
* <li>version_format: &lt;field&gt;&lt;field&gt;
* <li>field_format: &lt;field_id&gt;&lt;field_format&gt;
* <li>Fields:
* <ul>
* <li>TraceId: (field_id = 0, len = 16, default = &#34;0000000000000000&#34;) -
* 16-byte array representing the trace_id.
* <li>SpanId: (field_id = 1, len = 8, default = &#34;00000000&#34;) - 8-byte array
* representing the span_id.
* <li>TraceFlags: (field_id = 2, len = 1, default = &#34;0&#34;) - 1-byte array
* representing the trace_flags.
* </ul>
* <li>Fields MUST be encoded using the field id order (smaller to higher).
* <li>Valid value example:
* <ul>
* <li>{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}
* <li>version_id = 0;
* <li>trace_id = {64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}
* <li>span_id = {97, 98, 99, 100, 101, 102, 103, 104};
* <li>trace_flags = {1};
* </ul>
* </ul>
* </ul>
*/
final class BinaryFormat implements Metadata.BinaryMarshaller<SpanContext> {
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 BinaryFormat INSTANCE = new BinaryFormat();

public static BinaryFormat getInstance() {
return INSTANCE;
}

@Override
public byte[] toBytes(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 parseBytes(byte[] serialized) {
checkNotNull(serialized, "bytes");
if (serialized.length == 0 || serialized[0] != VERSION_ID) {
throw new IllegalArgumentException("Unsupported version.");
}
if (serialized.length < REQUIRED_FORMAT_LENGTH) {
throw new IllegalArgumentException("Invalid input: truncated");
}
String traceId;
String spanId;
TraceFlags traceFlags = TraceFlags.getDefault();
int pos = 1;
if (serialized[pos] == TRACE_ID_FIELD_ID) {
traceId = TraceId.fromBytes(
Arrays.copyOfRange(serialized, 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 (serialized[pos] == SPAN_ID_FIELD_ID) {
spanId = SpanId.fromBytes(
Arrays.copyOfRange(serialized, 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 (serialized.length > pos && serialized[pos] == TRACE_FLAG_FIELD_ID) {
if (serialized.length < ALL_FORMAT_LENGTH) {
throw new IllegalArgumentException("Invalid input: truncated");
}
traceFlags = TraceFlags.fromByte(serialized[pos + ID_SIZE]);
}
return SpanContext.create(traceId, spanId, traceFlags, TraceState.getDefault());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* 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 static io.grpc.InternalMetadata.BASE64_ENCODING_OMIT_PADDING;

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.
*
* <p>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}.
*
* <p>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 Metadata.BinaryMarshaller<SpanContext> binaryFormat;
private static final GrpcTraceBinContextPropagator INSTANCE =
new GrpcTraceBinContextPropagator(BinaryFormat.getInstance());

public static GrpcTraceBinContextPropagator defaultInstance() {
return INSTANCE;
}

@VisibleForTesting
GrpcTraceBinContextPropagator(Metadata.BinaryMarshaller<SpanContext> binaryFormat) {
this.binaryFormat = checkNotNull(binaryFormat, "binaryFormat");
}

@Override
public Collection<String> fields() {
return Collections.singleton(GRPC_TRACE_BIN_HEADER);
}

@Override
public <C> void inject(Context context, @Nullable C carrier, TextMapSetter<C> setter) {
if (context == null || setter == null) {
return;
}
SpanContext spanContext = Span.fromContext(context).getSpanContext();
if (!spanContext.isValid()) {
return;
}
try {
byte[] b = binaryFormat.toBytes(spanContext);
if (setter instanceof MetadataSetter) {
((MetadataSetter) setter).set((Metadata) carrier, GRPC_TRACE_BIN_HEADER, b);
} else {
setter.set(carrier, GRPC_TRACE_BIN_HEADER, BASE64_ENCODING_OMIT_PADDING.encode(b));
}
} catch (Exception e) {
log.log(Level.FINE, "Set grpc-trace-bin spanContext failed", e);
}
}

@Override
public <C> Context extract(Context context, @Nullable C carrier, TextMapGetter<C> getter) {
if (context == null) {
return Context.root();
}
if (getter == null) {
return context;
}
byte[] b;
if (getter instanceof MetadataGetter) {
try {
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;
}
} catch (Exception e) {
log.log(Level.FINE, "Get 'grpc-trace-bin' from MetadataGetter failed", e);
return context;
}
} else {
String value;
try {
value = getter.get(carrier, GRPC_TRACE_BIN_HEADER);
if (value == null) {
log.log(Level.FINE, "No grpc-trace-bin present in carrier");
return context;
}
} catch (Exception e) {
log.log(Level.FINE, "Get 'grpc-trace-bin' from getter failed", e);
return context;
}
try {
b = BaseEncoding.base64().decode(value);
} catch (Exception e) {
log.log(Level.FINE, "Base64-decode spanContext bytes failed", e);
return context;
}
}

SpanContext spanContext;
try {
spanContext = binaryFormat.parseBytes(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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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 io.grpc.InternalMetadata.BASE64_ENCODING_OMIT_PADDING;

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<Metadata> {
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<String> 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;
}
try {
if (key.equals("grpc-trace-bin")) {
byte[] value = carrier.get(Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER));
if (value == null) {
return null;
}
return BASE64_ENCODING_OMIT_PADDING.encode(value);
} else {
return carrier.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER));
}
} catch (Exception e) {
logger.log(Level.FINE, String.format("Failed to get metadata key %s", key), e);
return null;
}
}

@Nullable
public byte[] getBinary(@Nullable Metadata carrier, String key) {
if (carrier == null) {
logger.log(Level.FINE, "Carrier is null, getting no data");
return null;
}
if (!key.equals("grpc-trace-bin")) {
logger.log(Level.FINE, "Only support 'grpc-trace-bin' binary header. Get no data");
return null;
}
try {
return carrier.get(Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER));
} catch (Exception e) {
logger.log(Level.FINE, String.format("Failed to get metadata key %s", key), e);
return null;
}
}
}
Loading