Skip to content

Commit f0eaa5f

Browse files
Sergei Malafeevtrask
Sergei Malafeev
andauthored
JDK 11 HttpClient instrumentation (#1121)
* JDK 11 HttpClient instrumentation Signed-off-by: Sergei Malafeev <[email protected]> * get rid of semicolon Signed-off-by: Sergei Malafeev <[email protected]> * trace sync request Signed-off-by: Sergei Malafeev <[email protected]> * get rid of unnecessary gradle options Signed-off-by: Sergei Malafeev <[email protected]> * wip Signed-off-by: Sergei Malafeev <[email protected]> * end span when future completed Signed-off-by: Sergei Malafeev <[email protected]> * remove unneeded check Signed-off-by: Sergei Malafeev <[email protected]> * get rid of repeated test code Signed-off-by: Sergei Malafeev <[email protected]> * get rid of unneeded code Signed-off-by: Sergei Malafeev <[email protected]> * remove semicolon Signed-off-by: Sergei Malafeev <[email protected]> * implement review comments Signed-off-by: Sergei Malafeev <[email protected]> * Update instrumentation/httpclient/src/main/java/io/opentelemetry/instrumentation/auto/httpclient/HttpClientInstrumentation.java Co-authored-by: Trask Stalnaker <[email protected]> * Update instrumentation/httpclient/src/main/java/io/opentelemetry/instrumentation/auto/httpclient/HttpClientInstrumentation.java Co-authored-by: Trask Stalnaker <[email protected]> * Update instrumentation/httpclient/src/test/groovy/JdkHttpClientTest.groovy Co-authored-by: Trask Stalnaker <[email protected]> * rename to java-httpclient Signed-off-by: Sergei Malafeev <[email protected]> * remove unused Signed-off-by: Sergei Malafeev <[email protected]> * follow redirects Signed-off-by: Sergei Malafeev <[email protected]> * remove semicolon Signed-off-by: Sergei Malafeev <[email protected]> Co-authored-by: Trask Stalnaker <[email protected]>
1 parent 16b7f5b commit f0eaa5f

File tree

11 files changed

+535
-0
lines changed

11 files changed

+535
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
ext {
2+
minJavaVersionForTests = JavaVersion.VERSION_11
3+
}
4+
5+
apply from: "$rootDir/gradle/instrumentation.gradle"
6+
7+
muzzle {
8+
pass {
9+
coreJdk()
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright The OpenTelemetry 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.auto.httpclient;
18+
19+
import static io.opentelemetry.instrumentation.auto.httpclient.JdkHttpClientTracer.TRACER;
20+
import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed;
21+
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.extendsClass;
22+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
23+
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
24+
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
25+
import static net.bytebuddy.matcher.ElementMatchers.named;
26+
import static net.bytebuddy.matcher.ElementMatchers.not;
27+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
28+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
29+
30+
import com.google.auto.service.AutoService;
31+
import io.opentelemetry.context.Scope;
32+
import io.opentelemetry.instrumentation.auto.api.CallDepthThreadLocalMap.Depth;
33+
import io.opentelemetry.javaagent.tooling.Instrumenter;
34+
import io.opentelemetry.trace.Span;
35+
import java.net.http.HttpRequest;
36+
import java.net.http.HttpResponse;
37+
import java.util.HashMap;
38+
import java.util.Map;
39+
import java.util.concurrent.CompletableFuture;
40+
import net.bytebuddy.asm.Advice;
41+
import net.bytebuddy.description.method.MethodDescription;
42+
import net.bytebuddy.description.type.TypeDescription;
43+
import net.bytebuddy.matcher.ElementMatcher;
44+
45+
@AutoService(Instrumenter.class)
46+
public class HttpClientInstrumentation extends Instrumenter.Default {
47+
48+
public HttpClientInstrumentation() {
49+
super("httpclient");
50+
}
51+
52+
@Override
53+
public ElementMatcher<ClassLoader> classLoaderMatcher() {
54+
// Optimization for expensive typeMatcher.
55+
return hasClassesNamed("java.net.http.HttpClient");
56+
}
57+
58+
@Override
59+
public ElementMatcher<TypeDescription> typeMatcher() {
60+
return nameStartsWith("java.net.")
61+
.or(nameStartsWith("jdk.internal."))
62+
.and(not(named("jdk.internal.net.http.HttpClientFacade")))
63+
.and(extendsClass(named("java.net.http.HttpClient")));
64+
}
65+
66+
@Override
67+
public String[] helperClassNames() {
68+
return new String[] {
69+
packageName + ".HttpHeadersInjectAdapter",
70+
packageName + ".JdkHttpClientTracer",
71+
packageName + ".ResponseConsumer"
72+
};
73+
}
74+
75+
@Override
76+
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
77+
Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
78+
79+
transformers.put(
80+
isMethod()
81+
.and(named("send"))
82+
.and(isPublic())
83+
.and(takesArguments(2))
84+
.and(takesArgument(0, named("java.net.http.HttpRequest"))),
85+
HttpClientInstrumentation.class.getName() + "$SendAdvice");
86+
87+
transformers.put(
88+
isMethod()
89+
.and(named("sendAsync"))
90+
.and(isPublic())
91+
.and(takesArgument(0, named("java.net.http.HttpRequest"))),
92+
HttpClientInstrumentation.class.getName() + "$SendAsyncAdvice");
93+
94+
return transformers;
95+
}
96+
97+
public static class SendAdvice {
98+
99+
@Advice.OnMethodEnter(suppress = Throwable.class)
100+
public static void methodEnter(
101+
@Advice.Argument(value = 0) HttpRequest httpRequest,
102+
@Advice.Local("otelSpan") Span span,
103+
@Advice.Local("otelScope") Scope scope,
104+
@Advice.Local("otelCallDepth") Depth callDepth) {
105+
106+
callDepth = TRACER.getCallDepth();
107+
if (callDepth.getAndIncrement() == 0) {
108+
span = TRACER.startSpan(httpRequest);
109+
if (span.getContext().isValid()) {
110+
scope = TRACER.startScope(span, httpRequest);
111+
}
112+
}
113+
}
114+
115+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
116+
public static void methodExit(
117+
@Advice.Return HttpResponse result,
118+
@Advice.Thrown Throwable throwable,
119+
@Advice.Local("otelSpan") Span span,
120+
@Advice.Local("otelScope") Scope scope,
121+
@Advice.Local("otelCallDepth") Depth callDepth) {
122+
123+
if (callDepth.decrementAndGet() == 0 && scope != null) {
124+
scope.close();
125+
if (throwable == null) {
126+
TRACER.end(span, result);
127+
} else {
128+
TRACER.endExceptionally(span, result, throwable);
129+
}
130+
}
131+
}
132+
}
133+
134+
public static class SendAsyncAdvice {
135+
136+
@Advice.OnMethodEnter(suppress = Throwable.class)
137+
public static void methodEnter(
138+
@Advice.Argument(value = 0) HttpRequest httpRequest,
139+
@Advice.Local("otelSpan") Span span,
140+
@Advice.Local("otelScope") Scope scope,
141+
@Advice.Local("otelCallDepth") Depth callDepth) {
142+
143+
callDepth = TRACER.getCallDepth();
144+
if (callDepth.getAndIncrement() == 0) {
145+
span = TRACER.startSpan(httpRequest);
146+
if (span.getContext().isValid()) {
147+
scope = TRACER.startScope(span, httpRequest);
148+
}
149+
}
150+
}
151+
152+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
153+
public static void methodExit(
154+
@Advice.Return(readOnly = false) CompletableFuture<HttpResponse<?>> future,
155+
@Advice.Thrown Throwable throwable,
156+
@Advice.Local("otelSpan") Span span,
157+
@Advice.Local("otelScope") Scope scope,
158+
@Advice.Local("otelCallDepth") Depth callDepth) {
159+
160+
if (callDepth.decrementAndGet() == 0 && scope != null) {
161+
scope.close();
162+
if (throwable != null) {
163+
TRACER.endExceptionally(span, null, throwable);
164+
} else {
165+
future = future.whenComplete(new ResponseConsumer(span));
166+
}
167+
}
168+
}
169+
}
170+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright The OpenTelemetry 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.auto.httpclient;
18+
19+
import io.opentelemetry.context.propagation.TextMapPropagator;
20+
import java.net.http.HttpRequest;
21+
22+
/** Context propagation is implemented via {@link HttpHeadersInstrumentation} */
23+
public class HttpHeadersInjectAdapter implements TextMapPropagator.Setter<HttpRequest> {
24+
public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter();
25+
26+
@Override
27+
public void set(HttpRequest carrier, String key, String value) {
28+
// Don't do anything because headers are immutable
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright The OpenTelemetry 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.auto.httpclient;
18+
19+
import static io.opentelemetry.instrumentation.auto.httpclient.JdkHttpClientTracer.TRACER;
20+
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.extendsClass;
21+
import static java.util.Collections.singletonMap;
22+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
23+
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
24+
import static net.bytebuddy.matcher.ElementMatchers.named;
25+
26+
import com.google.auto.service.AutoService;
27+
import io.opentelemetry.javaagent.tooling.Instrumenter;
28+
import java.net.http.HttpHeaders;
29+
import java.util.Map;
30+
import net.bytebuddy.asm.Advice;
31+
import net.bytebuddy.description.method.MethodDescription;
32+
import net.bytebuddy.description.type.TypeDescription;
33+
import net.bytebuddy.matcher.ElementMatcher;
34+
35+
@AutoService(Instrumenter.class)
36+
public class HttpHeadersInstrumentation extends Instrumenter.Default {
37+
38+
public HttpHeadersInstrumentation() {
39+
super("httpclient");
40+
}
41+
42+
@Override
43+
public ElementMatcher<TypeDescription> typeMatcher() {
44+
return nameStartsWith("java.net.")
45+
.or(nameStartsWith("jdk.internal."))
46+
.and(extendsClass(named("java.net.http.HttpRequest")));
47+
}
48+
49+
@Override
50+
public String[] helperClassNames() {
51+
return new String[] {
52+
packageName + ".HttpHeadersInjectAdapter", packageName + ".JdkHttpClientTracer"
53+
};
54+
}
55+
56+
@Override
57+
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
58+
return singletonMap(
59+
isMethod().and(named("headers")),
60+
HttpHeadersInstrumentation.class.getName() + "$HeadersAdvice");
61+
}
62+
63+
public static class HeadersAdvice {
64+
65+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
66+
public static void methodExit(@Advice.Return(readOnly = false) HttpHeaders headers) {
67+
if (TRACER.getCurrentSpan().isRecording()) {
68+
headers = TRACER.inject(headers);
69+
}
70+
}
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright The OpenTelemetry 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.auto.httpclient;
18+
19+
import io.grpc.Context;
20+
import io.opentelemetry.OpenTelemetry;
21+
import io.opentelemetry.context.propagation.TextMapPropagator.Setter;
22+
import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer;
23+
import io.opentelemetry.instrumentation.auto.api.CallDepthThreadLocalMap;
24+
import io.opentelemetry.instrumentation.auto.api.CallDepthThreadLocalMap.Depth;
25+
import java.net.URI;
26+
import java.net.http.HttpClient;
27+
import java.net.http.HttpHeaders;
28+
import java.net.http.HttpRequest;
29+
import java.net.http.HttpResponse;
30+
import java.util.Collections;
31+
import java.util.HashMap;
32+
import java.util.List;
33+
import java.util.Map;
34+
import java.util.concurrent.CompletionException;
35+
36+
public class JdkHttpClientTracer extends HttpClientTracer<HttpRequest, HttpRequest, HttpResponse> {
37+
public static final JdkHttpClientTracer TRACER = new JdkHttpClientTracer();
38+
39+
public Depth getCallDepth() {
40+
return CallDepthThreadLocalMap.getCallDepth(HttpClient.class);
41+
}
42+
43+
@Override
44+
protected String getInstrumentationName() {
45+
return "io.opentelemetry.auto.java-httpclient";
46+
}
47+
48+
@Override
49+
protected String method(HttpRequest httpRequest) {
50+
return httpRequest.method();
51+
}
52+
53+
@Override
54+
protected URI url(HttpRequest httpRequest) {
55+
return httpRequest.uri();
56+
}
57+
58+
@Override
59+
protected Integer status(HttpResponse httpResponse) {
60+
return httpResponse.statusCode();
61+
}
62+
63+
@Override
64+
protected String requestHeader(HttpRequest httpRequest, String name) {
65+
return httpRequest.headers().firstValue(name).orElse(null);
66+
}
67+
68+
@Override
69+
protected String responseHeader(HttpResponse httpResponse, String name) {
70+
return httpResponse.headers().firstValue(name).orElse(null);
71+
}
72+
73+
@Override
74+
protected Setter<HttpRequest> getSetter() {
75+
return HttpHeadersInjectAdapter.SETTER;
76+
}
77+
78+
@Override
79+
protected Throwable unwrapThrowable(Throwable throwable) {
80+
if (throwable instanceof CompletionException) {
81+
return throwable.getCause();
82+
}
83+
return super.unwrapThrowable(throwable);
84+
}
85+
86+
public HttpHeaders inject(HttpHeaders original) {
87+
Map<String, List<String>> headerMap = new HashMap<>();
88+
89+
OpenTelemetry.getPropagators()
90+
.getTextMapPropagator()
91+
.inject(
92+
Context.current(),
93+
headerMap,
94+
(carrier, key, value) -> carrier.put(key, Collections.singletonList(value)));
95+
headerMap.putAll(original.map());
96+
97+
return HttpHeaders.of(headerMap, (s, s2) -> true);
98+
}
99+
}

0 commit comments

Comments
 (0)