diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ContextSpanProcessor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ContextSpanProcessor.java new file mode 100644 index 000000000000..d45334c7a62e --- /dev/null +++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ContextSpanProcessor.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ImplicitContextKeyed; +import io.opentelemetry.instrumentation.api.internal.ContextSpanProcessorImpl; +import java.util.function.BiConsumer; + +/** + * A span processing function that can be stored in context. When stored in context this function + * will be applied to all spans create while the context is active. Processing function is called + * synchronously on the execution thread, it should not throw or block the execution thread. + * + *

NOTE: This API is EXPERIMENTAL, it may be removed or + * changed. + */ +public interface ContextSpanProcessor extends ImplicitContextKeyed { + + /** + * Wrap a {@link BiConsumer} so that it can be stored in context as a span processing function. + */ + static ContextSpanProcessor wrap(BiConsumer processor) { + return new ContextSpanProcessorImpl(processor); + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/internal/ContextSpanProcessorImpl.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/internal/ContextSpanProcessorImpl.java new file mode 100644 index 000000000000..d495086f2f38 --- /dev/null +++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/internal/ContextSpanProcessorImpl.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.instrumenter.ContextSpanProcessor; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class ContextSpanProcessorImpl implements ContextSpanProcessor { + private static final ContextKey> KEY = + ContextKey.named("opentelemetry-context-span-processor"); + + private final BiConsumer processor; + + public ContextSpanProcessorImpl(BiConsumer processor) { + this.processor = processor; + } + + public static void onStart(Context context, Span span) { + BiConsumer processor = fromContextOrNull(context); + if (processor != null) { + processor.accept(context, span); + } + } + + @Override + public Context storeInContext(Context context) { + return storeInContext(context, processor); + } + + // instrumented by ContextSpanProcessorUtilInstrumentation + public static Context storeInContext(Context context, BiConsumer processor) { + return context.with(KEY, processor); + } + + @Nullable + // instrumented by ContextSpanProcessorImplInstrumentation + public static BiConsumer fromContextOrNull(Context context) { + return context.get(KEY); + } +} 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 8e45354aa6e3..90ae065a8475 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 @@ -11,6 +11,7 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.internal.ContextSpanProcessorInvoker; import io.opentelemetry.instrumentation.api.internal.SupportabilityMetrics; import java.time.Instant; import java.util.ArrayList; @@ -183,6 +184,7 @@ private Context doStart(Context parentContext, REQUEST request, @Nullable Instan spanBuilder.setAllAttributes(attributes); Span span = spanBuilder.startSpan(); + ContextSpanProcessorInvoker.onStart(context, span); context = context.with(span); for (ContextCustomizer contextCustomizer : contextCustomizers) { diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ContextSpanProcessorInvoker.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ContextSpanProcessorInvoker.java new file mode 100644 index 000000000000..ad2207787647 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ContextSpanProcessorInvoker.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import java.lang.reflect.Method; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class ContextSpanProcessorInvoker { + private static final Method onStartMethod = getMethod(); + + private static Method getMethod() { + try { + Class clazz = + Class.forName("io.opentelemetry.instrumentation.api.internal.ContextSpanProcessorImpl"); + return clazz.getMethod("onStart", Context.class, Span.class); + } catch (ClassNotFoundException | NoSuchMethodException exception) { + return null; + } + } + + public static void onStart(Context context, Span span) { + if (onStartMethod == null) { + return; + } + try { + onStartMethod.invoke(null, context, span); + } catch (Exception exception) { + throw new IllegalStateException(exception); + } + } + + private ContextSpanProcessorInvoker() {} +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SpanKey.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SpanKey.java index 0cf43cc6162a..ed61260c2f3d 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SpanKey.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SpanKey.java @@ -76,11 +76,13 @@ private SpanKey(ContextKey key) { this.key = key; } + // instumented by SpanKeyInstrumentation public Context storeInContext(Context context, Span span) { return context.with(key, span); } @Nullable + // instumented by SpanKeyInstrumentation public Span fromContextOrNull(Context context) { return context.get(key); } diff --git a/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextSpanProcessorImplInstrumentation.java b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextSpanProcessorImplInstrumentation.java new file mode 100644 index 000000000000..3f9b5a964d6d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextSpanProcessorImplInstrumentation.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.instrumentationapi; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import application.io.opentelemetry.api.trace.Span; +import application.io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.internal.ContextSpanProcessorImpl; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context.AgentContextStorage; +import java.util.function.BiConsumer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +final class ContextSpanProcessorImplInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named( + "application.io.opentelemetry.instrumentation.api.internal.ContextSpanProcessorImpl"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("storeInContext") + .and(takesArgument(0, named("application.io.opentelemetry.context.Context"))) + .and(takesArgument(1, BiConsumer.class)), + this.getClass().getName() + "$StoreInContextAdvice"); + transformer.applyAdviceToMethod( + named("fromContextOrNull") + .and(takesArgument(0, named("application.io.opentelemetry.context.Context"))), + this.getClass().getName() + "$FromContextOrNullAdvice"); + } + + @SuppressWarnings("unused") + public static class StoreInContextAdvice { + @Advice.OnMethodEnter(skipOn = Advice.OnDefaultValue.class) + public static Object onEnter() { + return null; + } + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.Argument(0) Context applicationContext, + @Advice.Argument(1) BiConsumer processor, + @Advice.Return(readOnly = false) Context newApplicationContext) { + + io.opentelemetry.context.Context agentContext = + AgentContextStorage.getAgentContext(applicationContext); + + BiConsumer agentProcessor = + ContextSpanProcessorWrapper.wrap(processor); + io.opentelemetry.context.Context newAgentContext = + ContextSpanProcessorImpl.storeInContext(agentContext, agentProcessor); + + newApplicationContext = AgentContextStorage.toApplicationContext(newAgentContext); + } + } + + @SuppressWarnings("unused") + public static class FromContextOrNullAdvice { + @Advice.OnMethodEnter(skipOn = Advice.OnDefaultValue.class) + public static Object onEnter() { + return null; + } + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.Argument(0) Context applicationContext, + @Advice.Return(readOnly = false) BiConsumer processor) { + + io.opentelemetry.context.Context agentContext = + AgentContextStorage.getAgentContext(applicationContext); + BiConsumer agentProcessor = + ContextSpanProcessorImpl.fromContextOrNull(agentContext); + processor = ContextSpanProcessorWrapper.unwrap(agentProcessor); + } + } +} diff --git a/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextSpanProcessorWrapper.java b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextSpanProcessorWrapper.java new file mode 100644 index 000000000000..5a5ec6edfa72 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextSpanProcessorWrapper.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.instrumentationapi; + +import application.io.opentelemetry.api.trace.Span; +import application.io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context.AgentContextStorage; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import java.util.function.BiConsumer; + +/** + * Wrapper that translates agent context and span to application context and span before invoking + * the delegate. + */ +public final class ContextSpanProcessorWrapper + implements BiConsumer { + + private final BiConsumer delegate; + + private ContextSpanProcessorWrapper(BiConsumer delegate) { + this.delegate = delegate; + } + + public static BiConsumer wrap( + BiConsumer processor) { + return new ContextSpanProcessorWrapper(processor); + } + + public static BiConsumer unwrap( + BiConsumer processor) { + if (processor instanceof ContextSpanProcessorWrapper) { + return ((ContextSpanProcessorWrapper) processor).delegate; + } + return null; + } + + @Override + public void accept( + io.opentelemetry.context.Context context, io.opentelemetry.api.trace.Span span) { + delegate.accept( + AgentContextStorage.toApplicationContext(context), Bridging.toApplication(span)); + } +} diff --git a/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/InstrumentationApiInstrumentationModule.java b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/InstrumentationApiInstrumentationModule.java index 7fa3004db58f..471887cfcb49 100644 --- a/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/InstrumentationApiInstrumentationModule.java +++ b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/InstrumentationApiInstrumentationModule.java @@ -28,6 +28,9 @@ public ElementMatcher.Junction classLoaderMatcher() { @Override public List typeInstrumentations() { - return asList(new HttpRouteStateInstrumentation(), new SpanKeyInstrumentation()); + return asList( + new HttpRouteStateInstrumentation(), + new SpanKeyInstrumentation(), + new ContextSpanProcessorImplInstrumentation()); } } diff --git a/instrumentation/opentelemetry-instrumentation-api/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextSpanProcessorTest.java b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextSpanProcessorTest.java new file mode 100644 index 000000000000..cb73c2111352 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextSpanProcessorTest.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.instrumentationapi; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.ContextSpanProcessor; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.javaagent.instrumentation.testing.AgentSpanTesting; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ContextSpanProcessorTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void testUpdateSpanName() { + Context context = + Context.current() + .with(ContextSpanProcessor.wrap((context1, span) -> span.updateName("new span name"))); + + try (Scope scope = context.makeCurrent()) { + testing.runWithSpan("old span name", () -> {}); + } + + testing.waitAndAssertTraces( + trace -> trace.hasSpansSatisfyingExactly(span -> span.hasName("new span name"))); + } + + @Test + void testUpdateAgentSpanName() { + Context context = + Context.current() + .with(ContextSpanProcessor.wrap((context1, span) -> span.updateName("new span name"))); + + try (Scope scope = context.makeCurrent()) { + AgentSpanTesting.runWithAllSpanKeys("old span name", () -> {}); + } + + testing.waitAndAssertTraces( + trace -> trace.hasSpansSatisfyingExactly(span -> span.hasName("new span name"))); + } +}