Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude uri from otel tracing #43885

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,67 @@ 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)) {
continue;
}

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)) {
continue;
}

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 +339,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,128 @@
package io.quarkus.opentelemetry.deployment;

import static org.hamcrest.Matchers.is;

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.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"));

exporter.getSpanExporter().getFinishedSpanItems(1);

}

@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"));

// should have only one
exporter.getSpanExporter().getFinishedSpanItems(1);
}

@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"));

// should have only one
exporter.getSpanExporter().getFinishedSpanItems(1);
}

@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"));

// should have only one
exporter.getSpanExporter().getFinishedSpanItems(1);
}
}
Loading
Loading