From 6dbb413d2a69e9c50804cbfb5c02de7eb97bcd19 Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Tue, 4 Nov 2025 15:55:35 +0200 Subject: [PATCH 1/6] Add http client url template customizer --- .../javaagent-testing/build.gradle.kts | 14 ++++ .../HttpClientUrlTemplateCustomizerTest.java | 67 +++++++++++++++ .../DefaultHttpClientInstrumenterBuilder.java | 18 +---- .../http/HttpClientUrlTemplateCustomizer.java | 28 +++++++ .../HttpExperimentalAttributesExtractor.java | 11 ++- .../internal/HttpClientUrlTemplateUtil.java | 75 +++++++++++++++++ .../http/RegexUrlTemplateCustomizer.java | 46 +++++++++++ ...RegexUrlTemplateCustomizerInitializer.java | 81 +++++++++++++++++++ .../http/UrlTemplateRules.java | 57 +++++++++++++ .../http/RegexUrlTemplateParserTest.java | 63 +++++++++++++++ settings.gradle.kts | 1 + 11 files changed, 441 insertions(+), 20 deletions(-) create mode 100644 instrumentation-api-incubator/javaagent-testing/build.gradle.kts create mode 100644 instrumentation-api-incubator/javaagent-testing/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientUrlTemplateCustomizerTest.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientUrlTemplateCustomizer.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/internal/HttpClientUrlTemplateUtil.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizer.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizerInitializer.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/UrlTemplateRules.java create mode 100644 javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateParserTest.java diff --git a/instrumentation-api-incubator/javaagent-testing/build.gradle.kts b/instrumentation-api-incubator/javaagent-testing/build.gradle.kts new file mode 100644 index 000000000000..bf270e044fa4 --- /dev/null +++ b/instrumentation-api-incubator/javaagent-testing/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("otel.javaagent-testing") +} + +dependencies { + testInstrumentation(project(":instrumentation:http-url-connection:javaagent")) + + testImplementation(project(":testing-common")) +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.http.client.emit-experimental-telemetry=true") + jvmArgs("-Dotel.instrumentation.http-client-url-template-rules=http://localhost:.*/hello/.*,/hello/*") +} diff --git a/instrumentation-api-incubator/javaagent-testing/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientUrlTemplateCustomizerTest.java b/instrumentation-api-incubator/javaagent-testing/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientUrlTemplateCustomizerTest.java new file mode 100644 index 000000000000..f8330a6579cf --- /dev/null +++ b/instrumentation-api-incubator/javaagent-testing/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientUrlTemplateCustomizerTest.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.http; + +import static io.opentelemetry.semconv.incubating.UrlIncubatingAttributes.URL_TEMPLATE; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.net.HttpURLConnection; +import java.net.URI; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class HttpClientUrlTemplateCustomizerTest { + private static HttpClientTestServer server; + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + @RegisterExtension + static InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @BeforeAll + static void setUp() { + server = new HttpClientTestServer(testing.getOpenTelemetry()); + server.start(); + } + + @AfterAll + static void tearDown() { + server.stop(); + } + + @Test + void test() throws Exception { + URI uri = URI.create("http://localhost:" + server.httpPort() + "/hello/world"); + HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); + connection.getInputStream().close(); + int responseCode = connection.getResponseCode(); + connection.disconnect(); + + assertThat(responseCode).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET /hello/*") + .hasNoParent() + .hasKind(SpanKind.CLIENT) + .hasAttribute(URL_TEMPLATE, "/hello/*") + .hasStatus(StatusData.unset()), + span -> + span.hasName("test-http-server") + .hasParent(trace.getSpan(0)) + .hasKind(SpanKind.SERVER))); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java index 4f40eac327b7..f15578321819 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java @@ -11,11 +11,10 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.propagation.TextMapSetter; import io.opentelemetry.instrumentation.api.incubator.config.internal.CommonConfig; -import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalAttributesGetter; import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalMetrics; import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientPeerServiceAttributesExtractor; -import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientUrlTemplate; import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.internal.HttpClientUrlTemplateUtil; import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceResolver; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; @@ -36,7 +35,6 @@ import java.util.List; import java.util.Objects; import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Supplier; import java.util.function.UnaryOperator; import javax.annotation.Nullable; @@ -221,19 +219,11 @@ public DefaultHttpClientInstrumenterBuilder setBuilderCustomi public Instrumenter build() { if (emitExperimentalHttpClientTelemetry) { - Function urlTemplateExtractorFunction = unused -> null; - if (attributesGetter instanceof HttpClientExperimentalAttributesGetter) { - HttpClientExperimentalAttributesGetter experimentalAttributesGetter = - (HttpClientExperimentalAttributesGetter) attributesGetter; - urlTemplateExtractorFunction = experimentalAttributesGetter::getUrlTemplate; - } - Function urlTemplateExtractor = urlTemplateExtractorFunction; Experimental.setUrlTemplateExtractor( httpSpanNameExtractorBuilder, - request -> { - String urlTemplate = HttpClientUrlTemplate.get(Context.current()); - return urlTemplate != null ? urlTemplate : urlTemplateExtractor.apply(request); - }); + request -> + HttpClientUrlTemplateUtil.getUrlTemplate( + Context.current(), request, attributesGetter)); } SpanNameExtractor spanNameExtractor = spanNameExtractorTransformer.apply(httpSpanNameExtractorBuilder.build()); diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientUrlTemplateCustomizer.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientUrlTemplateCustomizer.java new file mode 100644 index 000000000000..351ac56ac6e4 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientUrlTemplateCustomizer.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.http; + +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import javax.annotation.Nullable; + +/** A service provider interface (SPI) for customizing http client url template. */ +public interface HttpClientUrlTemplateCustomizer { + + /** + * Customize url template for given request. Typically, the customizer will extract full url from + * the request and apply some logic (e.g. regex matching) to generate url template. The customizer + * can choose to override existing url template or skip customization when a url template is + * already set. + * + * @param urlTemplate existing url template, can be null + * @param request current request + * @param getter request attributes getter + * @return customized url template, or null + */ + @Nullable + String customize( + @Nullable String urlTemplate, REQUEST request, HttpClientAttributesGetter getter); +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractor.java index 4ee1fe09a319..e68f0af4cc3b 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractor.java @@ -10,6 +10,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.internal.HttpClientUrlTemplateUtil; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import io.opentelemetry.instrumentation.api.semconv.http.HttpCommonAttributesGetter; @@ -65,13 +66,11 @@ public void onEnd( internalSet(attributes, HTTP_RESPONSE_BODY_SIZE, responseBodySize); } - String urlTemplate = HttpClientUrlTemplate.get(context); - if (urlTemplate != null) { + if (getter instanceof HttpClientAttributesGetter) { + HttpClientAttributesGetter clientGetter = + (HttpClientAttributesGetter) getter; + String urlTemplate = HttpClientUrlTemplateUtil.getUrlTemplate(context, request, clientGetter); internalSet(attributes, URL_TEMPLATE, urlTemplate); - } else if (getter instanceof HttpClientExperimentalAttributesGetter) { - HttpClientExperimentalAttributesGetter experimentalGetter = - (HttpClientExperimentalAttributesGetter) getter; - internalSet(attributes, URL_TEMPLATE, experimentalGetter.getUrlTemplate(request)); } } diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/internal/HttpClientUrlTemplateUtil.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/internal/HttpClientUrlTemplateUtil.java new file mode 100644 index 000000000000..f6f6630035c6 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/internal/HttpClientUrlTemplateUtil.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.http.internal; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientUrlTemplate; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientUrlTemplateCustomizer; +import io.opentelemetry.instrumentation.api.internal.InstrumenterContext; +import io.opentelemetry.instrumentation.api.internal.ServiceLoaderUtil; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class HttpClientUrlTemplateUtil { + + private static final List customizers = new ArrayList<>(); + + static { + for (HttpClientUrlTemplateCustomizer customizer : + ServiceLoaderUtil.load(HttpClientUrlTemplateCustomizer.class)) { + customizers.add(customizer); + } + } + + @Nullable + public static String getUrlTemplate( + Context context, REQUEST request, HttpClientAttributesGetter getter) { + // first, try to get url template from context + String urlTemplate = HttpClientUrlTemplate.get(context); + if (urlTemplate == null && getter instanceof HttpClientExperimentalAttributesGetter) { + HttpClientExperimentalAttributesGetter experimentalGetter = + (HttpClientExperimentalAttributesGetter) getter; + // next, try to get url template from getter + urlTemplate = experimentalGetter.getUrlTemplate(request); + } + + return customizeUrlTemplate(urlTemplate, request, getter); + } + + @Nullable + private static String customizeUrlTemplate( + @Nullable String urlTemplate, + REQUEST request, + HttpClientAttributesGetter getter) { + if (customizers.isEmpty()) { + return urlTemplate; + } + + // we cache the computation in InstrumenterContext because url template is used by both + // HttpSpanNameExtractor and HttpExperimentalAttributesExtractor + return InstrumenterContext.computeIfAbsent( + "url.template", + unused -> { + for (HttpClientUrlTemplateCustomizer customizer : customizers) { + String result = customizer.customize(urlTemplate, request, getter); + if (result != null) { + return result; + } + } + + return urlTemplate; + }); + } + + private HttpClientUrlTemplateUtil() {} +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizer.java new file mode 100644 index 000000000000..fa00e2b3520a --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizer.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.http; + +import static io.opentelemetry.javaagent.tooling.instrumentation.http.UrlTemplateRules.getRules; + +import com.google.auto.service.AutoService; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientUrlTemplateCustomizer; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import io.opentelemetry.javaagent.tooling.instrumentation.http.UrlTemplateRules.Rule; +import java.util.regex.Pattern; +import javax.annotation.Nullable; + +@AutoService(HttpClientUrlTemplateCustomizer.class) +public final class RegexUrlTemplateCustomizer implements HttpClientUrlTemplateCustomizer { + + @Override + @Nullable + public String customize( + @Nullable String urlTemplate, + REQUEST request, + HttpClientAttributesGetter getter) { + String url = getter.getUrlFull(request); + if (url == null) { + return null; + } + + for (Rule rule : getRules()) { + if (urlTemplate != null && !rule.getOverride()) { + continue; + } + + Pattern pattern = rule.getPattern(); + // to generate the url template, we apply the regex replacement on the full url + String result = pattern.matcher(url).replaceFirst(rule.getReplacement()); + if (!url.equals(result)) { + return result; + } + } + + return null; + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizerInitializer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizerInitializer.java new file mode 100644 index 000000000000..09ae62bc26f9 --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizerInitializer.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.http; + +import static java.util.logging.Level.WARNING; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.tooling.BeforeAgentListener; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +@AutoService(BeforeAgentListener.class) +public final class RegexUrlTemplateCustomizerInitializer implements BeforeAgentListener { + private static final Logger logger = + Logger.getLogger(RegexUrlTemplateCustomizerInitializer.class.getName()); + + @Override + public void beforeAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { + ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredOpenTelemetrySdk); + if (config == null) { + return; + } + // url template is emitted only when http client experimental telemetry is enabled + boolean urlTemplateEnabled = + config.getBoolean("otel.instrumentation.http.client.emit-experimental-telemetry", false); + if (!urlTemplateEnabled) { + return; + } + String rules = config.getString("otel.instrumentation.http-client-url-template-rules"); + if (rules != null && !rules.isEmpty()) { + parse(rules); + } + } + + // visible for testing + static void parse(String rules) { + // We are expecting a semicolon-separated list of rules in the form + // pattern,replacement[,override] + // Where pattern is a regex, replacement is the url template to use when the pattern matches, + // override is an optional boolean (default false) indicating whether this rule should override + // an existing url template. The pattern should match the entire url. + for (String rule : rules.split(";")) { + String[] parts = rule.split(","); + if (parts.length != 2 && parts.length != 3) { + logger.log( + WARNING, "Invalid http client url template customization rule \"" + rule + "\"."); + continue; + } + + Pattern pattern; + try { + String patternString = parts[0].trim(); + // ensure that pattern matches the whole url + if (!patternString.startsWith("^")) { + patternString = "^" + patternString; + } + if (!patternString.endsWith("$")) { + patternString = patternString + "$"; + } + pattern = Pattern.compile(patternString); + } catch (PatternSyntaxException exception) { + logger.log( + WARNING, + "Invalid pattern in http client url template customization rule \"" + + parts[0].trim() + + "\".", + exception); + continue; + } + UrlTemplateRules.addRule( + pattern, parts[1].trim(), parts.length == 3 && Boolean.parseBoolean(parts[2].trim())); + } + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/UrlTemplateRules.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/UrlTemplateRules.java new file mode 100644 index 000000000000..66071bec0a84 --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/UrlTemplateRules.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.http; + +import static java.util.logging.Level.FINE; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +final class UrlTemplateRules { + private static final Logger logger = Logger.getLogger(UrlTemplateRules.class.getName()); + private static final List rules = new ArrayList<>(); + + static List getRules() { + return rules; + } + + static void addRule(Pattern pattern, String replacement, boolean override) { + logger.log( + FINE, + "Adding http client url template customization rule: pattern=\"{0}\", replacement=\"{1}\", override={2}.", + new Object[] {pattern, replacement, override}); + + rules.add(new Rule(pattern, replacement, override)); + } + + static final class Rule { + private final Pattern pattern; + private final String replacement; + private final boolean override; + + Rule(Pattern pattern, String replacement, boolean override) { + this.pattern = pattern; + this.replacement = replacement; + this.override = override; + } + + Pattern getPattern() { + return pattern; + } + + String getReplacement() { + return replacement; + } + + boolean getOverride() { + return override; + } + } + + private UrlTemplateRules() {} +} diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateParserTest.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateParserTest.java new file mode 100644 index 000000000000..bcb1a02a69f6 --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateParserTest.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.http; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class RegexUrlTemplateParserTest { + + @AfterEach + void reset() { + UrlTemplateRules.getRules().clear(); + } + + @ParameterizedTest + @CsvSource( + value = { + "a", "a,", "a;b", "a,;b", "(a,b", + }, + delimiter = '|') + void invalid(String rules) { + RegexUrlTemplateCustomizerInitializer.parse(rules); + assertThat(UrlTemplateRules.getRules()).isEmpty(); + } + + @Test + void parse() { + RegexUrlTemplateCustomizerInitializer.parse( + "pattern1,replacement1;" + + "pattern2,replacement2,false;" + + "pattern3,replacement3,true;" + + " pattern4 , replacement4 , true ;"); + assertThat(UrlTemplateRules.getRules()) + .satisfiesExactly( + rule -> { + assertThat(rule.getPattern().pattern()).isEqualTo("^pattern1$"); + assertThat(rule.getReplacement()).isEqualTo("replacement1"); + assertThat(rule.getOverride()).isFalse(); + }, + rule -> { + assertThat(rule.getPattern().pattern()).isEqualTo("^pattern2$"); + assertThat(rule.getReplacement()).isEqualTo("replacement2"); + assertThat(rule.getOverride()).isFalse(); + }, + rule -> { + assertThat(rule.getPattern().pattern()).isEqualTo("^pattern3$"); + assertThat(rule.getReplacement()).isEqualTo("replacement3"); + assertThat(rule.getOverride()).isTrue(); + }, + rule -> { + assertThat(rule.getPattern().pattern()).isEqualTo("^pattern4$"); + assertThat(rule.getReplacement()).isEqualTo("replacement4"); + assertThat(rule.getOverride()).isTrue(); + }); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index d97ed912157c..887ca65a29c2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -127,6 +127,7 @@ include(":bom") include(":bom-alpha") include(":instrumentation-api") include(":instrumentation-api-incubator") +include(":instrumentation-api-incubator:javaagent-testing") include(":instrumentation-annotations") include(":instrumentation-annotations-support") include(":instrumentation-annotations-support-testing") From 3bcf70ce4ec70d6084b82d501323518f2ba21170 Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Tue, 4 Nov 2025 17:08:05 +0200 Subject: [PATCH 2/6] extract url template in onStart --- .../HttpExperimentalAttributesExtractor.java | 17 +++++++++-------- ...HttpExperimentalAttributesExtractorTest.java | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractor.java index e68f0af4cc3b..d025e089e95e 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractor.java @@ -48,7 +48,15 @@ private HttpExperimentalAttributesExtractor( } @Override - public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {} + public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { + if (getter instanceof HttpClientAttributesGetter) { + HttpClientAttributesGetter clientGetter = + (HttpClientAttributesGetter) getter; + String urlTemplate = + HttpClientUrlTemplateUtil.getUrlTemplate(parentContext, request, clientGetter); + internalSet(attributes, URL_TEMPLATE, urlTemplate); + } + } @Override public void onEnd( @@ -65,13 +73,6 @@ public void onEnd( Long responseBodySize = responseBodySize(request, response); internalSet(attributes, HTTP_RESPONSE_BODY_SIZE, responseBodySize); } - - if (getter instanceof HttpClientAttributesGetter) { - HttpClientAttributesGetter clientGetter = - (HttpClientAttributesGetter) getter; - String urlTemplate = HttpClientUrlTemplateUtil.getUrlTemplate(context, request, clientGetter); - internalSet(attributes, URL_TEMPLATE, urlTemplate); - } } @Nullable diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractorTest.java index b4f4fa886829..9ec4fe24d8f2 100644 --- a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractorTest.java @@ -60,7 +60,7 @@ void runTest( AttributesBuilder attributes = Attributes.builder(); extractor.onStart(attributes, Context.root(), "request"); - assertThat(attributes.build()).isEmpty(); + assertThat(attributes.build().asMap()).containsExactlyInAnyOrderEntriesOf(expected); extractor.onEnd(attributes, Context.root(), "request", "response", null); Map, Object> expectedAttributes = new HashMap<>(expected); From 02472791c09e674f8664d66bf0e8eac36adf183b Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Wed, 5 Nov 2025 13:18:47 +0200 Subject: [PATCH 3/6] address review comment --- .../javaagent-testing/build.gradle.kts | 2 +- .../http/RegexUrlTemplateCustomizerInitializer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation-api-incubator/javaagent-testing/build.gradle.kts b/instrumentation-api-incubator/javaagent-testing/build.gradle.kts index bf270e044fa4..6f201fb947cd 100644 --- a/instrumentation-api-incubator/javaagent-testing/build.gradle.kts +++ b/instrumentation-api-incubator/javaagent-testing/build.gradle.kts @@ -10,5 +10,5 @@ dependencies { tasks.withType().configureEach { jvmArgs("-Dotel.instrumentation.http.client.emit-experimental-telemetry=true") - jvmArgs("-Dotel.instrumentation.http-client-url-template-rules=http://localhost:.*/hello/.*,/hello/*") + jvmArgs("-Dotel.instrumentation.http.client.url-template-rules=http://localhost:.*/hello/.*,/hello/*") } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizerInitializer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizerInitializer.java index 09ae62bc26f9..116ae607b5d1 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizerInitializer.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizerInitializer.java @@ -33,7 +33,7 @@ public void beforeAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemet if (!urlTemplateEnabled) { return; } - String rules = config.getString("otel.instrumentation.http-client-url-template-rules"); + String rules = config.getString("otel.instrumentation.http.client.url-template-rules"); if (rules != null && !rules.isEmpty()) { parse(rules); } From 831bb4d6ac97dc6c1acaef35c74e376da864b71a Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Wed, 5 Nov 2025 14:34:08 +0200 Subject: [PATCH 4/6] add declarative config --- .../javaagent-testing/build.gradle.kts | 21 ++++- ...RegexUrlTemplateCustomizerInitializer.java | 79 ++++++++++++------- 2 files changed, 70 insertions(+), 30 deletions(-) diff --git a/instrumentation-api-incubator/javaagent-testing/build.gradle.kts b/instrumentation-api-incubator/javaagent-testing/build.gradle.kts index 6f201fb947cd..492d58de9b66 100644 --- a/instrumentation-api-incubator/javaagent-testing/build.gradle.kts +++ b/instrumentation-api-incubator/javaagent-testing/build.gradle.kts @@ -8,7 +8,22 @@ dependencies { testImplementation(project(":testing-common")) } -tasks.withType().configureEach { - jvmArgs("-Dotel.instrumentation.http.client.emit-experimental-telemetry=true") - jvmArgs("-Dotel.instrumentation.http.client.url-template-rules=http://localhost:.*/hello/.*,/hello/*") +tasks { + test { + jvmArgs("-Dotel.instrumentation.http.client.emit-experimental-telemetry=true") + jvmArgs("-Dotel.instrumentation.http.client.url-template-rules=http://localhost:.*/hello/.*,/hello/*") + } + + val declarativeConfigTest by registering(Test::class) { + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + + jvmArgs( + "-Dotel.experimental.config.file=$projectDir/src/test/resources/declarative-config.yaml" + ) + } + + check { + dependsOn(declarativeConfigTest) + } } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizerInitializer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizerInitializer.java index 116ae607b5d1..e234920e553a 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizerInitializer.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/http/RegexUrlTemplateCustomizerInitializer.java @@ -5,13 +5,16 @@ package io.opentelemetry.javaagent.tooling.instrumentation.http; +import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty; +import static java.util.Collections.emptyList; import static java.util.logging.Level.WARNING; import com.google.auto.service.AutoService; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.tooling.BeforeAgentListener; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; -import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -23,19 +26,36 @@ public final class RegexUrlTemplateCustomizerInitializer implements BeforeAgentL @Override public void beforeAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { - ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredOpenTelemetrySdk); - if (config == null) { - return; - } + InstrumentationConfig config = AgentInstrumentationConfig.get(); // url template is emitted only when http client experimental telemetry is enabled boolean urlTemplateEnabled = config.getBoolean("otel.instrumentation.http.client.emit-experimental-telemetry", false); if (!urlTemplateEnabled) { return; } - String rules = config.getString("otel.instrumentation.http.client.url-template-rules"); - if (rules != null && !rules.isEmpty()) { - parse(rules); + if (config.isDeclarative()) { + DeclarativeConfigProperties configuration = + config.getDeclarativeConfig("http").getStructured("client", empty()); + configuration + .getStructuredList("url_template_rules", emptyList()) + .forEach( + rule -> { + String patternString = rule.getString("pattern", ""); + String template = rule.getString("template", ""); + if (patternString.isEmpty() || template.isEmpty()) { + return; + } + boolean override = rule.getBoolean("override", false); + Pattern pattern = toPattern(patternString); + if (pattern != null) { + UrlTemplateRules.addRule(pattern, template, override); + } + }); + } else { + String rules = config.getString("otel.instrumentation.http.client.url-template-rules"); + if (rules != null && !rules.isEmpty()) { + parse(rules); + } } } @@ -54,28 +74,33 @@ static void parse(String rules) { continue; } - Pattern pattern; - try { - String patternString = parts[0].trim(); - // ensure that pattern matches the whole url - if (!patternString.startsWith("^")) { - patternString = "^" + patternString; - } - if (!patternString.endsWith("$")) { - patternString = patternString + "$"; - } - pattern = Pattern.compile(patternString); - } catch (PatternSyntaxException exception) { - logger.log( - WARNING, - "Invalid pattern in http client url template customization rule \"" - + parts[0].trim() - + "\".", - exception); + Pattern pattern = toPattern(parts[0].trim()); + if (pattern == null) { continue; } UrlTemplateRules.addRule( pattern, parts[1].trim(), parts.length == 3 && Boolean.parseBoolean(parts[2].trim())); } } + + private static Pattern toPattern(String patternString) { + try { + // ensure that pattern matches the whole url + if (!patternString.startsWith("^")) { + patternString = "^" + patternString; + } + if (!patternString.endsWith("$")) { + patternString = patternString + "$"; + } + return Pattern.compile(patternString); + } catch (PatternSyntaxException exception) { + logger.log( + WARNING, + "Invalid pattern in http client url template customization rule \"" + + patternString + + "\".", + exception); + return null; + } + } } From 54077d3d0ca368950e0b41a1c9c159f1154b57e8 Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Wed, 5 Nov 2025 15:10:24 +0200 Subject: [PATCH 5/6] add config file --- .../src/test/resources/declarative-config.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 instrumentation-api-incubator/javaagent-testing/src/test/resources/declarative-config.yaml diff --git a/instrumentation-api-incubator/javaagent-testing/src/test/resources/declarative-config.yaml b/instrumentation-api-incubator/javaagent-testing/src/test/resources/declarative-config.yaml new file mode 100644 index 000000000000..8c6c7dbe60ef --- /dev/null +++ b/instrumentation-api-incubator/javaagent-testing/src/test/resources/declarative-config.yaml @@ -0,0 +1,11 @@ +file_format: "1.0-rc.1" +propagator: + composite_list: "tracecontext" +instrumentation/development: + java: + http: + client: + emit_experimental_telemetry: true + url_template_rules: + - pattern: "http://localhost:.*/hello/.*" + template: "/hello/*" From 30b2b6fbf06301b3865fc22c88b6a3c7c6dc60fa Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Wed, 5 Nov 2025 15:11:42 +0200 Subject: [PATCH 6/6] remove unused code --- .../semconv/http/HttpClientUrlTemplateCustomizerTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/instrumentation-api-incubator/javaagent-testing/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientUrlTemplateCustomizerTest.java b/instrumentation-api-incubator/javaagent-testing/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientUrlTemplateCustomizerTest.java index f8330a6579cf..eff3beafe79a 100644 --- a/instrumentation-api-incubator/javaagent-testing/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientUrlTemplateCustomizerTest.java +++ b/instrumentation-api-incubator/javaagent-testing/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientUrlTemplateCustomizerTest.java @@ -9,7 +9,6 @@ import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer; @@ -24,8 +23,6 @@ class HttpClientUrlTemplateCustomizerTest { private static HttpClientTestServer server; - @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); - @RegisterExtension static InstrumentationExtension testing = AgentInstrumentationExtension.create();