diff --git a/examples/distro/README.md b/examples/distro/README.md
index 781be60215ae..c35d49d88468 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 the instrumentation
+
+The `InstrumenterCustomizerProvider` extension point allows you to customize instrumentation behavior without modifying the instrumentation:
+
+- Add custom attributes and metrics to existing instrumentations
+- Customize context
+- Transform span names to match your naming conventions
+- Apply customizations conditionally based on instrumentation name
+
### 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/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..f2669d9041b2
--- /dev/null
+++ b/examples/distro/custom/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 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.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;
+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 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();
+ if (isHttpServerInstrumentation(instrumentationName)) {
+ 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
+ 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..fdd99a1fec16 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 the instrumentation"
+
+The `InstrumenterCustomizerProvider` extension point allows you to customize instrumentation behavior without modifying the instrumentation:
+
+- Add custom attributes and metrics to existing instrumentations
+- Customize context
+- Transform span names to match your naming conventions
+- Apply customizations conditionally based on instrumentation name
+
### "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..00562a8a499a 100644
--- a/examples/extension/build.gradle
+++ b/examples/extension/build.gradle
@@ -73,6 +73,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..411558ecce1e
--- /dev/null
+++ b/examples/extension/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java
@@ -0,0 +1,135 @@
+/*
+ * 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.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;
+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 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();
+ if (isHttpServerInstrumentation(instrumentationName)) {
+ 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
+ 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
new file mode 100644
index 000000000000..6a7d914399d6
--- /dev/null
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizer.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+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;
+
+/**
+ * Provides customizations for instrumentation, including operation metrics, attributes extraction,
+ * and context customization.
+ *
+ * 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.
+ *
+ * @return the name of the instrumentation this customizer targets
+ */
+ String getInstrumentationName();
+
+ /**
+ * 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
+ */
+ InstrumenterCustomizer addAttributesExtractor(AttributesExtractor, ?> extractor);
+
+ /**
+ * 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
+ */
+ InstrumenterCustomizer addAttributesExtractors(
+ Iterable extends AttributesExtractor, ?>> extractors);
+
+ /**
+ * 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
+ */
+ InstrumenterCustomizer addOperationMetrics(OperationMetrics operationMetrics);
+
+ /**
+ * Adds a {@link ContextCustomizer} that will customize the context during {@link
+ * Instrumenter#start(Context, Object)}.
+ *
+ * @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 {@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
+ */
+ InstrumenterCustomizer setSpanNameExtractor(
+ Function, SpanNameExtractor>> spanNameExtractorTransformer);
+}
diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizerProvider.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizerProvider.java
new file mode 100644
index 000000000000..006369065a2b
--- /dev/null
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/InstrumenterCustomizerProvider.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.incubator.instrumenter;
+
+/**
+ * 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 {
+
+ /**
+ * 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-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InstrumenterCustomizerImpl.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InstrumenterCustomizerImpl.java
new file mode 100644
index 000000000000..322a5c02cbcf
--- /dev/null
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/instrumenter/internal/InstrumenterCustomizerImpl.java
@@ -0,0 +1,64 @@
+/*
+ * 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.InstrumenterCustomizer;
+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 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.
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public final class InstrumenterCustomizerImpl implements InstrumenterCustomizer {
+ private final InternalInstrumenterCustomizer customizer;
+
+ public InstrumenterCustomizerImpl(InternalInstrumenterCustomizer customizer) {
+ this.customizer = customizer;
+ }
+
+ @Override
+ public String getInstrumentationName() {
+ return customizer.getInstrumentationName();
+ }
+
+ @Override
+ public InstrumenterCustomizer addAttributesExtractor(AttributesExtractor, ?> extractor) {
+ customizer.addAttributesExtractor(extractor);
+ return this;
+ }
+
+ @Override
+ public InstrumenterCustomizer addAttributesExtractors(
+ Iterable extends AttributesExtractor, ?>> extractors) {
+ customizer.addAttributesExtractors(extractors);
+ return this;
+ }
+
+ @Override
+ public InstrumenterCustomizer addOperationMetrics(OperationMetrics operationMetrics) {
+ customizer.addOperationMetrics(operationMetrics);
+ return this;
+ }
+
+ @Override
+ public InstrumenterCustomizer addContextCustomizer(ContextCustomizer> customizer) {
+ this.customizer.addContextCustomizer(customizer);
+ return this;
+ }
+
+ @Override
+ public InstrumenterCustomizer setSpanNameExtractor(
+ Function, SpanNameExtractor>> spanNameExtractorTransformer) {
+ 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 6b41ae6c60ab..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
@@ -23,12 +23,16 @@
import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties;
import io.opentelemetry.instrumentation.api.internal.InstrumenterBuilderAccess;
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.SpanKey;
import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import java.util.function.Function;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -51,7 +55,7 @@ public final class InstrumenterBuilder {
final OpenTelemetry openTelemetry;
final String instrumentationName;
- final SpanNameExtractor super REQUEST> spanNameExtractor;
+ SpanNameExtractor super REQUEST> spanNameExtractor;
final List> spanLinksExtractors = new ArrayList<>();
final List> attributesExtractors =
@@ -281,6 +285,9 @@ public Instrumenter buildInstrumenter(
private Instrumenter buildInstrumenter(
InstrumenterConstructor constructor,
SpanKindExtractor super REQUEST> spanKindExtractor) {
+
+ applyCustomizers(this);
+
this.spanKindExtractor = spanKindExtractor;
return constructor.create(this);
}
@@ -375,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 extends AttributesExtractor> 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 super REQUEST>>
+ 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 extends AttributesExtractor> extractors);
+
+ void addOperationMetrics(OperationMetrics operationMetrics);
+
+ void addContextCustomizer(ContextCustomizer customizer);
+
+ void setSpanNameExtractor(
+ Function, SpanNameExtractor super REQUEST>>
+ 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..f5d0ea4985c0
--- /dev/null
+++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InternalInstrumenterCustomizerUtil.java
@@ -0,0 +1,41 @@
+/*
+ * 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 {
+ instrumenterCustomizerProviders = Collections.emptyList();
+ 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;
+
+ 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
new file mode 100644
index 000000000000..2ed6155adb88
--- /dev/null
+++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ServiceLoaderUtil.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.internal;
+
+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 volatile Function, Iterable>> loadFunction = ServiceLoader::load;
+
+ private ServiceLoaderUtil() {}
+
+ @SuppressWarnings("unchecked")
+ public static Iterable load(Class clazz) {
+ return (Iterable) loadFunction.apply(clazz);
+ }
+
+ 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
new file mode 100644
index 000000000000..c3c99ce1b71e
--- /dev/null
+++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/InstrumentationCustomizerTest.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.internal;
+
+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.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.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 {
+
+ @RegisterExtension
+ static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create();
+
+ 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;
+
+ private List originalCustomizerProviders;
+
+ @BeforeEach
+ void beforeEach() {
+ originalCustomizerProviders =
+ InternalInstrumenterCustomizerUtil.getInstrumenterCustomizerProviders();
+ }
+
+ @AfterEach
+ void afterEach() {
+ InternalInstrumenterCustomizerUtil.setInstrumenterCustomizerProviders(
+ originalCustomizerProviders);
+ }
+
+ static void setCustomizer(InstrumenterCustomizerProvider provider) {
+ InternalInstrumenterCustomizerUtil.setInstrumenterCustomizerProviders(
+ singletonList(new InternalInstrumenterCustomizerProviderImpl(provider)));
+ }
+
+ @Test
+ void testGetInstrumentationName() {
+ 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() {
+ 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();
+
+ 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() {
+ 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();
+
+ 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_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() {
+ 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();
+
+ 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() {
+ 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();
+
+ 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
+ void testSetSpanNameExtractor() {
+ 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"));
+ }
+ }
+
+ static class AttributesExtractor2
+ implements AttributesExtractor, Map> {
+
+ @Override
+ public void onStart(
+ AttributesBuilder attributes, Context parentContext, Map request) {
+ attributes.put("req3", request.get("req3"));
+ attributes.put("req2", request.get("req2_2"));
+ }
+
+ @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 ab5cb93c22ed..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
@@ -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;
@@ -72,6 +73,8 @@ public void start() {
EarlyInitAgentConfig earlyConfig = EarlyInitAgentConfig.create();
extensionClassLoader = createExtensionClassLoader(getClass().getClassLoader(), earlyConfig);
+ // 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