Skip to content

Commit 7d1aeb4

Browse files
authored
Add experimental http client url.template attribute (#13581)
1 parent 4ce03e4 commit 7d1aeb4

File tree

8 files changed

+119
-16
lines changed

8 files changed

+119
-16
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.opentelemetry.api.common.AttributeKey;
1111
import io.opentelemetry.context.propagation.TextMapSetter;
1212
import io.opentelemetry.instrumentation.api.incubator.config.internal.CommonConfig;
13+
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalAttributesGetter;
1314
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalMetrics;
1415
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientPeerServiceAttributesExtractor;
1516
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor;
@@ -218,6 +219,13 @@ public DefaultHttpClientInstrumenterBuilder<REQUEST, RESPONSE> setBuilderCustomi
218219
}
219220

220221
public Instrumenter<REQUEST, RESPONSE> build() {
222+
if (emitExperimentalHttpClientTelemetry
223+
&& attributesGetter instanceof HttpClientExperimentalAttributesGetter) {
224+
HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE> experimentalAttributesGetter =
225+
(HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE>) attributesGetter;
226+
Experimental.setUrlTemplateExtractor(
227+
httpSpanNameExtractorBuilder, experimentalAttributesGetter::getUrlTemplate);
228+
}
221229
SpanNameExtractor<? super REQUEST> spanNameExtractor =
222230
spanNameExtractorTransformer.apply(httpSpanNameExtractorBuilder.build());
223231

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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.instrumentation.api.semconv.http.HttpClientAttributesGetter;
9+
import javax.annotation.Nullable;
10+
11+
/** An interface for getting experimental HTTP client attributes. */
12+
public interface HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE>
13+
extends HttpClientAttributesGetter<REQUEST, RESPONSE> {
14+
15+
/**
16+
* Returns the template used by the http client framework to build the request URL.
17+
*
18+
* <p>Examples: {@code /users/:userID?}, {@code {controller}/{action}/{id?}}
19+
*/
20+
@Nullable
21+
String getUrlTemplate(REQUEST request);
22+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ public final class HttpExperimentalAttributesExtractor<REQUEST, RESPONSE>
2626
static final AttributeKey<Long> HTTP_RESPONSE_BODY_SIZE =
2727
AttributeKey.longKey("http.response.body.size");
2828

29+
// copied from UrlIncubatingAttributes
30+
private static final AttributeKey<String> URL_TEMPLATE = AttributeKey.stringKey("url.template");
31+
2932
public static <REQUEST, RESPONSE> AttributesExtractor<REQUEST, RESPONSE> create(
3033
HttpClientAttributesGetter<REQUEST, RESPONSE> getter) {
3134
return new HttpExperimentalAttributesExtractor<>(getter);
@@ -61,6 +64,12 @@ public void onEnd(
6164
Long responseBodySize = responseBodySize(request, response);
6265
internalSet(attributes, HTTP_RESPONSE_BODY_SIZE, responseBodySize);
6366
}
67+
68+
if (getter instanceof HttpClientExperimentalAttributesGetter) {
69+
HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE> experimentalGetter =
70+
(HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE>) getter;
71+
internalSet(attributes, URL_TEMPLATE, experimentalGetter.getUrlTemplate(request));
72+
}
6473
}
6574

6675
@Nullable

instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractorTest.java

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,20 @@
77

88
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
99
import static java.util.Collections.singletonList;
10-
import static org.assertj.core.api.Assertions.entry;
1110
import static org.mockito.Mockito.when;
1211

12+
import io.opentelemetry.api.common.AttributeKey;
1313
import io.opentelemetry.api.common.Attributes;
1414
import io.opentelemetry.api.common.AttributesBuilder;
1515
import io.opentelemetry.context.Context;
1616
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
17-
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter;
1817
import io.opentelemetry.instrumentation.api.semconv.http.HttpCommonAttributesGetter;
1918
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter;
2019
import io.opentelemetry.semconv.incubating.HttpIncubatingAttributes;
20+
import io.opentelemetry.semconv.incubating.UrlIncubatingAttributes;
21+
import java.util.Collections;
22+
import java.util.HashMap;
23+
import java.util.Map;
2124
import org.junit.jupiter.api.Test;
2225
import org.junit.jupiter.api.extension.ExtendWith;
2326
import org.mockito.Mock;
@@ -26,22 +29,30 @@
2629
@ExtendWith(MockitoExtension.class)
2730
class HttpExperimentalAttributesExtractorTest {
2831

29-
@Mock HttpClientAttributesGetter<String, String> clientGetter;
32+
@Mock HttpClientExperimentalAttributesGetter<String, String> clientGetter;
3033
@Mock HttpServerAttributesGetter<String, String> serverGetter;
3134

3235
@Test
3336
void shouldExtractRequestAndResponseSizes_client() {
34-
runTest(clientGetter, HttpExperimentalAttributesExtractor.create(clientGetter));
37+
when(clientGetter.getUrlTemplate("request")).thenReturn("template");
38+
runTest(
39+
clientGetter,
40+
HttpExperimentalAttributesExtractor.create(clientGetter),
41+
Collections.singletonMap(UrlIncubatingAttributes.URL_TEMPLATE, "template"));
3542
}
3643

3744
@Test
3845
void shouldExtractRequestAndResponseSizes_server() {
39-
runTest(serverGetter, HttpExperimentalAttributesExtractor.create(serverGetter));
46+
runTest(
47+
serverGetter,
48+
HttpExperimentalAttributesExtractor.create(serverGetter),
49+
Collections.emptyMap());
4050
}
4151

4252
void runTest(
4353
HttpCommonAttributesGetter<String, String> getter,
44-
AttributesExtractor<String, String> extractor) {
54+
AttributesExtractor<String, String> extractor,
55+
Map<AttributeKey<?>, ?> expected) {
4556

4657
when(getter.getHttpRequestHeader("request", "content-length")).thenReturn(singletonList("123"));
4758
when(getter.getHttpResponseHeader("request", "response", "content-length"))
@@ -52,9 +63,9 @@ void runTest(
5263
assertThat(attributes.build()).isEmpty();
5364

5465
extractor.onEnd(attributes, Context.root(), "request", "response", null);
55-
assertThat(attributes.build())
56-
.containsOnly(
57-
entry(HttpIncubatingAttributes.HTTP_REQUEST_BODY_SIZE, 123L),
58-
entry(HttpIncubatingAttributes.HTTP_RESPONSE_BODY_SIZE, 42L));
66+
Map<AttributeKey<?>, Object> expectedAttributes = new HashMap<>(expected);
67+
expectedAttributes.put(HttpIncubatingAttributes.HTTP_REQUEST_BODY_SIZE, 123L);
68+
expectedAttributes.put(HttpIncubatingAttributes.HTTP_RESPONSE_BODY_SIZE, 42L);
69+
assertThat(attributes.build().asMap()).containsExactlyInAnyOrderEntriesOf(expectedAttributes);
5970
}
6071
}

instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/Experimental.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
package io.opentelemetry.instrumentation.api.internal;
77

88
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder;
9+
import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder;
910
import java.util.function.BiConsumer;
11+
import java.util.function.Function;
1012
import javax.annotation.Nullable;
1113

1214
/**
@@ -19,6 +21,10 @@ public final class Experimental {
1921
private static volatile BiConsumer<HttpClientAttributesExtractorBuilder<?, ?>, Boolean>
2022
redactHttpClientQueryParameters;
2123

24+
@Nullable
25+
private static volatile BiConsumer<HttpSpanNameExtractorBuilder<?>, Function<?, String>>
26+
urlTemplateExtractorSetter;
27+
2228
private Experimental() {}
2329

2430
public static void setRedactQueryParameters(
@@ -33,4 +39,19 @@ public static void internalSetRedactHttpClientQueryParameters(
3339
redactHttpClientQueryParameters) {
3440
Experimental.redactHttpClientQueryParameters = redactHttpClientQueryParameters;
3541
}
42+
43+
public static <REQUEST> void setUrlTemplateExtractor(
44+
HttpSpanNameExtractorBuilder<REQUEST> builder,
45+
Function<REQUEST, String> urlTemplateExtractor) {
46+
if (urlTemplateExtractorSetter != null) {
47+
urlTemplateExtractorSetter.accept(builder, urlTemplateExtractor);
48+
}
49+
}
50+
51+
@SuppressWarnings({"rawtypes", "unchecked"})
52+
public static <REQUEST> void internalSetUrlTemplateExtractor(
53+
BiConsumer<HttpSpanNameExtractorBuilder<REQUEST>, Function<REQUEST, String>>
54+
urlTemplateExtractorSetter) {
55+
Experimental.urlTemplateExtractorSetter = (BiConsumer) urlTemplateExtractorSetter;
56+
}
3657
}

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
1010
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
1111
import java.util.Set;
12+
import java.util.function.Function;
1213

1314
/**
1415
* Extractor of the <a
@@ -61,19 +62,28 @@ static final class Client<REQUEST> implements SpanNameExtractor<REQUEST> {
6162

6263
private final HttpClientAttributesGetter<REQUEST, ?> getter;
6364
private final Set<String> knownMethods;
65+
private final Function<REQUEST, String> urlTemplateExtractor;
6466

65-
Client(HttpClientAttributesGetter<REQUEST, ?> getter, Set<String> knownMethods) {
67+
Client(
68+
HttpClientAttributesGetter<REQUEST, ?> getter,
69+
Set<String> knownMethods,
70+
Function<REQUEST, String> urlTemplateExtractor) {
6671
this.getter = getter;
6772
this.knownMethods = knownMethods;
73+
this.urlTemplateExtractor = urlTemplateExtractor;
6874
}
6975

7076
@Override
7177
public String extract(REQUEST request) {
7278
String method = getter.getHttpRequestMethod(request);
73-
if (method == null || !knownMethods.contains(method)) {
79+
if (method == null) {
7480
return "HTTP";
7581
}
76-
return method;
82+
if (!knownMethods.contains(method)) {
83+
method = "HTTP";
84+
}
85+
String template = urlTemplateExtractor.apply(request);
86+
return template == null ? method : method + " " + template;
7787
}
7888
}
7989

@@ -90,13 +100,13 @@ static final class Server<REQUEST> implements SpanNameExtractor<REQUEST> {
90100
@Override
91101
public String extract(REQUEST request) {
92102
String method = getter.getHttpRequestMethod(request);
93-
String route = getter.getHttpRoute(request);
94103
if (method == null) {
95104
return "HTTP";
96105
}
97106
if (!knownMethods.contains(method)) {
98107
method = "HTTP";
99108
}
109+
String route = getter.getHttpRoute(request);
100110
return route == null ? method : method + " " + route;
101111
}
102112
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
import io.opentelemetry.api.OpenTelemetry;
1212
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
1313
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
14+
import io.opentelemetry.instrumentation.api.internal.Experimental;
1415
import io.opentelemetry.instrumentation.api.internal.HttpConstants;
1516
import java.util.Collection;
1617
import java.util.HashSet;
1718
import java.util.Set;
19+
import java.util.function.Function;
1820
import javax.annotation.Nullable;
1921

2022
/**
@@ -27,6 +29,12 @@ public final class HttpSpanNameExtractorBuilder<REQUEST> {
2729
@Nullable final HttpClientAttributesGetter<REQUEST, ?> clientGetter;
2830
@Nullable final HttpServerAttributesGetter<REQUEST, ?> serverGetter;
2931
Set<String> knownMethods = HttpConstants.KNOWN_METHODS;
32+
Function<REQUEST, String> urlTemplateExtractor = unused -> null;
33+
34+
static {
35+
Experimental.internalSetUrlTemplateExtractor(
36+
(builder, urlTemplateExtractor) -> builder.urlTemplateExtractor = urlTemplateExtractor);
37+
}
3038

3139
public HttpSpanNameExtractorBuilder(
3240
@Nullable HttpClientAttributesGetter<REQUEST, ?> clientGetter,
@@ -87,7 +95,7 @@ public HttpSpanNameExtractorBuilder<REQUEST> setKnownMethods(Set<String> knownMe
8795
public SpanNameExtractor<REQUEST> build() {
8896
Set<String> knownMethods = new HashSet<>(this.knownMethods);
8997
return clientGetter != null
90-
? new HttpSpanNameExtractor.Client<>(clientGetter, knownMethods)
98+
? new HttpSpanNameExtractor.Client<>(clientGetter, knownMethods, urlTemplateExtractor)
9199
: new HttpSpanNameExtractor.Server<>(requireNonNull(serverGetter), knownMethods);
92100
}
93101
}

instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanNameExtractorTest.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import static org.mockito.ArgumentMatchers.anyMap;
1010
import static org.mockito.Mockito.when;
1111

12+
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalAttributesGetter;
13+
import io.opentelemetry.instrumentation.api.internal.Experimental;
1214
import java.util.Collections;
1315
import java.util.Map;
1416
import org.junit.jupiter.api.Test;
@@ -22,7 +24,9 @@
2224
@MockitoSettings(strictness = Strictness.LENIENT)
2325
class HttpSpanNameExtractorTest {
2426

25-
@Mock private HttpClientAttributesGetter<Map<String, String>, Map<String, String>> clientGetter;
27+
@Mock
28+
private HttpClientExperimentalAttributesGetter<Map<String, String>, Map<String, String>>
29+
clientGetter;
2630

2731
@Mock private HttpServerAttributesGetter<Map<String, String>, Map<String, String>> serverGetter;
2832

@@ -41,6 +45,16 @@ void method() {
4145
.isEqualTo("GET");
4246
}
4347

48+
@Test
49+
void methodAndTemplate() {
50+
when(clientGetter.getUrlTemplate(anyMap())).thenReturn("/cats/{id}");
51+
when(clientGetter.getHttpRequestMethod(anyMap())).thenReturn("GET");
52+
HttpSpanNameExtractorBuilder<Map<String, String>> builder =
53+
HttpSpanNameExtractor.builder(clientGetter);
54+
Experimental.setUrlTemplateExtractor(builder, clientGetter::getUrlTemplate);
55+
assertThat(builder.build().extract(Collections.emptyMap())).isEqualTo("GET /cats/{id}");
56+
}
57+
4458
@Test
4559
void nothing() {
4660
assertThat(HttpSpanNameExtractor.create(clientGetter).extract(Collections.emptyMap()))

0 commit comments

Comments
 (0)