Skip to content

Commit

Permalink
Merge pull request #43885 from mcruzdev/issue-42659
Browse files Browse the repository at this point in the history
Exclude uri from otel tracing
  • Loading branch information
brunobat authored Nov 14, 2024
2 parents 8a57c8f + 88f628d commit 309646d
Show file tree
Hide file tree
Showing 21 changed files with 1,257 additions and 2 deletions.
75 changes: 75 additions & 0 deletions docs/src/main/asciidoc/opentelemetry-tracing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@
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;

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;
Expand Down Expand Up @@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -131,15 +137,31 @@ UnremovableBeanBuildItem ensureProducersAreRetained(
return new UnremovableBeanBuildItem(new UnremovableBeanBuildItem.BeanClassNamesExclusion(retainProducers));
}

@BuildStep
void dropApplicationUris(
CombinedIndexBuildItem combinedIndexBuildItem,
BuildProducer<DropApplicationUrisBuildItem> uris) {
String rootPath = ConfigProvider.getConfig().getOptionalValue("quarkus.http.root-path", String.class).orElse("/");
IndexView index = combinedIndexBuildItem.getIndex();
Collection<AnnotationInstance> annotations = index.getAnnotations(TRACELESS);
Set<String> tracelessUris = generateTracelessUris(annotations.stream().toList(), rootPath);
for (String uri : tracelessUris) {
uris.produce(new DropApplicationUrisBuildItem(uri));
}
}

@BuildStep
void dropNames(
Optional<FrameworkEndpointsBuildItem> frameworkEndpoints,
Optional<StaticResourcesBuildItem> staticResources,
BuildProducer<DropNonApplicationUrisBuildItem> dropNonApplicationUris,
BuildProducer<DropStaticResourcesBuildItem> dropStaticResources) {
BuildProducer<DropStaticResourcesBuildItem> dropStaticResources,
List<DropApplicationUrisBuildItem> applicationUris) {

List<String> nonApplicationUris = new ArrayList<>(
applicationUris.stream().map(DropApplicationUrisBuildItem::uri).toList());

// Drop framework paths
List<String> nonApplicationUris = new ArrayList<>();
frameworkEndpoints.ifPresent(
frameworkEndpointsBuildItem -> {
for (String endpoint : frameworkEndpointsBuildItem.getEndpoints()) {
Expand Down Expand Up @@ -170,6 +192,77 @@ void dropNames(
dropStaticResources.produce(new DropStaticResourcesBuildItem(resources));
}

private Set<String> generateTracelessUris(final List<AnnotationInstance> annotations, final String rootPath) {
final Set<String> 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) {
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SpanData> 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<SpanData> spans = exporter.getSpanExporter().getFinishedSpanItems(1);

assertThat(spans)
.hasSize(1)
.satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me"));
}
}
Loading

0 comments on commit 309646d

Please sign in to comment.