Skip to content

Commit 043ba55

Browse files
authored
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
1 parent d1dcfb0 commit 043ba55

File tree

7 files changed

+943
-0
lines changed

7 files changed

+943
-0
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2024 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.opentelemetry;
18+
19+
20+
import static com.google.common.base.Preconditions.checkNotNull;
21+
22+
import io.grpc.Metadata;
23+
import io.opentelemetry.api.trace.SpanContext;
24+
import io.opentelemetry.api.trace.SpanId;
25+
import io.opentelemetry.api.trace.TraceFlags;
26+
import io.opentelemetry.api.trace.TraceId;
27+
import io.opentelemetry.api.trace.TraceState;
28+
import java.util.Arrays;
29+
30+
/**
31+
* Binary encoded {@link SpanContext} for context propagation. This is adapted from OpenCensus
32+
* binary format.
33+
*
34+
* <p>BinaryFormat format:
35+
*
36+
* <ul>
37+
* <li>Binary value: &lt;version_id&gt;&lt;version_format&gt;
38+
* <li>version_id: 1-byte representing the version id.
39+
* <li>For version_id = 0:
40+
* <ul>
41+
* <li>version_format: &lt;field&gt;&lt;field&gt;
42+
* <li>field_format: &lt;field_id&gt;&lt;field_format&gt;
43+
* <li>Fields:
44+
* <ul>
45+
* <li>TraceId: (field_id = 0, len = 16, default = &#34;0000000000000000&#34;) -
46+
* 16-byte array representing the trace_id.
47+
* <li>SpanId: (field_id = 1, len = 8, default = &#34;00000000&#34;) - 8-byte array
48+
* representing the span_id.
49+
* <li>TraceFlags: (field_id = 2, len = 1, default = &#34;0&#34;) - 1-byte array
50+
* representing the trace_flags.
51+
* </ul>
52+
* <li>Fields MUST be encoded using the field id order (smaller to higher).
53+
* <li>Valid value example:
54+
* <ul>
55+
* <li>{0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97,
56+
* 98, 99, 100, 101, 102, 103, 104, 2, 1}
57+
* <li>version_id = 0;
58+
* <li>trace_id = {64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}
59+
* <li>span_id = {97, 98, 99, 100, 101, 102, 103, 104};
60+
* <li>trace_flags = {1};
61+
* </ul>
62+
* </ul>
63+
* </ul>
64+
*/
65+
final class BinaryFormat implements Metadata.BinaryMarshaller<SpanContext> {
66+
private static final byte VERSION_ID = 0;
67+
private static final int VERSION_ID_OFFSET = 0;
68+
private static final byte ID_SIZE = 1;
69+
private static final byte TRACE_ID_FIELD_ID = 0;
70+
71+
private static final int TRACE_ID_FIELD_ID_OFFSET = VERSION_ID_OFFSET + ID_SIZE;
72+
private static final int TRACE_ID_OFFSET = TRACE_ID_FIELD_ID_OFFSET + ID_SIZE;
73+
private static final int TRACE_ID_SIZE = TraceId.getLength() / 2;
74+
75+
private static final byte SPAN_ID_FIELD_ID = 1;
76+
private static final int SPAN_ID_FIELD_ID_OFFSET = TRACE_ID_OFFSET + TRACE_ID_SIZE;
77+
private static final int SPAN_ID_OFFSET = SPAN_ID_FIELD_ID_OFFSET + ID_SIZE;
78+
private static final int SPAN_ID_SIZE = SpanId.getLength() / 2;
79+
80+
private static final byte TRACE_FLAG_FIELD_ID = 2;
81+
private static final int TRACE_FLAG_FIELD_ID_OFFSET = SPAN_ID_OFFSET + SPAN_ID_SIZE;
82+
private static final int TRACE_FLAG_OFFSET = TRACE_FLAG_FIELD_ID_OFFSET + ID_SIZE;
83+
private static final int REQUIRED_FORMAT_LENGTH = 3 * ID_SIZE + TRACE_ID_SIZE + SPAN_ID_SIZE;
84+
private static final int TRACE_FLAG_SIZE = TraceFlags.getLength() / 2;
85+
private static final int ALL_FORMAT_LENGTH = REQUIRED_FORMAT_LENGTH + ID_SIZE + TRACE_FLAG_SIZE;
86+
87+
private static final BinaryFormat INSTANCE = new BinaryFormat();
88+
89+
public static BinaryFormat getInstance() {
90+
return INSTANCE;
91+
}
92+
93+
@Override
94+
public byte[] toBytes(SpanContext spanContext) {
95+
checkNotNull(spanContext, "spanContext");
96+
byte[] bytes = new byte[ALL_FORMAT_LENGTH];
97+
bytes[VERSION_ID_OFFSET] = VERSION_ID;
98+
bytes[TRACE_ID_FIELD_ID_OFFSET] = TRACE_ID_FIELD_ID;
99+
System.arraycopy(spanContext.getTraceIdBytes(), 0, bytes, TRACE_ID_OFFSET, TRACE_ID_SIZE);
100+
bytes[SPAN_ID_FIELD_ID_OFFSET] = SPAN_ID_FIELD_ID;
101+
System.arraycopy(spanContext.getSpanIdBytes(), 0, bytes, SPAN_ID_OFFSET, SPAN_ID_SIZE);
102+
bytes[TRACE_FLAG_FIELD_ID_OFFSET] = TRACE_FLAG_FIELD_ID;
103+
bytes[TRACE_FLAG_OFFSET] = spanContext.getTraceFlags().asByte();
104+
return bytes;
105+
}
106+
107+
108+
@Override
109+
public SpanContext parseBytes(byte[] serialized) {
110+
checkNotNull(serialized, "bytes");
111+
if (serialized.length == 0 || serialized[0] != VERSION_ID) {
112+
throw new IllegalArgumentException("Unsupported version.");
113+
}
114+
if (serialized.length < REQUIRED_FORMAT_LENGTH) {
115+
throw new IllegalArgumentException("Invalid input: truncated");
116+
}
117+
String traceId;
118+
String spanId;
119+
TraceFlags traceFlags = TraceFlags.getDefault();
120+
int pos = 1;
121+
if (serialized[pos] == TRACE_ID_FIELD_ID) {
122+
traceId = TraceId.fromBytes(
123+
Arrays.copyOfRange(serialized, pos + ID_SIZE, pos + ID_SIZE + TRACE_ID_SIZE));
124+
pos += ID_SIZE + TRACE_ID_SIZE;
125+
} else {
126+
throw new IllegalArgumentException("Invalid input: expected trace ID at offset " + pos);
127+
}
128+
if (serialized[pos] == SPAN_ID_FIELD_ID) {
129+
spanId = SpanId.fromBytes(
130+
Arrays.copyOfRange(serialized, pos + ID_SIZE, pos + ID_SIZE + SPAN_ID_SIZE));
131+
pos += ID_SIZE + SPAN_ID_SIZE;
132+
} else {
133+
throw new IllegalArgumentException("Invalid input: expected span ID at offset " + pos);
134+
}
135+
if (serialized.length > pos && serialized[pos] == TRACE_FLAG_FIELD_ID) {
136+
if (serialized.length < ALL_FORMAT_LENGTH) {
137+
throw new IllegalArgumentException("Invalid input: truncated");
138+
}
139+
traceFlags = TraceFlags.fromByte(serialized[pos + ID_SIZE]);
140+
}
141+
return SpanContext.create(traceId, spanId, traceFlags, TraceState.getDefault());
142+
}
143+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright 2024 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.opentelemetry;
18+
19+
20+
import static com.google.common.base.Preconditions.checkNotNull;
21+
import static io.grpc.InternalMetadata.BASE64_ENCODING_OMIT_PADDING;
22+
23+
import com.google.common.annotations.VisibleForTesting;
24+
import com.google.common.io.BaseEncoding;
25+
import io.grpc.ExperimentalApi;
26+
import io.grpc.Metadata;
27+
import io.opentelemetry.api.trace.Span;
28+
import io.opentelemetry.api.trace.SpanContext;
29+
import io.opentelemetry.context.Context;
30+
import io.opentelemetry.context.propagation.TextMapGetter;
31+
import io.opentelemetry.context.propagation.TextMapPropagator;
32+
import io.opentelemetry.context.propagation.TextMapSetter;
33+
import java.util.Collection;
34+
import java.util.Collections;
35+
import java.util.logging.Level;
36+
import java.util.logging.Logger;
37+
import javax.annotation.Nullable;
38+
39+
/**
40+
* A {@link TextMapPropagator} for transmitting "grpc-trace-bin" span context.
41+
*
42+
* <p>This propagator can transmit the "grpc-trace-bin" context in either binary or Base64-encoded
43+
* text format, depending on the capabilities of the provided {@link TextMapGetter} and
44+
* {@link TextMapSetter}.
45+
*
46+
* <p>If the {@code TextMapGetter} and {@code TextMapSetter} only support text format, Base64
47+
* encoding and decoding will be used when communicating with the carrier API. But gRPC uses
48+
* it with gRPC's metadata-based getter/setter, and the propagator can directly transmit the binary
49+
* header, avoiding the need for Base64 encoding.
50+
*/
51+
52+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/11400")
53+
public final class GrpcTraceBinContextPropagator implements TextMapPropagator {
54+
private static final Logger log = Logger.getLogger(GrpcTraceBinContextPropagator.class.getName());
55+
public static final String GRPC_TRACE_BIN_HEADER = "grpc-trace-bin";
56+
private final Metadata.BinaryMarshaller<SpanContext> binaryFormat;
57+
private static final GrpcTraceBinContextPropagator INSTANCE =
58+
new GrpcTraceBinContextPropagator(BinaryFormat.getInstance());
59+
60+
public static GrpcTraceBinContextPropagator defaultInstance() {
61+
return INSTANCE;
62+
}
63+
64+
@VisibleForTesting
65+
GrpcTraceBinContextPropagator(Metadata.BinaryMarshaller<SpanContext> binaryFormat) {
66+
this.binaryFormat = checkNotNull(binaryFormat, "binaryFormat");
67+
}
68+
69+
@Override
70+
public Collection<String> fields() {
71+
return Collections.singleton(GRPC_TRACE_BIN_HEADER);
72+
}
73+
74+
@Override
75+
public <C> void inject(Context context, @Nullable C carrier, TextMapSetter<C> setter) {
76+
if (context == null || setter == null) {
77+
return;
78+
}
79+
SpanContext spanContext = Span.fromContext(context).getSpanContext();
80+
if (!spanContext.isValid()) {
81+
return;
82+
}
83+
try {
84+
byte[] b = binaryFormat.toBytes(spanContext);
85+
if (setter instanceof MetadataSetter) {
86+
((MetadataSetter) setter).set((Metadata) carrier, GRPC_TRACE_BIN_HEADER, b);
87+
} else {
88+
setter.set(carrier, GRPC_TRACE_BIN_HEADER, BASE64_ENCODING_OMIT_PADDING.encode(b));
89+
}
90+
} catch (Exception e) {
91+
log.log(Level.FINE, "Set grpc-trace-bin spanContext failed", e);
92+
}
93+
}
94+
95+
@Override
96+
public <C> Context extract(Context context, @Nullable C carrier, TextMapGetter<C> getter) {
97+
if (context == null) {
98+
return Context.root();
99+
}
100+
if (getter == null) {
101+
return context;
102+
}
103+
byte[] b;
104+
if (getter instanceof MetadataGetter) {
105+
try {
106+
b = ((MetadataGetter) getter).getBinary((Metadata) carrier, GRPC_TRACE_BIN_HEADER);
107+
if (b == null) {
108+
log.log(Level.FINE, "No grpc-trace-bin present in carrier");
109+
return context;
110+
}
111+
} catch (Exception e) {
112+
log.log(Level.FINE, "Get 'grpc-trace-bin' from MetadataGetter failed", e);
113+
return context;
114+
}
115+
} else {
116+
String value;
117+
try {
118+
value = getter.get(carrier, GRPC_TRACE_BIN_HEADER);
119+
if (value == null) {
120+
log.log(Level.FINE, "No grpc-trace-bin present in carrier");
121+
return context;
122+
}
123+
} catch (Exception e) {
124+
log.log(Level.FINE, "Get 'grpc-trace-bin' from getter failed", e);
125+
return context;
126+
}
127+
try {
128+
b = BaseEncoding.base64().decode(value);
129+
} catch (Exception e) {
130+
log.log(Level.FINE, "Base64-decode spanContext bytes failed", e);
131+
return context;
132+
}
133+
}
134+
135+
SpanContext spanContext;
136+
try {
137+
spanContext = binaryFormat.parseBytes(b);
138+
} catch (Exception e) {
139+
log.log(Level.FINE, "Failed to parse tracing header", e);
140+
return context;
141+
}
142+
if (!spanContext.isValid()) {
143+
return context;
144+
}
145+
return context.with(Span.wrap(spanContext));
146+
}
147+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2024 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.opentelemetry;
18+
19+
20+
import static io.grpc.InternalMetadata.BASE64_ENCODING_OMIT_PADDING;
21+
22+
import io.grpc.Metadata;
23+
import io.opentelemetry.context.propagation.TextMapGetter;
24+
import java.util.logging.Level;
25+
import java.util.logging.Logger;
26+
import javax.annotation.Nullable;
27+
28+
/**
29+
* A TextMapGetter that reads value from gRPC {@link Metadata}. Supports both text and binary
30+
* headers. Supporting binary header is an optimization path for GrpcTraceBinContextPropagator
31+
* to work around the lack of binary propagator API and thus avoid
32+
* base64 (de)encoding when passing data between propagator API interfaces.
33+
*/
34+
final class MetadataGetter implements TextMapGetter<Metadata> {
35+
private static final Logger logger = Logger.getLogger(MetadataGetter.class.getName());
36+
private static final MetadataGetter INSTANCE = new MetadataGetter();
37+
38+
public static MetadataGetter getInstance() {
39+
return INSTANCE;
40+
}
41+
42+
@Override
43+
public Iterable<String> keys(Metadata carrier) {
44+
return carrier.keys();
45+
}
46+
47+
@Nullable
48+
@Override
49+
public String get(@Nullable Metadata carrier, String key) {
50+
if (carrier == null) {
51+
logger.log(Level.FINE, "Carrier is null, getting no data");
52+
return null;
53+
}
54+
try {
55+
if (key.equals("grpc-trace-bin")) {
56+
byte[] value = carrier.get(Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER));
57+
if (value == null) {
58+
return null;
59+
}
60+
return BASE64_ENCODING_OMIT_PADDING.encode(value);
61+
} else {
62+
return carrier.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER));
63+
}
64+
} catch (Exception e) {
65+
logger.log(Level.FINE, String.format("Failed to get metadata key %s", key), e);
66+
return null;
67+
}
68+
}
69+
70+
@Nullable
71+
public byte[] getBinary(@Nullable Metadata carrier, String key) {
72+
if (carrier == null) {
73+
logger.log(Level.FINE, "Carrier is null, getting no data");
74+
return null;
75+
}
76+
if (!key.equals("grpc-trace-bin")) {
77+
logger.log(Level.FINE, "Only support 'grpc-trace-bin' binary header. Get no data");
78+
return null;
79+
}
80+
try {
81+
return carrier.get(Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER));
82+
} catch (Exception e) {
83+
logger.log(Level.FINE, String.format("Failed to get metadata key %s", key), e);
84+
return null;
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)