Skip to content

Commit 853d4c6

Browse files
committed
Add http client url template customizer (open-telemetry#15217)
1 parent 8f594bd commit 853d4c6

File tree

12 files changed

+389
-25
lines changed

12 files changed

+389
-25
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
plugins {
2+
id("otel.javaagent-testing")
3+
}
4+
5+
dependencies {
6+
testInstrumentation(project(":instrumentation:http-url-connection:javaagent"))
7+
8+
testImplementation(project(":testing-common"))
9+
}
10+
11+
tasks {
12+
test {
13+
jvmArgs(
14+
"-Dotel.experimental.config.file=$projectDir/src/test/resources/declarative-config.yaml"
15+
)
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 static io.opentelemetry.semconv.incubating.UrlIncubatingAttributes.URL_TEMPLATE;
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
import io.opentelemetry.api.trace.SpanKind;
12+
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
13+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
14+
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer;
15+
import io.opentelemetry.sdk.trace.data.StatusData;
16+
import java.net.HttpURLConnection;
17+
import java.net.URI;
18+
import org.junit.jupiter.api.AfterAll;
19+
import org.junit.jupiter.api.BeforeAll;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.extension.RegisterExtension;
22+
23+
class HttpClientUrlTemplateCustomizerTest {
24+
private static HttpClientTestServer server;
25+
26+
@RegisterExtension
27+
static InstrumentationExtension testing = AgentInstrumentationExtension.create();
28+
29+
@BeforeAll
30+
static void setUp() {
31+
server = new HttpClientTestServer(testing.getOpenTelemetry());
32+
server.start();
33+
}
34+
35+
@AfterAll
36+
static void tearDown() {
37+
server.stop();
38+
}
39+
40+
@Test
41+
void test() throws Exception {
42+
URI uri = URI.create("http://localhost:" + server.httpPort() + "/hello/world");
43+
HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
44+
connection.getInputStream().close();
45+
int responseCode = connection.getResponseCode();
46+
connection.disconnect();
47+
48+
assertThat(responseCode).isEqualTo(200);
49+
50+
testing.waitAndAssertTraces(
51+
trace ->
52+
trace.hasSpansSatisfyingExactly(
53+
span ->
54+
span.hasName("GET /hello/*")
55+
.hasNoParent()
56+
.hasKind(SpanKind.CLIENT)
57+
.hasAttribute(URL_TEMPLATE, "/hello/*")
58+
.hasStatus(StatusData.unset()),
59+
span ->
60+
span.hasName("test-http-server")
61+
.hasParent(trace.getSpan(0))
62+
.hasKind(SpanKind.SERVER)));
63+
}
64+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
file_format: "1.0-rc.1"
2+
propagator:
3+
composite_list: "tracecontext"
4+
instrumentation/development:
5+
java:
6+
http:
7+
client:
8+
emit_experimental_telemetry: true
9+
url_template_rules:
10+
- pattern: "http://localhost:.*/hello/.*"
11+
template: "/hello/*"

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

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@
1111
import io.opentelemetry.context.Context;
1212
import io.opentelemetry.context.propagation.TextMapSetter;
1313
import io.opentelemetry.instrumentation.api.incubator.config.internal.CommonConfig;
14-
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalAttributesGetter;
1514
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalMetrics;
1615
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientPeerServiceAttributesExtractor;
17-
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientUrlTemplate;
1816
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor;
17+
import io.opentelemetry.instrumentation.api.incubator.semconv.http.internal.HttpClientUrlTemplateUtil;
1918
import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceResolver;
2019
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
2120
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
@@ -37,7 +36,6 @@
3736
import java.util.List;
3837
import java.util.Objects;
3938
import java.util.function.Consumer;
40-
import java.util.function.Function;
4139
import java.util.function.Supplier;
4240
import java.util.function.UnaryOperator;
4341
import javax.annotation.Nullable;
@@ -222,19 +220,11 @@ public DefaultHttpClientInstrumenterBuilder<REQUEST, RESPONSE> setBuilderCustomi
222220

223221
public Instrumenter<REQUEST, RESPONSE> build() {
224222
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;
232223
Experimental.setUrlTemplateExtractor(
233224
httpSpanNameExtractorBuilder,
234-
request -> {
235-
String urlTemplate = HttpClientUrlTemplate.get(Context.current());
236-
return urlTemplate != null ? urlTemplate : urlTemplateExtractor.apply(request);
237-
});
225+
request ->
226+
HttpClientUrlTemplateUtil.getUrlTemplate(
227+
Context.current(), request, attributesGetter));
238228
}
239229
SpanNameExtractor<? super REQUEST> spanNameExtractor =
240230
spanNameExtractorTransformer.apply(httpSpanNameExtractorBuilder.build());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
/** A service provider interface (SPI) for customizing http client url template. */
12+
public interface HttpClientUrlTemplateCustomizer {
13+
14+
/**
15+
* Customize url template for given request. Typically, the customizer will extract full url from
16+
* the request and apply some logic (e.g. regex matching) to generate url template. The customizer
17+
* can choose to override existing url template or skip customization when a url template is
18+
* already set.
19+
*
20+
* @param urlTemplate existing url template, can be null
21+
* @param request current request
22+
* @param getter request attributes getter
23+
* @return customized url template, or null
24+
*/
25+
@Nullable
26+
<REQUEST> String customize(
27+
@Nullable String urlTemplate, REQUEST request, HttpClientAttributesGetter<REQUEST, ?> getter);
28+
}

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.opentelemetry.api.common.AttributeKey;
1111
import io.opentelemetry.api.common.AttributesBuilder;
1212
import io.opentelemetry.context.Context;
13+
import io.opentelemetry.instrumentation.api.incubator.semconv.http.internal.HttpClientUrlTemplateUtil;
1314
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
1415
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter;
1516
import io.opentelemetry.instrumentation.api.semconv.http.HttpCommonAttributesGetter;
@@ -47,7 +48,15 @@ private HttpExperimentalAttributesExtractor(
4748
}
4849

4950
@Override
50-
public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {}
51+
public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {
52+
if (getter instanceof HttpClientAttributesGetter) {
53+
HttpClientAttributesGetter<REQUEST, RESPONSE> clientGetter =
54+
(HttpClientAttributesGetter<REQUEST, RESPONSE>) getter;
55+
String urlTemplate =
56+
HttpClientUrlTemplateUtil.getUrlTemplate(parentContext, request, clientGetter);
57+
internalSet(attributes, URL_TEMPLATE, urlTemplate);
58+
}
59+
}
5160

5261
@Override
5362
public void onEnd(
@@ -64,15 +73,6 @@ public void onEnd(
6473
Long responseBodySize = responseBodySize(request, response);
6574
internalSet(attributes, HTTP_RESPONSE_BODY_SIZE, responseBodySize);
6675
}
67-
68-
String urlTemplate = HttpClientUrlTemplate.get(context);
69-
if (urlTemplate != null) {
70-
internalSet(attributes, URL_TEMPLATE, urlTemplate);
71-
} else if (getter instanceof HttpClientExperimentalAttributesGetter) {
72-
HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE> experimentalGetter =
73-
(HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE>) getter;
74-
internalSet(attributes, URL_TEMPLATE, experimentalGetter.getUrlTemplate(request));
75-
}
7676
}
7777

7878
@Nullable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.semconv.http.internal;
7+
8+
import io.opentelemetry.context.Context;
9+
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalAttributesGetter;
10+
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientUrlTemplate;
11+
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientUrlTemplateCustomizer;
12+
import io.opentelemetry.instrumentation.api.internal.InstrumenterContext;
13+
import io.opentelemetry.instrumentation.api.internal.ServiceLoaderUtil;
14+
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter;
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
import javax.annotation.Nullable;
18+
19+
/**
20+
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
21+
* any time.
22+
*/
23+
public final class HttpClientUrlTemplateUtil {
24+
25+
private static final List<HttpClientUrlTemplateCustomizer> customizers = new ArrayList<>();
26+
27+
static {
28+
for (HttpClientUrlTemplateCustomizer customizer :
29+
ServiceLoaderUtil.load(HttpClientUrlTemplateCustomizer.class)) {
30+
customizers.add(customizer);
31+
}
32+
}
33+
34+
@Nullable
35+
public static <REQUEST> String getUrlTemplate(
36+
Context context, REQUEST request, HttpClientAttributesGetter<REQUEST, ?> getter) {
37+
// first, try to get url template from context
38+
String urlTemplate = HttpClientUrlTemplate.get(context);
39+
if (urlTemplate == null && getter instanceof HttpClientExperimentalAttributesGetter) {
40+
HttpClientExperimentalAttributesGetter<REQUEST, ?> experimentalGetter =
41+
(HttpClientExperimentalAttributesGetter<REQUEST, ?>) getter;
42+
// next, try to get url template from getter
43+
urlTemplate = experimentalGetter.getUrlTemplate(request);
44+
}
45+
46+
return customizeUrlTemplate(urlTemplate, request, getter);
47+
}
48+
49+
@Nullable
50+
private static <REQUEST> String customizeUrlTemplate(
51+
@Nullable String urlTemplate,
52+
REQUEST request,
53+
HttpClientAttributesGetter<REQUEST, ?> getter) {
54+
if (customizers.isEmpty()) {
55+
return urlTemplate;
56+
}
57+
58+
// we cache the computation in InstrumenterContext because url template is used by both
59+
// HttpSpanNameExtractor and HttpExperimentalAttributesExtractor
60+
return InstrumenterContext.computeIfAbsent(
61+
"url.template",
62+
unused -> {
63+
for (HttpClientUrlTemplateCustomizer customizer : customizers) {
64+
String result = customizer.customize(urlTemplate, request, getter);
65+
if (result != null) {
66+
return result;
67+
}
68+
}
69+
70+
return urlTemplate;
71+
});
72+
}
73+
74+
private HttpClientUrlTemplateUtil() {}
75+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ void runTest(
6060

6161
AttributesBuilder attributes = Attributes.builder();
6262
extractor.onStart(attributes, Context.root(), "request");
63-
assertThat(attributes.build()).isEmpty();
63+
assertThat(attributes.build().asMap()).containsExactlyInAnyOrderEntriesOf(expected);
6464

6565
extractor.onEnd(attributes, Context.root(), "request", "response", null);
6666
Map<AttributeKey<?>, Object> expectedAttributes = new HashMap<>(expected);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.tooling.instrumentation.http;
7+
8+
import static io.opentelemetry.javaagent.tooling.instrumentation.http.UrlTemplateRules.getRules;
9+
10+
import com.google.auto.service.AutoService;
11+
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientUrlTemplateCustomizer;
12+
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter;
13+
import io.opentelemetry.javaagent.tooling.instrumentation.http.UrlTemplateRules.Rule;
14+
import java.util.regex.Pattern;
15+
import javax.annotation.Nullable;
16+
17+
@AutoService(HttpClientUrlTemplateCustomizer.class)
18+
public final class RegexUrlTemplateCustomizer implements HttpClientUrlTemplateCustomizer {
19+
20+
@Override
21+
@Nullable
22+
public <REQUEST> String customize(
23+
@Nullable String urlTemplate,
24+
REQUEST request,
25+
HttpClientAttributesGetter<REQUEST, ?> getter) {
26+
String url = getter.getUrlFull(request);
27+
if (url == null) {
28+
return null;
29+
}
30+
31+
for (Rule rule : getRules()) {
32+
if (urlTemplate != null && !rule.getOverride()) {
33+
continue;
34+
}
35+
36+
Pattern pattern = rule.getPattern();
37+
// to generate the url template, we apply the regex replacement on the full url
38+
String result = pattern.matcher(url).replaceFirst(rule.getReplacement());
39+
if (!url.equals(result)) {
40+
return result;
41+
}
42+
}
43+
44+
return null;
45+
}
46+
}

0 commit comments

Comments
 (0)