Skip to content

Commit 88f628d

Browse files
committed
Add annotation and cofiguration for disabling tracing
1 parent 6c6a730 commit 88f628d

File tree

21 files changed

+1257
-2
lines changed

21 files changed

+1257
-2
lines changed

docs/src/main/asciidoc/opentelemetry-tracing.adoc

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,81 @@ quarkus.otel.instrument.rest=false
589589
quarkus.otel.instrument.resteasy=false
590590
----
591591

592+
=== Disable specific REST endpoints
593+
594+
There are two ways to disable tracing for a specific REST endpoint.
595+
596+
You can use the `@io.quarkus.opentelemetry.runtime.tracing.Traceless` (or simply `@Traceless`) annotation to disable tracing for a specific endpoint.
597+
598+
Examples:
599+
600+
==== `@Traceless` annotation on a class
601+
602+
[source,java]
603+
.PingResource.java
604+
----
605+
@Path("/health")
606+
public class PingResource {
607+
608+
@Path("/ping")
609+
public String ping() {
610+
return "pong";
611+
}
612+
}
613+
----
614+
615+
When the `@Traceless` annotation is placed on a class, all methods annotated with `@Path` will be excluded from tracing.
616+
617+
==== `@Traceless` annotation on a method
618+
619+
[source,java]
620+
.TraceResource.java
621+
----
622+
@Path("/trace")
623+
@Traceless
624+
public class TraceResource {
625+
626+
@Path("/no")
627+
@GET
628+
@Traceless
629+
public String noTrace() {
630+
return "no";
631+
}
632+
633+
@Path("/yes")
634+
@GET
635+
public String withTrace() {
636+
return "yes";
637+
}
638+
}
639+
----
640+
641+
In the example above, only `GET /trace/yes` will be included in tracing.
642+
643+
==== Disable using configuration
644+
645+
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.
646+
647+
Example:
648+
649+
[source,properties]
650+
.application.properties
651+
----
652+
quarkus.otel.traces.suppress-application-uris=trace,ping,people*
653+
----
654+
655+
This configuration will:
656+
657+
- Disable tracing for the `/trace` URI;
658+
- Disable tracing for the `/ping` URI;
659+
- Disable tracing for the `/people` URI and all other URIs under it, e.g., `/people/1`, `/people/1/cars`.
660+
661+
[NOTE]
662+
====
663+
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.
664+
====
665+
666+
592667
[[configuration-reference]]
593668
== OpenTelemetry Configuration Reference
594669

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.quarkus.opentelemetry.deployment.tracing;
2+
3+
import io.quarkus.builder.item.MultiBuildItem;
4+
5+
/**
6+
* Represents an application uri that must be ignored for tracing.
7+
*/
8+
public final class DropApplicationUrisBuildItem extends MultiBuildItem {
9+
10+
private final String uri;
11+
12+
public DropApplicationUrisBuildItem(String uri) {
13+
this.uri = uri;
14+
}
15+
16+
public String uri() {
17+
return uri;
18+
}
19+
}

extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@
1010
import java.util.Collection;
1111
import java.util.HashSet;
1212
import java.util.List;
13+
import java.util.Objects;
1314
import java.util.Optional;
1415
import java.util.Set;
1516
import java.util.function.BooleanSupplier;
1617

1718
import jakarta.enterprise.inject.spi.EventContext;
1819
import jakarta.inject.Singleton;
1920

21+
import org.eclipse.microprofile.config.ConfigProvider;
2022
import org.jboss.jandex.AnnotationInstance;
2123
import org.jboss.jandex.AnnotationTarget;
24+
import org.jboss.jandex.ClassInfo;
2225
import org.jboss.jandex.DotName;
2326
import org.jboss.jandex.FieldInfo;
2427
import org.jboss.jandex.IndexView;
@@ -53,6 +56,7 @@
5356
import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig;
5457
import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType;
5558
import io.quarkus.opentelemetry.runtime.tracing.DelayedAttributes;
59+
import io.quarkus.opentelemetry.runtime.tracing.Traceless;
5660
import io.quarkus.opentelemetry.runtime.tracing.TracerRecorder;
5761
import io.quarkus.opentelemetry.runtime.tracing.cdi.TracerProducer;
5862
import io.quarkus.opentelemetry.runtime.tracing.security.EndUserSpanProcessor;
@@ -69,6 +73,8 @@ public class TracerProcessor {
6973
private static final DotName SPAN_EXPORTER = DotName.createSimple(SpanExporter.class.getName());
7074
private static final DotName SPAN_PROCESSOR = DotName.createSimple(SpanProcessor.class.getName());
7175
private static final DotName TEXT_MAP_PROPAGATOR = DotName.createSimple(TextMapPropagator.class.getName());
76+
private static final DotName TRACELESS = DotName.createSimple(Traceless.class.getName());
77+
private static final DotName PATH = DotName.createSimple("jakarta.ws.rs.Path");
7278

7379
@BuildStep
7480
UnremovableBeanBuildItem ensureProducersAreRetained(
@@ -131,15 +137,31 @@ UnremovableBeanBuildItem ensureProducersAreRetained(
131137
return new UnremovableBeanBuildItem(new UnremovableBeanBuildItem.BeanClassNamesExclusion(retainProducers));
132138
}
133139

140+
@BuildStep
141+
void dropApplicationUris(
142+
CombinedIndexBuildItem combinedIndexBuildItem,
143+
BuildProducer<DropApplicationUrisBuildItem> uris) {
144+
String rootPath = ConfigProvider.getConfig().getOptionalValue("quarkus.http.root-path", String.class).orElse("/");
145+
IndexView index = combinedIndexBuildItem.getIndex();
146+
Collection<AnnotationInstance> annotations = index.getAnnotations(TRACELESS);
147+
Set<String> tracelessUris = generateTracelessUris(annotations.stream().toList(), rootPath);
148+
for (String uri : tracelessUris) {
149+
uris.produce(new DropApplicationUrisBuildItem(uri));
150+
}
151+
}
152+
134153
@BuildStep
135154
void dropNames(
136155
Optional<FrameworkEndpointsBuildItem> frameworkEndpoints,
137156
Optional<StaticResourcesBuildItem> staticResources,
138157
BuildProducer<DropNonApplicationUrisBuildItem> dropNonApplicationUris,
139-
BuildProducer<DropStaticResourcesBuildItem> dropStaticResources) {
158+
BuildProducer<DropStaticResourcesBuildItem> dropStaticResources,
159+
List<DropApplicationUrisBuildItem> applicationUris) {
160+
161+
List<String> nonApplicationUris = new ArrayList<>(
162+
applicationUris.stream().map(DropApplicationUrisBuildItem::uri).toList());
140163

141164
// Drop framework paths
142-
List<String> nonApplicationUris = new ArrayList<>();
143165
frameworkEndpoints.ifPresent(
144166
frameworkEndpointsBuildItem -> {
145167
for (String endpoint : frameworkEndpointsBuildItem.getEndpoints()) {
@@ -170,6 +192,77 @@ void dropNames(
170192
dropStaticResources.produce(new DropStaticResourcesBuildItem(resources));
171193
}
172194

195+
private Set<String> generateTracelessUris(final List<AnnotationInstance> annotations, final String rootPath) {
196+
final Set<String> applicationUris = new HashSet<>();
197+
for (AnnotationInstance annotation : annotations) {
198+
AnnotationTarget.Kind kind = annotation.target().kind();
199+
200+
switch (kind) {
201+
case CLASS -> {
202+
AnnotationInstance classAnnotated = annotation.target().asClass().annotations()
203+
.stream().filter(TracerProcessor::isClassAnnotatedWithPath).findFirst().orElse(null);
204+
205+
if (Objects.isNull(classAnnotated)) {
206+
throw new IllegalStateException(
207+
String.format(
208+
"The class '%s' is annotated with @Traceless but is missing the required @Path annotation. "
209+
+
210+
"Please ensure that the class is properly annotated with @Path annotation.",
211+
annotation.target().asClass().name()));
212+
}
213+
214+
String classPath = classAnnotated.value().asString();
215+
String finalPath = combinePaths(rootPath, classPath);
216+
217+
if (containsPathExpression(finalPath)) {
218+
applicationUris.add(sanitizeForTraceless(finalPath) + "*");
219+
continue;
220+
}
221+
222+
applicationUris.add(finalPath + "*");
223+
applicationUris.add(finalPath);
224+
}
225+
case METHOD -> {
226+
ClassInfo classInfo = annotation.target().asMethod().declaringClass();
227+
228+
AnnotationInstance possibleClassAnnotatedWithPath = classInfo.asClass()
229+
.annotations()
230+
.stream()
231+
.filter(TracerProcessor::isClassAnnotatedWithPath)
232+
.findFirst()
233+
.orElse(null);
234+
235+
if (Objects.isNull(possibleClassAnnotatedWithPath)) {
236+
throw new IllegalStateException(
237+
String.format(
238+
"The class '%s' contains a method annotated with @Traceless but is missing the required @Path annotation. "
239+
+
240+
"Please ensure that the class is properly annotated with @Path annotation.",
241+
classInfo.name()));
242+
}
243+
244+
String finalPath;
245+
String classPath = possibleClassAnnotatedWithPath.value().asString();
246+
AnnotationInstance possibleMethodAnnotatedWithPath = annotation.target().annotation(PATH);
247+
if (possibleMethodAnnotatedWithPath != null) {
248+
String methodValue = possibleMethodAnnotatedWithPath.value().asString();
249+
finalPath = combinePaths(rootPath, combinePaths(classPath, methodValue));
250+
} else {
251+
finalPath = combinePaths(rootPath, classPath);
252+
}
253+
254+
if (containsPathExpression(finalPath)) {
255+
applicationUris.add(sanitizeForTraceless(finalPath) + "*");
256+
continue;
257+
}
258+
259+
applicationUris.add(finalPath);
260+
}
261+
}
262+
}
263+
return applicationUris;
264+
}
265+
173266
@BuildStep
174267
@Record(ExecutionTime.STATIC_INIT)
175268
SyntheticBeanBuildItem setupDelayedAttribute(TracerRecorder recorder, ApplicationInfoBuildItem appInfo) {
@@ -256,6 +349,37 @@ private static ObserverConfiguratorBuildItem createEventObserver(
256349
}));
257350
}
258351

352+
private static boolean containsPathExpression(String value) {
353+
return value.indexOf('{') != -1;
354+
}
355+
356+
private static String sanitizeForTraceless(final String path) {
357+
int braceIndex = path.indexOf('{');
358+
if (braceIndex == -1) {
359+
return path;
360+
}
361+
if (braceIndex > 0 && path.charAt(braceIndex - 1) == '/') {
362+
return path.substring(0, braceIndex - 1);
363+
} else {
364+
return path.substring(0, braceIndex);
365+
}
366+
}
367+
368+
private static boolean isClassAnnotatedWithPath(AnnotationInstance annotation) {
369+
return annotation.target().kind().equals(AnnotationTarget.Kind.CLASS) &&
370+
annotation.name().equals(PATH);
371+
}
372+
373+
private String combinePaths(String basePath, String relativePath) {
374+
if (!basePath.endsWith("/")) {
375+
basePath += "/";
376+
}
377+
if (relativePath.startsWith("/")) {
378+
relativePath = relativePath.substring(1);
379+
}
380+
return basePath + relativePath;
381+
}
382+
259383
static final class SecurityEventsEnabled implements BooleanSupplier {
260384

261385
private final boolean enabled;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package io.quarkus.opentelemetry.deployment;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.hamcrest.Matchers.is;
5+
6+
import java.util.List;
7+
8+
import jakarta.inject.Inject;
9+
10+
import org.jboss.shrinkwrap.api.ShrinkWrap;
11+
import org.jboss.shrinkwrap.api.asset.StringAsset;
12+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
13+
import org.junit.jupiter.api.BeforeEach;
14+
import org.junit.jupiter.api.DisplayName;
15+
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.extension.RegisterExtension;
17+
18+
import io.opentelemetry.sdk.trace.data.SpanData;
19+
import io.quarkus.opentelemetry.deployment.common.TracerRouter;
20+
import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryExporter;
21+
import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryMetricExporterProvider;
22+
import io.quarkus.opentelemetry.deployment.common.traces.TraceMeResource;
23+
import io.quarkus.test.QuarkusUnitTest;
24+
import io.restassured.RestAssured;
25+
26+
public class OpenTelemetrySuppressAppUrisTest {
27+
@RegisterExtension
28+
static final QuarkusUnitTest TEST = new QuarkusUnitTest().setArchiveProducer(
29+
() -> ShrinkWrap.create(JavaArchive.class)
30+
.addPackage(InMemoryExporter.class.getPackage())
31+
.addAsResource("resource-config/application.properties", "application.properties")
32+
.addAsResource(
33+
"META-INF/services-config/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider",
34+
"META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")
35+
.addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()),
36+
"META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")
37+
.addClasses(TracerRouter.class, TraceMeResource.class))
38+
.overrideConfigKey("quarkus.otel.traces.suppress-application-uris", "tracer,/hello/Itachi");
39+
40+
@Inject
41+
InMemoryExporter exporter;
42+
43+
@BeforeEach
44+
void setup() {
45+
exporter.reset();
46+
}
47+
48+
@Test
49+
@DisplayName("Should not trace when the using configuration quarkus.otel.traces.suppress-application-uris without slash")
50+
void testingSuppressAppUrisWithoutSlash() {
51+
RestAssured.when()
52+
.get("/tracer").then()
53+
.statusCode(200)
54+
.body(is("Hello Tracer!"));
55+
56+
RestAssured.when()
57+
.get("/trace-me").then()
58+
.statusCode(200)
59+
.body(is("trace-me"));
60+
61+
List<SpanData> spans = exporter.getSpanExporter().getFinishedSpanItems(1);
62+
63+
assertThat(spans)
64+
.hasSize(1)
65+
.satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me"));
66+
}
67+
68+
@Test
69+
@DisplayName("Should not trace when the using configuration quarkus.otel.traces.suppress-application-uris with slash")
70+
void testingSuppressAppUrisWithSlash() {
71+
RestAssured.when()
72+
.get("/hello/Itachi").then()
73+
.statusCode(200)
74+
.body(is("Amaterasu!"));
75+
76+
RestAssured.when()
77+
.get("/trace-me").then()
78+
.statusCode(200)
79+
.body(is("trace-me"));
80+
81+
List<SpanData> spans = exporter.getSpanExporter().getFinishedSpanItems(1);
82+
83+
assertThat(spans)
84+
.hasSize(1)
85+
.satisfiesOnlyOnce(span -> assertThat(span.getName()).containsOnlyOnce("trace-me"));
86+
}
87+
}

0 commit comments

Comments
 (0)