From 7b4c56d6675a5e0e87e67785b295117b96f1b51b Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Sat, 24 May 2025 20:57:52 +0800 Subject: [PATCH 01/22] Support extensions for attributesExtractors, contextCustomizers and operationListeners --- ...onditionalAttributesExtractorProvider.java | 14 +++++ .../ConditionalContextCustomizerProvider.java | 14 +++++ .../ConditionalOperationMetricsProvider.java | 14 +++++ .../api/instrumenter/InstrumenterBuilder.java | 62 +++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalAttributesExtractorProvider.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalContextCustomizerProvider.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalOperationMetricsProvider.java diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalAttributesExtractorProvider.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalAttributesExtractorProvider.java new file mode 100644 index 000000000000..e7334e2416ff --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalAttributesExtractorProvider.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import java.util.List; + +public interface ConditionalAttributesExtractorProvider { + List supportedNames(); + + AttributesExtractor get(); +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalContextCustomizerProvider.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalContextCustomizerProvider.java new file mode 100644 index 000000000000..8b46954fba53 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalContextCustomizerProvider.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import java.util.List; + +public interface ConditionalContextCustomizerProvider { + List supportedNames(); + + ContextCustomizer get(); +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalOperationMetricsProvider.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalOperationMetricsProvider.java new file mode 100644 index 000000000000..f0cfa820e2f2 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalOperationMetricsProvider.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import java.util.List; + +public interface ConditionalOperationMetricsProvider { + List supportedNames(); + + OperationMetrics get(); +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java index 6b41ae6c60ab..8ba37881c7a1 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java @@ -27,7 +27,10 @@ import io.opentelemetry.instrumentation.api.internal.SpanKey; import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; import java.util.Set; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -69,6 +72,41 @@ public final class InstrumenterBuilder { boolean propagateOperationListenersToOnEnd = false; boolean enabled = true; + private static final Map> + CONTEXT_CUSTOMIZER_MAP = new HashMap<>(); + private static final Map> + ATTRIBUTES_EXTRACTOR_MAP = new HashMap<>(); + private static final Map> + OPERATION_METRICS_MAP = new HashMap<>(); + + static { + ServiceLoader.load(ConditionalContextCustomizerProvider.class) + .forEach( + provider -> { + for (String name : provider.supportedNames()) { + CONTEXT_CUSTOMIZER_MAP.computeIfAbsent(name, k -> new ArrayList<>()).add(provider); + } + }); + + ServiceLoader.load(ConditionalAttributesExtractorProvider.class) + .forEach( + provider -> { + for (String name : provider.supportedNames()) { + ATTRIBUTES_EXTRACTOR_MAP + .computeIfAbsent(name, k -> new ArrayList<>()) + .add(provider); + } + }); + + ServiceLoader.load(ConditionalOperationMetricsProvider.class) + .forEach( + provider -> { + for (String name : provider.supportedNames()) { + OPERATION_METRICS_MAP.computeIfAbsent(name, k -> new ArrayList<>()).add(provider); + } + }); + } + InstrumenterBuilder( OpenTelemetry openTelemetry, String instrumentationName, @@ -78,6 +116,30 @@ public final class InstrumenterBuilder { this.spanNameExtractor = spanNameExtractor; this.instrumentationVersion = EmbeddedInstrumentationProperties.findVersion(instrumentationName); + + List contextProviders = + CONTEXT_CUSTOMIZER_MAP.get(instrumentationName); + if (contextProviders != null) { + for (ConditionalContextCustomizerProvider provider : contextProviders) { + addContextCustomizer(provider.get()); + } + } + + List attributeProviders = + ATTRIBUTES_EXTRACTOR_MAP.get(instrumentationName); + if (attributeProviders != null) { + for (ConditionalAttributesExtractorProvider provider : attributeProviders) { + addAttributesExtractor(provider.get()); + } + } + + List metricsProviders = + OPERATION_METRICS_MAP.get(instrumentationName); + if (metricsProviders != null) { + for (ConditionalOperationMetricsProvider provider : metricsProviders) { + addOperationMetrics(provider.get()); + } + } } /** From 8ba2bf01d846904f9e5166710faf5c869f5f0995 Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Mon, 26 May 2025 23:56:07 +0800 Subject: [PATCH 02/22] Fix failing test --- ...ntelemetry-instrumentation-annotations.txt | 2 +- .../opentelemetry-instrumentation-api.txt | 20 +++++++++++++++++-- ...pentelemetry-spring-boot-autoconfigure.txt | 2 +- .../opentelemetry-spring-boot-starter.txt | 2 +- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt index 43492d97122f..5f17b3af5af7 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-instrumentation-annotations-2.16.0-SNAPSHOT.jar against opentelemetry-instrumentation-annotations-2.15.0.jar +Comparing source compatibility of opentelemetry-instrumentation-annotations-2.16.0-SNAPSHOT.jar against opentelemetry-instrumentation-annotations-2.16.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt index a3aeb8e8fd35..2883d7d243fd 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt @@ -1,2 +1,18 @@ -Comparing source compatibility of opentelemetry-instrumentation-api-2.16.0-SNAPSHOT.jar against opentelemetry-instrumentation-api-2.15.0.jar -No changes. \ No newline at end of file +Comparing source compatibility of opentelemetry-instrumentation-api-2.16.0-SNAPSHOT.jar against opentelemetry-instrumentation-api-2.16.0.jar ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.ConditionalAttributesExtractorProvider (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor get() + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List supportedNames() ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.ConditionalContextCustomizerProvider (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer get() + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List supportedNames() ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.ConditionalOperationMetricsProvider (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics get() + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List supportedNames() diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt index 4bceebba9ea9..a8caa59854fa 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-spring-boot-autoconfigure-2.16.0-SNAPSHOT.jar against opentelemetry-spring-boot-autoconfigure-2.15.0.jar +Comparing source compatibility of opentelemetry-spring-boot-autoconfigure-2.16.0-SNAPSHOT.jar against opentelemetry-spring-boot-autoconfigure-2.16.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt index 687af965b556..649c71a97e41 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-spring-boot-starter-2.16.0-SNAPSHOT.jar against opentelemetry-spring-boot-starter-2.15.0.jar +Comparing source compatibility of opentelemetry-spring-boot-starter-2.16.0-SNAPSHOT.jar against opentelemetry-spring-boot-starter-2.16.0.jar No changes. \ No newline at end of file From 5531d1685b65fbb318c44181833edaf00a7c7ca6 Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Tue, 27 May 2025 14:28:14 +0800 Subject: [PATCH 03/22] Fix failing CI --- .../opentelemetry-instrumentation-annotations.txt | 1 + .../current_vs_latest/opentelemetry-instrumentation-api.txt | 2 +- .../opentelemetry-spring-boot-autoconfigure.txt | 3 ++- .../current_vs_latest/opentelemetry-spring-boot-starter.txt | 3 ++- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt index f1f2d60dc88e..144ffe04ad94 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt @@ -1 +1,2 @@ Comparing source compatibility of opentelemetry-instrumentation-annotations-2.17.0-SNAPSHOT.jar against opentelemetry-instrumentation-annotations-2.16.0.jar +No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt index 2883d7d243fd..d449bad2a274 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt @@ -1,4 +1,4 @@ -Comparing source compatibility of opentelemetry-instrumentation-api-2.16.0-SNAPSHOT.jar against opentelemetry-instrumentation-api-2.16.0.jar +Comparing source compatibility of opentelemetry-instrumentation-api-2.17.0-SNAPSHOT.jar against opentelemetry-instrumentation-api-2.16.0.jar +++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.ConditionalAttributesExtractorProvider (not serializable) +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW SUPERCLASS: java.lang.Object diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt index d034f3941373..4be81e19734c 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt @@ -1 +1,2 @@ -Comparing source compatibility of opentelemetry-spring-boot-autoconfigure-2.16.0-SNAPSHOT.jar against opentelemetry-spring-boot-autoconfigure-2.16.0.jar +Comparing source compatibility of opentelemetry-spring-boot-autoconfigure-2.17.0-SNAPSHOT.jar against opentelemetry-spring-boot-autoconfigure-2.16.0.jar +No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt index b62026dd66a3..0305aa080adc 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt @@ -1 +1,2 @@ -Comparing source compatibility of opentelemetry-spring-boot-starter-2.16.0-SNAPSHOT.jar against opentelemetry-spring-boot-starter-2.16.0.jar +Comparing source compatibility of opentelemetry-spring-boot-starter-2.17.0-SNAPSHOT.jar against opentelemetry-spring-boot-starter-2.16.0.jar +No changes. \ No newline at end of file From 35da7059ea02ad5933e520230748c9482b1b9b1a Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Wed, 28 May 2025 00:40:31 +0800 Subject: [PATCH 04/22] Add necessary description of classes --- .../opentelemetry-instrumentation-api.txt | 12 ++--- .../AttributesExtractorCustomizer.java | 38 ++++++++++++++ ...onditionalAttributesExtractorProvider.java | 14 ----- .../ConditionalContextCustomizerProvider.java | 14 ----- .../ConditionalOperationMetricsProvider.java | 14 ----- .../ContextCustomizerCustomizer.java | 38 ++++++++++++++ .../api/instrumenter/InstrumenterBuilder.java | 52 ++++++++++--------- .../OperationMetricsCustomizer.java | 37 +++++++++++++ 8 files changed, 147 insertions(+), 72 deletions(-) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractorCustomizer.java delete mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalAttributesExtractorProvider.java delete mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalContextCustomizerProvider.java delete mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalOperationMetricsProvider.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ContextCustomizerCustomizer.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/OperationMetricsCustomizer.java diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt index d449bad2a274..d39e3b86be7c 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt @@ -1,18 +1,18 @@ Comparing source compatibility of opentelemetry-instrumentation-api-2.17.0-SNAPSHOT.jar against opentelemetry-instrumentation-api-2.16.0.jar -+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.ConditionalAttributesExtractorProvider (not serializable) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractorCustomizer (not serializable) +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW SUPERCLASS: java.lang.Object +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor get() GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List supportedNames() -+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.ConditionalContextCustomizerProvider (not serializable) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List instrumentationNames() ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizerCustomizer (not serializable) +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW SUPERCLASS: java.lang.Object +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer get() GENERIC TEMPLATES: +++ REQUEST:java.lang.Object - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List supportedNames() -+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.ConditionalOperationMetricsProvider (not serializable) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List instrumentationNames() ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.OperationMetricsCustomizer (not serializable) +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW SUPERCLASS: java.lang.Object +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics get() - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List supportedNames() + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List instrumentationNames() diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractorCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractorCustomizer.java new file mode 100644 index 000000000000..56284410f5e1 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractorCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import java.util.List; + +/** + * A service provider interface (SPI) for providing {@link AttributesExtractorCustomizer} instances + * that are conditionally applied based on the instrumentation name. + * + *

This allows external modules or plugins to contribute custom attribute extraction logic for + * specific instrumented libraries, without modifying core instrumentation code. + */ +public interface AttributesExtractorCustomizer { + /** + * Returns a list of instrumentation names that this customizer supports. + * + *

The customizer will only be applied if the current instrumentation matches one of the + * returned names. For example: ["io.opentelemetry.netty-3.8", + * "io.opentelemetry.apache-httpclient-4.3"]. + * + * @return a list of supported instrumentation names + */ + List instrumentationNames(); + + /** + * Returns a new instance of an {@link AttributesExtractor} that will extract attributes from + * requests and responses during the instrumentation process. + * + * @param the type of request object used by the instrumented library + * @param the type of response object used by the instrumented library + * @return an attributes extractor instance + */ + AttributesExtractor get(); +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalAttributesExtractorProvider.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalAttributesExtractorProvider.java deleted file mode 100644 index e7334e2416ff..000000000000 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalAttributesExtractorProvider.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter; - -import java.util.List; - -public interface ConditionalAttributesExtractorProvider { - List supportedNames(); - - AttributesExtractor get(); -} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalContextCustomizerProvider.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalContextCustomizerProvider.java deleted file mode 100644 index 8b46954fba53..000000000000 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalContextCustomizerProvider.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter; - -import java.util.List; - -public interface ConditionalContextCustomizerProvider { - List supportedNames(); - - ContextCustomizer get(); -} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalOperationMetricsProvider.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalOperationMetricsProvider.java deleted file mode 100644 index f0cfa820e2f2..000000000000 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ConditionalOperationMetricsProvider.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter; - -import java.util.List; - -public interface ConditionalOperationMetricsProvider { - List supportedNames(); - - OperationMetrics get(); -} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ContextCustomizerCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ContextCustomizerCustomizer.java new file mode 100644 index 000000000000..c895ddc3fb4b --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ContextCustomizerCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import java.util.List; + +/** + * A service provider interface (SPI) for providing custom {@link ContextCustomizer} implementations + * that are conditionally applied based on the instrumentation name. + * + *

This allows external modules or plugins to customize context propagation and initialization + * logic for specific instrumented libraries, without modifying core instrumentation code. + */ +public interface ContextCustomizerCustomizer { + + /** + * Returns a list of instrumentation names that this customizer supports. + * + *

The customizer will only be applied if the current instrumentation matches one of the + * returned names. For example: ["io.opentelemetry.netty-3.8", + * "io.opentelemetry.apache-httpclient-4.3"]. + * + * @return a list of supported instrumentation names + */ + List instrumentationNames(); + + /** + * Returns a new instance of a {@link ContextCustomizer} that will customize the tracing context + * during request processing. + * + * @param the type of request object used by the instrumented library + * @return a context customizer instance + */ + ContextCustomizer get(); +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java index 8ba37881c7a1..ec3830769c67 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java @@ -72,37 +72,41 @@ public final class InstrumenterBuilder { boolean propagateOperationListenersToOnEnd = false; boolean enabled = true; - private static final Map> - CONTEXT_CUSTOMIZER_MAP = new HashMap<>(); - private static final Map> - ATTRIBUTES_EXTRACTOR_MAP = new HashMap<>(); - private static final Map> - OPERATION_METRICS_MAP = new HashMap<>(); + private static final Map> CONTEXT_CUSTOMIZER_MAP = + new HashMap<>(); + private static final Map> ATTRIBUTES_EXTRACTOR_MAP = + new HashMap<>(); + private static final Map> OPERATION_METRICS_MAP = + new HashMap<>(); static { - ServiceLoader.load(ConditionalContextCustomizerProvider.class) + ServiceLoader.load(ContextCustomizerCustomizer.class) .forEach( - provider -> { - for (String name : provider.supportedNames()) { - CONTEXT_CUSTOMIZER_MAP.computeIfAbsent(name, k -> new ArrayList<>()).add(provider); + customizers -> { + for (String name : customizers.instrumentationNames()) { + CONTEXT_CUSTOMIZER_MAP + .computeIfAbsent(name, k -> new ArrayList<>()) + .add(customizers); } }); - ServiceLoader.load(ConditionalAttributesExtractorProvider.class) + ServiceLoader.load(AttributesExtractorCustomizer.class) .forEach( - provider -> { - for (String name : provider.supportedNames()) { + customizers -> { + for (String name : customizers.instrumentationNames()) { ATTRIBUTES_EXTRACTOR_MAP .computeIfAbsent(name, k -> new ArrayList<>()) - .add(provider); + .add(customizers); } }); - ServiceLoader.load(ConditionalOperationMetricsProvider.class) + ServiceLoader.load(OperationMetricsCustomizer.class) .forEach( - provider -> { - for (String name : provider.supportedNames()) { - OPERATION_METRICS_MAP.computeIfAbsent(name, k -> new ArrayList<>()).add(provider); + customizers -> { + for (String name : customizers.instrumentationNames()) { + OPERATION_METRICS_MAP + .computeIfAbsent(name, k -> new ArrayList<>()) + .add(customizers); } }); } @@ -117,26 +121,26 @@ public final class InstrumenterBuilder { this.instrumentationVersion = EmbeddedInstrumentationProperties.findVersion(instrumentationName); - List contextProviders = + List contextProviders = CONTEXT_CUSTOMIZER_MAP.get(instrumentationName); if (contextProviders != null) { - for (ConditionalContextCustomizerProvider provider : contextProviders) { + for (ContextCustomizerCustomizer provider : contextProviders) { addContextCustomizer(provider.get()); } } - List attributeProviders = + List attributeProviders = ATTRIBUTES_EXTRACTOR_MAP.get(instrumentationName); if (attributeProviders != null) { - for (ConditionalAttributesExtractorProvider provider : attributeProviders) { + for (AttributesExtractorCustomizer provider : attributeProviders) { addAttributesExtractor(provider.get()); } } - List metricsProviders = + List metricsProviders = OPERATION_METRICS_MAP.get(instrumentationName); if (metricsProviders != null) { - for (ConditionalOperationMetricsProvider provider : metricsProviders) { + for (OperationMetricsCustomizer provider : metricsProviders) { addOperationMetrics(provider.get()); } } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/OperationMetricsCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/OperationMetricsCustomizer.java new file mode 100644 index 000000000000..20506671ec77 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/OperationMetricsCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import java.util.List; + +/** + * A service provider interface (SPI) for providing custom {@link OperationMetrics} instances that + * are conditionally applied based on the instrumentation name. + * + *

This allows external modules or plugins to contribute custom metrics collection logic for + * specific instrumented operations, without modifying core instrumentation code. + */ +public interface OperationMetricsCustomizer { + + /** + * Returns a list of instrumentation names that this metrics customizer supports. + * + *

The customizer will only be applied if the current instrumentation matches one of the + * returned names. For example: ["io.opentelemetry.spring-webmvc-5.0", + * "io.opentelemetry.netty-3.8"]. + * + * @return a list of supported instrumentation names + */ + List instrumentationNames(); + + /** + * Returns a new instance of an {@link OperationMetrics} that will record metrics for the + * instrumented operation. + * + * @return an operation metrics instance + */ + OperationMetrics get(); +} From 30182a3a0318dbed4ec75fc7979641dcff0770d5 Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Sun, 1 Jun 2025 16:49:20 +0800 Subject: [PATCH 05/22] Address review comments --- .../opentelemetry-instrumentation-api.txt | 18 ++-- .../AttributesExtractorCustomizer.java | 38 -------- .../ContextCustomizerCustomizer.java | 38 -------- .../InstrumentationCustomizer.java | 63 +++++++++++++ .../api/instrumenter/InstrumenterBuilder.java | 89 +++++++------------ .../OperationMetricsCustomizer.java | 37 -------- 6 files changed, 98 insertions(+), 185 deletions(-) delete mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractorCustomizer.java delete mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ContextCustomizerCustomizer.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizer.java delete mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/OperationMetricsCustomizer.java diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt index d39e3b86be7c..a601f4e15d69 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt @@ -1,18 +1,10 @@ Comparing source compatibility of opentelemetry-instrumentation-api-2.17.0-SNAPSHOT.jar against opentelemetry-instrumentation-api-2.16.0.jar -+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractorCustomizer (not serializable) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.InstrumentationCustomizer (not serializable) +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW SUPERCLASS: java.lang.Object - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor get() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor getAttributesExtractor() GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List instrumentationNames() -+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizerCustomizer (not serializable) - +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. - +++ NEW SUPERCLASS: java.lang.Object - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer get() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer getContextCustomizer() GENERIC TEMPLATES: +++ REQUEST:java.lang.Object - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List instrumentationNames() -+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.OperationMetricsCustomizer (not serializable) - +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. - +++ NEW SUPERCLASS: java.lang.Object - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics get() - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List instrumentationNames() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics getOperationMetrics() + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.function.Predicate instrumentationNamePredicate() diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractorCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractorCustomizer.java deleted file mode 100644 index 56284410f5e1..000000000000 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractorCustomizer.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter; - -import java.util.List; - -/** - * A service provider interface (SPI) for providing {@link AttributesExtractorCustomizer} instances - * that are conditionally applied based on the instrumentation name. - * - *

This allows external modules or plugins to contribute custom attribute extraction logic for - * specific instrumented libraries, without modifying core instrumentation code. - */ -public interface AttributesExtractorCustomizer { - /** - * Returns a list of instrumentation names that this customizer supports. - * - *

The customizer will only be applied if the current instrumentation matches one of the - * returned names. For example: ["io.opentelemetry.netty-3.8", - * "io.opentelemetry.apache-httpclient-4.3"]. - * - * @return a list of supported instrumentation names - */ - List instrumentationNames(); - - /** - * Returns a new instance of an {@link AttributesExtractor} that will extract attributes from - * requests and responses during the instrumentation process. - * - * @param the type of request object used by the instrumented library - * @param the type of response object used by the instrumented library - * @return an attributes extractor instance - */ - AttributesExtractor get(); -} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ContextCustomizerCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ContextCustomizerCustomizer.java deleted file mode 100644 index c895ddc3fb4b..000000000000 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ContextCustomizerCustomizer.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter; - -import java.util.List; - -/** - * A service provider interface (SPI) for providing custom {@link ContextCustomizer} implementations - * that are conditionally applied based on the instrumentation name. - * - *

This allows external modules or plugins to customize context propagation and initialization - * logic for specific instrumented libraries, without modifying core instrumentation code. - */ -public interface ContextCustomizerCustomizer { - - /** - * Returns a list of instrumentation names that this customizer supports. - * - *

The customizer will only be applied if the current instrumentation matches one of the - * returned names. For example: ["io.opentelemetry.netty-3.8", - * "io.opentelemetry.apache-httpclient-4.3"]. - * - * @return a list of supported instrumentation names - */ - List instrumentationNames(); - - /** - * Returns a new instance of a {@link ContextCustomizer} that will customize the tracing context - * during request processing. - * - * @param the type of request object used by the instrumented library - * @return a context customizer instance - */ - ContextCustomizer get(); -} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizer.java new file mode 100644 index 000000000000..889c8450d48a --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizer.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import java.util.function.Predicate; + +/** + * A service provider interface (SPI) for providing customizations for instrumentation, including + * operation metrics, attributes extraction, and context customization. + * + *

This allows external modules or plugins to contribute custom logic for specific instrumented + * libraries, without modifying core instrumentation code. + */ +public interface InstrumentationCustomizer { + + /** + * Returns a predicate that determines whether this customizer supports a given instrumentation + * name. + * + *

The customizer will only be applied if the current instrumentation name matches the + * predicate. For example, the predicate might match names like "io.opentelemetry.netty-3.8" or + * "io.opentelemetry.apache-httpclient-4.3". + * + * @return a predicate for supported instrumentation names + */ + Predicate instrumentationNamePredicate(); + + /** + * Returns a new instance of an {@link OperationMetrics} that will record metrics for the + * instrumented operation. + * + * @return an operation metrics instance, or null if not applicable + */ + default OperationMetrics getOperationMetrics() { + return null; + } + + /** + * Returns a new instance of an {@link AttributesExtractor} that will extract attributes from + * requests and responses during the instrumentation process. + * + * @param the type of request object used by the instrumented library + * @param the type of response object used by the instrumented library + * @return an attributes extractor instance, or null if not applicable + */ + default AttributesExtractor getAttributesExtractor() { + return null; + } + + /** + * Returns a new instance of a {@link ContextCustomizer} that will customize the tracing context + * during request processing. + * + * @param the type of request object used by the instrumented library + * @return a context customizer instance, or null if not applicable + */ + default ContextCustomizer getContextCustomizer() { + return null; + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java index ec3830769c67..0c5cd4ec0262 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.ServiceLoader; import java.util.Set; +import java.util.function.Predicate; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -72,43 +73,18 @@ public final class InstrumenterBuilder { boolean propagateOperationListenersToOnEnd = false; boolean enabled = true; - private static final Map> CONTEXT_CUSTOMIZER_MAP = - new HashMap<>(); - private static final Map> ATTRIBUTES_EXTRACTOR_MAP = - new HashMap<>(); - private static final Map> OPERATION_METRICS_MAP = - new HashMap<>(); + private static final Map, List> + INSTRUMENTATION_CUSTOMIZER_MAP = new HashMap<>(); static { - ServiceLoader.load(ContextCustomizerCustomizer.class) - .forEach( - customizers -> { - for (String name : customizers.instrumentationNames()) { - CONTEXT_CUSTOMIZER_MAP - .computeIfAbsent(name, k -> new ArrayList<>()) - .add(customizers); - } - }); - - ServiceLoader.load(AttributesExtractorCustomizer.class) - .forEach( - customizers -> { - for (String name : customizers.instrumentationNames()) { - ATTRIBUTES_EXTRACTOR_MAP - .computeIfAbsent(name, k -> new ArrayList<>()) - .add(customizers); - } - }); - - ServiceLoader.load(OperationMetricsCustomizer.class) - .forEach( - customizers -> { - for (String name : customizers.instrumentationNames()) { - OPERATION_METRICS_MAP - .computeIfAbsent(name, k -> new ArrayList<>()) - .add(customizers); - } - }); + ServiceLoader serviceLoader = + ServiceLoader.load(InstrumentationCustomizer.class); + + for (InstrumentationCustomizer customizer : serviceLoader) { + INSTRUMENTATION_CUSTOMIZER_MAP + .computeIfAbsent(customizer.instrumentationNamePredicate(), k -> new ArrayList<>()) + .add(customizer); + } } InstrumenterBuilder( @@ -120,30 +96,6 @@ public final class InstrumenterBuilder { this.spanNameExtractor = spanNameExtractor; this.instrumentationVersion = EmbeddedInstrumentationProperties.findVersion(instrumentationName); - - List contextProviders = - CONTEXT_CUSTOMIZER_MAP.get(instrumentationName); - if (contextProviders != null) { - for (ContextCustomizerCustomizer provider : contextProviders) { - addContextCustomizer(provider.get()); - } - } - - List attributeProviders = - ATTRIBUTES_EXTRACTOR_MAP.get(instrumentationName); - if (attributeProviders != null) { - for (AttributesExtractorCustomizer provider : attributeProviders) { - addAttributesExtractor(provider.get()); - } - } - - List metricsProviders = - OPERATION_METRICS_MAP.get(instrumentationName); - if (metricsProviders != null) { - for (OperationMetricsCustomizer provider : metricsProviders) { - addOperationMetrics(provider.get()); - } - } } /** @@ -347,6 +299,25 @@ public Instrumenter buildInstrumenter( private Instrumenter buildInstrumenter( InstrumenterConstructor constructor, SpanKindExtractor spanKindExtractor) { + List customizers = + INSTRUMENTATION_CUSTOMIZER_MAP.entrySet().stream() + .filter(entry -> entry.getKey().test(instrumentationName)) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + if (customizers != null) { + for (InstrumentationCustomizer customizer : customizers) { + if (customizer.getContextCustomizer() != null) { + addContextCustomizer(customizer.getContextCustomizer()); + } + if (customizer.getAttributesExtractor() != null) { + addAttributesExtractor(customizer.getAttributesExtractor()); + } + if (customizer.getOperationMetrics() != null) { + addOperationMetrics(customizer.getOperationMetrics()); + } + } + } this.spanKindExtractor = spanKindExtractor; return constructor.create(this); } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/OperationMetricsCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/OperationMetricsCustomizer.java deleted file mode 100644 index 20506671ec77..000000000000 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/OperationMetricsCustomizer.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter; - -import java.util.List; - -/** - * A service provider interface (SPI) for providing custom {@link OperationMetrics} instances that - * are conditionally applied based on the instrumentation name. - * - *

This allows external modules or plugins to contribute custom metrics collection logic for - * specific instrumented operations, without modifying core instrumentation code. - */ -public interface OperationMetricsCustomizer { - - /** - * Returns a list of instrumentation names that this metrics customizer supports. - * - *

The customizer will only be applied if the current instrumentation matches one of the - * returned names. For example: ["io.opentelemetry.spring-webmvc-5.0", - * "io.opentelemetry.netty-3.8"]. - * - * @return a list of supported instrumentation names - */ - List instrumentationNames(); - - /** - * Returns a new instance of an {@link OperationMetrics} that will record metrics for the - * instrumented operation. - * - * @return an operation metrics instance - */ - OperationMetrics get(); -} From 6972f96831ee2fdc3cd19363ec50fffb3763f392 Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Mon, 2 Jun 2025 21:02:05 +0800 Subject: [PATCH 06/22] Address review comments --- .../api/instrumenter/InstrumenterBuilder.java | 9 ++- .../api/internal/ServiceLoaderUtil.java | 41 ++++++++++++ .../InstrumentationCustomizerTest.java | 67 +++++++++++++++++++ .../javaagent/bootstrap/AgentInitializer.java | 18 +++++ 4 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ServiceLoaderUtil.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizerTest.java diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java index 0c5cd4ec0262..338a0718bbfb 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java @@ -24,13 +24,13 @@ import io.opentelemetry.instrumentation.api.internal.InstrumenterBuilderAccess; import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; import io.opentelemetry.instrumentation.api.internal.SchemaUrlProvider; +import io.opentelemetry.instrumentation.api.internal.ServiceLoaderUtil; import io.opentelemetry.instrumentation.api.internal.SpanKey; import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.ServiceLoader; import java.util.Set; import java.util.function.Predicate; import java.util.logging.Logger; @@ -77,10 +77,9 @@ public final class InstrumenterBuilder { INSTRUMENTATION_CUSTOMIZER_MAP = new HashMap<>(); static { - ServiceLoader serviceLoader = - ServiceLoader.load(InstrumentationCustomizer.class); - - for (InstrumentationCustomizer customizer : serviceLoader) { + List customizers = + ServiceLoaderUtil.load(InstrumentationCustomizer.class); + for (InstrumentationCustomizer customizer : customizers) { INSTRUMENTATION_CUSTOMIZER_MAP .computeIfAbsent(customizer.instrumentationNamePredicate(), k -> new ArrayList<>()) .add(customizer); diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ServiceLoaderUtil.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ServiceLoaderUtil.java new file mode 100644 index 000000000000..8d29f5fbf261 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ServiceLoaderUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; +import java.util.function.Function; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class ServiceLoaderUtil { + + private static Function, List> loaderFunction = + clazz -> { + List instances = new ArrayList<>(); + ServiceLoader serviceLoader = ServiceLoader.load(clazz); + for (Object instance : serviceLoader) { + instances.add(instance); + } + return instances; + }; + + private ServiceLoaderUtil() { + // Utility class, no instantiation + } + + @SuppressWarnings("unchecked") + public static List load(Class clazz) { + return (List) loaderFunction.apply(clazz); + } + + public static void setLoaderFunction(Function, List> customLoaderFunction) { + loaderFunction = customLoaderFunction; + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizerTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizerTest.java new file mode 100644 index 000000000000..279f026ffc02 --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizerTest.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import java.util.function.Predicate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class InstrumentationCustomizerTest { + + @Mock private AttributesExtractor attributesExtractor; + @Mock private OperationMetrics operationMetrics; + + private InstrumentationCustomizer customizer; + + @BeforeEach + @SuppressWarnings("unchecked") + void setUp() { + customizer = + new InstrumentationCustomizer() { + @Override + public Predicate instrumentationNamePredicate() { + return name -> name.startsWith("test.instrumentation"); + } + + @Override + public + AttributesExtractor getAttributesExtractor() { + return (AttributesExtractor) attributesExtractor; + } + + @Override + public OperationMetrics getOperationMetrics() { + return operationMetrics; + } + }; + } + + @Test + void testInstrumentationNamePredicate() { + assertThat(customizer.instrumentationNamePredicate().test("test.instrumentation.example")) + .isTrue(); + assertThat(customizer.instrumentationNamePredicate().test("other.instrumentation.example")) + .isFalse(); + } + + @Test + void testGetAttributesExtractor() { + AttributesExtractor extractor = customizer.getAttributesExtractor(); + assertThat(extractor).isSameAs(attributesExtractor); + } + + @Test + void testGetOperationMetrics() { + OperationMetrics metrics = customizer.getOperationMetrics(); + assertThat(metrics).isSameAs(operationMetrics); + } +} diff --git a/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java index 509d13964d3b..3197bb0f615f 100644 --- a/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java +++ b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java @@ -6,11 +6,15 @@ package io.opentelemetry.javaagent.bootstrap; import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; +import io.opentelemetry.instrumentation.api.internal.ServiceLoaderUtil; import java.io.File; import java.lang.instrument.Instrumentation; import java.lang.reflect.Constructor; import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; import javax.annotation.Nullable; /** @@ -62,6 +66,7 @@ public Void run() { public Void run() throws Exception { agentClassLoader = createAgentClassLoader("inst", javaagentFile); agentStarter = createAgentStarter(agentClassLoader, inst, javaagentFile); + setLoaderFunction(); if (!fromPremain || !delayAgentStart()) { agentStarter.start(); agentStarted = true; @@ -212,6 +217,19 @@ private static AgentStarter createAgentStarter( constructor.newInstance(instrumentation, javaagentFile, isSecurityManagerSupportEnabled); } + /** Sets a custom loader function for the ServiceLoaderUtil to use the agentClassLoader. */ + private static void setLoaderFunction() { + ServiceLoaderUtil.setLoaderFunction( + clazz -> { + List instances = new ArrayList<>(); + ServiceLoader serviceLoader = ServiceLoader.load(clazz, agentClassLoader); + for (Object instance : serviceLoader) { + instances.add(instance); + } + return instances; + }); + } + private AgentInitializer() {} @SuppressWarnings("SystemOut") From 59662dac374bd6c9294303aef623d4b221af960f Mon Sep 17 00:00:00 2001 From: chengpu Date: Wed, 4 Jun 2025 11:04:08 +0800 Subject: [PATCH 07/22] Moved API to internal directory --- .../opentelemetry-instrumentation-api.txt | 2 +- .../api/instrumenter/InstrumenterBuilder.java | 1 + .../InstrumentationCustomizer.java | 8 ++++++-- 3 files changed, 8 insertions(+), 3 deletions(-) rename instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/{instrumenter => internal}/InstrumentationCustomizer.java (82%) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt index a601f4e15d69..65135e0dab7c 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt @@ -1,5 +1,5 @@ Comparing source compatibility of opentelemetry-instrumentation-api-2.17.0-SNAPSHOT.jar against opentelemetry-instrumentation-api-2.16.0.jar -+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.instrumenter.InstrumentationCustomizer (not serializable) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.internal.InstrumentationCustomizer (not serializable) +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW SUPERCLASS: java.lang.Object +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor getAttributesExtractor() diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java index 338a0718bbfb..0aff6b0c0834 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java @@ -21,6 +21,7 @@ import io.opentelemetry.context.propagation.TextMapSetter; import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties; +import io.opentelemetry.instrumentation.api.internal.InstrumentationCustomizer; import io.opentelemetry.instrumentation.api.internal.InstrumenterBuilderAccess; import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; import io.opentelemetry.instrumentation.api.internal.SchemaUrlProvider; diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java similarity index 82% rename from instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizer.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java index 889c8450d48a..4df1f5267960 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizer.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java @@ -3,8 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter; +package io.opentelemetry.instrumentation.api.internal; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; import java.util.function.Predicate; /** @@ -12,7 +15,8 @@ * operation metrics, attributes extraction, and context customization. * *

This allows external modules or plugins to contribute custom logic for specific instrumented - * libraries, without modifying core instrumentation code. + * libraries, without modifying core instrumentation code. This class is internal and is hence not + * for public use. Its APIs are unstable and can change at any time. */ public interface InstrumentationCustomizer { From 798ba564aaddaaf9884296196cdaca6e89567baa Mon Sep 17 00:00:00 2001 From: chengpu Date: Wed, 4 Jun 2025 11:44:49 +0800 Subject: [PATCH 08/22] Moved API to internal directory --- .../InstrumentationCustomizerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/{instrumenter => internal}/InstrumentationCustomizerTest.java (97%) diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizerTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java similarity index 97% rename from instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizerTest.java rename to instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java index 279f026ffc02..e49771be4129 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumentationCustomizerTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter; +package io.opentelemetry.instrumentation.api.internal; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; From 611aa909e4d2a773df9e555a40bd03c37f0b57dd Mon Sep 17 00:00:00 2001 From: chengpu Date: Wed, 4 Jun 2025 19:44:51 +0800 Subject: [PATCH 09/22] Moved API to internal directory --- .../internal/InstrumentationCustomizerTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java index e49771be4129..936649f53eaf 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java @@ -7,6 +7,9 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; import java.util.function.Predicate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,6 +22,7 @@ class InstrumentationCustomizerTest { @Mock private AttributesExtractor attributesExtractor; @Mock private OperationMetrics operationMetrics; + @Mock private ContextCustomizer contextCustomizer; private InstrumentationCustomizer customizer; @@ -27,6 +31,11 @@ class InstrumentationCustomizerTest { void setUp() { customizer = new InstrumentationCustomizer() { + @Override + public ContextCustomizer getContextCustomizer() { + return (ContextCustomizer) contextCustomizer; + } + @Override public Predicate instrumentationNamePredicate() { return name -> name.startsWith("test.instrumentation"); @@ -45,6 +54,11 @@ public OperationMetrics getOperationMetrics() { }; } + @Test + void testGetContextCustomizer() { + assertThat(customizer.getContextCustomizer()).isSameAs(contextCustomizer); + } + @Test void testInstrumentationNamePredicate() { assertThat(customizer.instrumentationNamePredicate().test("test.instrumentation.example")) From 891822aa4d369caec773b7eab8496477c181b43d Mon Sep 17 00:00:00 2001 From: chengpu Date: Wed, 4 Jun 2025 21:24:25 +0800 Subject: [PATCH 10/22] Fix CI failing --- .../opentelemetry-instrumentation-api.txt | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt index 65135e0dab7c..5926786b4592 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt @@ -1,10 +1,2 @@ Comparing source compatibility of opentelemetry-instrumentation-api-2.17.0-SNAPSHOT.jar against opentelemetry-instrumentation-api-2.16.0.jar -+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.internal.InstrumentationCustomizer (not serializable) - +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. - +++ NEW SUPERCLASS: java.lang.Object - +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor getAttributesExtractor() - GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object - +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer getContextCustomizer() - GENERIC TEMPLATES: +++ REQUEST:java.lang.Object - +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics getOperationMetrics() - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.function.Predicate instrumentationNamePredicate() +No changes. \ No newline at end of file From 7ed6e431b3b706e88cdb99ca9ede8db1922db1ed Mon Sep 17 00:00:00 2001 From: chengpu Date: Thu, 5 Jun 2025 11:55:38 +0800 Subject: [PATCH 11/22] Add support for spanNameExtractor --- .../api/instrumenter/InstrumenterBuilder.java | 5 +- .../internal/InstrumentationCustomizer.java | 17 ++++++- .../InstrumentationCustomizerTest.java | 48 ++++++++++++------- 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java index 0aff6b0c0834..e187e5a0b5f0 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java @@ -56,7 +56,7 @@ public final class InstrumenterBuilder { final OpenTelemetry openTelemetry; final String instrumentationName; - final SpanNameExtractor spanNameExtractor; + SpanNameExtractor spanNameExtractor; final List> spanLinksExtractors = new ArrayList<>(); final List> attributesExtractors = @@ -316,6 +316,9 @@ private Instrumenter buildInstrumenter( if (customizer.getOperationMetrics() != null) { addOperationMetrics(customizer.getOperationMetrics()); } + if (customizer.getSpanNameExtractor() != null) { + this.spanNameExtractor = customizer.getSpanNameExtractor(); + } } } this.spanKindExtractor = spanKindExtractor; diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java index 4df1f5267960..18c016522089 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java @@ -8,6 +8,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; import java.util.function.Predicate; /** @@ -50,7 +51,8 @@ default OperationMetrics getOperationMetrics() { * @param the type of response object used by the instrumented library * @return an attributes extractor instance, or null if not applicable */ - default AttributesExtractor getAttributesExtractor() { + default + AttributesExtractor getAttributesExtractor() { return null; } @@ -61,7 +63,18 @@ default AttributesExtractor getAttributes * @param the type of request object used by the instrumented library * @return a context customizer instance, or null if not applicable */ - default ContextCustomizer getContextCustomizer() { + default ContextCustomizer getContextCustomizer() { + return null; + } + + /** + * Returns a new instance of a {@link SpanNameExtractor} that will customize the span name during + * request processing. + * + * @param the type of request object used by the instrumented library + * @return a customized {@link SpanNameExtractor}, or null if not applicable + */ + default SpanNameExtractor getSpanNameExtractor() { return null; } } diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java index 936649f53eaf..0f34cf961840 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java @@ -10,6 +10,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; import java.util.function.Predicate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -20,9 +21,10 @@ @ExtendWith(MockitoExtension.class) class InstrumentationCustomizerTest { - @Mock private AttributesExtractor attributesExtractor; @Mock private OperationMetrics operationMetrics; + @Mock private AttributesExtractor attributesExtractor; @Mock private ContextCustomizer contextCustomizer; + @Mock private SpanNameExtractor spanNameExtractor; private InstrumentationCustomizer customizer; @@ -31,32 +33,33 @@ class InstrumentationCustomizerTest { void setUp() { customizer = new InstrumentationCustomizer() { - @Override - public ContextCustomizer getContextCustomizer() { - return (ContextCustomizer) contextCustomizer; - } @Override public Predicate instrumentationNamePredicate() { return name -> name.startsWith("test.instrumentation"); } + @Override + public OperationMetrics getOperationMetrics() { + return operationMetrics; + } + @Override public - AttributesExtractor getAttributesExtractor() { - return (AttributesExtractor) attributesExtractor; + AttributesExtractor getAttributesExtractor() { + return (AttributesExtractor) attributesExtractor; } @Override - public OperationMetrics getOperationMetrics() { - return operationMetrics; + public ContextCustomizer getContextCustomizer() { + return (ContextCustomizer) contextCustomizer; } - }; - } - @Test - void testGetContextCustomizer() { - assertThat(customizer.getContextCustomizer()).isSameAs(contextCustomizer); + @Override + public SpanNameExtractor getSpanNameExtractor() { + return (SpanNameExtractor) spanNameExtractor; + } + }; } @Test @@ -67,6 +70,12 @@ void testInstrumentationNamePredicate() { .isFalse(); } + @Test + void testGetOperationMetrics() { + OperationMetrics metrics = customizer.getOperationMetrics(); + assertThat(metrics).isSameAs(operationMetrics); + } + @Test void testGetAttributesExtractor() { AttributesExtractor extractor = customizer.getAttributesExtractor(); @@ -74,8 +83,13 @@ void testGetAttributesExtractor() { } @Test - void testGetOperationMetrics() { - OperationMetrics metrics = customizer.getOperationMetrics(); - assertThat(metrics).isSameAs(operationMetrics); + void testGetContextCustomizer() { + assertThat(customizer.getContextCustomizer()).isSameAs(contextCustomizer); + } + + @Test + void testGetSpanNameExtractor() { + SpanNameExtractor extractor = customizer.getSpanNameExtractor(); + assertThat(extractor).isSameAs(spanNameExtractor); } } From da46f5734ffa517df2e24786f7d78e9e38ae4615 Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Thu, 5 Jun 2025 14:06:00 +0800 Subject: [PATCH 12/22] Support extensions for spanNameExtractor --- .../api/internal/InstrumentationCustomizer.java | 7 +++---- .../api/internal/InstrumentationCustomizerTest.java | 12 ++++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java index 18c016522089..533ebb005882 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java @@ -51,8 +51,7 @@ default OperationMetrics getOperationMetrics() { * @param the type of response object used by the instrumented library * @return an attributes extractor instance, or null if not applicable */ - default - AttributesExtractor getAttributesExtractor() { + default AttributesExtractor getAttributesExtractor() { return null; } @@ -63,7 +62,7 @@ default OperationMetrics getOperationMetrics() { * @param the type of request object used by the instrumented library * @return a context customizer instance, or null if not applicable */ - default ContextCustomizer getContextCustomizer() { + default ContextCustomizer getContextCustomizer() { return null; } @@ -74,7 +73,7 @@ default ContextCustomizer getContextCustomizer() { * @param the type of request object used by the instrumented library * @return a customized {@link SpanNameExtractor}, or null if not applicable */ - default SpanNameExtractor getSpanNameExtractor() { + default SpanNameExtractor getSpanNameExtractor() { return null; } } diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java index 0f34cf961840..3d873f8afa49 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java @@ -46,18 +46,18 @@ public OperationMetrics getOperationMetrics() { @Override public - AttributesExtractor getAttributesExtractor() { - return (AttributesExtractor) attributesExtractor; + AttributesExtractor getAttributesExtractor() { + return (AttributesExtractor) attributesExtractor; } @Override - public ContextCustomizer getContextCustomizer() { - return (ContextCustomizer) contextCustomizer; + public ContextCustomizer getContextCustomizer() { + return (ContextCustomizer) contextCustomizer; } @Override - public SpanNameExtractor getSpanNameExtractor() { - return (SpanNameExtractor) spanNameExtractor; + public SpanNameExtractor getSpanNameExtractor() { + return (SpanNameExtractor) spanNameExtractor; } }; } From 710996200a0fc68dc0596e4d2b44b9f91f10d5bf Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Fri, 6 Jun 2025 00:08:11 +0800 Subject: [PATCH 13/22] Optimize codes --- .../api/instrumenter/InstrumenterBuilder.java | 8 +++++--- .../internal/InstrumentationCustomizer.java | 19 +++++++++++++++++++ .../InstrumentationCustomizerTest.java | 15 +++++++++++++++ .../javaagent/bootstrap/AgentInitializer.java | 18 ------------------ .../javaagent/tooling/AgentStarterImpl.java | 17 +++++++++++++++++ 5 files changed, 56 insertions(+), 21 deletions(-) diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java index e187e5a0b5f0..b4bb26436ecf 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java @@ -302,9 +302,8 @@ private Instrumenter buildInstrumenter( List customizers = INSTRUMENTATION_CUSTOMIZER_MAP.entrySet().stream() .filter(entry -> entry.getKey().test(instrumentationName)) - .map(Map.Entry::getValue) - .findFirst() - .orElse(null); + .flatMap(entry -> entry.getValue().stream()) + .collect(Collectors.toList()); if (customizers != null) { for (InstrumentationCustomizer customizer : customizers) { if (customizer.getContextCustomizer() != null) { @@ -313,6 +312,9 @@ private Instrumenter buildInstrumenter( if (customizer.getAttributesExtractor() != null) { addAttributesExtractor(customizer.getAttributesExtractor()); } + if (customizer.getAttributesExtractors() != null) { + addAttributesExtractors(customizer.getAttributesExtractors()); + } if (customizer.getOperationMetrics() != null) { addOperationMetrics(customizer.getOperationMetrics()); } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java index 533ebb005882..2d6453dbdfe7 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java @@ -9,7 +9,9 @@ import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import java.util.List; import java.util.function.Predicate; +import javax.annotation.Nullable; /** * A service provider interface (SPI) for providing customizations for instrumentation, including @@ -55,6 +57,23 @@ default AttributesExtractor getAttributes return null; } + /** + * Returns a list of {@link AttributesExtractor}s that will extract attributes from requests and + * responses during the instrumentation process. + * + *

This allows providing multiple extractors for a single instrumentation. The default + * implementation returns {@code null} for backward compatibility. + * + * @param the type of request object used by the instrumented library + * @param the type of response object used by the instrumented library + * @return a list of attributes extractors, or null if not applicable + */ + @Nullable + default + List> getAttributesExtractors() { + return null; + } + /** * Returns a new instance of a {@link ContextCustomizer} that will customize the tracing context * during request processing. diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java index 3d873f8afa49..566d2fbc3df9 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java @@ -11,6 +11,8 @@ import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import java.util.Collections; +import java.util.List; import java.util.function.Predicate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -50,6 +52,13 @@ AttributesExtractor getAttributesExtractor() { return (AttributesExtractor) attributesExtractor; } + @Override + public + List> getAttributesExtractors() { + return Collections.singletonList( + (AttributesExtractor) attributesExtractor); + } + @Override public ContextCustomizer getContextCustomizer() { return (ContextCustomizer) contextCustomizer; @@ -82,6 +91,12 @@ void testGetAttributesExtractor() { assertThat(extractor).isSameAs(attributesExtractor); } + @Test + void testGetAttributesExtractors() { + List> extractors = customizer.getAttributesExtractors(); + assertThat(extractors).containsExactly(attributesExtractor); + } + @Test void testGetContextCustomizer() { assertThat(customizer.getContextCustomizer()).isSameAs(contextCustomizer); diff --git a/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java index 3197bb0f615f..509d13964d3b 100644 --- a/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java +++ b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java @@ -6,15 +6,11 @@ package io.opentelemetry.javaagent.bootstrap; import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; -import io.opentelemetry.instrumentation.api.internal.ServiceLoaderUtil; import java.io.File; import java.lang.instrument.Instrumentation; import java.lang.reflect.Constructor; import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; -import java.util.ArrayList; -import java.util.List; -import java.util.ServiceLoader; import javax.annotation.Nullable; /** @@ -66,7 +62,6 @@ public Void run() { public Void run() throws Exception { agentClassLoader = createAgentClassLoader("inst", javaagentFile); agentStarter = createAgentStarter(agentClassLoader, inst, javaagentFile); - setLoaderFunction(); if (!fromPremain || !delayAgentStart()) { agentStarter.start(); agentStarted = true; @@ -217,19 +212,6 @@ private static AgentStarter createAgentStarter( constructor.newInstance(instrumentation, javaagentFile, isSecurityManagerSupportEnabled); } - /** Sets a custom loader function for the ServiceLoaderUtil to use the agentClassLoader. */ - private static void setLoaderFunction() { - ServiceLoaderUtil.setLoaderFunction( - clazz -> { - List instances = new ArrayList<>(); - ServiceLoader serviceLoader = ServiceLoader.load(clazz, agentClassLoader); - for (Object instance : serviceLoader) { - instances.add(instance); - } - return instances; - }); - } - private AgentInitializer() {} @SuppressWarnings("SystemOut") diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentStarterImpl.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentStarterImpl.java index ab5cb93c22ed..81f19fe497bd 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentStarterImpl.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentStarterImpl.java @@ -6,6 +6,7 @@ package io.opentelemetry.javaagent.tooling; import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.internal.ServiceLoaderUtil; import io.opentelemetry.instrumentation.api.internal.cache.weaklockfree.WeakConcurrentMapCleaner; import io.opentelemetry.javaagent.bootstrap.AgentInitializer; import io.opentelemetry.javaagent.bootstrap.AgentStarter; @@ -16,6 +17,8 @@ import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.List; import java.util.ServiceLoader; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -72,6 +75,7 @@ public void start() { EarlyInitAgentConfig earlyConfig = EarlyInitAgentConfig.create(); extensionClassLoader = createExtensionClassLoader(getClass().getClassLoader(), earlyConfig); + setLoaderFunction(); String loggerImplementationName = earlyConfig.getString("otel.javaagent.logging"); // default to the built-in stderr slf4j-simple logger @@ -246,4 +250,17 @@ public void visitMethodInsn( return hookInserted ? cw.toByteArray() : null; } } + + /** Sets a custom loader function for the ServiceLoaderUtil to use the agentClassLoader. */ + private void setLoaderFunction() { + ServiceLoaderUtil.setLoaderFunction( + clazz -> { + List instances = new ArrayList<>(); + ServiceLoader serviceLoader = ServiceLoader.load(clazz, extensionClassLoader); + for (Object instance : serviceLoader) { + instances.add(instance); + } + return instances; + }); + } } From 159e53d4234324bdbcc416d7346ae1a2fb6f5015 Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Mon, 16 Jun 2025 17:42:10 +0800 Subject: [PATCH 14/22] Refactor extension to be consistent with existence. --- .../opentelemetry-instrumentation-api.txt | 5 +- .../api/instrumenter/InstrumenterBuilder.java | 62 +++++----- .../internal/InstrumentationCustomizer.java | 98 ---------------- .../api/internal/InstrumenterCustomizer.java | 79 +++++++++++++ .../internal/InstrumenterCustomizerImpl.java | 64 ++++++++++ .../InstrumenterCustomizerProvider.java | 33 ++++++ .../InstrumentationCustomizerTest.java | 109 +++++++++--------- 7 files changed, 263 insertions(+), 187 deletions(-) delete mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizer.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerImpl.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerProvider.java diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt index 5926786b4592..89fa510bbcc2 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt @@ -1,2 +1,5 @@ Comparing source compatibility of opentelemetry-instrumentation-api-2.17.0-SNAPSHOT.jar against opentelemetry-instrumentation-api-2.16.0.jar -No changes. \ No newline at end of file +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + GENERIC TEMPLATES: === REQUEST:java.lang.Object, === RESPONSE:java.lang.Object + *** MODIFIED FIELD: PUBLIC (<- PACKAGE_PROTECTED) NON_FINAL (<- FINAL) io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor spanNameExtractor diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java index b4bb26436ecf..9a267f1ae197 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java @@ -21,8 +21,10 @@ import io.opentelemetry.context.propagation.TextMapSetter; import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties; -import io.opentelemetry.instrumentation.api.internal.InstrumentationCustomizer; import io.opentelemetry.instrumentation.api.internal.InstrumenterBuilderAccess; +import io.opentelemetry.instrumentation.api.internal.InstrumenterCustomizer; +import io.opentelemetry.instrumentation.api.internal.InstrumenterCustomizerImpl; +import io.opentelemetry.instrumentation.api.internal.InstrumenterCustomizerProvider; import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; import io.opentelemetry.instrumentation.api.internal.SchemaUrlProvider; import io.opentelemetry.instrumentation.api.internal.ServiceLoaderUtil; @@ -33,7 +35,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Predicate; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -56,7 +57,7 @@ public final class InstrumenterBuilder { final OpenTelemetry openTelemetry; final String instrumentationName; - SpanNameExtractor spanNameExtractor; + public SpanNameExtractor spanNameExtractor; final List> spanLinksExtractors = new ArrayList<>(); final List> attributesExtractors = @@ -74,16 +75,19 @@ public final class InstrumenterBuilder { boolean propagateOperationListenersToOnEnd = false; boolean enabled = true; - private static final Map, List> + private static final Map> INSTRUMENTATION_CUSTOMIZER_MAP = new HashMap<>(); static { - List customizers = - ServiceLoaderUtil.load(InstrumentationCustomizer.class); - for (InstrumentationCustomizer customizer : customizers) { - INSTRUMENTATION_CUSTOMIZER_MAP - .computeIfAbsent(customizer.instrumentationNamePredicate(), k -> new ArrayList<>()) - .add(customizer); + List providers = + ServiceLoaderUtil.load(InstrumenterCustomizerProvider.class); + for (InstrumenterCustomizerProvider provider : providers) { + String instrumentationName = provider.getInstrumentationName(); + if (instrumentationName != null) { + INSTRUMENTATION_CUSTOMIZER_MAP + .computeIfAbsent(instrumentationName, k -> new ArrayList<>()) + .add(provider); + } } } @@ -299,30 +303,24 @@ public Instrumenter buildInstrumenter( private Instrumenter buildInstrumenter( InstrumenterConstructor constructor, SpanKindExtractor spanKindExtractor) { - List customizers = - INSTRUMENTATION_CUSTOMIZER_MAP.entrySet().stream() - .filter(entry -> entry.getKey().test(instrumentationName)) - .flatMap(entry -> entry.getValue().stream()) - .collect(Collectors.toList()); - if (customizers != null) { - for (InstrumentationCustomizer customizer : customizers) { - if (customizer.getContextCustomizer() != null) { - addContextCustomizer(customizer.getContextCustomizer()); - } - if (customizer.getAttributesExtractor() != null) { - addAttributesExtractor(customizer.getAttributesExtractor()); - } - if (customizer.getAttributesExtractors() != null) { - addAttributesExtractors(customizer.getAttributesExtractors()); - } - if (customizer.getOperationMetrics() != null) { - addOperationMetrics(customizer.getOperationMetrics()); - } - if (customizer.getSpanNameExtractor() != null) { - this.spanNameExtractor = customizer.getSpanNameExtractor(); - } + + List providers = + INSTRUMENTATION_CUSTOMIZER_MAP.get(instrumentationName); + + if (providers != null && !providers.isEmpty()) { + InstrumenterCustomizer customizer = + new InstrumenterCustomizerImpl(this) { + @Override + public String getInstrumentationName() { + return instrumentationName; + } + }; + + for (InstrumenterCustomizerProvider provider : providers) { + provider.customize(customizer); } } + this.spanKindExtractor = spanKindExtractor; return constructor.create(this); } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java deleted file mode 100644 index 2d6453dbdfe7..000000000000 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizer.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.internal; - -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; -import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; -import java.util.List; -import java.util.function.Predicate; -import javax.annotation.Nullable; - -/** - * A service provider interface (SPI) for providing customizations for instrumentation, including - * operation metrics, attributes extraction, and context customization. - * - *

This allows external modules or plugins to contribute custom logic for specific instrumented - * libraries, without modifying core instrumentation code. This class is internal and is hence not - * for public use. Its APIs are unstable and can change at any time. - */ -public interface InstrumentationCustomizer { - - /** - * Returns a predicate that determines whether this customizer supports a given instrumentation - * name. - * - *

The customizer will only be applied if the current instrumentation name matches the - * predicate. For example, the predicate might match names like "io.opentelemetry.netty-3.8" or - * "io.opentelemetry.apache-httpclient-4.3". - * - * @return a predicate for supported instrumentation names - */ - Predicate instrumentationNamePredicate(); - - /** - * Returns a new instance of an {@link OperationMetrics} that will record metrics for the - * instrumented operation. - * - * @return an operation metrics instance, or null if not applicable - */ - default OperationMetrics getOperationMetrics() { - return null; - } - - /** - * Returns a new instance of an {@link AttributesExtractor} that will extract attributes from - * requests and responses during the instrumentation process. - * - * @param the type of request object used by the instrumented library - * @param the type of response object used by the instrumented library - * @return an attributes extractor instance, or null if not applicable - */ - default AttributesExtractor getAttributesExtractor() { - return null; - } - - /** - * Returns a list of {@link AttributesExtractor}s that will extract attributes from requests and - * responses during the instrumentation process. - * - *

This allows providing multiple extractors for a single instrumentation. The default - * implementation returns {@code null} for backward compatibility. - * - * @param the type of request object used by the instrumented library - * @param the type of response object used by the instrumented library - * @return a list of attributes extractors, or null if not applicable - */ - @Nullable - default - List> getAttributesExtractors() { - return null; - } - - /** - * Returns a new instance of a {@link ContextCustomizer} that will customize the tracing context - * during request processing. - * - * @param the type of request object used by the instrumented library - * @return a context customizer instance, or null if not applicable - */ - default ContextCustomizer getContextCustomizer() { - return null; - } - - /** - * Returns a new instance of a {@link SpanNameExtractor} that will customize the span name during - * request processing. - * - * @param the type of request object used by the instrumented library - * @return a customized {@link SpanNameExtractor}, or null if not applicable - */ - default SpanNameExtractor getSpanNameExtractor() { - return null; - } -} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizer.java new file mode 100644 index 000000000000..d7a33769c129 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizer.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import java.util.function.Function; + +/** + * A service provider interface (SPI) for providing customizations for instrumentation, including + * operation metrics, attributes extraction, and context customization. + * + *

This allows external modules or plugins to contribute custom logic for specific instrumented + * libraries, without modifying core instrumentation code. This class is internal and is hence not + * for public use. Its APIs are unstable and can change at any time. + */ +public interface InstrumenterCustomizer { + + /** + * Returns the name of the instrumentation that this customizer applies to. This allows for + * efficient mapping of customizers to specific instrumentations rather than using predicates for + * matching. + * + * @return the name of the instrumentation this customizer targets + */ + String getInstrumentationName(); + + /** + * Adds a single AttributesExtractor to the instrumenter. This extractor will be used to extract + * attributes from requests and responses during span creation and enrichment. + * + * @param extractor the attributes extractor to add + * @return this InstrumenterCustomizer for method chaining + */ + InstrumenterCustomizer addAttributesExtractor(AttributesExtractor extractor); + + /** + * Adds multiple AttributesExtractors to the instrumenter. These extractors will be used to + * extract attributes from requests and responses during span creation and enrichment. + * + * @param extractors the collection of attributes extractors to add + * @return this InstrumenterCustomizer for method chaining + */ + InstrumenterCustomizer addAttributesExtractors( + Iterable> extractors); + + /** + * Adds an OperationMetrics implementation to the instrumenter. This will be used to create + * metrics capturing the request processing metrics for the instrumented operations. + * + * @param operationMetrics the metrics factory to add + * @return this InstrumenterCustomizer for method chaining + */ + InstrumenterCustomizer addOperationMetrics(OperationMetrics operationMetrics); + + /** + * Sets a ContextCustomizer for the instrumenter. The customizer will modify the context during + * the Instrumenter.start() operation, allowing custom context propagation or enrichment. + * + * @param customizer the context customizer to set + * @return this InstrumenterCustomizer for method chaining + */ + InstrumenterCustomizer addContextCustomizer(ContextCustomizer customizer); + + /** + * Sets a transformer function that will modify the SpanNameExtractor. This allows customizing how + * span names are generated for the instrumented operations. + * + * @param spanNameExtractorTransformer function that transforms the original span name extractor + * @return this InstrumenterCustomizer for method chaining + */ + InstrumenterCustomizer setSpanNameExtractor( + Function, SpanNameExtractor> spanNameExtractorTransformer); +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerImpl.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerImpl.java new file mode 100644 index 000000000000..3ea2493ced23 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerImpl.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import java.util.function.Function; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public abstract class InstrumenterCustomizerImpl implements InstrumenterCustomizer { + + private final InstrumenterBuilder builder; + + public InstrumenterCustomizerImpl(InstrumenterBuilder builder) { + this.builder = builder; + } + + @Override + @SuppressWarnings("unchecked") + public InstrumenterCustomizer addAttributesExtractor(AttributesExtractor extractor) { + builder.addAttributesExtractor((AttributesExtractor) extractor); + return this; + } + + @Override + @SuppressWarnings("unchecked") + public InstrumenterCustomizer addAttributesExtractors( + Iterable> extractors) { + builder.addAttributesExtractors((Iterable>) extractors); + return this; + } + + @Override + public InstrumenterCustomizer addOperationMetrics(OperationMetrics operationMetrics) { + builder.addOperationMetrics(operationMetrics); + return this; + } + + @Override + @SuppressWarnings("unchecked") + public InstrumenterCustomizer addContextCustomizer(ContextCustomizer customizer) { + builder.addContextCustomizer((ContextCustomizer) customizer); + return this; + } + + @Override + @SuppressWarnings("unchecked") + public InstrumenterCustomizer setSpanNameExtractor( + Function, SpanNameExtractor> spanNameExtractorTransformer) { + builder.spanNameExtractor = + (SpanNameExtractor) + spanNameExtractorTransformer.apply(builder.spanNameExtractor); + return this; + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerProvider.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerProvider.java new file mode 100644 index 000000000000..6a11dbae88ad --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +/** + * A service provider interface (SPI) for customizing instrumentation behavior. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public interface InstrumenterCustomizerProvider { + + /** + * Returns a predicate that matches the instrumentation name for which this provider is + * applicable. + * + * @return a predicate that matches the instrumentation name for which this provider is applicable + */ + String getInstrumentationName(); + + /** + * Customizes the given instrumenter. + * + *

This method is called for each instrumenter being built. Implementations can use the + * provided customizer to add or modify behavior of the instrumenter. + * + * @param customizer the customizer for the instrumenter being built + */ + void customize(InstrumenterCustomizer customizer); +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java index 566d2fbc3df9..b7b28c509e97 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java @@ -6,14 +6,15 @@ package io.opentelemetry.instrumentation.api.internal; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.mockito.Mockito.verify; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; -import java.util.Collections; -import java.util.List; -import java.util.function.Predicate; +import java.util.Arrays; +import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,88 +24,84 @@ @ExtendWith(MockitoExtension.class) class InstrumentationCustomizerTest { + private static final String TEST_INSTRUMENTATION_NAME = "test.instrumentation"; + + @Mock private InstrumenterBuilder builder; @Mock private OperationMetrics operationMetrics; @Mock private AttributesExtractor attributesExtractor; + @Mock private AttributesExtractor secondAttributesExtractor; @Mock private ContextCustomizer contextCustomizer; @Mock private SpanNameExtractor spanNameExtractor; - private InstrumentationCustomizer customizer; + private InstrumenterCustomizer customizer; @BeforeEach - @SuppressWarnings("unchecked") void setUp() { + builder.spanNameExtractor = spanNameExtractor; customizer = - new InstrumentationCustomizer() { - - @Override - public Predicate instrumentationNamePredicate() { - return name -> name.startsWith("test.instrumentation"); - } - - @Override - public OperationMetrics getOperationMetrics() { - return operationMetrics; - } - + new InstrumenterCustomizerImpl(builder) { @Override - public - AttributesExtractor getAttributesExtractor() { - return (AttributesExtractor) attributesExtractor; - } - - @Override - public - List> getAttributesExtractors() { - return Collections.singletonList( - (AttributesExtractor) attributesExtractor); - } - - @Override - public ContextCustomizer getContextCustomizer() { - return (ContextCustomizer) contextCustomizer; - } - - @Override - public SpanNameExtractor getSpanNameExtractor() { - return (SpanNameExtractor) spanNameExtractor; + public String getInstrumentationName() { + return TEST_INSTRUMENTATION_NAME; } }; } @Test - void testInstrumentationNamePredicate() { - assertThat(customizer.instrumentationNamePredicate().test("test.instrumentation.example")) - .isTrue(); - assertThat(customizer.instrumentationNamePredicate().test("other.instrumentation.example")) - .isFalse(); + void testGetInstrumentationName() { + assertThat(customizer.getInstrumentationName()).isEqualTo(TEST_INSTRUMENTATION_NAME); } @Test - void testGetOperationMetrics() { - OperationMetrics metrics = customizer.getOperationMetrics(); - assertThat(metrics).isSameAs(operationMetrics); + void testAddAttributesExtractor() { + InstrumenterCustomizer result = customizer.addAttributesExtractor(attributesExtractor); + + verify(builder).addAttributesExtractor(attributesExtractor); + assertThat(result).isSameAs(customizer); } @Test - void testGetAttributesExtractor() { - AttributesExtractor extractor = customizer.getAttributesExtractor(); - assertThat(extractor).isSameAs(attributesExtractor); + void testAddAttributesExtractors() { + Iterable> extractors = + Arrays.asList(attributesExtractor, secondAttributesExtractor); + + InstrumenterCustomizer result = customizer.addAttributesExtractors(extractors); + + verify(builder).addAttributesExtractors(extractors); + assertThat(result).isSameAs(customizer); } @Test - void testGetAttributesExtractors() { - List> extractors = customizer.getAttributesExtractors(); - assertThat(extractors).containsExactly(attributesExtractor); + void testAddOperationMetrics() { + InstrumenterCustomizer result = customizer.addOperationMetrics(operationMetrics); + + verify(builder).addOperationMetrics(operationMetrics); + assertThat(result).isSameAs(customizer); } @Test - void testGetContextCustomizer() { - assertThat(customizer.getContextCustomizer()).isSameAs(contextCustomizer); + void testAddContextCustomizer() { + InstrumenterCustomizer result = customizer.addContextCustomizer(contextCustomizer); + + verify(builder).addContextCustomizer(contextCustomizer); + assertThat(result).isSameAs(customizer); } @Test - void testGetSpanNameExtractor() { - SpanNameExtractor extractor = customizer.getSpanNameExtractor(); - assertThat(extractor).isSameAs(spanNameExtractor); + @SuppressWarnings("unchecked") + void testSetSpanNameExtractor() { + Function, SpanNameExtractor> transformer = + original -> + (SpanNameExtractor) + request -> "custom:" + ((SpanNameExtractor) original).extract(request); + + SpanNameExtractor originalExtractor = request -> "original"; + builder.spanNameExtractor = originalExtractor; + + customizer.setSpanNameExtractor(transformer); + + SpanNameExtractor updatedExtractor = builder.spanNameExtractor; + String result = updatedExtractor.extract(new Object()); + assertThat(result).isEqualTo("custom:original"); } } From e7d8de36e960a921f2257f3e8b6dc3ef40c360ee Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Tue, 17 Jun 2025 15:57:37 +0300 Subject: [PATCH 15/22] Move customizer api to incubator module (#9) --- .../opentelemetry-instrumentation-api.txt | 5 +- .../instrumenter}/InstrumenterCustomizer.java | 2 +- .../InstrumenterCustomizerProvider.java | 10 +- .../internal/InstrumenterCustomizerImpl.java | 36 +- .../internal/InstrumenterCustomizerUtil.java | 31 ++ ...nalInstrumenterCustomizerProviderImpl.java | 28 ++ .../api/instrumenter/InstrumenterBuilder.java | 88 +++-- .../InternalInstrumenterCustomizer.java | 34 ++ ...nternalInstrumenterCustomizerProvider.java | 15 + .../InternalInstrumenterCustomizerUtil.java | 40 ++ .../api/internal/ServiceLoaderUtil.java | 24 +- .../InstrumentationCustomizerTest.java | 350 +++++++++++++++--- .../javaagent/tooling/AgentStarterImpl.java | 18 +- 13 files changed, 531 insertions(+), 150 deletions(-) rename {instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter}/InstrumenterCustomizer.java (97%) rename {instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter}/InstrumenterCustomizerProvider.java (68%) rename {instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter}/internal/InstrumenterCustomizerImpl.java (56%) create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InstrumenterCustomizerUtil.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InternalInstrumenterCustomizerProviderImpl.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizer.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizerProvider.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizerUtil.java diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt index 89fa510bbcc2..5926786b4592 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt @@ -1,5 +1,2 @@ Comparing source compatibility of opentelemetry-instrumentation-api-2.17.0-SNAPSHOT.jar against opentelemetry-instrumentation-api-2.16.0.jar -*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder (not serializable) - === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 - GENERIC TEMPLATES: === REQUEST:java.lang.Object, === RESPONSE:java.lang.Object - *** MODIFIED FIELD: PUBLIC (<- PACKAGE_PROTECTED) NON_FINAL (<- FINAL) io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor spanNameExtractor +No changes. \ No newline at end of file diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizer.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizer.java similarity index 97% rename from instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizer.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizer.java index d7a33769c129..8ab9a1a3edd6 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizer.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizer.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.internal; +package io.opentelemetry.instrumentation.api.incubator.instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerProvider.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizerProvider.java similarity index 68% rename from instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerProvider.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizerProvider.java index 6a11dbae88ad..006369065a2b 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerProvider.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizerProvider.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.internal; +package io.opentelemetry.instrumentation.api.incubator.instrumenter; /** * A service provider interface (SPI) for customizing instrumentation behavior. @@ -13,14 +13,6 @@ */ public interface InstrumenterCustomizerProvider { - /** - * Returns a predicate that matches the instrumentation name for which this provider is - * applicable. - * - * @return a predicate that matches the instrumentation name for which this provider is applicable - */ - String getInstrumentationName(); - /** * Customizes the given instrumenter. * diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerImpl.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InstrumenterCustomizerImpl.java similarity index 56% rename from instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerImpl.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InstrumenterCustomizerImpl.java index 3ea2493ced23..322a5c02cbcf 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterCustomizerImpl.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InstrumenterCustomizerImpl.java @@ -3,62 +3,62 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.internal; +package io.opentelemetry.instrumentation.api.incubator.instrumenter.internal; +import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizer; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; -import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.internal.InternalInstrumenterCustomizer; import java.util.function.Function; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at * any time. */ -public abstract class InstrumenterCustomizerImpl implements InstrumenterCustomizer { +@SuppressWarnings({"unchecked", "rawtypes"}) +public final class InstrumenterCustomizerImpl implements InstrumenterCustomizer { + private final InternalInstrumenterCustomizer customizer; - private final InstrumenterBuilder builder; + public InstrumenterCustomizerImpl(InternalInstrumenterCustomizer customizer) { + this.customizer = customizer; + } - public InstrumenterCustomizerImpl(InstrumenterBuilder builder) { - this.builder = builder; + @Override + public String getInstrumentationName() { + return customizer.getInstrumentationName(); } @Override - @SuppressWarnings("unchecked") public InstrumenterCustomizer addAttributesExtractor(AttributesExtractor extractor) { - builder.addAttributesExtractor((AttributesExtractor) extractor); + customizer.addAttributesExtractor(extractor); return this; } @Override - @SuppressWarnings("unchecked") public InstrumenterCustomizer addAttributesExtractors( Iterable> extractors) { - builder.addAttributesExtractors((Iterable>) extractors); + customizer.addAttributesExtractors(extractors); return this; } @Override public InstrumenterCustomizer addOperationMetrics(OperationMetrics operationMetrics) { - builder.addOperationMetrics(operationMetrics); + customizer.addOperationMetrics(operationMetrics); return this; } @Override - @SuppressWarnings("unchecked") public InstrumenterCustomizer addContextCustomizer(ContextCustomizer customizer) { - builder.addContextCustomizer((ContextCustomizer) customizer); + this.customizer.addContextCustomizer(customizer); return this; } @Override - @SuppressWarnings("unchecked") public InstrumenterCustomizer setSpanNameExtractor( Function, SpanNameExtractor> spanNameExtractorTransformer) { - builder.spanNameExtractor = - (SpanNameExtractor) - spanNameExtractorTransformer.apply(builder.spanNameExtractor); - return this; + customizer.setSpanNameExtractor(spanNameExtractorTransformer); + return null; } } diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InstrumenterCustomizerUtil.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InstrumenterCustomizerUtil.java new file mode 100644 index 000000000000..022b22404b84 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InstrumenterCustomizerUtil.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.instrumenter.internal; + +import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizerProvider; +import io.opentelemetry.instrumentation.api.internal.InternalInstrumenterCustomizerProvider; +import io.opentelemetry.instrumentation.api.internal.InternalInstrumenterCustomizerUtil; +import io.opentelemetry.instrumentation.api.internal.ServiceLoaderUtil; +import java.util.ArrayList; +import java.util.List; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class InstrumenterCustomizerUtil { + + static { + List providers = new ArrayList<>(); + for (InstrumenterCustomizerProvider provider : + ServiceLoaderUtil.load(InstrumenterCustomizerProvider.class)) { + providers.add(new InternalInstrumenterCustomizerProviderImpl(provider)); + } + InternalInstrumenterCustomizerUtil.setInstrumenterCustomizerProviders(providers); + } + + private InstrumenterCustomizerUtil() {} +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InternalInstrumenterCustomizerProviderImpl.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InternalInstrumenterCustomizerProviderImpl.java new file mode 100644 index 000000000000..e9b4641b0956 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InternalInstrumenterCustomizerProviderImpl.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.instrumenter.internal; + +import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizerProvider; +import io.opentelemetry.instrumentation.api.internal.InternalInstrumenterCustomizer; +import io.opentelemetry.instrumentation.api.internal.InternalInstrumenterCustomizerProvider; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class InternalInstrumenterCustomizerProviderImpl + implements InternalInstrumenterCustomizerProvider { + private final InstrumenterCustomizerProvider provider; + + public InternalInstrumenterCustomizerProviderImpl(InstrumenterCustomizerProvider provider) { + this.provider = provider; + } + + @Override + public void customize(InternalInstrumenterCustomizer customizer) { + provider.customize(new InstrumenterCustomizerImpl(customizer)); + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java index 9a267f1ae197..3f44c44c8749 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java @@ -22,19 +22,17 @@ import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties; import io.opentelemetry.instrumentation.api.internal.InstrumenterBuilderAccess; -import io.opentelemetry.instrumentation.api.internal.InstrumenterCustomizer; -import io.opentelemetry.instrumentation.api.internal.InstrumenterCustomizerImpl; -import io.opentelemetry.instrumentation.api.internal.InstrumenterCustomizerProvider; import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.internal.InternalInstrumenterCustomizer; +import io.opentelemetry.instrumentation.api.internal.InternalInstrumenterCustomizerProvider; +import io.opentelemetry.instrumentation.api.internal.InternalInstrumenterCustomizerUtil; import io.opentelemetry.instrumentation.api.internal.SchemaUrlProvider; -import io.opentelemetry.instrumentation.api.internal.ServiceLoaderUtil; import io.opentelemetry.instrumentation.api.internal.SpanKey; import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -57,7 +55,7 @@ public final class InstrumenterBuilder { final OpenTelemetry openTelemetry; final String instrumentationName; - public SpanNameExtractor spanNameExtractor; + SpanNameExtractor spanNameExtractor; final List> spanLinksExtractors = new ArrayList<>(); final List> attributesExtractors = @@ -75,22 +73,6 @@ public final class InstrumenterBuilder { boolean propagateOperationListenersToOnEnd = false; boolean enabled = true; - private static final Map> - INSTRUMENTATION_CUSTOMIZER_MAP = new HashMap<>(); - - static { - List providers = - ServiceLoaderUtil.load(InstrumenterCustomizerProvider.class); - for (InstrumenterCustomizerProvider provider : providers) { - String instrumentationName = provider.getInstrumentationName(); - if (instrumentationName != null) { - INSTRUMENTATION_CUSTOMIZER_MAP - .computeIfAbsent(instrumentationName, k -> new ArrayList<>()) - .add(provider); - } - } - } - InstrumenterBuilder( OpenTelemetry openTelemetry, String instrumentationName, @@ -304,22 +286,7 @@ private Instrumenter buildInstrumenter( InstrumenterConstructor constructor, SpanKindExtractor spanKindExtractor) { - List providers = - INSTRUMENTATION_CUSTOMIZER_MAP.get(instrumentationName); - - if (providers != null && !providers.isEmpty()) { - InstrumenterCustomizer customizer = - new InstrumenterCustomizerImpl(this) { - @Override - public String getInstrumentationName() { - return instrumentationName; - } - }; - - for (InstrumenterCustomizerProvider provider : providers) { - provider.customize(customizer); - } - } + applyCustomizers(this); this.spanKindExtractor = spanKindExtractor; return constructor.create(this); @@ -415,6 +382,49 @@ private void propagateOperationListenersToOnEnd() { propagateOperationListenersToOnEnd = true; } + private static void applyCustomizers( + InstrumenterBuilder builder) { + for (InternalInstrumenterCustomizerProvider provider : + InternalInstrumenterCustomizerUtil.getInstrumenterCustomizerProviders()) { + provider.customize( + new InternalInstrumenterCustomizer() { + @Override + public String getInstrumentationName() { + return builder.instrumentationName; + } + + @Override + public void addAttributesExtractor(AttributesExtractor extractor) { + builder.addAttributesExtractor(extractor); + } + + @Override + public void addAttributesExtractors( + Iterable> extractors) { + builder.addAttributesExtractors(extractors); + } + + @Override + public void addOperationMetrics(OperationMetrics operationMetrics) { + builder.addOperationMetrics(operationMetrics); + } + + @Override + public void addContextCustomizer(ContextCustomizer customizer) { + builder.addContextCustomizer(customizer); + } + + @Override + public void setSpanNameExtractor( + Function, SpanNameExtractor> + spanNameExtractorTransformer) { + builder.spanNameExtractor = + spanNameExtractorTransformer.apply(builder.spanNameExtractor); + } + }); + } + } + private interface InstrumenterConstructor { Instrumenter create(InstrumenterBuilder builder); diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizer.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizer.java new file mode 100644 index 000000000000..d1a0efd7b330 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizer.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import java.util.function.Function; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public interface InternalInstrumenterCustomizer { + + String getInstrumentationName(); + + void addAttributesExtractor(AttributesExtractor extractor); + + void addAttributesExtractors( + Iterable> extractors); + + void addOperationMetrics(OperationMetrics operationMetrics); + + void addContextCustomizer(ContextCustomizer customizer); + + void setSpanNameExtractor( + Function, SpanNameExtractor> + spanNameExtractorTransformer); +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizerProvider.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizerProvider.java new file mode 100644 index 000000000000..c35e64479cc8 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizerProvider.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public interface InternalInstrumenterCustomizerProvider { + + void customize(InternalInstrumenterCustomizer customizer); +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizerUtil.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizerUtil.java new file mode 100644 index 000000000000..f73958e6fa5c --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizerUtil.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import java.util.Collections; +import java.util.List; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class InternalInstrumenterCustomizerUtil { + static { + try { + // initializing InstrumenterCustomizerUtil will call setInstrumenterCustomizerProviders on + // this class + Class.forName( + "io.opentelemetry.instrumentation.api.incubator.instrumenter.internal.InstrumenterCustomizerUtil"); + } catch (ClassNotFoundException exception) { + // incubator api not available, ignore + } + } + + private static volatile List + instrumenterCustomizerProviders = Collections.emptyList(); + + public static void setInstrumenterCustomizerProviders( + List providers) { + instrumenterCustomizerProviders = providers; + } + + public static List getInstrumenterCustomizerProviders() { + return instrumenterCustomizerProviders; + } + + private InternalInstrumenterCustomizerUtil() {} +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ServiceLoaderUtil.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ServiceLoaderUtil.java index 8d29f5fbf261..f8793e0b790c 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ServiceLoaderUtil.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ServiceLoaderUtil.java @@ -5,8 +5,6 @@ package io.opentelemetry.instrumentation.api.internal; -import java.util.ArrayList; -import java.util.List; import java.util.ServiceLoader; import java.util.function.Function; @@ -16,26 +14,16 @@ */ public final class ServiceLoaderUtil { - private static Function, List> loaderFunction = - clazz -> { - List instances = new ArrayList<>(); - ServiceLoader serviceLoader = ServiceLoader.load(clazz); - for (Object instance : serviceLoader) { - instances.add(instance); - } - return instances; - }; + private static Function, Iterable> loadFunction = ServiceLoader::load; - private ServiceLoaderUtil() { - // Utility class, no instantiation - } + private ServiceLoaderUtil() {} @SuppressWarnings("unchecked") - public static List load(Class clazz) { - return (List) loaderFunction.apply(clazz); + public static Iterable load(Class clazz) { + return (Iterable) loadFunction.apply(clazz); } - public static void setLoaderFunction(Function, List> customLoaderFunction) { - loaderFunction = customLoaderFunction; + public static void setLoadFunction(Function, Iterable> customLoadFunction) { + loadFunction = customLoadFunction; } } diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java index b7b28c509e97..5da49235c359 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java @@ -5,103 +5,363 @@ package io.opentelemetry.instrumentation.api.internal; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizerProvider; +import io.opentelemetry.instrumentation.api.incubator.instrumenter.internal.InternalInstrumenterCustomizerProviderImpl; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; -import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.StatusData; import java.util.Arrays; -import java.util.function.Function; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class InstrumentationCustomizerTest { - private static final String TEST_INSTRUMENTATION_NAME = "test.instrumentation"; + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); - @Mock private InstrumenterBuilder builder; - @Mock private OperationMetrics operationMetrics; - @Mock private AttributesExtractor attributesExtractor; - @Mock private AttributesExtractor secondAttributesExtractor; + private static final Map REQUEST = + Collections.unmodifiableMap( + Stream.of( + entry("req1", "req1_value"), + entry("req2", "req2_value"), + entry("req2_2", "req2_2_value"), + entry("req3", "req3_value")) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + + private static final Map RESPONSE = + Collections.unmodifiableMap( + Stream.of( + entry("resp1", "resp1_value"), + entry("resp2", "resp2_value"), + entry("resp2_2", "resp2_2_value"), + entry("resp3", "resp3_value")) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + + @Mock private OperationListener operationListener; @Mock private ContextCustomizer contextCustomizer; - @Mock private SpanNameExtractor spanNameExtractor; - private InstrumenterCustomizer customizer; + private List originalCustomizerProviders; @BeforeEach void setUp() { - builder.spanNameExtractor = spanNameExtractor; - customizer = - new InstrumenterCustomizerImpl(builder) { - @Override - public String getInstrumentationName() { - return TEST_INSTRUMENTATION_NAME; - } - }; + originalCustomizerProviders = + InternalInstrumenterCustomizerUtil.getInstrumenterCustomizerProviders(); + } + + @AfterEach + void afterEach() { + InternalInstrumenterCustomizerUtil.setInstrumenterCustomizerProviders( + originalCustomizerProviders); + } + + static void setCustomizer(InstrumenterCustomizerProvider provider) { + InternalInstrumenterCustomizerUtil.setInstrumenterCustomizerProviders( + singletonList(new InternalInstrumenterCustomizerProviderImpl(provider))); } @Test void testGetInstrumentationName() { - assertThat(customizer.getInstrumentationName()).isEqualTo(TEST_INSTRUMENTATION_NAME); + AtomicBoolean customizerCalled = new AtomicBoolean(); + setCustomizer( + customizer -> { + customizerCalled.set(true); + assertThat(customizer.getInstrumentationName()).isEqualTo("test"); + }); + + Instrumenter., Map>builder( + otelTesting.getOpenTelemetry(), "test", unused -> "span") + .buildInstrumenter(); + + assertThat(customizerCalled).isTrue(); } @Test void testAddAttributesExtractor() { - InstrumenterCustomizer result = customizer.addAttributesExtractor(attributesExtractor); + AtomicBoolean customizerCalled = new AtomicBoolean(); + setCustomizer( + customizer -> { + customizerCalled.set(true); + customizer.addAttributesExtractor(new AttributesExtractor1()); + }); + + Instrumenter, Map> instrumenter = + Instrumenter., Map>builder( + otelTesting.getOpenTelemetry(), "test", unused -> "span") + .buildInstrumenter(); + + assertThat(customizerCalled).isTrue(); - verify(builder).addAttributesExtractor(attributesExtractor); - assertThat(result).isSameAs(customizer); + Context context = instrumenter.start(Context.root(), REQUEST); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + assertThat(spanContext.isValid()).isTrue(); + + instrumenter.end(context, REQUEST, RESPONSE, null); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("span") + .hasKind(SpanKind.INTERNAL) + .hasInstrumentationScopeInfo(InstrumentationScopeInfo.create("test")) + .hasTraceId(spanContext.getTraceId()) + .hasSpanId(spanContext.getSpanId()) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.unset()) + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("req1"), "req1_value"), + equalTo(AttributeKey.stringKey("req2"), "req2_value"), + equalTo(AttributeKey.stringKey("resp1"), "resp1_value"), + equalTo(AttributeKey.stringKey("resp2"), "resp2_value")))); } @Test void testAddAttributesExtractors() { - Iterable> extractors = - Arrays.asList(attributesExtractor, secondAttributesExtractor); + AtomicBoolean customizerCalled = new AtomicBoolean(); + setCustomizer( + customizer -> { + customizerCalled.set(true); + customizer.addAttributesExtractors( + Arrays.asList(new AttributesExtractor1(), new AttributesExtractor2())); + }); + + Instrumenter, Map> instrumenter = + Instrumenter., Map>builder( + otelTesting.getOpenTelemetry(), "test", unused -> "span") + .buildInstrumenter(); + + assertThat(customizerCalled).isTrue(); + + Context context = instrumenter.start(Context.root(), REQUEST); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + assertThat(spanContext.isValid()).isTrue(); - InstrumenterCustomizer result = customizer.addAttributesExtractors(extractors); + instrumenter.end(context, REQUEST, RESPONSE, null); - verify(builder).addAttributesExtractors(extractors); - assertThat(result).isSameAs(customizer); + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("span") + .hasKind(SpanKind.INTERNAL) + .hasInstrumentationScopeInfo(InstrumentationScopeInfo.create("test")) + .hasTraceId(spanContext.getTraceId()) + .hasSpanId(spanContext.getSpanId()) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.unset()) + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("req1"), "req1_value"), + equalTo(AttributeKey.stringKey("req2"), "req2_2_value"), + equalTo(AttributeKey.stringKey("req3"), "req3_value"), + equalTo(AttributeKey.stringKey("resp1"), "resp1_value"), + equalTo(AttributeKey.stringKey("resp2"), "resp2_2_value"), + equalTo(AttributeKey.stringKey("resp3"), "resp3_value")))); } @Test void testAddOperationMetrics() { - InstrumenterCustomizer result = customizer.addOperationMetrics(operationMetrics); + when(operationListener.onStart(any(), any(), anyLong())).thenAnswer(i -> i.getArguments()[0]); + + AtomicBoolean customizerCalled = new AtomicBoolean(); + setCustomizer( + customizer -> { + customizerCalled.set(true); + customizer.addOperationMetrics(meter -> operationListener); + }); + + Instrumenter, Map> instrumenter = + Instrumenter., Map>builder( + otelTesting.getOpenTelemetry(), "test", unused -> "span") + .buildInstrumenter(); + + assertThat(customizerCalled).isTrue(); + + Context context = instrumenter.start(Context.root(), REQUEST); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + assertThat(spanContext.isValid()).isTrue(); - verify(builder).addOperationMetrics(operationMetrics); - assertThat(result).isSameAs(customizer); + instrumenter.end(context, REQUEST, RESPONSE, null); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("span") + .hasKind(SpanKind.INTERNAL) + .hasInstrumentationScopeInfo(InstrumentationScopeInfo.create("test")) + .hasTraceId(spanContext.getTraceId()) + .hasSpanId(spanContext.getSpanId()) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.unset()) + .hasAttributes(Attributes.empty()))); + + verify(operationListener).onStart(any(), any(), anyLong()); + verify(operationListener).onEnd(any(), any(), anyLong()); } @Test void testAddContextCustomizer() { - InstrumenterCustomizer result = customizer.addContextCustomizer(contextCustomizer); + when(contextCustomizer.onStart(any(), any(), any())).thenAnswer(i -> i.getArguments()[0]); + + AtomicBoolean customizerCalled = new AtomicBoolean(); + setCustomizer( + customizer -> { + customizerCalled.set(true); + customizer.addContextCustomizer(contextCustomizer); + }); + + Instrumenter, Map> instrumenter = + Instrumenter., Map>builder( + otelTesting.getOpenTelemetry(), "test", unused -> "span") + .buildInstrumenter(); + + assertThat(customizerCalled).isTrue(); - verify(builder).addContextCustomizer(contextCustomizer); - assertThat(result).isSameAs(customizer); + Context context = instrumenter.start(Context.root(), REQUEST); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + assertThat(spanContext.isValid()).isTrue(); + + instrumenter.end(context, REQUEST, RESPONSE, null); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("span") + .hasKind(SpanKind.INTERNAL) + .hasInstrumentationScopeInfo(InstrumentationScopeInfo.create("test")) + .hasTraceId(spanContext.getTraceId()) + .hasSpanId(spanContext.getSpanId()) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.unset()) + .hasAttributes(Attributes.empty()))); + + verify(contextCustomizer).onStart(any(), any(), any()); } @Test - @SuppressWarnings("unchecked") void testSetSpanNameExtractor() { - Function, SpanNameExtractor> transformer = - original -> - (SpanNameExtractor) - request -> "custom:" + ((SpanNameExtractor) original).extract(request); + AtomicBoolean customizerCalled = new AtomicBoolean(); + setCustomizer( + customizer -> { + customizerCalled.set(true); + customizer.setSpanNameExtractor( + unused -> (SpanNameExtractor) object -> "new name"); + }); + + Instrumenter, Map> instrumenter = + Instrumenter., Map>builder( + otelTesting.getOpenTelemetry(), "test", unused -> "span") + .buildInstrumenter(); + + assertThat(customizerCalled).isTrue(); + + Context context = instrumenter.start(Context.root(), REQUEST); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + assertThat(spanContext.isValid()).isTrue(); + + instrumenter.end(context, REQUEST, RESPONSE, null); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("new name") + .hasKind(SpanKind.INTERNAL) + .hasInstrumentationScopeInfo(InstrumentationScopeInfo.create("test")) + .hasTraceId(spanContext.getTraceId()) + .hasSpanId(spanContext.getSpanId()) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.unset()) + .hasAttributes(Attributes.empty()))); + } + + static class AttributesExtractor1 + implements AttributesExtractor, Map> { + + @Override + public void onStart( + AttributesBuilder attributes, Context parentContext, Map request) { + attributes.put("req1", request.get("req1")); + attributes.put("req2", request.get("req2")); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + Map request, + Map response, + @Nullable Throwable error) { + attributes.put("resp1", response.get("resp1")); + attributes.put("resp2", response.get("resp2")); + } + } - SpanNameExtractor originalExtractor = request -> "original"; - builder.spanNameExtractor = originalExtractor; + static class AttributesExtractor2 + implements AttributesExtractor, Map> { - customizer.setSpanNameExtractor(transformer); + @Override + public void onStart( + AttributesBuilder attributes, Context parentContext, Map request) { + attributes.put("req3", request.get("req3")); + attributes.put("req2", request.get("req2_2")); + } - SpanNameExtractor updatedExtractor = builder.spanNameExtractor; - String result = updatedExtractor.extract(new Object()); - assertThat(result).isEqualTo("custom:original"); + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + Map request, + Map response, + @Nullable Throwable error) { + attributes.put("resp3", response.get("resp3")); + attributes.put("resp2", response.get("resp2_2")); + } } } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentStarterImpl.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentStarterImpl.java index 81f19fe497bd..c501509f771c 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentStarterImpl.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentStarterImpl.java @@ -17,8 +17,6 @@ import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; import java.security.ProtectionDomain; -import java.util.ArrayList; -import java.util.List; import java.util.ServiceLoader; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -75,7 +73,8 @@ public void start() { EarlyInitAgentConfig earlyConfig = EarlyInitAgentConfig.create(); extensionClassLoader = createExtensionClassLoader(getClass().getClassLoader(), earlyConfig); - setLoaderFunction(); + // allows loading instrumenter customizers from agent and extensions + ServiceLoaderUtil.setLoadFunction(clazz -> ServiceLoader.load(clazz, extensionClassLoader)); String loggerImplementationName = earlyConfig.getString("otel.javaagent.logging"); // default to the built-in stderr slf4j-simple logger @@ -250,17 +249,4 @@ public void visitMethodInsn( return hookInserted ? cw.toByteArray() : null; } } - - /** Sets a custom loader function for the ServiceLoaderUtil to use the agentClassLoader. */ - private void setLoaderFunction() { - ServiceLoaderUtil.setLoaderFunction( - clazz -> { - List instances = new ArrayList<>(); - ServiceLoader serviceLoader = ServiceLoader.load(clazz, extensionClassLoader); - for (Object instance : serviceLoader) { - instances.add(instance); - } - return instances; - }); - } } From fb9d6c628b4fe2bca7cc2d6c9e4d9f68f0fa0550 Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Tue, 17 Jun 2025 21:05:27 +0800 Subject: [PATCH 16/22] Optimize codes --- .../instrumentation/api/internal/ServiceLoaderUtil.java | 2 +- .../api/internal/InstrumentationCustomizerTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ServiceLoaderUtil.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ServiceLoaderUtil.java index f8793e0b790c..2ed6155adb88 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ServiceLoaderUtil.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ServiceLoaderUtil.java @@ -14,7 +14,7 @@ */ public final class ServiceLoaderUtil { - private static Function, Iterable> loadFunction = ServiceLoader::load; + private static volatile Function, Iterable> loadFunction = ServiceLoader::load; private ServiceLoaderUtil() {} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java index 5da49235c359..c3c99ce1b71e 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java @@ -78,7 +78,7 @@ class InstrumentationCustomizerTest { private List originalCustomizerProviders; @BeforeEach - void setUp() { + void beforeEach() { originalCustomizerProviders = InternalInstrumenterCustomizerUtil.getInstrumenterCustomizerProviders(); } From 5c0489c3985a9e130260c50ebd9c969df5cb1fc5 Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Mon, 7 Jul 2025 23:56:28 +0800 Subject: [PATCH 17/22] Add example --- examples/distro/README.md | 10 ++ examples/distro/build.gradle | 1 + examples/distro/custom/build.gradle | 2 + .../DemoInstrumenterCustomizerProvider.java | 130 +++++++++++++++++ ...nstrumenter.InstrumenterCustomizerProvider | 1 + examples/extension/README.md | 11 ++ examples/extension/build.gradle | 2 + .../DemoInstrumenterCustomizerProvider.java | 133 ++++++++++++++++++ .../InternalInstrumenterCustomizerUtil.java | 3 +- 9 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java create mode 100644 examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizerProvider create mode 100644 examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java diff --git a/examples/distro/README.md b/examples/distro/README.md index 781be60215ae..856ce7d8b92b 100644 --- a/examples/distro/README.md +++ b/examples/distro/README.md @@ -20,6 +20,7 @@ This repository has four main submodules: ## Extensions examples - [DemoIdGenerator](custom/src/main/java/com/example/javaagent/DemoIdGenerator.java) - custom `IdGenerator` +- [DemoInstrumenterCustomizerProvider](custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java) - custom instrumentation customization - [DemoPropagator](custom/src/main/java/com/example/javaagent/DemoPropagator.java) - custom `TextMapPropagator` - [DemoSampler](custom/src/main/java/com/example/javaagent/DemoSampler.java) - custom `Sampler` - [DemoSpanProcessor](custom/src/main/java/com/example/javaagent/DemoSpanProcessor.java) - custom `SpanProcessor` @@ -36,6 +37,15 @@ The following description follows one specific use-case: As an example, let us take some database client instrumentation that creates a span for database call and extracts data from db connection to provide attributes for that span. +### I want to customize instrumentation without modifying core code + +The `InstrumenterCustomizerProvider` extension point allows you to customize instrumentation behavior without modifying core code: + +- Add custom attributes and metrics to existing instrumentations +- Implement context customizers for request correlation +- Transform span names to match your naming conventions +- Apply customizations conditionally based on instrumentation type and span kind + ### I don't want this span at all The easiest case. You can just pre-configure your distribution and disable given instrumentation. diff --git a/examples/distro/build.gradle b/examples/distro/build.gradle index 1e85cf968a2a..c21710a2032c 100644 --- a/examples/distro/build.gradle +++ b/examples/distro/build.gradle @@ -45,6 +45,7 @@ subprojects { } repositories { + mavenLocal() mavenCentral() maven { name = "sonatype" diff --git a/examples/distro/custom/build.gradle b/examples/distro/custom/build.gradle index 39b0a25030d9..1b1b20ab764e 100644 --- a/examples/distro/custom/build.gradle +++ b/examples/distro/custom/build.gradle @@ -8,4 +8,6 @@ dependencies { compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-tooling") + compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") + compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator") } diff --git a/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java b/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java new file mode 100644 index 000000000000..e17ce832251a --- /dev/null +++ b/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java @@ -0,0 +1,130 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.example.javaagent; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizer; +import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizerProvider; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.context.ContextKey; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This example demonstrates how to use the InstrumenterCustomizerProvider SPI to customize + * instrumentation behavior in a custom distribution. + * + *

This customizer adds: + *

    + *
  • Custom attributes to HTTP server spans
  • + *
  • Custom metrics for HTTP operations
  • + *
  • Request correlation IDs via context customization
  • + *
  • Custom span name transformation
  • + *
+ * + *

The customizer will be automatically applied to instrumenters that match the specified + * instrumentation name and span kind. + * + * @see InstrumenterCustomizerProvider + * @see InstrumenterCustomizer + */ +public class DemoInstrumenterCustomizerProvider implements InstrumenterCustomizerProvider { + + @Override + public void customize(InstrumenterCustomizer customizer) { + String instrumentationName = customizer.getInstrumentationName(); + SpanKind spanKind = customizer.getSpanKind(); + + // Customize HTTP server spans + if (isHttpServerInstrumentation(instrumentationName) && spanKind == SpanKind.SERVER) { + customizeHttpServer(customizer); + } + } + + private boolean isHttpServerInstrumentation(String instrumentationName) { + return instrumentationName.contains("servlet") + || instrumentationName.contains("jetty") + || instrumentationName.contains("tomcat") + || instrumentationName.contains("undertow") + || instrumentationName.contains("spring-webmvc"); + } + + private void customizeHttpServer(InstrumenterCustomizer customizer) { + customizer.addAttributesExtractor(new DemoAttributesExtractor()); + customizer.addOperationMetrics(new DemoMetrics()); + customizer.addContextCustomizer(new DemoContextCustomizer()); + customizer.setSpanNameExtractor( + unused -> (SpanNameExtractor) object -> "CustomHTTP/" + object.toString()); + } + + /** Custom attributes extractor that adds demo-specific attributes. */ + private static class DemoAttributesExtractor implements AttributesExtractor { + private static final AttributeKey CUSTOM_ATTR = AttributeKey.stringKey("demo.custom"); + private static final AttributeKey ERROR_ATTR = AttributeKey.stringKey("demo.error"); + + @Override + public void onStart(AttributesBuilder attributes, Context context, Object request) { + attributes.put(CUSTOM_ATTR, "demo-distro"); + } + + @Override + public void onEnd(AttributesBuilder attributes, Context context, Object request, Object response, Throwable error) { + if (error != null) { + attributes.put(ERROR_ATTR, error.getClass().getSimpleName()); + } + } + } + + /** Custom metrics that track request counts. */ + private static class DemoMetrics implements OperationMetrics { + @Override + public OperationListener create(Meter meter) { + LongCounter requestCounter = meter.counterBuilder("demo.requests") + .setDescription("Number of requests") + .setUnit("requests") + .build(); + + return new OperationListener() { + @Override + public Context onStart(Context context, Attributes attributes, long startNanos) { + requestCounter.add(1, attributes); + return context; + } + + @Override + public void onEnd(Context context, Attributes attributes, long endNanos) { + // Could add duration metrics here if needed + } + }; + } + } + + /** Context customizer that adds request correlation IDs and custom context data. */ + private static class DemoContextCustomizer implements ContextCustomizer { + private static final AtomicLong requestIdCounter = new AtomicLong(1); + private static final ContextKey REQUEST_ID_KEY = ContextKey.named("demo.request.id"); + + @Override + public Context onStart(Context context, Object request, Attributes startAttributes) { + // Generate a unique request ID for correlation + String requestId = "req-" + requestIdCounter.getAndIncrement(); + + // Add custom context data that can be accessed throughout the request lifecycle + // This follows the pattern used in real implementations like UndertowSingletons + context = context.with(REQUEST_ID_KEY, requestId); + return context; + } + } +} diff --git a/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizerProvider b/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizerProvider new file mode 100644 index 000000000000..0575a70a3d85 --- /dev/null +++ b/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizerProvider @@ -0,0 +1 @@ +com.example.javaagent.DemoInstrumenterCustomizerProvider diff --git a/examples/extension/README.md b/examples/extension/README.md index c82a33472dda..3b39279aaea9 100644 --- a/examples/extension/README.md +++ b/examples/extension/README.md @@ -36,6 +36,7 @@ For more information, see the `extendedAgent` task in [build.gradle](https://git [DemoAutoConfigurationCustomizerProvider]: src/main/java/com/example/javaagent/DemoAutoConfigurationCustomizerProvider.java [DemoIdGenerator]: src/main/java/com/example/javaagent/DemoIdGenerator.java +[DemoInstrumenterCustomizerProvider]: src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java [DemoPropagator]: src/main/java/com/example/javaagent/DemoPropagator.java [DemoSampler]: src/main/java/com/example/javaagent/DemoSampler.java [DemoSpanProcessor]: src/main/java/com/example/javaagent/DemoSpanProcessor.java @@ -49,6 +50,7 @@ For more information, see the `extendedAgent` task in [build.gradle](https://git - Custom `SpanProcessor`: [DemoSpanProcessor][DemoSpanProcessor] - Custom `SpanExporter`: [DemoSpanExporter][DemoSpanExporter] - Additional instrumentation: [DemoServlet3InstrumentationModule][DemoServlet3InstrumentationModule] +- Instrumenter Customization: [DemoInstrumenterCustomizerProvider][DemoInstrumenterCustomizerProvider] - Add custom attributes, metrics, context customizers, and span name transformations to existing instrumentations `ConfigurablePropagatorProvider` and `AutoConfigurationCustomizer` implementations and custom instrumentation (`InstrumentationModule`) need the correct SPI (through `@AutoService`) in @@ -65,6 +67,15 @@ Extensions are designed to override or customize the instrumentation provided by Consider an instrumented database client that creates a span per database call and extracts data from the database connection to provide span attributes. The following are sample use cases for that scenario that can be solved by using extensions. +### "I want to customize instrumentation without modifying core code" + +The `InstrumenterCustomizerProvider` extension point allows you to customize instrumentation behavior without modifying core code: + +- Add custom attributes and metrics to existing instrumentations +- Implement context customizers for request correlation +- Transform span names to match your naming conventions +- Apply customizations conditionally based on instrumentation type and span kind + ### "I don't want this span at all" Create an extension to disable selected instrumentation by providing new default settings. diff --git a/examples/extension/build.gradle b/examples/extension/build.gradle index 938753cecb64..71718a5386a5 100644 --- a/examples/extension/build.gradle +++ b/examples/extension/build.gradle @@ -36,6 +36,7 @@ ext { } repositories { + mavenLocal() mavenCentral() maven { name = "sonatype" @@ -73,6 +74,7 @@ dependencies { */ compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") + compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator") compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") //Provides @AutoService annotation that makes registration of our SPI implementations much easier diff --git a/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java b/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java new file mode 100644 index 000000000000..d5e7bad1fc61 --- /dev/null +++ b/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.example.javaagent; + +import com.google.auto.service.AutoService; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizer; +import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizerProvider; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.context.ContextKey; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This example demonstrates how to use the InstrumenterCustomizerProvider SPI to customize + * instrumentation behavior without modifying the core instrumentation code. + * + *

This customizer adds: + *

    + *
  • Custom attributes to HTTP server spans
  • + *
  • Custom metrics for HTTP operations
  • + *
  • Request correlation IDs via context customization
  • + *
  • Custom span name transformation
  • + *
+ * + *

The customizer will be automatically applied to instrumenters that match the specified + * instrumentation name and span kind. + * + * @see InstrumenterCustomizerProvider + * @see InstrumenterCustomizer + */ +@AutoService(InstrumenterCustomizerProvider.class) +public class DemoInstrumenterCustomizerProvider implements InstrumenterCustomizerProvider { + + @Override + public void customize(InstrumenterCustomizer customizer) { + String instrumentationName = customizer.getInstrumentationName(); + SpanKind spanKind = customizer.getSpanKind(); + + // Apply customizations only to HTTP server spans + if (isHttpServerInstrumentation(instrumentationName) && spanKind == SpanKind.SERVER) { + customizeHttpServer(customizer); + } + } + + private boolean isHttpServerInstrumentation(String instrumentationName) { + return instrumentationName.contains("servlet") + || instrumentationName.contains("jetty") + || instrumentationName.contains("tomcat") + || instrumentationName.contains("undertow") + || instrumentationName.contains("spring-webmvc"); + } + + private void customizeHttpServer(InstrumenterCustomizer customizer) { + customizer.addAttributesExtractor(new DemoAttributesExtractor()); + customizer.addOperationMetrics(new DemoMetrics()); + customizer.addContextCustomizer(new DemoContextCustomizer()); + customizer.setSpanNameExtractor( + unused -> (SpanNameExtractor) object -> "CustomHTTP/" + object.toString()); + } + + /** Custom attributes extractor that adds demo-specific attributes. */ + private static class DemoAttributesExtractor implements AttributesExtractor { + private static final AttributeKey CUSTOM_ATTR = AttributeKey.stringKey("demo.custom"); + private static final AttributeKey ERROR_ATTR = AttributeKey.stringKey("demo.error"); + + @Override + public void onStart(AttributesBuilder attributes, Context context, Object request) { + attributes.put(CUSTOM_ATTR, "demo-extension"); + } + + @Override + public void onEnd( + AttributesBuilder attributes, Context context, Object request, Object response, Throwable error) { + if (error != null) { + attributes.put(ERROR_ATTR, error.getClass().getSimpleName()); + } + } + } + + /** Custom metrics that track request counts. */ + private static class DemoMetrics implements OperationMetrics { + @Override + public OperationListener create(Meter meter) { + LongCounter requestCounter = meter.counterBuilder("demo.requests") + .setDescription("Number of requests") + .setUnit("requests") + .build(); + + return new OperationListener() { + @Override + public Context onStart(Context context, Attributes attributes, long startNanos) { + requestCounter.add(1, attributes); + return context; + } + + @Override + public void onEnd(Context context, Attributes attributes, long endNanos) { + // Could add duration metrics here if needed + } + }; + } + } + + /** Context customizer that adds request correlation IDs and custom context data. */ + private static class DemoContextCustomizer implements ContextCustomizer { + private static final AtomicLong requestIdCounter = new AtomicLong(1); + private static final ContextKey REQUEST_ID_KEY = ContextKey.named("demo.request.id"); + + @Override + public Context onStart(Context context, Object request, Attributes startAttributes) { + // Generate a unique request ID for correlation + String requestId = "req-" + requestIdCounter.getAndIncrement(); + + // Add custom context data that can be accessed throughout the request lifecycle + // This follows the pattern used in real implementations like UndertowSingletons + context = context.with(REQUEST_ID_KEY, requestId); + return context; + } + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizerUtil.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizerUtil.java index f73958e6fa5c..f5d0ea4985c0 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizerUtil.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizerUtil.java @@ -14,6 +14,7 @@ */ public class InternalInstrumenterCustomizerUtil { static { + instrumenterCustomizerProviders = Collections.emptyList(); try { // initializing InstrumenterCustomizerUtil will call setInstrumenterCustomizerProviders on // this class @@ -25,7 +26,7 @@ public class InternalInstrumenterCustomizerUtil { } private static volatile List - instrumenterCustomizerProviders = Collections.emptyList(); + instrumenterCustomizerProviders; public static void setInstrumenterCustomizerProviders( List providers) { From f11181d0d3de7d09aa5fe6725b3b65becff03478 Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Tue, 8 Jul 2025 20:41:06 +0800 Subject: [PATCH 18/22] Removed unexpected part in example --- .../javaagent/DemoInstrumenterCustomizerProvider.java | 5 +---- .../javaagent/DemoInstrumenterCustomizerProvider.java | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java b/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java index e17ce832251a..fb9e94a8c169 100644 --- a/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java +++ b/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java @@ -45,10 +45,7 @@ public class DemoInstrumenterCustomizerProvider implements InstrumenterCustomize @Override public void customize(InstrumenterCustomizer customizer) { String instrumentationName = customizer.getInstrumentationName(); - SpanKind spanKind = customizer.getSpanKind(); - - // Customize HTTP server spans - if (isHttpServerInstrumentation(instrumentationName) && spanKind == SpanKind.SERVER) { + if (isHttpServerInstrumentation(instrumentationName)) { customizeHttpServer(customizer); } } diff --git a/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java b/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java index d5e7bad1fc61..8facad9b5415 100644 --- a/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java +++ b/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java @@ -47,10 +47,7 @@ public class DemoInstrumenterCustomizerProvider implements InstrumenterCustomize @Override public void customize(InstrumenterCustomizer customizer) { String instrumentationName = customizer.getInstrumentationName(); - SpanKind spanKind = customizer.getSpanKind(); - - // Apply customizations only to HTTP server spans - if (isHttpServerInstrumentation(instrumentationName) && spanKind == SpanKind.SERVER) { + if (isHttpServerInstrumentation(instrumentationName)) { customizeHttpServer(customizer); } } From 47bf9daa8ef53f83a719a2c8f2b2ce7cf9f67b19 Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Wed, 9 Jul 2025 09:50:33 +0800 Subject: [PATCH 19/22] Fix formatting in DemoInstrumenterCustomizerProvider --- .../DemoInstrumenterCustomizerProvider.java | 33 +++++++++++-------- .../DemoInstrumenterCustomizerProvider.java | 30 ++++++++++------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java b/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java index fb9e94a8c169..12b4fbce55aa 100644 --- a/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java +++ b/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java @@ -10,8 +10,8 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizer; import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizerProvider; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; @@ -19,7 +19,6 @@ import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; -import io.opentelemetry.context.ContextKey; import java.util.concurrent.atomic.AtomicLong; /** @@ -27,11 +26,12 @@ * instrumentation behavior in a custom distribution. * *

This customizer adds: + * *

    - *
  • Custom attributes to HTTP server spans
  • - *
  • Custom metrics for HTTP operations
  • - *
  • Request correlation IDs via context customization
  • - *
  • Custom span name transformation
  • + *
  • Custom attributes to HTTP server spans + *
  • Custom metrics for HTTP operations + *
  • Request correlation IDs via context customization + *
  • Custom span name transformation *
* *

The customizer will be automatically applied to instrumenters that match the specified @@ -77,7 +77,12 @@ public void onStart(AttributesBuilder attributes, Context context, Object reques } @Override - public void onEnd(AttributesBuilder attributes, Context context, Object request, Object response, Throwable error) { + public void onEnd( + AttributesBuilder attributes, + Context context, + Object request, + Object response, + Throwable error) { if (error != null) { attributes.put(ERROR_ATTR, error.getClass().getSimpleName()); } @@ -88,10 +93,12 @@ public void onEnd(AttributesBuilder attributes, Context context, Object request, private static class DemoMetrics implements OperationMetrics { @Override public OperationListener create(Meter meter) { - LongCounter requestCounter = meter.counterBuilder("demo.requests") - .setDescription("Number of requests") - .setUnit("requests") - .build(); + LongCounter requestCounter = + meter + .counterBuilder("demo.requests") + .setDescription("Number of requests") + .setUnit("requests") + .build(); return new OperationListener() { @Override @@ -117,11 +124,11 @@ private static class DemoContextCustomizer implements ContextCustomizer public Context onStart(Context context, Object request, Attributes startAttributes) { // Generate a unique request ID for correlation String requestId = "req-" + requestIdCounter.getAndIncrement(); - + // Add custom context data that can be accessed throughout the request lifecycle // This follows the pattern used in real implementations like UndertowSingletons context = context.with(REQUEST_ID_KEY, requestId); return context; } } -} +} diff --git a/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java b/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java index 8facad9b5415..f4f011832466 100644 --- a/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java +++ b/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java @@ -11,8 +11,8 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizer; import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizerProvider; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; @@ -20,7 +20,6 @@ import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; -import io.opentelemetry.context.ContextKey; import java.util.concurrent.atomic.AtomicLong; /** @@ -28,11 +27,12 @@ * instrumentation behavior without modifying the core instrumentation code. * *

This customizer adds: + * *

    - *
  • Custom attributes to HTTP server spans
  • - *
  • Custom metrics for HTTP operations
  • - *
  • Request correlation IDs via context customization
  • - *
  • Custom span name transformation
  • + *
  • Custom attributes to HTTP server spans + *
  • Custom metrics for HTTP operations + *
  • Request correlation IDs via context customization + *
  • Custom span name transformation *
* *

The customizer will be automatically applied to instrumenters that match the specified @@ -80,7 +80,11 @@ public void onStart(AttributesBuilder attributes, Context context, Object reques @Override public void onEnd( - AttributesBuilder attributes, Context context, Object request, Object response, Throwable error) { + AttributesBuilder attributes, + Context context, + Object request, + Object response, + Throwable error) { if (error != null) { attributes.put(ERROR_ATTR, error.getClass().getSimpleName()); } @@ -91,10 +95,12 @@ public void onEnd( private static class DemoMetrics implements OperationMetrics { @Override public OperationListener create(Meter meter) { - LongCounter requestCounter = meter.counterBuilder("demo.requests") - .setDescription("Number of requests") - .setUnit("requests") - .build(); + LongCounter requestCounter = + meter + .counterBuilder("demo.requests") + .setDescription("Number of requests") + .setUnit("requests") + .build(); return new OperationListener() { @Override @@ -120,7 +126,7 @@ private static class DemoContextCustomizer implements ContextCustomizer public Context onStart(Context context, Object request, Attributes startAttributes) { // Generate a unique request ID for correlation String requestId = "req-" + requestIdCounter.getAndIncrement(); - + // Add custom context data that can be accessed throughout the request lifecycle // This follows the pattern used in real implementations like UndertowSingletons context = context.with(REQUEST_ID_KEY, requestId); From 49a2e90b79c30a0fd315b45e0e33bbefeffb2378 Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Fri, 25 Jul 2025 23:32:21 +0800 Subject: [PATCH 20/22] Address review comments --- examples/distro/README.md | 6 +-- examples/distro/build.gradle | 1 - .../DemoInstrumenterCustomizerProvider.java | 1 - examples/extension/README.md | 6 +-- examples/extension/build.gradle | 1 - .../DemoInstrumenterCustomizerProvider.java | 1 - .../instrumenter/InstrumenterCustomizer.java | 38 +++++++++---------- 7 files changed, 25 insertions(+), 29 deletions(-) diff --git a/examples/distro/README.md b/examples/distro/README.md index 856ce7d8b92b..1a3843535ea1 100644 --- a/examples/distro/README.md +++ b/examples/distro/README.md @@ -37,12 +37,12 @@ The following description follows one specific use-case: As an example, let us take some database client instrumentation that creates a span for database call and extracts data from db connection to provide attributes for that span. -### I want to customize instrumentation without modifying core code +### I want to customize instrumentation without modifying the instrumentation -The `InstrumenterCustomizerProvider` extension point allows you to customize instrumentation behavior without modifying core code: +The `InstrumenterCustomizerProvider` extension point allows you to customize instrumentation behavior without modifying the instrumentation: - Add custom attributes and metrics to existing instrumentations -- Implement context customizers for request correlation +- Customize context - Transform span names to match your naming conventions - Apply customizations conditionally based on instrumentation type and span kind diff --git a/examples/distro/build.gradle b/examples/distro/build.gradle index c21710a2032c..1e85cf968a2a 100644 --- a/examples/distro/build.gradle +++ b/examples/distro/build.gradle @@ -45,7 +45,6 @@ subprojects { } repositories { - mavenLocal() mavenCentral() maven { name = "sonatype" diff --git a/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java b/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java index 12b4fbce55aa..f2669d9041b2 100644 --- a/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java +++ b/examples/distro/custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java @@ -126,7 +126,6 @@ public Context onStart(Context context, Object request, Attributes startAttribut String requestId = "req-" + requestIdCounter.getAndIncrement(); // Add custom context data that can be accessed throughout the request lifecycle - // This follows the pattern used in real implementations like UndertowSingletons context = context.with(REQUEST_ID_KEY, requestId); return context; } diff --git a/examples/extension/README.md b/examples/extension/README.md index 3b39279aaea9..466d61c1bd58 100644 --- a/examples/extension/README.md +++ b/examples/extension/README.md @@ -67,12 +67,12 @@ Extensions are designed to override or customize the instrumentation provided by Consider an instrumented database client that creates a span per database call and extracts data from the database connection to provide span attributes. The following are sample use cases for that scenario that can be solved by using extensions. -### "I want to customize instrumentation without modifying core code" +### "I want to customize instrumentation without modifying the instrumentation" -The `InstrumenterCustomizerProvider` extension point allows you to customize instrumentation behavior without modifying core code: +The `InstrumenterCustomizerProvider` extension point allows you to customize instrumentation behavior without modifying the instrumentation: - Add custom attributes and metrics to existing instrumentations -- Implement context customizers for request correlation +- Customize context - Transform span names to match your naming conventions - Apply customizations conditionally based on instrumentation type and span kind diff --git a/examples/extension/build.gradle b/examples/extension/build.gradle index 71718a5386a5..00562a8a499a 100644 --- a/examples/extension/build.gradle +++ b/examples/extension/build.gradle @@ -36,7 +36,6 @@ ext { } repositories { - mavenLocal() mavenCentral() maven { name = "sonatype" diff --git a/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java b/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java index f4f011832466..411558ecce1e 100644 --- a/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java +++ b/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java @@ -128,7 +128,6 @@ public Context onStart(Context context, Object request, Attributes startAttribut String requestId = "req-" + requestIdCounter.getAndIncrement(); // Add custom context data that can be accessed throughout the request lifecycle - // This follows the pattern used in real implementations like UndertowSingletons context = context.with(REQUEST_ID_KEY, requestId); return context; } diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizer.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizer.java index 8ab9a1a3edd6..34741dfae3fd 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizer.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizer.java @@ -12,27 +12,27 @@ import java.util.function.Function; /** - * A service provider interface (SPI) for providing customizations for instrumentation, including - * operation metrics, attributes extraction, and context customization. + * Provides customizations for instrumentation, including operation metrics, attributes extraction, + * and context customization. * - *

This allows external modules or plugins to contribute custom logic for specific instrumented - * libraries, without modifying core instrumentation code. This class is internal and is hence not - * for public use. Its APIs are unstable and can change at any time. + *

This class is passed to {@link + * InstrumenterCustomizerProvider#customize(InstrumenterCustomizer)} to allow external modules or + * plugins to contribute custom logic for specific instrumented libraries, without modifying core + * instrumentation code. This class is internal and is hence not for public use. Its APIs are + * unstable and can change at any time. */ public interface InstrumenterCustomizer { /** - * Returns the name of the instrumentation that this customizer applies to. This allows for - * efficient mapping of customizers to specific instrumentations rather than using predicates for - * matching. + * Returns the name of the instrumentation that this customizer applies to. * * @return the name of the instrumentation this customizer targets */ String getInstrumentationName(); /** - * Adds a single AttributesExtractor to the instrumenter. This extractor will be used to extract - * attributes from requests and responses during span creation and enrichment. + * Adds a single {@link AttributesExtractor} to the instrumenter. This extractor will be used to + * extract attributes from requests and responses during the request lifecycle. * * @param extractor the attributes extractor to add * @return this InstrumenterCustomizer for method chaining @@ -40,8 +40,8 @@ public interface InstrumenterCustomizer { InstrumenterCustomizer addAttributesExtractor(AttributesExtractor extractor); /** - * Adds multiple AttributesExtractors to the instrumenter. These extractors will be used to - * extract attributes from requests and responses during span creation and enrichment. + * Adds multiple {@link AttributesExtractor}s to the instrumenter. These extractors will be used + * to extract attributes from requests and responses during the request lifecycle. * * @param extractors the collection of attributes extractors to add * @return this InstrumenterCustomizer for method chaining @@ -50,8 +50,8 @@ InstrumenterCustomizer addAttributesExtractors( Iterable> extractors); /** - * Adds an OperationMetrics implementation to the instrumenter. This will be used to create - * metrics capturing the request processing metrics for the instrumented operations. + * Adds an {@link OperationMetrics} implementation to the instrumenter. This will be used to + * create metrics for the instrumented operations. * * @param operationMetrics the metrics factory to add * @return this InstrumenterCustomizer for method chaining @@ -59,17 +59,17 @@ InstrumenterCustomizer addAttributesExtractors( InstrumenterCustomizer addOperationMetrics(OperationMetrics operationMetrics); /** - * Sets a ContextCustomizer for the instrumenter. The customizer will modify the context during - * the Instrumenter.start() operation, allowing custom context propagation or enrichment. + * Adds a {@link ContextCustomizer} that will customize the context during {@link + * Instrumenter#start(Context, Object)}. * - * @param customizer the context customizer to set + * @param customizer the context customizer to add * @return this InstrumenterCustomizer for method chaining */ InstrumenterCustomizer addContextCustomizer(ContextCustomizer customizer); /** - * Sets a transformer function that will modify the SpanNameExtractor. This allows customizing how - * span names are generated for the instrumented operations. + * Sets a transformer function that will modify the {@link SpanNameExtractor}. This allows + * customizing how span names are generated for the instrumented operations. * * @param spanNameExtractorTransformer function that transforms the original span name extractor * @return this InstrumenterCustomizer for method chaining From 26e16754e005ab7ebc70af2a360242f74f5aeae4 Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Sat, 26 Jul 2025 14:43:11 +0800 Subject: [PATCH 21/22] Add missing imports in InstrumenterCustomizer --- .../api/incubator/instrumenter/InstrumenterCustomizer.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizer.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizer.java index 34741dfae3fd..6a7d914399d6 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizer.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizer.java @@ -5,8 +5,10 @@ package io.opentelemetry.instrumentation.api.incubator.instrumenter; +import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; import java.util.function.Function; From 5ca4e613e32d4011eab9279386ca01ace4d50755 Mon Sep 17 00:00:00 2001 From: Steve Rao Date: Wed, 30 Jul 2025 17:54:43 +0800 Subject: [PATCH 22/22] Update README to clarify customization conditions based on instrumentation name --- examples/distro/README.md | 2 +- examples/extension/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/distro/README.md b/examples/distro/README.md index 1a3843535ea1..c35d49d88468 100644 --- a/examples/distro/README.md +++ b/examples/distro/README.md @@ -44,7 +44,7 @@ The `InstrumenterCustomizerProvider` extension point allows you to customize ins - Add custom attributes and metrics to existing instrumentations - Customize context - Transform span names to match your naming conventions -- Apply customizations conditionally based on instrumentation type and span kind +- Apply customizations conditionally based on instrumentation name ### I don't want this span at all diff --git a/examples/extension/README.md b/examples/extension/README.md index 466d61c1bd58..fdd99a1fec16 100644 --- a/examples/extension/README.md +++ b/examples/extension/README.md @@ -74,7 +74,7 @@ The `InstrumenterCustomizerProvider` extension point allows you to customize ins - Add custom attributes and metrics to existing instrumentations - Customize context - Transform span names to match your naming conventions -- Apply customizations conditionally based on instrumentation type and span kind +- Apply customizations conditionally based on instrumentation name ### "I don't want this span at all"