Skip to content

Commit f4fa189

Browse files
authored
Capture HTTP2 headers in netty gRPC client/server (#158)
* Capture HTTP2 headers in gRPC client/server Signed-off-by: Pavol Loffay <[email protected]> * Fixes Signed-off-by: Pavol Loffay <[email protected]> * Fixes 2 Signed-off-by: Pavol Loffay <[email protected]> * Use suffix for HT keys Signed-off-by: Pavol Loffay <[email protected]> * Use . Signed-off-by: Pavol Loffay <[email protected]> * Add unique name Signed-off-by: Pavol Loffay <[email protected]> * Fix Signed-off-by: Pavol Loffay <[email protected]>
1 parent a7e502d commit f4fa189

File tree

5 files changed

+240
-1
lines changed

5 files changed

+240
-1
lines changed

instrumentation/grpc-1.5/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ muzzle {
1616
versions = "[1.5.0, 1.33.0)" // see https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1453
1717
// for body capture via com.google.protobuf.util.JsonFormat
1818
extraDependency("io.grpc:grpc-protobuf:1.5.0")
19+
extraDependency("io.grpc:grpc-netty:1.5.0")
1920
}
2021
}
2122

@@ -57,11 +58,12 @@ val versions: Map<String, String> by extra
5758

5859
dependencies {
5960
api("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-grpc-1.5:${versions["opentelemetry_java_agent"]}")
60-
api("io.opentelemetry.instrumentation:opentelemetry-grpc-1.5:0.11.0")
61+
api("io.opentelemetry.instrumentation:opentelemetry-grpc-1.5:${versions["opentelemetry_java_agent"]}")
6162

6263
compileOnly("io.grpc:grpc-core:1.5.0")
6364
compileOnly("io.grpc:grpc-protobuf:1.5.0")
6465
compileOnly("io.grpc:grpc-stub:1.5.0")
66+
compileOnly("io.grpc:grpc-netty:1.5.0")
6567

6668
implementation("javax.annotation:javax.annotation-api:1.3.2")
6769

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright The Hypertrace 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.opentelemetry.instrumentation.hypertrace.grpc.v1_5;
18+
19+
import io.grpc.Metadata;
20+
21+
public class GrpcSemanticAttributes {
22+
private GrpcSemanticAttributes() {}
23+
24+
public static final String SCHEME = ":scheme";
25+
public static final String PATH = ":path";
26+
public static final String AUTHORITY = ":authority";
27+
public static final String METHOD = ":method";
28+
29+
/**
30+
* These metadata headers are added in Http2Headers instrumentation. We use different names than
31+
* original HTTP2 header names to avoid any collisions with app code.
32+
*
33+
* <p>We cannot use prefix because e.g. ht.:path is not a valid key.
34+
*/
35+
private static final String SUFFIX = ".ht";
36+
37+
public static final Metadata.Key<String> SCHEME_METADATA_KEY =
38+
Metadata.Key.of(SCHEME + SUFFIX, Metadata.ASCII_STRING_MARSHALLER);
39+
public static final Metadata.Key<String> PATH_METADATA_KEY =
40+
Metadata.Key.of(PATH + SUFFIX, Metadata.ASCII_STRING_MARSHALLER);
41+
public static final Metadata.Key<String> AUTHORITY_METADATA_KEY =
42+
Metadata.Key.of(AUTHORITY + SUFFIX, Metadata.ASCII_STRING_MARSHALLER);
43+
public static final Metadata.Key<String> METHOD_METADATA_KEY =
44+
Metadata.Key.of(METHOD + SUFFIX, Metadata.ASCII_STRING_MARSHALLER);
45+
46+
public static String removeHypertracePrefix(String key) {
47+
if (key.endsWith(SUFFIX)) {
48+
return key.replace(SUFFIX, "");
49+
}
50+
return key;
51+
}
52+
}

instrumentation/grpc-1.5/src/main/java/io/opentelemetry/instrumentation/hypertrace/grpc/v1_5/GrpcSpanDecorator.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public static void addMetadataAttributes(
5858
Key<String> stringKey = Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
5959
Iterable<String> stringValues = metadata.getAll(stringKey);
6060
for (String stringValue : stringValues) {
61+
key = GrpcSemanticAttributes.removeHypertracePrefix(key);
6162
span.setAttribute(keySupplier.apply(key), stringValue);
6263
}
6364
}
@@ -79,6 +80,7 @@ public static Map<String, String> metadataToMap(Metadata metadata) {
7980
Key<String> stringKey = Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
8081
Iterable<String> stringValues = metadata.getAll(stringKey);
8182
for (String stringValue : stringValues) {
83+
key = GrpcSemanticAttributes.removeHypertracePrefix(key);
8284
mapHeaders.put(key, stringValue);
8385
}
8486
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright The Hypertrace 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.opentelemetry.instrumentation.hypertrace.grpc.v1_5;
18+
19+
import static net.bytebuddy.matcher.ElementMatchers.failSafe;
20+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
21+
import static net.bytebuddy.matcher.ElementMatchers.named;
22+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
23+
24+
import com.google.auto.service.AutoService;
25+
import io.grpc.Metadata;
26+
import io.netty.handler.codec.http2.Http2Headers;
27+
import io.opentelemetry.api.trace.Span;
28+
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
29+
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
30+
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
31+
import java.util.ArrayList;
32+
import java.util.Arrays;
33+
import java.util.HashMap;
34+
import java.util.List;
35+
import java.util.Map;
36+
import net.bytebuddy.asm.Advice;
37+
import net.bytebuddy.description.method.MethodDescription;
38+
import net.bytebuddy.description.type.TypeDescription;
39+
import net.bytebuddy.matcher.ElementMatcher;
40+
import org.hypertrace.agent.core.HypertraceSemanticAttributes;
41+
42+
@AutoService(InstrumentationModule.class)
43+
public class NettyHttp2HeadersInstrumentationModule extends InstrumentationModule {
44+
45+
private static final List<String> INSTRUMENTATION_NAMES = new ArrayList<>();
46+
47+
static {
48+
INSTRUMENTATION_NAMES.add(GrpcInstrumentationName.PRIMARY);
49+
INSTRUMENTATION_NAMES.addAll(Arrays.asList(GrpcInstrumentationName.OTHER));
50+
INSTRUMENTATION_NAMES.add("grpc-netty-ht");
51+
}
52+
53+
public NettyHttp2HeadersInstrumentationModule() {
54+
super(INSTRUMENTATION_NAMES);
55+
}
56+
57+
@Override
58+
public List<TypeInstrumentation> typeInstrumentations() {
59+
return Arrays.asList(new NettyUtilsInstrumentation());
60+
}
61+
62+
/**
63+
* The server side HTTP2 headers are added in tracing gRPC interceptor. The headers are added to
64+
* metadata in {@link GrpcUtils_convertHeaders_Advice}.
65+
*
66+
* <p>The client side HTTP2 headers are added directly to span in {@link
67+
* Utils_convertClientHeaders_Advice}. TODO However it does not work for the first request
68+
* https://github.com/hypertrace/javaagent/issues/109#issuecomment-740918018.
69+
*/
70+
class NettyUtilsInstrumentation implements TypeInstrumentation {
71+
@Override
72+
public ElementMatcher<? super TypeDescription> typeMatcher() {
73+
return failSafe(named("io.grpc.netty.Utils"));
74+
}
75+
76+
@Override
77+
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
78+
Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
79+
transformers.put(
80+
isMethod().and(named("convertClientHeaders")).and(takesArguments(6)),
81+
Utils_convertClientHeaders_Advice.class.getName());
82+
transformers.put(
83+
isMethod().and(named("convertHeaders")).and(takesArguments(1)),
84+
GrpcUtils_convertHeaders_Advice.class.getName());
85+
return transformers;
86+
}
87+
}
88+
89+
static class Utils_convertClientHeaders_Advice {
90+
@Advice.OnMethodExit(suppress = Throwable.class)
91+
public static void exit(
92+
@Advice.Argument(1) Object scheme,
93+
@Advice.Argument(2) Object defaultPath,
94+
@Advice.Argument(3) Object authority,
95+
@Advice.Argument(4) Object method) {
96+
97+
Span currentSpan = Java8BytecodeBridge.currentSpan();
98+
if (scheme != null) {
99+
currentSpan.setAttribute(
100+
HypertraceSemanticAttributes.rpcRequestMetadata(GrpcSemanticAttributes.SCHEME),
101+
scheme.toString());
102+
}
103+
if (defaultPath != null) {
104+
currentSpan.setAttribute(
105+
HypertraceSemanticAttributes.rpcRequestMetadata(GrpcSemanticAttributes.PATH),
106+
defaultPath.toString());
107+
}
108+
if (authority != null) {
109+
currentSpan.setAttribute(
110+
HypertraceSemanticAttributes.rpcRequestMetadata(GrpcSemanticAttributes.AUTHORITY),
111+
authority.toString());
112+
}
113+
if (method != null) {
114+
currentSpan.setAttribute(
115+
HypertraceSemanticAttributes.rpcRequestMetadata(GrpcSemanticAttributes.METHOD),
116+
method.toString());
117+
}
118+
}
119+
}
120+
121+
/**
122+
* There are multiple implementations of {@link Http2Headers}. Only some of them support getting
123+
* authority, path etc. For instance {@code GrpcHttp2ResponseHeaders} throws unsupported exception
124+
* when accessing authority etc. This header is used client response.
125+
*
126+
* @see {@link io.grpc.netty.GrpcHttp2HeadersUtils}
127+
*/
128+
static class GrpcUtils_convertHeaders_Advice {
129+
@Advice.OnMethodExit(suppress = Throwable.class)
130+
public static void exit(
131+
@Advice.Argument(0) Http2Headers http2Headers, @Advice.Return Metadata metadata) {
132+
133+
if (http2Headers.authority() != null) {
134+
metadata.put(
135+
GrpcSemanticAttributes.AUTHORITY_METADATA_KEY, http2Headers.authority().toString());
136+
}
137+
if (http2Headers.path() != null) {
138+
metadata.put(GrpcSemanticAttributes.PATH_METADATA_KEY, http2Headers.path().toString());
139+
}
140+
if (http2Headers.method() != null) {
141+
metadata.put(GrpcSemanticAttributes.METHOD_METADATA_KEY, http2Headers.method().toString());
142+
}
143+
if (http2Headers.scheme() != null) {
144+
metadata.put(GrpcSemanticAttributes.SCHEME_METADATA_KEY, http2Headers.scheme().toString());
145+
}
146+
}
147+
}
148+
}

instrumentation/grpc-1.5/src/test/java/io/opentelemetry/instrumentation/hypertrace/grpc/v1_5/GrpcInstrumentationTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,16 @@
4848
import org.junit.jupiter.api.AfterEach;
4949
import org.junit.jupiter.api.Assertions;
5050
import org.junit.jupiter.api.BeforeAll;
51+
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
52+
import org.junit.jupiter.api.Order;
5153
import org.junit.jupiter.api.Test;
54+
import org.junit.jupiter.api.TestMethodOrder;
5255

56+
/**
57+
* TODO the HTTP2 headers for client does not work for the first request - therefore the explicit
58+
* ordering https://github.com/hypertrace/javaagent/issues/109#issuecomment-740918018
59+
*/
60+
@TestMethodOrder(OrderAnnotation.class)
5361
public class GrpcInstrumentationTest extends AbstractInstrumenterTest {
5462

5563
private static final Helloworld.Request REQUEST =
@@ -113,6 +121,7 @@ public void afterEach() {
113121
}
114122

115123
@Test
124+
@Order(2)
116125
public void blockingStub() throws IOException, TimeoutException, InterruptedException {
117126
Metadata headers = new Metadata();
118127
headers.put(CLIENT_STRING_METADATA_KEY, "clientheader");
@@ -135,9 +144,13 @@ public void blockingStub() throws IOException, TimeoutException, InterruptedExce
135144
assertBodiesAndHeaders(clientSpan, requestJson, responseJson);
136145
SpanData serverSpan = spans.get(1);
137146
assertBodiesAndHeaders(serverSpan, requestJson, responseJson);
147+
148+
assertHttp2HeadersForSayHelloMethod(serverSpan);
149+
assertHttp2HeadersForSayHelloMethod(clientSpan);
138150
}
139151

140152
@Test
153+
@Order(1)
141154
public void serverRequestBlocking() throws TimeoutException, InterruptedException {
142155
Metadata blockHeaders = new Metadata();
143156
blockHeaders.put(Metadata.Key.of("mockblock", Metadata.ASCII_STRING_MARSHALLER), "true");
@@ -167,9 +180,11 @@ public void serverRequestBlocking() throws TimeoutException, InterruptedExceptio
167180
serverSpan
168181
.getAttributes()
169182
.get(HypertraceSemanticAttributes.rpcRequestMetadata("mockblock")));
183+
assertHttp2HeadersForSayHelloMethod(serverSpan);
170184
}
171185

172186
@Test
187+
@Order(3)
173188
public void disabledInstrumentation_dynamicConfig()
174189
throws TimeoutException, InterruptedException {
175190
URL configUrl = getClass().getClassLoader().getResource("ht-config-all-disabled.yaml");
@@ -215,4 +230,24 @@ private void assertBodiesAndHeaders(SpanData span, String requestJson, String re
215230
HypertraceSemanticAttributes.rpcResponseMetadata(
216231
SERVER_STRING_METADATA_KEY.name())));
217232
}
233+
234+
private void assertHttp2HeadersForSayHelloMethod(SpanData span) {
235+
Assertions.assertEquals(
236+
"http",
237+
span.getAttributes()
238+
.get(HypertraceSemanticAttributes.rpcRequestMetadata(GrpcSemanticAttributes.SCHEME)));
239+
Assertions.assertEquals(
240+
"POST",
241+
span.getAttributes()
242+
.get(HypertraceSemanticAttributes.rpcRequestMetadata(GrpcSemanticAttributes.METHOD)));
243+
Assertions.assertEquals(
244+
String.format("localhost:%d", SERVER.getPort()),
245+
span.getAttributes()
246+
.get(
247+
HypertraceSemanticAttributes.rpcRequestMetadata(GrpcSemanticAttributes.AUTHORITY)));
248+
Assertions.assertEquals(
249+
"/org.hypertrace.example.Greeter/SayHello",
250+
span.getAttributes()
251+
.get(HypertraceSemanticAttributes.rpcRequestMetadata(GrpcSemanticAttributes.PATH)));
252+
}
218253
}

0 commit comments

Comments
 (0)