Skip to content

Commit e9746da

Browse files
steveraolaurit
andauthored
Support extensions for attributesExtractors, contextCustomizers, operationListeners and spanNameExtractor (#13917)
Co-authored-by: Lauri Tulmin <[email protected]>
1 parent e0973fa commit e9746da

File tree

19 files changed

+1062
-1
lines changed

19 files changed

+1062
-1
lines changed

examples/distro/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ This repository has four main submodules:
2020
## Extensions examples
2121

2222
- [DemoIdGenerator](custom/src/main/java/com/example/javaagent/DemoIdGenerator.java) - custom `IdGenerator`
23+
- [DemoInstrumenterCustomizerProvider](custom/src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java) - custom instrumentation customization
2324
- [DemoPropagator](custom/src/main/java/com/example/javaagent/DemoPropagator.java) - custom `TextMapPropagator`
2425
- [DemoSampler](custom/src/main/java/com/example/javaagent/DemoSampler.java) - custom `Sampler`
2526
- [DemoSpanProcessor](custom/src/main/java/com/example/javaagent/DemoSpanProcessor.java) - custom `SpanProcessor`
@@ -36,6 +37,15 @@ The following description follows one specific use-case:
3637
As an example, let us take some database client instrumentation that creates a span for database call
3738
and extracts data from db connection to provide attributes for that span.
3839

40+
### I want to customize instrumentation without modifying the instrumentation
41+
42+
The `InstrumenterCustomizerProvider` extension point allows you to customize instrumentation behavior without modifying the instrumentation:
43+
44+
- Add custom attributes and metrics to existing instrumentations
45+
- Customize context
46+
- Transform span names to match your naming conventions
47+
- Apply customizations conditionally based on instrumentation name
48+
3949
### I don't want this span at all
4050

4151
The easiest case. You can just pre-configure your distribution and disable given instrumentation.

examples/distro/custom/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ dependencies {
88
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
99
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api")
1010
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-tooling")
11+
compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api")
12+
compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator")
1113
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.example.javaagent;
7+
8+
import io.opentelemetry.api.common.AttributeKey;
9+
import io.opentelemetry.api.common.Attributes;
10+
import io.opentelemetry.api.common.AttributesBuilder;
11+
import io.opentelemetry.api.metrics.LongCounter;
12+
import io.opentelemetry.api.metrics.Meter;
13+
import io.opentelemetry.context.Context;
14+
import io.opentelemetry.context.ContextKey;
15+
import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizer;
16+
import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizerProvider;
17+
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
18+
import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer;
19+
import io.opentelemetry.instrumentation.api.instrumenter.OperationListener;
20+
import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics;
21+
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
22+
import java.util.concurrent.atomic.AtomicLong;
23+
24+
/**
25+
* This example demonstrates how to use the InstrumenterCustomizerProvider SPI to customize
26+
* instrumentation behavior in a custom distribution.
27+
*
28+
* <p>This customizer adds:
29+
*
30+
* <ul>
31+
* <li>Custom attributes to HTTP server spans
32+
* <li>Custom metrics for HTTP operations
33+
* <li>Request correlation IDs via context customization
34+
* <li>Custom span name transformation
35+
* </ul>
36+
*
37+
* <p>The customizer will be automatically applied to instrumenters that match the specified
38+
* instrumentation name and span kind.
39+
*
40+
* @see InstrumenterCustomizerProvider
41+
* @see InstrumenterCustomizer
42+
*/
43+
public class DemoInstrumenterCustomizerProvider implements InstrumenterCustomizerProvider {
44+
45+
@Override
46+
public void customize(InstrumenterCustomizer customizer) {
47+
String instrumentationName = customizer.getInstrumentationName();
48+
if (isHttpServerInstrumentation(instrumentationName)) {
49+
customizeHttpServer(customizer);
50+
}
51+
}
52+
53+
private boolean isHttpServerInstrumentation(String instrumentationName) {
54+
return instrumentationName.contains("servlet")
55+
|| instrumentationName.contains("jetty")
56+
|| instrumentationName.contains("tomcat")
57+
|| instrumentationName.contains("undertow")
58+
|| instrumentationName.contains("spring-webmvc");
59+
}
60+
61+
private void customizeHttpServer(InstrumenterCustomizer customizer) {
62+
customizer.addAttributesExtractor(new DemoAttributesExtractor());
63+
customizer.addOperationMetrics(new DemoMetrics());
64+
customizer.addContextCustomizer(new DemoContextCustomizer());
65+
customizer.setSpanNameExtractor(
66+
unused -> (SpanNameExtractor<Object>) object -> "CustomHTTP/" + object.toString());
67+
}
68+
69+
/** Custom attributes extractor that adds demo-specific attributes. */
70+
private static class DemoAttributesExtractor implements AttributesExtractor<Object, Object> {
71+
private static final AttributeKey<String> CUSTOM_ATTR = AttributeKey.stringKey("demo.custom");
72+
private static final AttributeKey<String> ERROR_ATTR = AttributeKey.stringKey("demo.error");
73+
74+
@Override
75+
public void onStart(AttributesBuilder attributes, Context context, Object request) {
76+
attributes.put(CUSTOM_ATTR, "demo-distro");
77+
}
78+
79+
@Override
80+
public void onEnd(
81+
AttributesBuilder attributes,
82+
Context context,
83+
Object request,
84+
Object response,
85+
Throwable error) {
86+
if (error != null) {
87+
attributes.put(ERROR_ATTR, error.getClass().getSimpleName());
88+
}
89+
}
90+
}
91+
92+
/** Custom metrics that track request counts. */
93+
private static class DemoMetrics implements OperationMetrics {
94+
@Override
95+
public OperationListener create(Meter meter) {
96+
LongCounter requestCounter =
97+
meter
98+
.counterBuilder("demo.requests")
99+
.setDescription("Number of requests")
100+
.setUnit("requests")
101+
.build();
102+
103+
return new OperationListener() {
104+
@Override
105+
public Context onStart(Context context, Attributes attributes, long startNanos) {
106+
requestCounter.add(1, attributes);
107+
return context;
108+
}
109+
110+
@Override
111+
public void onEnd(Context context, Attributes attributes, long endNanos) {
112+
// Could add duration metrics here if needed
113+
}
114+
};
115+
}
116+
}
117+
118+
/** Context customizer that adds request correlation IDs and custom context data. */
119+
private static class DemoContextCustomizer implements ContextCustomizer<Object> {
120+
private static final AtomicLong requestIdCounter = new AtomicLong(1);
121+
private static final ContextKey<String> REQUEST_ID_KEY = ContextKey.named("demo.request.id");
122+
123+
@Override
124+
public Context onStart(Context context, Object request, Attributes startAttributes) {
125+
// Generate a unique request ID for correlation
126+
String requestId = "req-" + requestIdCounter.getAndIncrement();
127+
128+
// Add custom context data that can be accessed throughout the request lifecycle
129+
context = context.with(REQUEST_ID_KEY, requestId);
130+
return context;
131+
}
132+
}
133+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
com.example.javaagent.DemoInstrumenterCustomizerProvider

examples/extension/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ For more information, see the `extendedAgent` task in [build.gradle](https://git
3636

3737
[DemoAutoConfigurationCustomizerProvider]: src/main/java/com/example/javaagent/DemoAutoConfigurationCustomizerProvider.java
3838
[DemoIdGenerator]: src/main/java/com/example/javaagent/DemoIdGenerator.java
39+
[DemoInstrumenterCustomizerProvider]: src/main/java/com/example/javaagent/DemoInstrumenterCustomizerProvider.java
3940
[DemoPropagator]: src/main/java/com/example/javaagent/DemoPropagator.java
4041
[DemoSampler]: src/main/java/com/example/javaagent/DemoSampler.java
4142
[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
4950
- Custom `SpanProcessor`: [DemoSpanProcessor][DemoSpanProcessor]
5051
- Custom `SpanExporter`: [DemoSpanExporter][DemoSpanExporter]
5152
- Additional instrumentation: [DemoServlet3InstrumentationModule][DemoServlet3InstrumentationModule]
53+
- Instrumenter Customization: [DemoInstrumenterCustomizerProvider][DemoInstrumenterCustomizerProvider] - Add custom attributes, metrics, context customizers, and span name transformations to existing instrumentations
5254

5355
`ConfigurablePropagatorProvider` and `AutoConfigurationCustomizer` implementations and custom
5456
instrumentation (`InstrumentationModule`) need the correct SPI (through `@AutoService`) in
@@ -65,6 +67,15 @@ Extensions are designed to override or customize the instrumentation provided by
6567

6668
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.
6769

70+
### "I want to customize instrumentation without modifying the instrumentation"
71+
72+
The `InstrumenterCustomizerProvider` extension point allows you to customize instrumentation behavior without modifying the instrumentation:
73+
74+
- Add custom attributes and metrics to existing instrumentations
75+
- Customize context
76+
- Transform span names to match your naming conventions
77+
- Apply customizations conditionally based on instrumentation name
78+
6879
### "I don't want this span at all"
6980

7081
Create an extension to disable selected instrumentation by providing new default settings.

examples/extension/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ dependencies {
7373
*/
7474
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
7575
compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api")
76+
compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator")
7677
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api")
7778

7879
//Provides @AutoService annotation that makes registration of our SPI implementations much easier
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.example.javaagent;
7+
8+
import com.google.auto.service.AutoService;
9+
import io.opentelemetry.api.common.AttributeKey;
10+
import io.opentelemetry.api.common.Attributes;
11+
import io.opentelemetry.api.common.AttributesBuilder;
12+
import io.opentelemetry.api.metrics.LongCounter;
13+
import io.opentelemetry.api.metrics.Meter;
14+
import io.opentelemetry.context.Context;
15+
import io.opentelemetry.context.ContextKey;
16+
import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizer;
17+
import io.opentelemetry.instrumentation.api.incubator.instrumenter.InstrumenterCustomizerProvider;
18+
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
19+
import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer;
20+
import io.opentelemetry.instrumentation.api.instrumenter.OperationListener;
21+
import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics;
22+
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
23+
import java.util.concurrent.atomic.AtomicLong;
24+
25+
/**
26+
* This example demonstrates how to use the InstrumenterCustomizerProvider SPI to customize
27+
* instrumentation behavior without modifying the core instrumentation code.
28+
*
29+
* <p>This customizer adds:
30+
*
31+
* <ul>
32+
* <li>Custom attributes to HTTP server spans
33+
* <li>Custom metrics for HTTP operations
34+
* <li>Request correlation IDs via context customization
35+
* <li>Custom span name transformation
36+
* </ul>
37+
*
38+
* <p>The customizer will be automatically applied to instrumenters that match the specified
39+
* instrumentation name and span kind.
40+
*
41+
* @see InstrumenterCustomizerProvider
42+
* @see InstrumenterCustomizer
43+
*/
44+
@AutoService(InstrumenterCustomizerProvider.class)
45+
public class DemoInstrumenterCustomizerProvider implements InstrumenterCustomizerProvider {
46+
47+
@Override
48+
public void customize(InstrumenterCustomizer customizer) {
49+
String instrumentationName = customizer.getInstrumentationName();
50+
if (isHttpServerInstrumentation(instrumentationName)) {
51+
customizeHttpServer(customizer);
52+
}
53+
}
54+
55+
private boolean isHttpServerInstrumentation(String instrumentationName) {
56+
return instrumentationName.contains("servlet")
57+
|| instrumentationName.contains("jetty")
58+
|| instrumentationName.contains("tomcat")
59+
|| instrumentationName.contains("undertow")
60+
|| instrumentationName.contains("spring-webmvc");
61+
}
62+
63+
private void customizeHttpServer(InstrumenterCustomizer customizer) {
64+
customizer.addAttributesExtractor(new DemoAttributesExtractor());
65+
customizer.addOperationMetrics(new DemoMetrics());
66+
customizer.addContextCustomizer(new DemoContextCustomizer());
67+
customizer.setSpanNameExtractor(
68+
unused -> (SpanNameExtractor<Object>) object -> "CustomHTTP/" + object.toString());
69+
}
70+
71+
/** Custom attributes extractor that adds demo-specific attributes. */
72+
private static class DemoAttributesExtractor implements AttributesExtractor<Object, Object> {
73+
private static final AttributeKey<String> CUSTOM_ATTR = AttributeKey.stringKey("demo.custom");
74+
private static final AttributeKey<String> ERROR_ATTR = AttributeKey.stringKey("demo.error");
75+
76+
@Override
77+
public void onStart(AttributesBuilder attributes, Context context, Object request) {
78+
attributes.put(CUSTOM_ATTR, "demo-extension");
79+
}
80+
81+
@Override
82+
public void onEnd(
83+
AttributesBuilder attributes,
84+
Context context,
85+
Object request,
86+
Object response,
87+
Throwable error) {
88+
if (error != null) {
89+
attributes.put(ERROR_ATTR, error.getClass().getSimpleName());
90+
}
91+
}
92+
}
93+
94+
/** Custom metrics that track request counts. */
95+
private static class DemoMetrics implements OperationMetrics {
96+
@Override
97+
public OperationListener create(Meter meter) {
98+
LongCounter requestCounter =
99+
meter
100+
.counterBuilder("demo.requests")
101+
.setDescription("Number of requests")
102+
.setUnit("requests")
103+
.build();
104+
105+
return new OperationListener() {
106+
@Override
107+
public Context onStart(Context context, Attributes attributes, long startNanos) {
108+
requestCounter.add(1, attributes);
109+
return context;
110+
}
111+
112+
@Override
113+
public void onEnd(Context context, Attributes attributes, long endNanos) {
114+
// Could add duration metrics here if needed
115+
}
116+
};
117+
}
118+
}
119+
120+
/** Context customizer that adds request correlation IDs and custom context data. */
121+
private static class DemoContextCustomizer implements ContextCustomizer<Object> {
122+
private static final AtomicLong requestIdCounter = new AtomicLong(1);
123+
private static final ContextKey<String> REQUEST_ID_KEY = ContextKey.named("demo.request.id");
124+
125+
@Override
126+
public Context onStart(Context context, Object request, Attributes startAttributes) {
127+
// Generate a unique request ID for correlation
128+
String requestId = "req-" + requestIdCounter.getAndIncrement();
129+
130+
// Add custom context data that can be accessed throughout the request lifecycle
131+
context = context.with(REQUEST_ID_KEY, requestId);
132+
return context;
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)