Skip to content

Commit 6fc2783

Browse files
authored
Add experimental api for adding operation attributes (#14590)
1 parent cf51791 commit 6fc2783

File tree

4 files changed

+132
-2
lines changed

4 files changed

+132
-2
lines changed

instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder
7878
private final AttributesExtractor<? super REQUEST, ? super RESPONSE>[] attributesExtractors;
7979
private final ContextCustomizer<? super REQUEST>[] contextCustomizers;
8080
private final OperationListener[] operationListeners;
81+
private final AttributesExtractor<? super REQUEST, ? super RESPONSE>[]
82+
operationListenerAttributesExtractors;
8183
private final ErrorCauseExtractor errorCauseExtractor;
8284
private final boolean propagateOperationListenersToOnEnd;
8385
private final boolean enabled;
@@ -94,6 +96,8 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder
9496
this.attributesExtractors = builder.attributesExtractors.toArray(new AttributesExtractor[0]);
9597
this.contextCustomizers = builder.contextCustomizers.toArray(new ContextCustomizer[0]);
9698
this.operationListeners = builder.buildOperationListeners().toArray(new OperationListener[0]);
99+
this.operationListenerAttributesExtractors =
100+
builder.operationListenerAttributesExtractors.toArray(new AttributesExtractor[0]);
97101
this.errorCauseExtractor = builder.errorCauseExtractor;
98102
this.propagateOperationListenersToOnEnd = builder.propagateOperationListenersToOnEnd;
99103
this.enabled = builder.enabled;
@@ -207,11 +211,21 @@ private Context doStartImpl(Context parentContext, REQUEST request, @Nullable In
207211
context = context.with(span);
208212

209213
if (operationListeners.length != 0) {
214+
if (operationListenerAttributesExtractors.length != 0) {
215+
UnsafeAttributes operationAttributes = new UnsafeAttributes();
216+
operationAttributes.putAll(attributes.asMap());
217+
for (AttributesExtractor<? super REQUEST, ? super RESPONSE> extractor :
218+
operationListenerAttributesExtractors) {
219+
extractor.onStart(operationAttributes, parentContext, request);
220+
}
221+
attributes = operationAttributes;
222+
}
223+
210224
// operation listeners run after span start, so that they have access to the current span
211225
// for capturing exemplars
212226
long startNanos = getNanos(startTime);
213-
for (int i = 0; i < operationListeners.length; i++) {
214-
context = operationListeners[i].onStart(context, attributes, startNanos);
227+
for (OperationListener operationListener : operationListeners) {
228+
context = operationListener.onStart(context, attributes, startNanos);
215229
}
216230
}
217231
if (propagateOperationListenersToOnEnd || context.get(START_OPERATION_LISTENERS) != null) {
@@ -258,6 +272,16 @@ private void doEnd(
258272
operationListeners = this.operationListeners;
259273
}
260274
if (operationListeners.length != 0) {
275+
if (operationListenerAttributesExtractors.length != 0) {
276+
UnsafeAttributes operationAttributes = new UnsafeAttributes();
277+
operationAttributes.putAll(attributes.asMap());
278+
for (AttributesExtractor<? super REQUEST, ? super RESPONSE> extractor :
279+
operationListenerAttributesExtractors) {
280+
extractor.onEnd(operationAttributes, context, request, response, error);
281+
}
282+
attributes = operationAttributes;
283+
}
284+
261285
long endNanos = getNanos(endTime);
262286
for (int i = operationListeners.length - 1; i >= 0; i--) {
263287
operationListeners[i].onEnd(context, attributes, endNanos);

instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.opentelemetry.context.propagation.TextMapSetter;
2222
import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil;
2323
import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties;
24+
import io.opentelemetry.instrumentation.api.internal.Experimental;
2425
import io.opentelemetry.instrumentation.api.internal.InstrumenterBuilderAccess;
2526
import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil;
2627
import io.opentelemetry.instrumentation.api.internal.InternalInstrumenterCustomizer;
@@ -60,6 +61,8 @@ public final class InstrumenterBuilder<REQUEST, RESPONSE> {
6061
final List<SpanLinksExtractor<? super REQUEST>> spanLinksExtractors = new ArrayList<>();
6162
final List<AttributesExtractor<? super REQUEST, ? super RESPONSE>> attributesExtractors =
6263
new ArrayList<>();
64+
final List<AttributesExtractor<? super REQUEST, ? super RESPONSE>>
65+
operationListenerAttributesExtractors = new ArrayList<>();
6366
final List<ContextCustomizer<? super REQUEST>> contextCustomizers = new ArrayList<>();
6467
private final List<OperationListener> operationListeners = new ArrayList<>();
6568
private final List<OperationMetrics> operationMetrics = new ArrayList<>();
@@ -73,6 +76,14 @@ public final class InstrumenterBuilder<REQUEST, RESPONSE> {
7376
boolean propagateOperationListenersToOnEnd = false;
7477
boolean enabled = true;
7578

79+
{
80+
Experimental.internalAddOperationListenerAttributesExtractor(
81+
(builder, operationListenerAttributesExtractor) ->
82+
this.operationListenerAttributesExtractors.add(
83+
requireNonNull(
84+
operationListenerAttributesExtractor, "operationListenerAttributesExtractor")));
85+
}
86+
7687
InstrumenterBuilder(
7788
OpenTelemetry openTelemetry,
7889
String instrumentationName,

instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/Experimental.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
package io.opentelemetry.instrumentation.api.internal;
77

8+
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
9+
import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
10+
import io.opentelemetry.instrumentation.api.instrumenter.OperationListener;
811
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder;
912
import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder;
1013
import java.util.function.BiConsumer;
@@ -25,6 +28,10 @@ public final class Experimental {
2528
private static volatile BiConsumer<HttpSpanNameExtractorBuilder<?>, Function<?, String>>
2629
urlTemplateExtractorSetter;
2730

31+
@Nullable
32+
private static volatile BiConsumer<InstrumenterBuilder<?, ?>, AttributesExtractor<?, ?>>
33+
operationListenerAttributesExtractorAdder;
34+
2835
private Experimental() {}
2936

3037
public static void setRedactQueryParameters(
@@ -54,4 +61,28 @@ public static <REQUEST> void internalSetUrlTemplateExtractor(
5461
urlTemplateExtractorSetter) {
5562
Experimental.urlTemplateExtractorSetter = (BiConsumer) urlTemplateExtractorSetter;
5663
}
64+
65+
/**
66+
* Add an {@link AttributesExtractor} to the given {@link InstrumenterBuilder} that provides
67+
* attributes that are passed to the {@link OperationListener}s. This can be used to add
68+
* attributes to the metrics without adding them to the span. To add attributes to the span use
69+
* {@link InstrumenterBuilder#addAttributesExtractor(AttributesExtractor)}.
70+
*/
71+
public static <REQUEST, RESPONSE> void addOperationListenerAttributesExtractor(
72+
InstrumenterBuilder<REQUEST, RESPONSE> builder,
73+
AttributesExtractor<? super REQUEST, ? super RESPONSE> attributesExtractor) {
74+
if (operationListenerAttributesExtractorAdder != null) {
75+
operationListenerAttributesExtractorAdder.accept(builder, attributesExtractor);
76+
}
77+
}
78+
79+
@SuppressWarnings({"rawtypes", "unchecked"})
80+
public static <REQUEST, RESPONSE> void internalAddOperationListenerAttributesExtractor(
81+
BiConsumer<
82+
InstrumenterBuilder<REQUEST, RESPONSE>,
83+
AttributesExtractor<? super REQUEST, ? super RESPONSE>>
84+
operationListenerAttributesExtractorAdder) {
85+
Experimental.operationListenerAttributesExtractorAdder =
86+
(BiConsumer) operationListenerAttributesExtractorAdder;
87+
}
5788
}

instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.opentelemetry.context.Context;
2626
import io.opentelemetry.context.ContextKey;
2727
import io.opentelemetry.context.propagation.TextMapGetter;
28+
import io.opentelemetry.instrumentation.api.internal.Experimental;
2829
import io.opentelemetry.instrumentation.api.internal.SchemaUrlProvider;
2930
import io.opentelemetry.instrumentation.api.internal.SpanKey;
3031
import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider;
@@ -503,6 +504,69 @@ public void onEnd(Context context, Attributes endAttributes, long endNanos) {
503504
assertThat(Span.fromContext(endContext.get()).getSpanContext().isValid()).isTrue();
504505
}
505506

507+
@Test
508+
void operationListenerAttributeExtractors() {
509+
AtomicReference<Attributes> startContext = new AtomicReference<>();
510+
AtomicReference<Attributes> endContext = new AtomicReference<>();
511+
512+
OperationListener operationListener =
513+
new OperationListener() {
514+
@Override
515+
public Context onStart(Context context, Attributes startAttributes, long startNanos) {
516+
startContext.set(startAttributes);
517+
return context;
518+
}
519+
520+
@Override
521+
public void onEnd(Context context, Attributes endAttributes, long endNanos) {
522+
endContext.set(endAttributes);
523+
}
524+
};
525+
526+
InstrumenterBuilder<Map<String, String>, Map<String, String>> builder =
527+
Instrumenter.<Map<String, String>, Map<String, String>>builder(
528+
otelTesting.getOpenTelemetry(), "test", unused -> "span")
529+
.addOperationListener(operationListener)
530+
.addAttributesExtractor(new AttributesExtractor1());
531+
Experimental.addOperationListenerAttributesExtractor(builder, new AttributesExtractor2());
532+
Instrumenter<Map<String, String>, Map<String, String>> instrumenter =
533+
builder.buildServerInstrumenter(new MapGetter());
534+
535+
Context context = instrumenter.start(Context.root(), REQUEST);
536+
SpanContext spanContext = Span.fromContext(context).getSpanContext();
537+
instrumenter.end(context, REQUEST, RESPONSE, null);
538+
539+
otelTesting
540+
.assertTraces()
541+
.hasTracesSatisfyingExactly(
542+
trace ->
543+
trace.hasSpansSatisfyingExactly(
544+
span ->
545+
span.hasName("span")
546+
.hasKind(SpanKind.SERVER)
547+
.hasInstrumentationScopeInfo(InstrumentationScopeInfo.create("test"))
548+
.hasTraceId(spanContext.getTraceId())
549+
.hasSpanId(spanContext.getSpanId())
550+
.hasParentSpanId(SpanId.getInvalid())
551+
.hasStatus(StatusData.unset())
552+
.hasAttributesSatisfyingExactly(
553+
equalTo(AttributeKey.stringKey("req1"), "req1_value"),
554+
equalTo(AttributeKey.stringKey("req2"), "req2_value"),
555+
equalTo(AttributeKey.stringKey("resp1"), "resp1_value"),
556+
equalTo(AttributeKey.stringKey("resp2"), "resp2_value"))));
557+
558+
assertThat(startContext.get())
559+
.hasSize(3)
560+
.containsEntry("req1", "req1_value")
561+
.containsEntry("req2", "req2_2_value")
562+
.containsEntry("req3", "req3_value");
563+
assertThat(endContext.get())
564+
.hasSize(3)
565+
.containsEntry("resp1", "resp1_value")
566+
.containsEntry("resp2", "resp2_2_value")
567+
.containsEntry("resp3", "resp3_value");
568+
}
569+
506570
@Test
507571
void shouldNotAddInvalidLink() {
508572
// given

0 commit comments

Comments
 (0)