-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
otel tracing: add binary format, grpcTraceBinContextPropagator (#11409)
* otel tracing: add binary format, grpcTraceBinContextPropagator * exception handling, use api base64 encoder omit padding remove binary format abstract class in favor of binary marshaller
- Loading branch information
1 parent
d1dcfb0
commit 043ba55
Showing
7 changed files
with
943 additions
and
0 deletions.
There are no files selected for viewing
143 changes: 143 additions & 0 deletions
143
opentelemetry/src/main/java/io/grpc/opentelemetry/BinaryFormat.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: <version_id><version_format> | ||
* <li>version_id: 1-byte representing the version id. | ||
* <li>For version_id = 0: | ||
* <ul> | ||
* <li>version_format: <field><field> | ||
* <li>field_format: <field_id><field_format> | ||
* <li>Fields: | ||
* <ul> | ||
* <li>TraceId: (field_id = 0, len = 16, default = "0000000000000000") - | ||
* 16-byte array representing the trace_id. | ||
* <li>SpanId: (field_id = 1, len = 8, default = "00000000") - 8-byte array | ||
* representing the span_id. | ||
* <li>TraceFlags: (field_id = 2, len = 1, default = "0") - 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()); | ||
} | ||
} |
147 changes: 147 additions & 0 deletions
147
opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcTraceBinContextPropagator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
87 changes: 87 additions & 0 deletions
87
opentelemetry/src/main/java/io/grpc/opentelemetry/MetadataGetter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.