Skip to content

Commit 230ffac

Browse files
committed
feat: Implement trace context extraction and injection with integration test
1 parent 4b5e8af commit 230ffac

File tree

12 files changed

+416
-17
lines changed

12 files changed

+416
-17
lines changed

sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcClientCalls.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,28 @@ public static <RequestT, ResponseT> ClientCall<RequestT, ResponseT> newCall(
9090
channel = ((ChannelPool) channel).getChannel(grpcContext.getChannelAffinity());
9191
}
9292

93-
if (!grpcContext.getExtraHeaders().isEmpty()) {
94-
ClientInterceptor interceptor =
95-
MetadataUtils.newAttachHeadersInterceptor(grpcContext.getMetadata());
93+
java.util.Map<String, String> traceContext = new java.util.HashMap<>();
94+
grpcContext.getTracer().injectTraceContext(traceContext);
95+
96+
if (!grpcContext.getExtraHeaders().isEmpty() || !traceContext.isEmpty()) {
97+
Metadata metadata = new Metadata();
98+
metadata.merge(grpcContext.getMetadata());
99+
for (java.util.Map.Entry<String, java.util.List<String>> entry :
100+
grpcContext.getExtraHeaders().entrySet()) {
101+
Metadata.Key<String> key =
102+
Metadata.Key.of(entry.getKey(), Metadata.ASCII_STRING_MARSHALLER);
103+
metadata.removeAll(key);
104+
for (String value : entry.getValue()) {
105+
metadata.put(key, value);
106+
}
107+
}
108+
for (java.util.Map.Entry<String, String> entry : traceContext.entrySet()) {
109+
Metadata.Key<String> key =
110+
Metadata.Key.of(entry.getKey(), Metadata.ASCII_STRING_MARSHALLER);
111+
metadata.removeAll(key);
112+
metadata.put(key, entry.getValue());
113+
}
114+
ClientInterceptor interceptor = MetadataUtils.newAttachHeadersInterceptor(metadata);
96115
channel = ClientInterceptors.intercept(channel, interceptor);
97116
}
98117

sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcClientCallsTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,48 @@ void testUniverseDomainNotReady_shouldRetry() throws IOException {
340340
Truth.assertThat(exception.isRetryable()).isTrue();
341341
Mockito.verify(mockChannel, Mockito.never()).newCall(descriptor, callOptions);
342342
}
343+
344+
@Test
345+
void testTraceContextHeaders() throws IOException {
346+
Metadata emptyHeaders = new Metadata();
347+
348+
MethodDescriptor<Color, Money> descriptor = FakeServiceGrpc.METHOD_RECOGNIZE;
349+
350+
@SuppressWarnings("unchecked")
351+
ClientCall<Color, Money> mockClientCall = Mockito.mock(ClientCall.class);
352+
353+
@SuppressWarnings("unchecked")
354+
ClientCall.Listener<Money> mockListener = Mockito.mock(ClientCall.Listener.class);
355+
356+
Channel mockChannel = Mockito.mock(ManagedChannel.class);
357+
com.google.api.gax.tracing.ApiTracer mockTracer =
358+
Mockito.mock(com.google.api.gax.tracing.ApiTracer.class);
359+
360+
Mockito.doAnswer(
361+
invocation -> {
362+
java.util.Map<String, String> carrier = invocation.getArgument(0);
363+
carrier.put("traceparent", "00-00000000000000000000000000000001-0000000000000002-01");
364+
return null;
365+
})
366+
.when(mockTracer)
367+
.injectTraceContext(Mockito.anyMap());
368+
369+
Mockito.doAnswer(
370+
invocation -> {
371+
Metadata clientCallHeaders = (Metadata) invocation.getArguments()[1];
372+
Metadata.Key<String> traceparentKey =
373+
Metadata.Key.of("traceparent", Metadata.ASCII_STRING_MARSHALLER);
374+
assertThat(clientCallHeaders.getAll(traceparentKey))
375+
.containsExactly("00-00000000000000000000000000000001-0000000000000002-01");
376+
return null;
377+
})
378+
.when(mockClientCall)
379+
.start(Mockito.<ClientCall.Listener<Money>>any(), Mockito.<Metadata>any());
380+
381+
Mockito.when(mockChannel.newCall(Mockito.eq(descriptor), Mockito.<CallOptions>any()))
382+
.thenReturn(mockClientCall);
383+
384+
GrpcCallContext context = defaultCallContext.withChannel(mockChannel).withTracer(mockTracer);
385+
GrpcClientCalls.newCall(descriptor, context).start(mockListener, emptyHeaders);
386+
}
343387
}

sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCalls.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,25 @@ public static <RequestT, ResponseT> HttpJsonClientCall<RequestT, ResponseT> newC
8080
return httpJsonContext.getChannel().newCall(methodDescriptor, httpJsonContext.getCallOptions());
8181
}
8282

83+
static HttpJsonMetadata getMetadataWithTraceContext(HttpJsonCallContext context) {
84+
java.util.Map<String, String> traceHeaders = new java.util.HashMap<>();
85+
context.getTracer().injectTraceContext(traceHeaders);
86+
87+
java.util.Map<String, java.util.List<String>> finalHeaders =
88+
new java.util.HashMap<>(context.getExtraHeaders());
89+
for (java.util.Map.Entry<String, String> entry : traceHeaders.entrySet()) {
90+
finalHeaders.put(entry.getKey(), java.util.Collections.singletonList(entry.getValue()));
91+
}
92+
return HttpJsonMetadata.newBuilder().build().withHeaders(finalHeaders);
93+
}
94+
8395
static <RequestT, ResponseT> ApiFuture<ResponseT> futureUnaryCall(
8496
HttpJsonClientCall<RequestT, ResponseT> clientCall,
8597
RequestT request,
8698
HttpJsonCallContext context) {
8799
// Start the call
88100
HttpJsonFuture<ResponseT> future = new HttpJsonFuture<>(clientCall);
89-
clientCall.start(
90-
new FutureListener<>(future),
91-
HttpJsonMetadata.newBuilder().build().withHeaders(context.getExtraHeaders()));
101+
clientCall.start(new FutureListener<>(future), getMetadataWithTraceContext(context));
92102

93103
// Send the request
94104
try {

sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientCallsTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,35 @@ void testUniverseDomainNotReady_shouldRetry() throws IOException {
139139
.isEqualTo(HttpJsonStatusCode.Code.UNAUTHENTICATED);
140140
Mockito.verify(mockChannel, Mockito.never()).newCall(descriptor, callOptions);
141141
}
142+
143+
@Test
144+
void testGetMetadataWithTraceContext() {
145+
com.google.api.gax.tracing.ApiTracer mockTracer =
146+
Mockito.mock(com.google.api.gax.tracing.ApiTracer.class);
147+
Mockito.doAnswer(
148+
invocation -> {
149+
java.util.Map<String, String> carrier = invocation.getArgument(0);
150+
carrier.put("traceparent", "00-00000000000000000000000000000001-0000000000000002-01");
151+
return null;
152+
})
153+
.when(mockTracer)
154+
.injectTraceContext(Mockito.anyMap());
155+
156+
java.util.Map<String, java.util.List<String>> extraHeaders = new java.util.HashMap<>();
157+
extraHeaders.put("existing-header", java.util.Collections.singletonList("existing-value"));
158+
159+
HttpJsonCallContext context =
160+
(HttpJsonCallContext)
161+
HttpJsonCallContext.createDefault()
162+
.withTracer(mockTracer)
163+
.withExtraHeaders(extraHeaders);
164+
165+
HttpJsonMetadata metadata = HttpJsonClientCalls.getMetadataWithTraceContext(context);
166+
167+
assertThat(metadata.getHeaders()).containsKey("existing-header");
168+
assertThat(metadata.getHeaders().get("existing-header").toString()).contains("existing-value");
169+
assertThat(metadata.getHeaders()).containsKey("traceparent");
170+
assertThat(metadata.getHeaders().get("traceparent").toString())
171+
.contains("00-00000000000000000000000000000001-0000000000000002-01");
172+
}
142173
}

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ default void requestSent() {}
196196
default void batchRequestSent(long elementCount, long requestSize) {}
197197
;
198198

199+
/** Extract the trace context from the tracer and add it to the given headers map. */
200+
default void injectTraceContext(java.util.Map<String, String> carrier) {}
201+
199202
/**
200203
* Annotates the attempt with the full resolved HTTP URL. Only relevant for HTTP transport.
201204
*

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,11 @@ public void batchRequestSent(long elementCount, long requestSize) {
213213
child.batchRequestSent(elementCount, requestSize);
214214
}
215215
}
216+
217+
@Override
218+
public void injectTraceContext(java.util.Map<String, String> carrier) {
219+
for (ApiTracer child : children) {
220+
child.injectTraceContext(carrier);
221+
}
222+
}
216223
}

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,23 @@ public class SpanTracer implements ApiTracer {
5757
private final ApiTracerContext apiTracerContext;
5858
private Span attemptSpan;
5959

60+
@Override
61+
public void injectTraceContext(java.util.Map<String, String> carrier) {
62+
if (attemptSpan != null) {
63+
io.opentelemetry.context.Context context =
64+
io.opentelemetry.context.Context.current().with(attemptSpan);
65+
io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator.getInstance()
66+
.inject(
67+
context,
68+
carrier,
69+
(c, k, v) -> {
70+
if (c != null) {
71+
c.put(k, v);
72+
}
73+
});
74+
}
75+
}
76+
6077
/**
6178
* Creates a new instance of {@code SpanTracer}.
6279
*

sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,13 @@ void testBatchRequestSent() {
270270
verify(child1).batchRequestSent(10L, 100L);
271271
verify(child2).batchRequestSent(10L, 100L);
272272
}
273+
274+
@Test
275+
void testInjectTraceContext() {
276+
java.util.Map<String, String> carrier = new java.util.HashMap<>();
277+
compositeTracer.injectTraceContext(carrier);
278+
InOrder inOrder = inOrder(child1, child2);
279+
inOrder.verify(child1).injectTraceContext(carrier);
280+
inOrder.verify(child2).injectTraceContext(carrier);
281+
}
273282
}

sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,4 +617,27 @@ public RedirectException(String message) {
617617
}
618618

619619
private static class UnknownClientException extends RuntimeException {}
620+
621+
@Test
622+
void testInjectTraceContext_addsHeaders() {
623+
io.opentelemetry.api.trace.SpanContext mockSpanContext =
624+
io.opentelemetry.api.trace.SpanContext.create(
625+
"00000000000000000000000000000001",
626+
"0000000000000002",
627+
io.opentelemetry.api.trace.TraceFlags.getSampled(),
628+
io.opentelemetry.api.trace.TraceState.getDefault());
629+
io.opentelemetry.api.trace.Span realSpan =
630+
io.opentelemetry.api.trace.Span.wrap(mockSpanContext);
631+
when(spanBuilder.startSpan()).thenReturn(realSpan);
632+
633+
spanTracer = new SpanTracer(tracer, ApiTracerContext.empty(), ATTEMPT_SPAN_NAME);
634+
spanTracer.attemptStarted(new Object(), 1);
635+
636+
java.util.Map<String, String> carrier = new java.util.HashMap<>();
637+
spanTracer.injectTraceContext(carrier);
638+
639+
assertThat(carrier).containsKey("traceparent");
640+
assertThat(carrier.get("traceparent")).contains("00000000000000000000000000000001");
641+
assertThat(carrier.get("traceparent")).contains("0000000000000002");
642+
}
620643
}

sdk-platform-java/java-showcase/gapic-showcase/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
</parent>
1818

1919
<properties>
20-
<gapic-showcase.version>0.36.2</gapic-showcase.version>
20+
<gapic-showcase.version>0.39.0</gapic-showcase.version>
2121
<!-- This is the last version supporting slf4j 1.x, do not upgrade -->
2222
<slf4j1-logback.version>1.2.13</slf4j1-logback.version>
2323
<slf4j2-logback.version>1.5.25</slf4j2-logback.version>

0 commit comments

Comments
 (0)