Skip to content

Commit 6dbb413

Browse files
committed
Add http client url template customizer
1 parent a08a6b4 commit 6dbb413

File tree

11 files changed

+441
-20
lines changed

11 files changed

+441
-20
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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.withType<Test>().configureEach {
12+
jvmArgs("-Dotel.instrumentation.http.client.emit-experimental-telemetry=true")
13+
jvmArgs("-Dotel.instrumentation.http-client-url-template-rules=http://localhost:.*/hello/.*,/hello/*")
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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.internal.AutoCleanupExtension;
13+
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
14+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
15+
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer;
16+
import io.opentelemetry.sdk.trace.data.StatusData;
17+
import java.net.HttpURLConnection;
18+
import java.net.URI;
19+
import org.junit.jupiter.api.AfterAll;
20+
import org.junit.jupiter.api.BeforeAll;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.extension.RegisterExtension;
23+
24+
class HttpClientUrlTemplateCustomizerTest {
25+
private static HttpClientTestServer server;
26+
27+
@RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create();
28+
29+
@RegisterExtension
30+
static InstrumentationExtension testing = AgentInstrumentationExtension.create();
31+
32+
@BeforeAll
33+
static void setUp() {
34+
server = new HttpClientTestServer(testing.getOpenTelemetry());
35+
server.start();
36+
}
37+
38+
@AfterAll
39+
static void tearDown() {
40+
server.stop();
41+
}
42+
43+
@Test
44+
void test() throws Exception {
45+
URI uri = URI.create("http://localhost:" + server.httpPort() + "/hello/world");
46+
HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
47+
connection.getInputStream().close();
48+
int responseCode = connection.getResponseCode();
49+
connection.disconnect();
50+
51+
assertThat(responseCode).isEqualTo(200);
52+
53+
testing.waitAndAssertTraces(
54+
trace ->
55+
trace.hasSpansSatisfyingExactly(
56+
span ->
57+
span.hasName("GET /hello/*")
58+
.hasNoParent()
59+
.hasKind(SpanKind.CLIENT)
60+
.hasAttribute(URL_TEMPLATE, "/hello/*")
61+
.hasStatus(StatusData.unset()),
62+
span ->
63+
span.hasName("test-http-server")
64+
.hasParent(trace.getSpan(0))
65+
.hasKind(SpanKind.SERVER)));
66+
}
67+
}

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;
@@ -36,7 +35,6 @@
3635
import java.util.List;
3736
import java.util.Objects;
3837
import java.util.function.Consumer;
39-
import java.util.function.Function;
4038
import java.util.function.Supplier;
4139
import java.util.function.UnaryOperator;
4240
import javax.annotation.Nullable;
@@ -221,19 +219,11 @@ public DefaultHttpClientInstrumenterBuilder<REQUEST, RESPONSE> setBuilderCustomi
221219

222220
public Instrumenter<REQUEST, RESPONSE> build() {
223221
if (emitExperimentalHttpClientTelemetry) {
224-
Function<REQUEST, String> urlTemplateExtractorFunction = unused -> null;
225-
if (attributesGetter instanceof HttpClientExperimentalAttributesGetter) {
226-
HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE> experimentalAttributesGetter =
227-
(HttpClientExperimentalAttributesGetter<REQUEST, RESPONSE>) attributesGetter;
228-
urlTemplateExtractorFunction = experimentalAttributesGetter::getUrlTemplate;
229-
}
230-
Function<REQUEST, String> urlTemplateExtractor = urlTemplateExtractorFunction;
231222
Experimental.setUrlTemplateExtractor(
232223
httpSpanNameExtractorBuilder,
233-
request -> {
234-
String urlTemplate = HttpClientUrlTemplate.get(Context.current());
235-
return urlTemplate != null ? urlTemplate : urlTemplateExtractor.apply(request);
236-
});
224+
request ->
225+
HttpClientUrlTemplateUtil.getUrlTemplate(
226+
Context.current(), request, attributesGetter));
237227
}
238228
SpanNameExtractor<? super REQUEST> spanNameExtractor =
239229
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: 5 additions & 6 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;
@@ -65,13 +66,11 @@ public void onEnd(
6566
internalSet(attributes, HTTP_RESPONSE_BODY_SIZE, responseBodySize);
6667
}
6768

68-
String urlTemplate = HttpClientUrlTemplate.get(context);
69-
if (urlTemplate != null) {
69+
if (getter instanceof HttpClientAttributesGetter) {
70+
HttpClientAttributesGetter<REQUEST, RESPONSE> clientGetter =
71+
(HttpClientAttributesGetter<REQUEST, RESPONSE>) getter;
72+
String urlTemplate = HttpClientUrlTemplateUtil.getUrlTemplate(context, request, clientGetter);
7073
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));
7574
}
7675
}
7776

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+
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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 java.util.logging.Level.WARNING;
9+
10+
import com.google.auto.service.AutoService;
11+
import io.opentelemetry.javaagent.tooling.BeforeAgentListener;
12+
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
13+
import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil;
14+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
15+
import java.util.logging.Logger;
16+
import java.util.regex.Pattern;
17+
import java.util.regex.PatternSyntaxException;
18+
19+
@AutoService(BeforeAgentListener.class)
20+
public final class RegexUrlTemplateCustomizerInitializer implements BeforeAgentListener {
21+
private static final Logger logger =
22+
Logger.getLogger(RegexUrlTemplateCustomizerInitializer.class.getName());
23+
24+
@Override
25+
public void beforeAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) {
26+
ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredOpenTelemetrySdk);
27+
if (config == null) {
28+
return;
29+
}
30+
// url template is emitted only when http client experimental telemetry is enabled
31+
boolean urlTemplateEnabled =
32+
config.getBoolean("otel.instrumentation.http.client.emit-experimental-telemetry", false);
33+
if (!urlTemplateEnabled) {
34+
return;
35+
}
36+
String rules = config.getString("otel.instrumentation.http-client-url-template-rules");
37+
if (rules != null && !rules.isEmpty()) {
38+
parse(rules);
39+
}
40+
}
41+
42+
// visible for testing
43+
static void parse(String rules) {
44+
// We are expecting a semicolon-separated list of rules in the form
45+
// pattern,replacement[,override]
46+
// Where pattern is a regex, replacement is the url template to use when the pattern matches,
47+
// override is an optional boolean (default false) indicating whether this rule should override
48+
// an existing url template. The pattern should match the entire url.
49+
for (String rule : rules.split(";")) {
50+
String[] parts = rule.split(",");
51+
if (parts.length != 2 && parts.length != 3) {
52+
logger.log(
53+
WARNING, "Invalid http client url template customization rule \"" + rule + "\".");
54+
continue;
55+
}
56+
57+
Pattern pattern;
58+
try {
59+
String patternString = parts[0].trim();
60+
// ensure that pattern matches the whole url
61+
if (!patternString.startsWith("^")) {
62+
patternString = "^" + patternString;
63+
}
64+
if (!patternString.endsWith("$")) {
65+
patternString = patternString + "$";
66+
}
67+
pattern = Pattern.compile(patternString);
68+
} catch (PatternSyntaxException exception) {
69+
logger.log(
70+
WARNING,
71+
"Invalid pattern in http client url template customization rule \""
72+
+ parts[0].trim()
73+
+ "\".",
74+
exception);
75+
continue;
76+
}
77+
UrlTemplateRules.addRule(
78+
pattern, parts[1].trim(), parts.length == 3 && Boolean.parseBoolean(parts[2].trim()));
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)