diff --git a/docs/src/main/asciidoc/opentelemetry-tracing.adoc b/docs/src/main/asciidoc/opentelemetry-tracing.adoc index d738f0788fe2e..f456d8dc8a221 100644 --- a/docs/src/main/asciidoc/opentelemetry-tracing.adoc +++ b/docs/src/main/asciidoc/opentelemetry-tracing.adoc @@ -590,6 +590,81 @@ quarkus.otel.instrument.rest=false quarkus.otel.instrument.resteasy=false ---- +=== Disable specific REST endpoints + +There are two ways to disable tracing for a specific REST endpoint. + +You can use the `@io.quarkus.opentelemetry.runtime.tracing.Traceless` (or simply `@Traceless`) annotation to disable tracing for a specific endpoint. + +Examples: + +==== `@Traceless` annotation on a class + +[source,java] +.PingResource.java +---- +@Path("/health") +public class PingResource { + + @Path("/ping") + public String ping() { + return "pong"; + } +} +---- + +When the `@Traceless` annotation is placed on a class, all methods annotated with `@Path` will be excluded from tracing. + +==== `@Traceless` annotation on a method + +[source,java] +.TraceResource.java +---- +@Path("/trace") +@Traceless +public class TraceResource { + + @Path("/no") + @GET + @Traceless + public String noTrace() { + return "no"; + } + + @Path("/yes") + @GET + public String withTrace() { + return "yes"; + } +} +---- + +In the example above, only `GET /trace/yes` will be included in tracing. + +==== Disable using configuration + +If you do not want to modify the source code, you can use your `application.properties` to disable a specific endpoint through the `quarkus.otel.traces.suppress-application-uris` property. + +Example: + +[source,properties] +.application.properties +---- +quarkus.otel.traces.suppress-application-uris=trace,ping,people* +---- + +This configuration will: + +- Disable tracing for the `/trace` URI; +- Disable tracing for the `/ping` URI; +- Disable tracing for the `/people` URI and all other URIs under it, e.g., `/people/1`, `/people/1/cars`. + +[NOTE] +==== +If you are using `quarkus.http.root-path`, you need to remember to include the root path in the configuration. Unlike `@Traceless`, this configuration does not automatically add the root path. +==== + + [[configuration-reference]] == OpenTelemetry Configuration Reference diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/DropApplicationUrisBuildItem.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/DropApplicationUrisBuildItem.java new file mode 100644 index 0000000000000..7d06ca4f4c66b --- /dev/null +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/DropApplicationUrisBuildItem.java @@ -0,0 +1,19 @@ +package io.quarkus.opentelemetry.deployment.tracing; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Represents an application uri that must be ignored for tracing. + */ +public final class DropApplicationUrisBuildItem extends MultiBuildItem { + + private final String uri; + + public DropApplicationUrisBuildItem(String uri) { + this.uri = uri; + } + + public String uri() { + return uri; + } +} 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..c516f1c20e2f1 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,8 +18,10 @@ import jakarta.enterprise.inject.spi.EventContext; import jakarta.inject.Singleton; +import org.eclipse.microprofile.config.ConfigProvider; 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.IndexView; @@ -53,6 +56,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 +73,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( @@ -131,15 +137,31 @@ UnremovableBeanBuildItem ensureProducersAreRetained( return new UnremovableBeanBuildItem(new UnremovableBeanBuildItem.BeanClassNamesExclusion(retainProducers)); } + @BuildStep + void dropApplicationUris( + CombinedIndexBuildItem combinedIndexBuildItem, + BuildProducer uris) { + String rootPath = ConfigProvider.getConfig().getOptionalValue("quarkus.http.root-path", String.class).orElse("/"); + IndexView index = combinedIndexBuildItem.getIndex(); + Collection annotations = index.getAnnotations(TRACELESS); + Set tracelessUris = generateTracelessUris(annotations.stream().toList(), rootPath); + for (String uri : tracelessUris) { + uris.produce(new DropApplicationUrisBuildItem(uri)); + } + } + @BuildStep void dropNames( Optional frameworkEndpoints, Optional staticResources, BuildProducer dropNonApplicationUris, - BuildProducer dropStaticResources) { + BuildProducer dropStaticResources, + List applicationUris) { + + List nonApplicationUris = new ArrayList<>( + applicationUris.stream().map(DropApplicationUrisBuildItem::uri).toList()); // Drop framework paths - List nonApplicationUris = new ArrayList<>(); frameworkEndpoints.ifPresent( frameworkEndpointsBuildItem -> { for (String endpoint : frameworkEndpointsBuildItem.getEndpoints()) { @@ -170,6 +192,77 @@ void dropNames( dropStaticResources.produce(new DropStaticResourcesBuildItem(resources)); } + private Set generateTracelessUris(final List annotations, final String rootPath) { + final Set applicationUris = new HashSet<>(); + for (AnnotationInstance annotation : annotations) { + AnnotationTarget.Kind kind = annotation.target().kind(); + + switch (kind) { + case CLASS -> { + AnnotationInstance classAnnotated = annotation.target().asClass().annotations() + .stream().filter(TracerProcessor::isClassAnnotatedWithPath).findFirst().orElse(null); + + if (Objects.isNull(classAnnotated)) { + throw new IllegalStateException( + String.format( + "The class '%s' is annotated with @Traceless but is missing the required @Path annotation. " + + + "Please ensure that the class is properly annotated with @Path annotation.", + annotation.target().asClass().name())); + } + + String classPath = classAnnotated.value().asString(); + String finalPath = combinePaths(rootPath, classPath); + + if (containsPathExpression(finalPath)) { + applicationUris.add(sanitizeForTraceless(finalPath) + "*"); + continue; + } + + applicationUris.add(finalPath + "*"); + applicationUris.add(finalPath); + } + case METHOD -> { + ClassInfo classInfo = annotation.target().asMethod().declaringClass(); + + AnnotationInstance possibleClassAnnotatedWithPath = classInfo.asClass() + .annotations() + .stream() + .filter(TracerProcessor::isClassAnnotatedWithPath) + .findFirst() + .orElse(null); + + if (Objects.isNull(possibleClassAnnotatedWithPath)) { + throw new IllegalStateException( + String.format( + "The class '%s' contains a method annotated with @Traceless but is missing the required @Path annotation. " + + + "Please ensure that the class is properly annotated with @Path annotation.", + classInfo.name())); + } + + String finalPath; + String classPath = possibleClassAnnotatedWithPath.value().asString(); + AnnotationInstance possibleMethodAnnotatedWithPath = annotation.target().annotation(PATH); + if (possibleMethodAnnotatedWithPath != null) { + String methodValue = possibleMethodAnnotatedWithPath.value().asString(); + finalPath = combinePaths(rootPath, combinePaths(classPath, methodValue)); + } else { + finalPath = combinePaths(rootPath, classPath); + } + + if (containsPathExpression(finalPath)) { + applicationUris.add(sanitizeForTraceless(finalPath) + "*"); + continue; + } + + applicationUris.add(finalPath); + } + } + } + return applicationUris; + } + @BuildStep @Record(ExecutionTime.STATIC_INIT) SyntheticBeanBuildItem setupDelayedAttribute(TracerRecorder recorder, ApplicationInfoBuildItem appInfo) { @@ -256,6 +349,37 @@ private static ObserverConfiguratorBuildItem createEventObserver( })); } + private static boolean containsPathExpression(String value) { + return value.indexOf('{') != -1; + } + + private static String sanitizeForTraceless(final String path) { + int braceIndex = path.indexOf('{'); + if (braceIndex == -1) { + return path; + } + if (braceIndex > 0 && path.charAt(braceIndex - 1) == '/') { + return path.substring(0, braceIndex - 1); + } else { + return path.substring(0, braceIndex); + } + } + + private static boolean isClassAnnotatedWithPath(AnnotationInstance annotation) { + return annotation.target().kind().equals(AnnotationTarget.Kind.CLASS) && + annotation.name().equals(PATH); + } + + private String combinePaths(String basePath, String relativePath) { + if (!basePath.endsWith("/")) { + basePath += "/"; + } + if (relativePath.startsWith("/")) { + relativePath = relativePath.substring(1); + } + return basePath + relativePath; + } + static final class SecurityEventsEnabled implements BooleanSupplier { private final boolean enabled; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySuppressAppUrisTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySuppressAppUrisTest.java new file mode 100644 index 0000000000000..003c21e5b8154 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySuppressAppUrisTest.java @@ -0,0 +1,87 @@ +package io.quarkus.opentelemetry.deployment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.List; + +import jakarta.inject.Inject; + +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.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.opentelemetry.deployment.common.TracerRouter; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryExporter; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryMetricExporterProvider; +import io.quarkus.opentelemetry.deployment.common.traces.TraceMeResource; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class OpenTelemetrySuppressAppUrisTest { + @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") + .addClasses(TracerRouter.class, TraceMeResource.class)) + .overrideConfigKey("quarkus.otel.traces.suppress-application-uris", "tracer,/hello/Itachi"); + + @Inject + InMemoryExporter exporter; + + @BeforeEach + void setup() { + exporter.reset(); + } + + @Test + @DisplayName("Should not trace when the using configuration quarkus.otel.traces.suppress-application-uris without slash") + void testingSuppressAppUrisWithoutSlash() { + RestAssured.when() + .get("/tracer").then() + .statusCode(200) + .body(is("Hello Tracer!")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the using configuration quarkus.otel.traces.suppress-application-uris with slash") + void testingSuppressAppUrisWithSlash() { + RestAssured.when() + .get("/hello/Itachi").then() + .statusCode(200) + .body(is("Amaterasu!")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTracelessOnMethodWithoutPathTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTracelessOnMethodWithoutPathTest.java new file mode 100644 index 0000000000000..fadb21d0a5690 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTracelessOnMethodWithoutPathTest.java @@ -0,0 +1,45 @@ +package io.quarkus.opentelemetry.deployment; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.ws.rs.core.Response; + +import org.assertj.core.api.Assertions; +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.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryExporter; +import io.quarkus.opentelemetry.runtime.tracing.Traceless; +import io.quarkus.test.QuarkusUnitTest; + +public class OpenTelemetryTracelessOnMethodWithoutPathTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addPackage(InMemoryExporter.class.getPackage()) + .addAsResource(new StringAsset( + ""), "application.properties") + .addClasses(TracelessWithoutPath.class)) + .assertException(throwable -> { + assertThat(throwable).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("The class") + .hasMessageContaining( + "contains a method annotated with @Traceless but is missing the required @Path annotation. Please ensure that the class is properly annotated with @Path annotation."); + }); + + @Test + void testClassAnnotatedWithTracelessMissingPath() { + Assertions.fail(); + } + + public static class TracelessWithoutPath { + + @Traceless + public Response hello() { + return Response.ok("hello").build(); + } + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTracelessTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTracelessTest.java new file mode 100644 index 0000000000000..bd61aacf40a31 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTracelessTest.java @@ -0,0 +1,141 @@ +package io.quarkus.opentelemetry.deployment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.List; + +import jakarta.inject.Inject; + +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.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryExporter; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryMetricExporterProvider; +import io.quarkus.opentelemetry.deployment.common.traces.TraceMeResource; +import io.quarkus.opentelemetry.deployment.common.traces.TracelessClassLevelResource; +import io.quarkus.opentelemetry.deployment.common.traces.TracelessHelloResource; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class OpenTelemetryTracelessTest { + @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") + .addClasses(TracelessHelloResource.class, TracelessClassLevelResource.class, TraceMeResource.class)); + + @Inject + InMemoryExporter exporter; + + @BeforeEach + void setup() { + exporter.reset(); + } + + @Test + @DisplayName("Should not trace when the method @Path uses @PathParam") + void testingWithPathParam() { + RestAssured.when() + .get("/hello/mask/1").then() + .statusCode(200) + .body(is("mask-1")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the annotation @Traceless is at method level") + void testingTracelessHelloHi() { + + RestAssured.when() + .get("/hello").then() + .statusCode(200) + .body(is("hello")); + + RestAssured.when() + .get("/hello/hi").then() + .statusCode(200) + .body(is("hi")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the method @Path is without '/'") + void testingHelloNoSlash() { + RestAssured.when() + .get("/hello/no-slash").then() + .statusCode(200) + .body(is("no-slash")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the annotation is at class level") + void testingTracelessAtClassLevel() { + + RestAssured.when() + .get("class-level").then() + .statusCode(200) + .body(is("class-level")); + + RestAssured.when() + .get("/class-level/first-method").then() + .statusCode(200) + .body(is("first-method")); + + RestAssured.when() + .get("/class-level/second-method").then() + .statusCode(200) + .body(is("second-method")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTracelessWithRootPathTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTracelessWithRootPathTest.java new file mode 100644 index 0000000000000..03c442a1e46ed --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTracelessWithRootPathTest.java @@ -0,0 +1,146 @@ +package io.quarkus.opentelemetry.deployment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.List; + +import jakarta.inject.Inject; + +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.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryExporter; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryMetricExporterProvider; +import io.quarkus.opentelemetry.deployment.common.traces.TraceMeResource; +import io.quarkus.opentelemetry.deployment.common.traces.TracelessClassLevelResource; +import io.quarkus.opentelemetry.deployment.common.traces.TracelessHelloResource; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class OpenTelemetryTracelessWithRootPathTest { + @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") + .addClasses(TracelessHelloResource.class, TracelessClassLevelResource.class, TraceMeResource.class)) + .overrideConfigKey("quarkus.http.root-path", "/api"); + + @Inject + InMemoryExporter exporter; + + @BeforeEach + void setup() { + exporter.reset(); + } + + @Test + @DisplayName("Should not trace when the annotation @Traceless is at method level") + void testingTracelessHelloHi() { + + RestAssured.when() + .get("/hello").then() + .statusCode(200) + .body(is("hello")); + + RestAssured.when() + .get("/hello/hi").then() + .statusCode(200) + .body(is("hi")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the method @Path uses @PathParam") + void testingWithPathParam() { + RestAssured.when() + .get("/hello/mask/1").then() + .statusCode(200) + .body(is("mask-1")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the method @Path is without '/'") + void testingHelloNoSlash() { + RestAssured.when() + .get("/hello/no-slash").then() + .statusCode(200) + .body(is("no-slash")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the annotation is at class level") + void testingTracelessAtClassLevel() { + + RestAssured.when() + .get("class-level").then() + .statusCode(200) + .body(is("class-level")); + + RestAssured.when() + .get("/class-level/first-method").then() + .statusCode(200) + .body(is("first-method")); + + RestAssured.when() + .get("/class-level/second-method").then() + .statusCode(200) + .body(is("second-method")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTracelessWithoutPathTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTracelessWithoutPathTest.java new file mode 100644 index 0000000000000..9da23e2199eaf --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTracelessWithoutPathTest.java @@ -0,0 +1,40 @@ +package io.quarkus.opentelemetry.deployment; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.assertj.core.api.Assertions; +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.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryExporter; +import io.quarkus.opentelemetry.runtime.tracing.Traceless; +import io.quarkus.test.QuarkusUnitTest; + +public class OpenTelemetryTracelessWithoutPathTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addPackage(InMemoryExporter.class.getPackage()) + .addAsResource(new StringAsset( + ""), "application.properties") + .addClasses(TracelessWithoutPath.class)) + .assertException(throwable -> { + assertThat(throwable).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("The class") + .hasMessageContaining( + "is annotated with @Traceless but is missing the required @Path annotation. Please ensure that the class is properly annotated with @Path annotation."); + }); + + @Test + void testClassAnnotatedWithTracelessMissingPath() { + Assertions.fail(); + } + + @Traceless + public static class TracelessWithoutPath { + + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/TracerRouter.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/TracerRouter.java index 688d1f3a1d976..17007e372da1c 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/TracerRouter.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/TracerRouter.java @@ -27,6 +27,8 @@ public void register(@Observes StartupEvent ev) { String name = rc.pathParam("name"); if (name.equals("Naruto")) { rc.response().end("hello " + name); + } else if (name.equals("Itachi")) { + rc.response().end("Amaterasu!"); } else { rc.response().setStatusCode(404).end(); } diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/traces/TraceMeResource.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/traces/TraceMeResource.java new file mode 100644 index 0000000000000..856d97666550c --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/traces/TraceMeResource.java @@ -0,0 +1,20 @@ +package io.quarkus.opentelemetry.deployment.common.traces; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.opentelemetry.api.metrics.Meter; + +@Path("/trace-me") +public class TraceMeResource { + + @Inject + Meter meter; + + @GET + public String traceMe() { + meter.counterBuilder("trace-me").build().add(1); + return "trace-me"; + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/traces/TracelessClassLevelResource.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/traces/TracelessClassLevelResource.java new file mode 100644 index 0000000000000..b27e2da3851c9 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/traces/TracelessClassLevelResource.java @@ -0,0 +1,36 @@ +package io.quarkus.opentelemetry.deployment.common.traces; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.opentelemetry.api.metrics.Meter; +import io.quarkus.opentelemetry.runtime.tracing.Traceless; + +@Path("/class-level") +@Traceless +public class TracelessClassLevelResource { + + @Inject + Meter meter; + + @GET + public String classLevel() { + meter.counterBuilder("class-level").build().add(1); + return "class-level"; + } + + @GET + @Path("/first-method") + public String firstMethod() { + meter.counterBuilder("first-method").build().add(1); + return "first-method"; + } + + @Path("/second-method") + @GET + public String secondMethod() { + meter.counterBuilder("second-method").build().add(1); + return "second-method"; + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/traces/TracelessHelloResource.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/traces/TracelessHelloResource.java new file mode 100644 index 0000000000000..f9824313798b4 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/traces/TracelessHelloResource.java @@ -0,0 +1,47 @@ +package io.quarkus.opentelemetry.deployment.common.traces; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import io.opentelemetry.api.metrics.Meter; +import io.quarkus.opentelemetry.runtime.tracing.Traceless; + +@Path("/hello") +public class TracelessHelloResource { + + @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"; + } + + @GET + @Path("/mask/{number}") + @Traceless + public String mask(@PathParam("number") String number) { + meter.counterBuilder("mask").build().add(Integer.parseInt(number)); + return "mask-" + number; + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySuppressAppUrisTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySuppressAppUrisTest.java new file mode 100644 index 0000000000000..3cf785619047d --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySuppressAppUrisTest.java @@ -0,0 +1,87 @@ +package io.quarkus.opentelemetry.deployment.traces; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.List; + +import jakarta.inject.Inject; + +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.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.opentelemetry.deployment.common.TracerRouter; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryExporter; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryMetricExporterProvider; +import io.quarkus.opentelemetry.deployment.common.traces.TraceMeResource; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class OpenTelemetrySuppressAppUrisTest { + @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") + .addClasses(TracerRouter.class, TraceMeResource.class)) + .overrideConfigKey("quarkus.otel.traces.suppress-application-uris", "tracer,/hello/Itachi"); + + @Inject + InMemoryExporter exporter; + + @BeforeEach + void setup() { + exporter.reset(); + } + + @Test + @DisplayName("Should not trace when the using configuration quarkus.otel.traces.suppress-application-uris without slash") + void testingSuppressAppUrisWithoutSlash() { + RestAssured.when() + .get("/tracer").then() + .statusCode(200) + .body(is("Hello Tracer!")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the using configuration quarkus.otel.traces.suppress-application-uris with slash") + void testingSuppressAppUrisWithSlash() { + RestAssured.when() + .get("/hello/Itachi").then() + .statusCode(200) + .body(is("Amaterasu!")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryTracelessTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryTracelessTest.java new file mode 100644 index 0000000000000..42f91ca3c7288 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryTracelessTest.java @@ -0,0 +1,141 @@ +package io.quarkus.opentelemetry.deployment.traces; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.List; + +import jakarta.inject.Inject; + +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.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryExporter; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryMetricExporterProvider; +import io.quarkus.opentelemetry.deployment.common.traces.TraceMeResource; +import io.quarkus.opentelemetry.deployment.common.traces.TracelessClassLevelResource; +import io.quarkus.opentelemetry.deployment.common.traces.TracelessHelloResource; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class OpenTelemetryTracelessTest { + @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") + .addClasses(TracelessHelloResource.class, TracelessClassLevelResource.class, TraceMeResource.class)); + + @Inject + InMemoryExporter exporter; + + @BeforeEach + void setup() { + exporter.reset(); + } + + @Test + @DisplayName("Should not trace when the method @Path uses @PathParam") + void testingWithPathParam() { + RestAssured.when() + .get("/hello/mask/1").then() + .statusCode(200) + .body(is("mask-1")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the annotation @Traceless is at method level") + void testingTracelessHelloHi() { + + RestAssured.when() + .get("/hello").then() + .statusCode(200) + .body(is("hello")); + + RestAssured.when() + .get("/hello/hi").then() + .statusCode(200) + .body(is("hi")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the method @Path is without '/'") + void testingHelloNoSlash() { + RestAssured.when() + .get("/hello/no-slash").then() + .statusCode(200) + .body(is("no-slash")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the annotation is at class level") + void testingTracelessAtClassLevel() { + + RestAssured.when() + .get("class-level").then() + .statusCode(200) + .body(is("class-level")); + + RestAssured.when() + .get("/class-level/first-method").then() + .statusCode(200) + .body(is("first-method")); + + RestAssured.when() + .get("/class-level/second-method").then() + .statusCode(200) + .body(is("second-method")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryTracelessWithRootPathTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryTracelessWithRootPathTest.java new file mode 100644 index 0000000000000..a36362a7e1760 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryTracelessWithRootPathTest.java @@ -0,0 +1,146 @@ +package io.quarkus.opentelemetry.deployment.traces; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.List; + +import jakarta.inject.Inject; + +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.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryExporter; +import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryMetricExporterProvider; +import io.quarkus.opentelemetry.deployment.common.traces.TraceMeResource; +import io.quarkus.opentelemetry.deployment.common.traces.TracelessClassLevelResource; +import io.quarkus.opentelemetry.deployment.common.traces.TracelessHelloResource; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class OpenTelemetryTracelessWithRootPathTest { + @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") + .addClasses(TracelessHelloResource.class, TracelessClassLevelResource.class, TraceMeResource.class)) + .overrideConfigKey("quarkus.http.root-path", "/api"); + + @Inject + InMemoryExporter exporter; + + @BeforeEach + void setup() { + exporter.reset(); + } + + @Test + @DisplayName("Should not trace when the annotation @Traceless is at method level") + void testingTracelessHelloHi() { + + RestAssured.when() + .get("/hello").then() + .statusCode(200) + .body(is("hello")); + + RestAssured.when() + .get("/hello/hi").then() + .statusCode(200) + .body(is("hi")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the method @Path uses @PathParam") + void testingWithPathParam() { + RestAssured.when() + .get("/hello/mask/1").then() + .statusCode(200) + .body(is("mask-1")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the method @Path is without '/'") + void testingHelloNoSlash() { + RestAssured.when() + .get("/hello/no-slash").then() + .statusCode(200) + .body(is("no-slash")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + + @Test + @DisplayName("Should not trace when the annotation is at class level") + void testingTracelessAtClassLevel() { + + RestAssured.when() + .get("class-level").then() + .statusCode(200) + .body(is("class-level")); + + RestAssured.when() + .get("/class-level/first-method").then() + .statusCode(200) + .body(is("first-method")); + + RestAssured.when() + .get("/class-level/second-method").then() + .statusCode(200) + .body(is("second-method")); + + RestAssured.when() + .get("/trace-me").then() + .statusCode(200) + .body(is("trace-me")); + + List spans = exporter.getSpanExporter().getFinishedSpanItems(1); + + assertThat(spans) + .hasSize(1) + .satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me")); + } + +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java index eac3ccb8c1e89..45eb960a00277 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Predicate; import jakarta.enterprise.inject.Any; @@ -177,6 +178,12 @@ public Sampler apply(Sampler existingSampler, ConfigProperties configProperties) if (!oTelRuntimeConfig.traces().includeStaticResources()) {// default is false dropTargets.addAll(TracerRecorder.dropStaticResourceTargets); } + if (oTelRuntimeConfig.traces().suppressApplicationUris().isPresent()) { + dropTargets.addAll(oTelRuntimeConfig.traces().suppressApplicationUris().get() + .stream().filter(Predicate.not(String::isEmpty)) + .map(addSlashIfNecessary()) + .toList()); + } // make sure dropped targets are not sampled if (!dropTargets.isEmpty()) { @@ -192,6 +199,19 @@ public Sampler apply(Sampler existingSampler, ConfigProperties configProperties) } } + private static Function addSlashIfNecessary() { + return new Function() { + @Override + public String apply(String item) { + if (item.startsWith("/")) { + return item; + } else { + return "/" + item; + } + } + }; + } + @Singleton final class TracerProviderCustomizer implements AutoConfiguredOpenTelemetrySdkBuilderCustomizer { diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/TracesRuntimeConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/TracesRuntimeConfig.java index 07b6abb8eaca5..8f00dbe67b03c 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/TracesRuntimeConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/TracesRuntimeConfig.java @@ -1,5 +1,6 @@ package io.quarkus.opentelemetry.runtime.config.runtime; +import java.util.List; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; @@ -25,6 +26,17 @@ public interface TracesRuntimeConfig { @WithDefault("true") Boolean suppressNonApplicationUris(); + /** + * Comma-separated, suppress application uris from trace collection. + *

+ * This will suppress all uris set by this property. + *

+ * If you are using quarkus.http.root-path, you need to consider it when setting your uris, in + * other words, you need to configure it using the root-path if necessary. + */ + @WithName("suppress-application-uris") + Optional> suppressApplicationUris(); + /** * Include static resources from trace collection. *

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..254022bb8ee36 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/Traceless.java @@ -0,0 +1,16 @@ +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 be select for tracing. + *

+ * 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/SimpleResource.java b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/SimpleResource.java index 5abc30290a410..dcb38b74ea92a 100644 --- a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/SimpleResource.java +++ b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/SimpleResource.java @@ -17,6 +17,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.context.Scope; +import io.quarkus.opentelemetry.runtime.tracing.Traceless; @Path("") @Produces(MediaType.APPLICATION_JSON) @@ -150,4 +151,21 @@ public String exception() { LOG.error("Oh no {}", exception.getMessage(), exception); return "Oh no! An exception"; } + + @GET + @Path("/suppress-app-uri") + public TraceData suppressAppUri() { + TraceData traceData = new TraceData(); + traceData.message = "Suppress me!"; + return traceData; + } + + @GET + @Path("/traceless") + @Traceless + public TraceData traceless() { + TraceData traceData = new TraceData(); + traceData.message = "@Traceless"; + return traceData; + } } diff --git a/integration-tests/opentelemetry/src/main/resources/application.properties b/integration-tests/opentelemetry/src/main/resources/application.properties index 68c3b9664276b..9d996417b4dad 100644 --- a/integration-tests/opentelemetry/src/main/resources/application.properties +++ b/integration-tests/opentelemetry/src/main/resources/application.properties @@ -21,4 +21,7 @@ quarkus.security.users.embedded.plain-text=true quarkus.security.users.embedded.enabled=true quarkus.http.auth.basic=true +quarkus.otel.traces.suppress-application-uris=/suppress-app-uri + quarkus.native.monitoring=jfr + diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/TracingTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/TracingTest.java index 43b8b52daa1b4..5dea04160a238 100644 --- a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/TracingTest.java +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/TracingTest.java @@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -27,6 +28,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import org.awaitility.core.ConditionTimeoutException; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -743,6 +745,34 @@ public void testNoEndUserAttributes() { .findAny().isEmpty()); } + @Test + public void testSuppressAppUri() { + RestAssured.given() + .when().get("/suppress-app-uri") + .then() + .statusCode(200) + .body("message", Matchers.is("Suppress me!")); + + // should throw because there are a configuration quarkus.otel.traces.suppress-app-uris=/suppress-app-uri + assertThrows(ConditionTimeoutException.class, () -> { + await().atMost(5, SECONDS).until(() -> !getSpans().isEmpty()); + }); + } + + @Test + public void testTracelessResource() { + RestAssured.given() + .when().get("/traceless") + .then() + .statusCode(200) + .body("message", Matchers.is("@Traceless")); + + // should throw because there is no span + assertThrows(ConditionTimeoutException.class, () -> { + await().atMost(5, SECONDS).until(() -> !getSpans().isEmpty()); + }); + } + private void verifyResource(Map spanData) { assertEquals("opentelemetry-integration-test", spanData.get("resource_service.name")); assertEquals("999-SNAPSHOT", spanData.get("resource_service.version"));