diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java index 557749f5c104a..90e366e252471 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java @@ -10,6 +10,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.BooleanSupplier; @@ -17,10 +18,14 @@ import jakarta.enterprise.inject.spi.EventContext; import jakarta.inject.Singleton; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.ConfigValue; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.Index; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.logging.Logger; @@ -46,6 +51,7 @@ import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.ApplicationIndexBuildItem; import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.gizmo.MethodDescriptor; @@ -53,6 +59,7 @@ import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType; import io.quarkus.opentelemetry.runtime.tracing.DelayedAttributes; +import io.quarkus.opentelemetry.runtime.tracing.Traceless; import io.quarkus.opentelemetry.runtime.tracing.TracerRecorder; import io.quarkus.opentelemetry.runtime.tracing.cdi.TracerProducer; import io.quarkus.opentelemetry.runtime.tracing.security.EndUserSpanProcessor; @@ -69,6 +76,8 @@ public class TracerProcessor { private static final DotName SPAN_EXPORTER = DotName.createSimple(SpanExporter.class.getName()); private static final DotName SPAN_PROCESSOR = DotName.createSimple(SpanProcessor.class.getName()); private static final DotName TEXT_MAP_PROPAGATOR = DotName.createSimple(TextMapPropagator.class.getName()); + private static final DotName TRACELESS = DotName.createSimple(Traceless.class.getName()); + private static final DotName PATH = DotName.createSimple("jakarta.ws.rs.Path"); @BuildStep UnremovableBeanBuildItem ensureProducersAreRetained( @@ -136,10 +145,58 @@ void dropNames( Optional frameworkEndpoints, Optional staticResources, BuildProducer dropNonApplicationUris, - BuildProducer dropStaticResources) { + BuildProducer dropStaticResources, + ApplicationIndexBuildItem appIndex) { - // Drop framework paths List nonApplicationUris = new ArrayList<>(); + + Index jandex = appIndex.getIndex(); + List annotations = jandex.getAnnotations(TRACELESS); + for (AnnotationInstance annotation : annotations) { + AnnotationTarget.Kind kind = annotation.target().kind(); + + switch (kind) { + case CLASS -> { + AnnotationInstance annotationInstance = annotation.target().asClass().annotations() + .stream().filter(TracerProcessor::isPathAnnotationAtClass).findFirst().orElse(null); + + if (Objects.isNull(annotationInstance)) { + continue; + } + + nonApplicationUris.add(annotationInstance.value().asString() + "*"); + } + case METHOD -> { + ClassInfo classInfo = annotation.target().asMethod().declaringClass(); + + AnnotationInstance annotationInstance = classInfo.asClass() + .annotations() + .stream() + .filter(TracerProcessor::isPathAnnotationAtClass) + .findFirst() + .orElse(null); + + if (Objects.isNull(annotationInstance)) { + continue; + } + + String finalPath; + String classPath = annotationInstance.value().asString(); + AnnotationInstance annotationMethodInstance = annotation.target().annotation(PATH); + if (annotationMethodInstance != null) { + String methodPath = annotationMethodInstance.value().asString(); + finalPath = normalizePath(classPath, methodPath); + } else { + finalPath = classPath; + } + + nonApplicationUris.add(finalPath); + + } + } + } + + // Drop framework paths frameworkEndpoints.ifPresent( frameworkEndpointsBuildItem -> { for (String endpoint : frameworkEndpointsBuildItem.getEndpoints()) { @@ -170,6 +227,21 @@ void dropNames( dropStaticResources.produce(new DropStaticResourcesBuildItem(resources)); } + private static boolean isPathAnnotationAtClass(AnnotationInstance ann) { + return ann.target().kind().equals(AnnotationTarget.Kind.CLASS) && + ann.name().equals(PATH); + } + + public String normalizePath(String classPath, String methodPath) { + if (!classPath.endsWith("/")) { + classPath += "/"; + } + if (methodPath.startsWith("/")) { + methodPath = methodPath.substring(1); + } + return classPath + methodPath; + } + @BuildStep @Record(ExecutionTime.STATIC_INIT) SyntheticBeanBuildItem setupDelayedAttribute(TracerRecorder recorder, ApplicationInfoBuildItem appInfo) { diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryExcludedResourceTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryExcludedResourceTest.java new file mode 100644 index 0000000000000..ff10ecd3f899c --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryExcludedResourceTest.java @@ -0,0 +1,149 @@ +package io.quarkus.opentelemetry.deployment; + +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.opentelemetry.api.metrics.Meter; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryExporter; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryMetricExporterProvider; +import io.quarkus.opentelemetry.runtime.tracing.Traceless; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.smallrye.config.SmallRyeConfig; + +public class OpenTelemetryExcludedResourceTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addPackage(InMemoryExporter.class.getPackage()) + .addAsResource("resource-config/application.properties", "application.properties") + .addAsResource( + "META-INF/services-config/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")); + private static final Logger log = LoggerFactory.getLogger(OpenTelemetryExcludedResourceTest.class); + + @Inject + SmallRyeConfig config; + @Inject + InMemoryExporter exporter; + + @BeforeEach + void setup() { + exporter.reset(); + } + + @Test + void testingHello() { + RestAssured.when() + .get("/hello").then() + .statusCode(200) + .body(is("hello")); + + // should fail, because there is no span + assertThrows(org.awaitility.core.ConditionTimeoutException.class, + () -> exporter.getSpanExporter().getFinishedSpanItems(1)); + } + + @Test + void testingHi() { + RestAssured.when() + .get("/hello/hi").then() + .statusCode(200) + .body(is("hi")); + + // should fail, because there is no span + assertThrows(org.awaitility.core.ConditionTimeoutException.class, + () -> exporter.getSpanExporter().getFinishedSpanItems(1)); + } + + @Test + void testingNoSlash() { + RestAssured.when() + .get("/hello/no-slash").then() + .statusCode(200) + .body(is("no-slash")); + + // should fail, because there is no span + assertThrows(org.awaitility.core.ConditionTimeoutException.class, + () -> exporter.getSpanExporter().getFinishedSpanItems(1)); + } + + @Test + void testingAtClassLevel() { + RestAssured.when() + .get("/class-level").then() + .statusCode(200) + .body(is("no-slash")); + + // should fail, because there is no span + assertThrows(org.awaitility.core.ConditionTimeoutException.class, + () -> exporter.getSpanExporter().getFinishedSpanItems(1)); + } + + @Path("/class-level") + @Traceless + public static class ClassLevelResource { + + @Inject + Meter meter; + + @GET + @Path("/first-method") + + public String firstMethod() { + meter.counterBuilder("method").build().add(1); + return "method"; + } + + @Path("/second-method") + @GET + public String secondMethod() { + meter.counterBuilder("method").build().add(1); + return "method"; + } + } + + @Path("/hello") + public static class HelloResource { + @Inject + Meter meter; + + @GET + @Traceless + public String hello() { + meter.counterBuilder("hello").build().add(1); + return "hello"; + } + + @Path("/hi") + @GET + @Traceless + public String hi() { + meter.counterBuilder("hi").build().add(1); + return "hi"; + } + + @Path("no-slash") + @GET + @Traceless + public String noSlash() { + meter.counterBuilder("no-slash").build().add(1); + return "no-slash"; + } + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/Traceless.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/Traceless.java new file mode 100644 index 0000000000000..7312b895295a1 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/Traceless.java @@ -0,0 +1,17 @@ +package io.quarkus.opentelemetry.runtime.tracing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Identifies that the current path should not select for + * adding trace headers. + *

+ * Used together with {@code jakarta.ws.rs.Path} annotation. + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Traceless { +} diff --git a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PathTemplateResource.java b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PathTemplateResource.java index c5650e9342dd0..939dbaa6936a1 100644 --- a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PathTemplateResource.java +++ b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PathTemplateResource.java @@ -1,11 +1,10 @@ package io.quarkus.it.opentelemetry; import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; -@Path("template/path/{value}") public class PathTemplateResource { + @GET public String get(@PathParam("value") String value) { return "Received: " + value;