diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java index e7e324f4807b..db6963a30904 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java @@ -78,6 +78,8 @@ public static InstrumenterBuilder builder private final AttributesExtractor[] attributesExtractors; private final ContextCustomizer[] contextCustomizers; private final OperationListener[] operationListeners; + private final AttributesExtractor[] + operationListenerAttributesExtractors; private final ErrorCauseExtractor errorCauseExtractor; private final boolean propagateOperationListenersToOnEnd; private final boolean enabled; @@ -94,6 +96,8 @@ public static InstrumenterBuilder builder this.attributesExtractors = builder.attributesExtractors.toArray(new AttributesExtractor[0]); this.contextCustomizers = builder.contextCustomizers.toArray(new ContextCustomizer[0]); this.operationListeners = builder.buildOperationListeners().toArray(new OperationListener[0]); + this.operationListenerAttributesExtractors = + builder.operationListenerAttributesExtractors.toArray(new AttributesExtractor[0]); this.errorCauseExtractor = builder.errorCauseExtractor; this.propagateOperationListenersToOnEnd = builder.propagateOperationListenersToOnEnd; this.enabled = builder.enabled; @@ -207,11 +211,21 @@ private Context doStartImpl(Context parentContext, REQUEST request, @Nullable In context = context.with(span); if (operationListeners.length != 0) { + if (operationListenerAttributesExtractors.length != 0) { + UnsafeAttributes operationAttributes = new UnsafeAttributes(); + operationAttributes.putAll(attributes.asMap()); + for (AttributesExtractor extractor : + operationListenerAttributesExtractors) { + extractor.onStart(operationAttributes, parentContext, request); + } + attributes = operationAttributes; + } + // operation listeners run after span start, so that they have access to the current span // for capturing exemplars long startNanos = getNanos(startTime); - for (int i = 0; i < operationListeners.length; i++) { - context = operationListeners[i].onStart(context, attributes, startNanos); + for (OperationListener operationListener : operationListeners) { + context = operationListener.onStart(context, attributes, startNanos); } } if (propagateOperationListenersToOnEnd || context.get(START_OPERATION_LISTENERS) != null) { @@ -258,6 +272,16 @@ private void doEnd( operationListeners = this.operationListeners; } if (operationListeners.length != 0) { + if (operationListenerAttributesExtractors.length != 0) { + UnsafeAttributes operationAttributes = new UnsafeAttributes(); + operationAttributes.putAll(attributes.asMap()); + for (AttributesExtractor extractor : + operationListenerAttributesExtractors) { + extractor.onEnd(operationAttributes, context, request, response, error); + } + attributes = operationAttributes; + } + long endNanos = getNanos(endTime); for (int i = operationListeners.length - 1; i >= 0; i--) { operationListeners[i].onEnd(context, attributes, endNanos); 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 3f44c44c8749..406c7b008591 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.Experimental; import io.opentelemetry.instrumentation.api.internal.InstrumenterBuilderAccess; import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; import io.opentelemetry.instrumentation.api.internal.InternalInstrumenterCustomizer; @@ -60,6 +61,8 @@ public final class InstrumenterBuilder { final List> spanLinksExtractors = new ArrayList<>(); final List> attributesExtractors = new ArrayList<>(); + final List> + operationListenerAttributesExtractors = new ArrayList<>(); final List> contextCustomizers = new ArrayList<>(); private final List operationListeners = new ArrayList<>(); private final List operationMetrics = new ArrayList<>(); @@ -73,6 +76,14 @@ public final class InstrumenterBuilder { boolean propagateOperationListenersToOnEnd = false; boolean enabled = true; + { + Experimental.internalAddOperationListenerAttributesExtractor( + (builder, operationListenerAttributesExtractor) -> + this.operationListenerAttributesExtractors.add( + requireNonNull( + operationListenerAttributesExtractor, "operationListenerAttributesExtractor"))); + } + InstrumenterBuilder( OpenTelemetry openTelemetry, String instrumentationName, diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/Experimental.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/Experimental.java index f9f24a2e9c42..597cbd5996fe 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/Experimental.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/Experimental.java @@ -5,6 +5,9 @@ package io.opentelemetry.instrumentation.api.internal; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder; import java.util.function.BiConsumer; @@ -25,6 +28,10 @@ public final class Experimental { private static volatile BiConsumer, Function> urlTemplateExtractorSetter; + @Nullable + private static volatile BiConsumer, AttributesExtractor> + operationListenerAttributesExtractorAdder; + private Experimental() {} public static void setRedactQueryParameters( @@ -54,4 +61,28 @@ public static void internalSetUrlTemplateExtractor( urlTemplateExtractorSetter) { Experimental.urlTemplateExtractorSetter = (BiConsumer) urlTemplateExtractorSetter; } + + /** + * Add an {@link AttributesExtractor} to the given {@link InstrumenterBuilder} that provides + * attributes that are passed to the {@link OperationListener}s. This can be used to add + * attributes to the metrics without adding them to the span. To add attributes to the span use + * {@link InstrumenterBuilder#addAttributesExtractor(AttributesExtractor)}. + */ + public static void addOperationListenerAttributesExtractor( + InstrumenterBuilder builder, + AttributesExtractor attributesExtractor) { + if (operationListenerAttributesExtractorAdder != null) { + operationListenerAttributesExtractorAdder.accept(builder, attributesExtractor); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static void internalAddOperationListenerAttributesExtractor( + BiConsumer< + InstrumenterBuilder, + AttributesExtractor> + operationListenerAttributesExtractorAdder) { + Experimental.operationListenerAttributesExtractorAdder = + (BiConsumer) operationListenerAttributesExtractorAdder; + } } diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java index fb9ae8622d8d..792193a749b0 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java @@ -25,6 +25,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.internal.Experimental; import io.opentelemetry.instrumentation.api.internal.SchemaUrlProvider; import io.opentelemetry.instrumentation.api.internal.SpanKey; import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; @@ -503,6 +504,69 @@ public void onEnd(Context context, Attributes endAttributes, long endNanos) { assertThat(Span.fromContext(endContext.get()).getSpanContext().isValid()).isTrue(); } + @Test + void operationListenerAttributeExtractors() { + AtomicReference startContext = new AtomicReference<>(); + AtomicReference endContext = new AtomicReference<>(); + + OperationListener operationListener = + new OperationListener() { + @Override + public Context onStart(Context context, Attributes startAttributes, long startNanos) { + startContext.set(startAttributes); + return context; + } + + @Override + public void onEnd(Context context, Attributes endAttributes, long endNanos) { + endContext.set(endAttributes); + } + }; + + InstrumenterBuilder, Map> builder = + Instrumenter., Map>builder( + otelTesting.getOpenTelemetry(), "test", unused -> "span") + .addOperationListener(operationListener) + .addAttributesExtractor(new AttributesExtractor1()); + Experimental.addOperationListenerAttributesExtractor(builder, new AttributesExtractor2()); + Instrumenter, Map> instrumenter = + builder.buildServerInstrumenter(new MapGetter()); + + Context context = instrumenter.start(Context.root(), REQUEST); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + instrumenter.end(context, REQUEST, RESPONSE, null); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("span") + .hasKind(SpanKind.SERVER) + .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")))); + + assertThat(startContext.get()) + .hasSize(3) + .containsEntry("req1", "req1_value") + .containsEntry("req2", "req2_2_value") + .containsEntry("req3", "req3_value"); + assertThat(endContext.get()) + .hasSize(3) + .containsEntry("resp1", "resp1_value") + .containsEntry("resp2", "resp2_2_value") + .containsEntry("resp3", "resp3_value"); + } + @Test void shouldNotAddInvalidLink() { // given