Skip to content

Commit

Permalink
Allow to drop application uri
Browse files Browse the repository at this point in the history
  • Loading branch information
mcruzdev committed Oct 20, 2024
1 parent 342e6f0 commit d282746
Show file tree
Hide file tree
Showing 8 changed files with 509 additions and 2 deletions.
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

0 comments on commit d282746

Please sign in to comment.