Skip to content

Commit 5b75b3f

Browse files
lauritjaydeluca
andauthored
Add url template to spring 6 RestTemplate (#14612)
Co-authored-by: Jay DeLuca <[email protected]>
1 parent a26a32a commit 5b75b3f

File tree

15 files changed

+347
-21
lines changed

15 files changed

+347
-21
lines changed

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
import com.google.errorprone.annotations.CanIgnoreReturnValue;
99
import io.opentelemetry.api.OpenTelemetry;
1010
import io.opentelemetry.api.common.AttributeKey;
11+
import io.opentelemetry.context.Context;
1112
import io.opentelemetry.context.propagation.TextMapSetter;
1213
import io.opentelemetry.instrumentation.api.incubator.config.internal.CommonConfig;
1314
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalAttributesGetter;
1415
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalMetrics;
1516
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientPeerServiceAttributesExtractor;
17+
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientUrlTemplate;
1618
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor;
1719
import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceResolver;
1820
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
@@ -82,7 +84,7 @@ public static <REQUEST, RESPONSE> DefaultHttpClientInstrumenterBuilder<REQUEST,
8284
String instrumentationName,
8385
OpenTelemetry openTelemetry,
8486
HttpClientAttributesGetter<REQUEST, RESPONSE> attributesGetter) {
85-
return new DefaultHttpClientInstrumenterBuilder<REQUEST, RESPONSE>(
87+
return new DefaultHttpClientInstrumenterBuilder<>(
8688
instrumentationName, openTelemetry, attributesGetter, null);
8789
}
8890

@@ -91,7 +93,7 @@ public static <REQUEST, RESPONSE> DefaultHttpClientInstrumenterBuilder<REQUEST,
9193
OpenTelemetry openTelemetry,
9294
HttpClientAttributesGetter<REQUEST, RESPONSE> attributesGetter,
9395
TextMapSetter<REQUEST> headerSetter) {
94-
return new DefaultHttpClientInstrumenterBuilder<REQUEST, RESPONSE>(
96+
return new DefaultHttpClientInstrumenterBuilder<>(
9597
instrumentationName,
9698
openTelemetry,
9799
attributesGetter,
@@ -219,12 +221,20 @@ public DefaultHttpClientInstrumenterBuilder<REQUEST, RESPONSE> setBuilderCustomi
219221
}
220222

221223
public Instrumenter<REQUEST, RESPONSE> build() {
222-
if (emitExperimentalHttpClientTelemetry
223-
&& attributesGetter instanceof HttpClientExperimentalAttributesGetter) {
224-
HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE> experimentalAttributesGetter =
225-
(HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE>) attributesGetter;
224+
if (emitExperimentalHttpClientTelemetry) {
225+
Function<REQUEST, String> urlTemplateExtractorFunction = unused -> null;
226+
if (attributesGetter instanceof HttpClientExperimentalAttributesGetter) {
227+
HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE> experimentalAttributesGetter =
228+
(HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE>) attributesGetter;
229+
urlTemplateExtractorFunction = experimentalAttributesGetter::getUrlTemplate;
230+
}
231+
Function<REQUEST, String> urlTemplateExtractor = urlTemplateExtractorFunction;
226232
Experimental.setUrlTemplateExtractor(
227-
httpSpanNameExtractorBuilder, experimentalAttributesGetter::getUrlTemplate);
233+
httpSpanNameExtractorBuilder,
234+
request -> {
235+
String urlTemplate = HttpClientUrlTemplate.get(Context.current());
236+
return urlTemplate != null ? urlTemplate : urlTemplateExtractor.apply(request);
237+
});
228238
}
229239
SpanNameExtractor<? super REQUEST> spanNameExtractor =
230240
spanNameExtractorTransformer.apply(httpSpanNameExtractorBuilder.build());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.semconv.http;
7+
8+
import io.opentelemetry.context.Context;
9+
import io.opentelemetry.context.ContextKey;
10+
import io.opentelemetry.context.ImplicitContextKeyed;
11+
import io.opentelemetry.context.Scope;
12+
import javax.annotation.Nullable;
13+
14+
/** A helper class for setting {@code url.template} attribute value for HTTP client calls. */
15+
public final class HttpClientUrlTemplate {
16+
17+
/**
18+
* Add url template to context and make the new context current. Http client calls made while the
19+
* context is active will set {@code url.template} attribute to the supplied value.
20+
*/
21+
public static Scope with(Context context, String urlTemplate) {
22+
return context.with(new UrlTemplateState(urlTemplate)).makeCurrent();
23+
}
24+
25+
@Nullable
26+
public static String get(Context context) {
27+
UrlTemplateState state = UrlTemplateState.fromContextOrNull(context);
28+
return state != null ? state.urlTemplate : null;
29+
}
30+
31+
private HttpClientUrlTemplate() {}
32+
33+
private static class UrlTemplateState implements ImplicitContextKeyed {
34+
35+
private static final ContextKey<UrlTemplateState> KEY =
36+
ContextKey.named("opentelemetry-http-client-url-template-key");
37+
38+
private final String urlTemplate;
39+
40+
@Nullable
41+
static UrlTemplateState fromContextOrNull(Context context) {
42+
return context.get(KEY);
43+
}
44+
45+
UrlTemplateState(String urlTemplate) {
46+
this.urlTemplate = urlTemplate;
47+
}
48+
49+
@Override
50+
public Context storeInContext(Context context) {
51+
return context.with(KEY, this);
52+
}
53+
}
54+
}

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractor.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ public void onEnd(
6565
internalSet(attributes, HTTP_RESPONSE_BODY_SIZE, responseBodySize);
6666
}
6767

68-
if (getter instanceof HttpClientExperimentalAttributesGetter) {
68+
String urlTemplate = HttpClientUrlTemplate.get(context);
69+
if (urlTemplate != null) {
70+
internalSet(attributes, URL_TEMPLATE, urlTemplate);
71+
} else if (getter instanceof HttpClientExperimentalAttributesGetter) {
6972
HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE> experimentalGetter =
7073
(HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE>) getter;
7174
internalSet(attributes, URL_TEMPLATE, experimentalGetter.getUrlTemplate(request));

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalMetricsAdvice.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55

66
package io.opentelemetry.instrumentation.api.incubator.semconv.http;
77

8+
import static io.opentelemetry.api.common.AttributeKey.stringKey;
89
import static java.util.Arrays.asList;
910

11+
import io.opentelemetry.api.common.AttributeKey;
1012
import io.opentelemetry.api.incubator.metrics.ExtendedLongHistogramBuilder;
1113
import io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounterBuilder;
1214
import io.opentelemetry.api.metrics.LongHistogramBuilder;
@@ -18,6 +20,8 @@
1820
import io.opentelemetry.semconv.UrlAttributes;
1921

2022
final class HttpExperimentalMetricsAdvice {
23+
// copied from UrlIncubatingAttributes
24+
private static final AttributeKey<String> URL_TEMPLATE = stringKey("url.template");
2125

2226
static void applyClientRequestSizeAdvice(LongHistogramBuilder builder) {
2327
if (!(builder instanceof ExtendedLongHistogramBuilder)) {
@@ -32,7 +36,8 @@ static void applyClientRequestSizeAdvice(LongHistogramBuilder builder) {
3236
NetworkAttributes.NETWORK_PROTOCOL_NAME,
3337
NetworkAttributes.NETWORK_PROTOCOL_VERSION,
3438
ServerAttributes.SERVER_ADDRESS,
35-
ServerAttributes.SERVER_PORT));
39+
ServerAttributes.SERVER_PORT,
40+
URL_TEMPLATE));
3641
}
3742

3843
static void applyServerRequestSizeAdvice(LongHistogramBuilder builder) {

instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpMetricsAdvice.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55

66
package io.opentelemetry.instrumentation.api.semconv.http;
77

8+
import static io.opentelemetry.api.common.AttributeKey.stringKey;
89
import static java.util.Arrays.asList;
910
import static java.util.Collections.unmodifiableList;
1011

12+
import io.opentelemetry.api.common.AttributeKey;
1113
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder;
1214
import io.opentelemetry.api.metrics.DoubleHistogramBuilder;
1315
import io.opentelemetry.semconv.ErrorAttributes;
@@ -23,6 +25,9 @@ final class HttpMetricsAdvice {
2325
unmodifiableList(
2426
asList(0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0));
2527

28+
// copied from UrlIncubatingAttributes
29+
private static final AttributeKey<String> URL_TEMPLATE = stringKey("url.template");
30+
2631
static void applyClientDurationAdvice(DoubleHistogramBuilder builder) {
2732
if (!(builder instanceof ExtendedDoubleHistogramBuilder)) {
2833
return;
@@ -36,7 +41,9 @@ static void applyClientDurationAdvice(DoubleHistogramBuilder builder) {
3641
NetworkAttributes.NETWORK_PROTOCOL_NAME,
3742
NetworkAttributes.NETWORK_PROTOCOL_VERSION,
3843
ServerAttributes.SERVER_ADDRESS,
39-
ServerAttributes.SERVER_PORT));
44+
ServerAttributes.SERVER_PORT,
45+
// we only add url.template when experimental http client telemetry is enabled
46+
URL_TEMPLATE));
4047
}
4148

4249
static void applyServerDurationAdvice(DoubleHistogramBuilder builder) {

instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpMethodAttributeExtractor.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import io.opentelemetry.api.common.AttributesBuilder;
1212
import io.opentelemetry.api.trace.Span;
1313
import io.opentelemetry.context.Context;
14+
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientUrlTemplate;
1415
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
16+
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
1517
import io.opentelemetry.semconv.HttpAttributes;
1618
import java.net.HttpURLConnection;
1719
import java.util.Set;
@@ -22,9 +24,12 @@ public class HttpMethodAttributeExtractor<
2224
implements AttributesExtractor<REQUEST, RESPONSE> {
2325

2426
private final Set<String> knownMethods;
27+
private final boolean emitExperimentalHttpClientTelemetry;
2528

2629
private HttpMethodAttributeExtractor(Set<String> knownMethods) {
2730
this.knownMethods = knownMethods;
31+
emitExperimentalHttpClientTelemetry =
32+
AgentCommonConfig.get().shouldEmitExperimentalHttpClientTelemetry();
2833
}
2934

3035
public static AttributesExtractor<? super HttpURLConnection, ? super Integer> create(
@@ -55,9 +60,12 @@ public void onEnd(
5560
} else {
5661
internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD, _OTHER);
5762
internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, method);
63+
method = "HTTP";
5864
}
5965
Span span = Span.fromContext(context);
60-
span.updateName(method);
66+
String urlTemplate =
67+
emitExperimentalHttpClientTelemetry ? HttpClientUrlTemplate.get(context) : null;
68+
span.updateName(method + (urlTemplate != null ? " " + urlTemplate : ""));
6169
}
6270
}
6371
}

instrumentation/spring/spring-web/spring-web-6.0/javaagent/build.gradle.kts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,16 @@ muzzle {
1414
}
1515

1616
dependencies {
17-
compileOnly("org.springframework:spring-web:6.0.0")
17+
library("org.springframework:spring-web:6.0.0")
18+
19+
testInstrumentation(project(":instrumentation:http-url-connection:javaagent"))
20+
}
21+
22+
// spring 6 requires java 17
23+
otelJava {
24+
minJavaVersionSupported.set(JavaVersion.VERSION_17)
25+
}
26+
27+
tasks.withType<Test>().configureEach {
28+
jvmArgs("-Dotel.instrumentation.http.client.emit-experimental-telemetry=true")
1829
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.spring.web.v6_0;
7+
8+
import static net.bytebuddy.matcher.ElementMatchers.named;
9+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
10+
11+
import io.opentelemetry.context.Scope;
12+
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientUrlTemplate;
13+
import io.opentelemetry.instrumentation.api.incubator.semconv.net.internal.UrlParser;
14+
import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge;
15+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
16+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
17+
import net.bytebuddy.asm.Advice;
18+
import net.bytebuddy.description.type.TypeDescription;
19+
import net.bytebuddy.matcher.ElementMatcher;
20+
import org.springframework.lang.Nullable;
21+
22+
public class RestTemplateInstrumentation implements TypeInstrumentation {
23+
24+
@Override
25+
public ElementMatcher<TypeDescription> typeMatcher() {
26+
return named("org.springframework.web.client.RestTemplate");
27+
}
28+
29+
@Override
30+
public void transform(TypeTransformer transformer) {
31+
transformer.applyAdviceToMethod(
32+
named("doExecute").and(takesArgument(1, String.class)),
33+
this.getClass().getName() + "$UrlTemplateAdvice");
34+
}
35+
36+
@SuppressWarnings("unused")
37+
public static class UrlTemplateAdvice {
38+
39+
@Advice.OnMethodEnter(suppress = Throwable.class)
40+
public static Scope onEnter(@Advice.Argument(1) String uriTemplate) {
41+
if (uriTemplate != null) {
42+
String path = UrlParser.getPath(uriTemplate);
43+
if (path != null) {
44+
return HttpClientUrlTemplate.with(Java8BytecodeBridge.currentContext(), path);
45+
}
46+
}
47+
return null;
48+
}
49+
50+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
51+
public static void onExit(@Advice.Enter @Nullable Scope scope) {
52+
if (scope != null) {
53+
scope.close();
54+
}
55+
}
56+
}
57+
}

instrumentation/spring/spring-web/spring-web-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/web/v6_0/SpringWebInstrumentationModule.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
package io.opentelemetry.javaagent.instrumentation.spring.web.v6_0;
77

88
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
9-
import static java.util.Collections.singletonList;
9+
import static java.util.Arrays.asList;
1010

1111
import com.google.auto.service.AutoService;
1212
import io.opentelemetry.javaagent.extension.instrumentation.HelperResourceBuilder;
@@ -53,6 +53,6 @@ public String getModuleGroup() {
5353

5454
@Override
5555
public List<TypeInstrumentation> typeInstrumentations() {
56-
return singletonList(new WebApplicationContextInstrumentation());
56+
return asList(new WebApplicationContextInstrumentation(), new RestTemplateInstrumentation());
5757
}
5858
}

instrumentation/spring/spring-web/spring-web-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/web/v6_0/WebApplicationContextInstrumentation.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public static class FilterInjectingAdvice {
5959

6060
@Advice.OnMethodEnter(suppress = Throwable.class)
6161
public static void onEnter(@Advice.Argument(0) ConfigurableListableBeanFactory beanFactory) {
62-
if (beanFactory instanceof BeanDefinitionRegistry
62+
if (beanFactory instanceof BeanDefinitionRegistry beanDefinitionRegistry
6363
&& !beanFactory.containsBean("otelAutoDispatcherFilter")) {
6464
// Explicitly loading classes allows to catch any class-loading issue or deal with cases
6565
// where the class is not visible.
@@ -83,8 +83,7 @@ public static void onEnter(@Advice.Argument(0) ConfigurableListableBeanFactory b
8383
beanDefinition.setScope(SCOPE_SINGLETON);
8484
beanDefinition.setBeanClass(clazz);
8585

86-
((BeanDefinitionRegistry) beanFactory)
87-
.registerBeanDefinition("otelAutoDispatcherFilter", beanDefinition);
86+
beanDefinitionRegistry.registerBeanDefinition("otelAutoDispatcherFilter", beanDefinition);
8887
} catch (ClassNotFoundException ignored) {
8988
// Ignore
9089
}

0 commit comments

Comments
 (0)