Skip to content

Commit 309646d

Browse files
authored
Merge pull request #43885 from mcruzdev/issue-42659
Exclude uri from otel tracing
2 parents 8a57c8f + 88f628d commit 309646d

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
@@ -590,6 +590,81 @@ quarkus.otel.instrument.rest=false
590590
quarkus.otel.instrument.resteasy=false
591591
----
592592

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

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)