diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java index 1f9d8ab6704ef..bb2b71fbbfe0e 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java @@ -3,11 +3,23 @@ import static io.quarkus.bootstrap.classloading.QuarkusClassLoader.isClassPresentAtRuntime; import static jakarta.interceptor.Interceptor.Priority.LIBRARY_AFTER; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.BooleanSupplier; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; +import org.jboss.resteasy.reactive.common.processor.EndpointIndexer; +import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationStore; +import org.jboss.resteasy.reactive.server.model.FixedHandlerChainCustomizer; +import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; +import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.Capabilities; @@ -18,6 +30,7 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; +import io.quarkus.opentelemetry.Traceless; import io.quarkus.opentelemetry.deployment.tracing.TracerEnabled; import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.InstrumentationRecorder; @@ -29,7 +42,9 @@ import io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.AttachExceptionHandler; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.OpenTelemetryClassicServerFilter; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.OpenTelemetryReactiveServerFilter; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.TracelessServerHandler; import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; import io.quarkus.resteasy.reactive.server.spi.PreExceptionMapperHandlerBuildItem; import io.quarkus.resteasy.reactive.spi.CustomContainerRequestFilterBuildItem; import io.quarkus.vertx.core.deployment.VertxOptionsConsumerBuildItem; @@ -145,12 +160,46 @@ void resteasyReactiveIntegration( Capabilities capabilities, BuildProducer containerRequestFilterBuildItemBuildProducer, BuildProducer preExceptionMapperHandlerBuildItemBuildProducer, + BuildProducer methodScannerBuildItemBuildProducer, OTelBuildConfig config) { if (capabilities.isPresent(Capability.RESTEASY_REACTIVE) && config.instrument().rest()) { containerRequestFilterBuildItemBuildProducer .produce(new CustomContainerRequestFilterBuildItem(OpenTelemetryReactiveServerFilter.class.getName())); preExceptionMapperHandlerBuildItemBuildProducer .produce(new PreExceptionMapperHandlerBuildItem(new AttachExceptionHandler())); + methodScannerBuildItemBuildProducer.produce(new MethodScannerBuildItem(new TracelessScanner())); + } + + } + + private static class TracelessScanner implements MethodScanner { + + static final DotName TRACELESS = DotName.createSimple(Traceless.class.getName()); + + @Override + public List scan(MethodInfo method, ClassInfo actualEndpointClass, + Map methodContext) { + + AnnotationStore annotationStore = (AnnotationStore) methodContext + .get(EndpointIndexer.METHOD_CONTEXT_ANNOTATION_STORE); + AnnotationInstance tracelessInstance = doScan(method, actualEndpointClass, annotationStore); + if (tracelessInstance != null) { + return List.of(new FixedHandlerChainCustomizer(new TracelessServerHandler(), + HandlerChainCustomizer.Phase.AFTER_MATCH)); + } + return Collections.emptyList(); + } + + private AnnotationInstance doScan(MethodInfo method, ClassInfo actualEndpointClass, + AnnotationStore annotationStore) { + AnnotationInstance annotationInstance = annotationStore.getAnnotation(method, TRACELESS); + if (annotationInstance == null) { + annotationInstance = annotationStore.getAnnotation(method.declaringClass(), TRACELESS); + if ((annotationInstance == null) && !actualEndpointClass.equals(method.declaringClass())) { + annotationInstance = annotationStore.getAnnotation(actualEndpointClass, TRACELESS); + } + } + return annotationInstance; } } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/Traceless.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/Traceless.java new file mode 100644 index 0000000000000..8527c36f22708 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/Traceless.java @@ -0,0 +1,17 @@ +package io.quarkus.opentelemetry; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.smallrye.common.annotation.Experimental; + +/** + * Identifies that the current path should not be select for tracing. + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Experimental("This annotation is only supported by Quarkus REST currently") +public @interface Traceless { +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundSpanProcessor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundSpanProcessor.java index 94663996276c0..40d33ef612c2f 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundSpanProcessor.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundSpanProcessor.java @@ -7,6 +7,7 @@ import io.opentelemetry.sdk.trace.ReadWriteSpan; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SpanProcessor; +import io.quarkus.opentelemetry.runtime.tracing.InternalAttributes; /** * Class to facilitate a delay in when the worker thread inside {@link SpanProcessor} @@ -43,6 +44,10 @@ public boolean isStartRequired() { @Override public void onEnd(ReadableSpan span) { + if (InternalAttributes.containsTraceless(span.getAttributes())) { + // this span was marked as one to be ignored + return; + } if (delegate == null) { logDelegateNotFound(); return; diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/InternalAttributes.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/InternalAttributes.java new file mode 100644 index 0000000000000..054322c193c73 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/InternalAttributes.java @@ -0,0 +1,19 @@ +package io.quarkus.opentelemetry.runtime.tracing; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; + +/** + * Contains utilities for working with internal Quarkus attributes + */ +public final class InternalAttributes { + + public static final AttributeKey TRACELESS = AttributeKey.stringKey("TRACELESS"); + + public static boolean containsTraceless(Attributes attributes) { + return Boolean.parseBoolean(attributes.get(TRACELESS)); + } + + private InternalAttributes() { + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/TracelessServerHandler.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/TracelessServerHandler.java new file mode 100644 index 0000000000000..b36eee110129b --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/TracelessServerHandler.java @@ -0,0 +1,16 @@ +package io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy; + +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.quarkus.opentelemetry.runtime.tracing.InternalAttributes; + +public class TracelessServerHandler implements ServerRestHandler { + @Override + public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { + Span localRootSpan = LocalRootSpan.current(); + localRootSpan.setAttribute(InternalAttributes.TRACELESS, "true"); + } +} diff --git a/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/ExporterResource.java b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/ExporterResource.java index be1350677b2fc..459d79375d6ff 100644 --- a/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/ExporterResource.java +++ b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/ExporterResource.java @@ -15,14 +15,17 @@ import org.jboss.resteasy.reactive.RestQuery; +import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.internal.data.ExceptionEventData; +import io.quarkus.opentelemetry.runtime.tracing.InternalAttributes; @Path("") public class ExporterResource { @Inject - InMemorySpanExporter inMemorySpanExporter; + CustomInMemorySpanExporter inMemorySpanExporter; @GET @Path("/reset") @@ -71,8 +74,37 @@ public List exportExceptionMessages() { static class InMemorySpanExporterProducer { @Produces @Singleton - InMemorySpanExporter inMemorySpanExporter() { - return InMemorySpanExporter.create(); + CustomInMemorySpanExporter inMemorySpanExporter() { + return new CustomInMemorySpanExporter(); + } + } + + static class CustomInMemorySpanExporter implements SpanExporter { + + private final InMemorySpanExporter delegate = InMemorySpanExporter.create(); + + public List getFinishedSpanItems() { + return delegate.getFinishedSpanItems(); + } + + public void reset() { + delegate.reset(); + } + + @Override + public CompletableResultCode export(Collection spans) { + return delegate.export(spans.stream().filter(sd -> !InternalAttributes.containsTraceless(sd.getAttributes())) + .collect(Collectors.toList())); + } + + @Override + public CompletableResultCode flush() { + return delegate.flush(); + } + + @Override + public CompletableResultCode shutdown() { + return delegate.shutdown(); } } } diff --git a/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/ReactiveResource.java b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/ReactiveResource.java index 75ab3afee8077..13026da83a230 100644 --- a/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/ReactiveResource.java +++ b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/ReactiveResource.java @@ -16,6 +16,7 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.quarkus.opentelemetry.Traceless; import io.smallrye.mutiny.Uni; import io.vertx.core.impl.NoStackTraceException; @@ -104,4 +105,17 @@ public Uni reactiveException() { throw new NoStackTraceException("dummy2"); }); } + + @GET + @Path("potentially-traceless") + @Traceless + public String traceless() { + return "@Traceless"; + } + + @POST + @Path("potentially-traceless") + public String notTraceless() { + return "Not-@Traceless"; + } } diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java index 794a5ab49ad3a..330b311b4c999 100644 --- a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java +++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java @@ -20,6 +20,7 @@ import static org.awaitility.Awaitility.await; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Duration; @@ -27,6 +28,8 @@ import java.util.Map; import java.util.Optional; +import org.awaitility.core.ConditionTimeoutException; +import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -339,6 +342,26 @@ void multipleUsingCombineDifferentPaths() { assertEquals("helloGetUniExecutor", helloGetUniExecutorInternal.get("name")); } + @Test + public void potentialTracelessResourceMethods() { + when().get("/reactive/potentially-traceless") + .then() + .statusCode(200) + .body(Matchers.is("@Traceless")); + + // should throw because there is no span + assertThrows(ConditionTimeoutException.class, () -> { + await().atMost(5, SECONDS).until(() -> !getSpans().isEmpty()); + }); + + when().post("/reactive/potentially-traceless") + .then() + .statusCode(200) + .body(Matchers.is("Not-@Traceless")); + + await().atMost(5, SECONDS).until(() -> getSpans().size() == 1); + } + @Test public void securedInvalidCredential() { given().auth().preemptive().basic("scott", "reader2").when().get("/foo/secured/item/something")