Skip to content

Commit

Permalink
Introduce @traceless annotation for selectively disabling tracing
Browse files Browse the repository at this point in the history
This currently only works for Quarkus REST

Resolves: quarkusio#44733
  • Loading branch information
geoand committed Dec 2, 2024
1 parent 2d19846 commit ee5e015
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,23 @@
import static io.quarkus.bootstrap.classloading.QuarkusClassLoader.isClassPresentAtRuntime;
import static jakarta.interceptor.Interceptor.Priority.LIBRARY_AFTER;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BooleanSupplier;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.resteasy.reactive.common.processor.EndpointIndexer;
import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationStore;
import org.jboss.resteasy.reactive.server.model.FixedHandlerChainCustomizer;
import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer;
import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.deployment.Capabilities;
Expand All @@ -18,6 +30,7 @@
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem;
import io.quarkus.opentelemetry.Traceless;
import io.quarkus.opentelemetry.deployment.tracing.TracerEnabled;
import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig;
import io.quarkus.opentelemetry.runtime.tracing.intrumentation.InstrumentationRecorder;
Expand All @@ -29,7 +42,9 @@
import io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.AttachExceptionHandler;
import io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.OpenTelemetryClassicServerFilter;
import io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.OpenTelemetryReactiveServerFilter;
import io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.TracelessServerHandler;
import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem;
import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem;
import io.quarkus.resteasy.reactive.server.spi.PreExceptionMapperHandlerBuildItem;
import io.quarkus.resteasy.reactive.spi.CustomContainerRequestFilterBuildItem;
import io.quarkus.vertx.core.deployment.VertxOptionsConsumerBuildItem;
Expand Down Expand Up @@ -145,12 +160,46 @@ void resteasyReactiveIntegration(
Capabilities capabilities,
BuildProducer<CustomContainerRequestFilterBuildItem> containerRequestFilterBuildItemBuildProducer,
BuildProducer<PreExceptionMapperHandlerBuildItem> preExceptionMapperHandlerBuildItemBuildProducer,
BuildProducer<MethodScannerBuildItem> methodScannerBuildItemBuildProducer,
OTelBuildConfig config) {
if (capabilities.isPresent(Capability.RESTEASY_REACTIVE) && config.instrument().rest()) {
containerRequestFilterBuildItemBuildProducer
.produce(new CustomContainerRequestFilterBuildItem(OpenTelemetryReactiveServerFilter.class.getName()));
preExceptionMapperHandlerBuildItemBuildProducer
.produce(new PreExceptionMapperHandlerBuildItem(new AttachExceptionHandler()));
methodScannerBuildItemBuildProducer.produce(new MethodScannerBuildItem(new TracelessScanner()));
}

}

private static class TracelessScanner implements MethodScanner {

static final DotName TRACELESS = DotName.createSimple(Traceless.class.getName());

@Override
public List<HandlerChainCustomizer> scan(MethodInfo method, ClassInfo actualEndpointClass,
Map<String, Object> methodContext) {

AnnotationStore annotationStore = (AnnotationStore) methodContext
.get(EndpointIndexer.METHOD_CONTEXT_ANNOTATION_STORE);
AnnotationInstance tracelessInstance = doScan(method, actualEndpointClass, annotationStore);
if (tracelessInstance != null) {
return List.of(new FixedHandlerChainCustomizer(new TracelessServerHandler(),
HandlerChainCustomizer.Phase.AFTER_MATCH));
}
return Collections.emptyList();
}

private AnnotationInstance doScan(MethodInfo method, ClassInfo actualEndpointClass,
AnnotationStore annotationStore) {
AnnotationInstance annotationInstance = annotationStore.getAnnotation(method, TRACELESS);
if (annotationInstance == null) {
annotationInstance = annotationStore.getAnnotation(method.declaringClass(), TRACELESS);
if ((annotationInstance == null) && !actualEndpointClass.equals(method.declaringClass())) {
annotationInstance = annotationStore.getAnnotation(actualEndpointClass, TRACELESS);
}
}
return annotationInstance;
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.opentelemetry;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import io.smallrye.common.annotation.Experimental;

/**
* Identifies that the current path should not be select for tracing.
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Experimental("This annotation is only supported by Quarkus REST currently")
public @interface Traceless {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.opentelemetry.sdk.trace.ReadWriteSpan;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.quarkus.opentelemetry.runtime.tracing.InternalAttributes;

/**
* Class to facilitate a delay in when the worker thread inside {@link SpanProcessor}
Expand Down Expand Up @@ -43,6 +44,10 @@ public boolean isStartRequired() {

@Override
public void onEnd(ReadableSpan span) {
if (InternalAttributes.containsTraceless(span.getAttributes())) {
// this span was marked as one to be ignored
return;
}
if (delegate == null) {
logDelegateNotFound();
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.quarkus.opentelemetry.runtime.tracing;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;

/**
* Contains utilities for working with internal Quarkus attributes
*/
public final class InternalAttributes {

public static final AttributeKey<String> TRACELESS = AttributeKey.stringKey("TRACELESS");

public static boolean containsTraceless(Attributes attributes) {
return Boolean.parseBoolean(attributes.get(TRACELESS));
}

private InternalAttributes() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy;

import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.jboss.resteasy.reactive.server.spi.ServerRestHandler;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan;
import io.quarkus.opentelemetry.runtime.tracing.InternalAttributes;

public class TracelessServerHandler implements ServerRestHandler {
@Override
public void handle(ResteasyReactiveRequestContext requestContext) throws Exception {
Span localRootSpan = LocalRootSpan.current();
localRootSpan.setAttribute(InternalAttributes.TRACELESS, "true");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@

import org.jboss.resteasy.reactive.RestQuery;

import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.sdk.trace.internal.data.ExceptionEventData;
import io.quarkus.opentelemetry.runtime.tracing.InternalAttributes;

@Path("")
public class ExporterResource {
@Inject
InMemorySpanExporter inMemorySpanExporter;
CustomInMemorySpanExporter inMemorySpanExporter;

@GET
@Path("/reset")
Expand Down Expand Up @@ -71,8 +74,37 @@ public List<String> exportExceptionMessages() {
static class InMemorySpanExporterProducer {
@Produces
@Singleton
InMemorySpanExporter inMemorySpanExporter() {
return InMemorySpanExporter.create();
CustomInMemorySpanExporter inMemorySpanExporter() {
return new CustomInMemorySpanExporter();
}
}

static class CustomInMemorySpanExporter implements SpanExporter {

private final InMemorySpanExporter delegate = InMemorySpanExporter.create();

public List<SpanData> getFinishedSpanItems() {
return delegate.getFinishedSpanItems();
}

public void reset() {
delegate.reset();
}

@Override
public CompletableResultCode export(Collection<SpanData> spans) {
return delegate.export(spans.stream().filter(sd -> !InternalAttributes.containsTraceless(sd.getAttributes()))
.collect(Collectors.toList()));
}

@Override
public CompletableResultCode flush() {
return delegate.flush();
}

@Override
public CompletableResultCode shutdown() {
return delegate.shutdown();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.quarkus.opentelemetry.Traceless;
import io.smallrye.mutiny.Uni;
import io.vertx.core.impl.NoStackTraceException;

Expand Down Expand Up @@ -104,4 +105,17 @@ public Uni<String> reactiveException() {
throw new NoStackTraceException("dummy2");
});
}

@GET
@Path("potentially-traceless")
@Traceless
public String traceless() {
return "@Traceless";
}

@POST
@Path("potentially-traceless")
public String notTraceless() {
return "Not-@Traceless";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.awaitility.core.ConditionTimeoutException;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -339,6 +342,26 @@ void multipleUsingCombineDifferentPaths() {
assertEquals("helloGetUniExecutor", helloGetUniExecutorInternal.get("name"));
}

@Test
public void potentialTracelessResourceMethods() {
when().get("/reactive/potentially-traceless")
.then()
.statusCode(200)
.body(Matchers.is("@Traceless"));

// should throw because there is no span
assertThrows(ConditionTimeoutException.class, () -> {
await().atMost(5, SECONDS).until(() -> !getSpans().isEmpty());
});

when().post("/reactive/potentially-traceless")
.then()
.statusCode(200)
.body(Matchers.is("Not-@Traceless"));

await().atMost(5, SECONDS).until(() -> getSpans().size() == 1);
}

@Test
public void securedInvalidCredential() {
given().auth().preemptive().basic("scott", "reader2").when().get("/foo/secured/item/something")
Expand Down

0 comments on commit ee5e015

Please sign in to comment.