diff --git a/docs/src/main/asciidoc/opentelemetry.adoc b/docs/src/main/asciidoc/opentelemetry.adoc index ae8f3e248007c..669f54e51f43d 100644 --- a/docs/src/main/asciidoc/opentelemetry.adoc +++ b/docs/src/main/asciidoc/opentelemetry.adoc @@ -496,8 +496,12 @@ The instrumentation documented in this section has been tested with Quarkus and Annotating a method in any CDI aware bean with the `io.opentelemetry.instrumentation.annotations.WithSpan` annotation will create a new Span and establish any required relationships with the current Trace context. +Annotating a method in any CDI aware bean with the `io.opentelemetry.instrumentation.annotations.AddingSpanAttributes` will not create a new span but will add annotated method parameters to attributes in the current span. + +If a method is annotated by mistake with `@AddingSpanAttributes` and `@WithSpan` annotations, the `@WithSpan` annotation will take precedence. + Method parameters can be annotated with the `io.opentelemetry.instrumentation.annotations.SpanAttribute` annotation to -indicate which method parameters should be part of the Trace. +indicate which method parameters should be part of the span. The parameter name can be customized as well. Example: [source,java] @@ -523,6 +527,11 @@ class SpanBean { void spanArgs(@SpanAttribute(value = "arg") String arg) { } + + @AddingSpanAttributes + void addArgumentToExistingSpan(@SpanAttribute(value = "arg") String arg) { + + } } ---- diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java index 983da2a912153..f263832b8d810 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import jakarta.enterprise.inject.Instance; import jakarta.inject.Singleton; @@ -19,12 +20,14 @@ import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.exporter.otlp.internal.OtlpSpanExporterProvider; +import io.opentelemetry.instrumentation.annotations.AddingSpanAttributes; import io.opentelemetry.instrumentation.annotations.SpanAttribute; import io.opentelemetry.instrumentation.annotations.WithSpan; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; @@ -42,6 +45,7 @@ import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.InterceptorBindingRegistrar; +import io.quarkus.arc.processor.Transformation; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -64,6 +68,7 @@ import io.quarkus.opentelemetry.runtime.config.build.ExporterType; import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; +import io.quarkus.opentelemetry.runtime.tracing.cdi.AddingSpanAttributesInterceptor; import io.quarkus.opentelemetry.runtime.tracing.cdi.WithSpanInterceptor; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.InstrumentationRecorder; import io.quarkus.runtime.LaunchMode; @@ -76,8 +81,17 @@ public class OpenTelemetryProcessor { private static final DotName LEGACY_WITH_SPAN = DotName.createSimple( io.opentelemetry.extension.annotations.WithSpan.class.getName()); private static final DotName WITH_SPAN = DotName.createSimple(WithSpan.class.getName()); + private static final DotName ADD_SPAN_ATTRIBUTES = DotName.createSimple(AddingSpanAttributes.class.getName()); + private static final Predicate isAddSpanAttribute = new Predicate<>() { + @Override + public boolean test(AnnotationInstance annotationInstance) { + return annotationInstance.name().equals(ADD_SPAN_ATTRIBUTES); + } + }; private static final DotName SPAN_KIND = DotName.createSimple(SpanKind.class.getName()); private static final DotName WITH_SPAN_INTERCEPTOR = DotName.createSimple(WithSpanInterceptor.class.getName()); + private static final DotName ADD_SPAN_ATTRIBUTES_INTERCEPTOR = DotName + .createSimple(AddingSpanAttributesInterceptor.class.getName()); private static final DotName SPAN_ATTRIBUTE = DotName.createSimple(SpanAttribute.class.getName()); @BuildStep @@ -168,11 +182,15 @@ void registerWithSpan( new InterceptorBindingRegistrar() { @Override public List getAdditionalBindings() { - return List.of(InterceptorBinding.of(WithSpan.class, Set.of("value", "kind"))); + return List.of( + InterceptorBinding.of(WithSpan.class, Set.of("value", "kind")), + InterceptorBinding.of(AddingSpanAttributes.class, Set.of("value"))); } })); - additionalBeans.produce(new AdditionalBeanBuildItem(WithSpanInterceptor.class)); + additionalBeans.produce(new AdditionalBeanBuildItem( + WithSpanInterceptor.class, + AddingSpanAttributesInterceptor.class)); } @BuildStep @@ -209,11 +227,21 @@ public void transform(TransformationContext context) { annotationsTransformer.produce(new AnnotationsTransformerBuildItem(transformationContext -> { AnnotationTarget target = transformationContext.getTarget(); + Transformation transform = transformationContext.transform(); if (target.kind().equals(AnnotationTarget.Kind.CLASS)) { if (target.asClass().name().equals(WITH_SPAN_INTERCEPTOR)) { - transformationContext.transform().add(WITH_SPAN).done(); + transform.add(WITH_SPAN); + } else if (target.asClass().name().equals(ADD_SPAN_ATTRIBUTES_INTERCEPTOR)) { + transform.add(ADD_SPAN_ATTRIBUTES); + } + } else if (target.kind() == AnnotationTarget.Kind.METHOD) { + MethodInfo methodInfo = target.asMethod(); + // WITH_SPAN_INTERCEPTOR and ADD_SPAN_ATTRIBUTES must not be applied at the same time and the first has priority. + if (methodInfo.hasAnnotation(WITH_SPAN) && methodInfo.hasAnnotation(ADD_SPAN_ATTRIBUTES)) { + transform.remove(isAddSpanAttribute); } } + transform.done(); })); } diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/AddingSpanAttributesInterceptorTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/AddingSpanAttributesInterceptorTest.java new file mode 100644 index 0000000000000..9853edf6aa817 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/AddingSpanAttributesInterceptorTest.java @@ -0,0 +1,167 @@ +package io.quarkus.opentelemetry.deployment.interceptor; + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.annotations.AddingSpanAttributes; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; +import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; +import io.quarkus.test.QuarkusUnitTest; + +public class AddingSpanAttributesInterceptorTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClass(HelloRouter.class) + .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) + .addAsManifestResource( + "META-INF/services-config/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", + "services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") + .addAsResource("resource-config/application.properties", "application.properties")); + + @Inject + HelloRouter helloRouter; + @Inject + Tracer tracer; + @Inject + TestSpanExporter spanExporter; + + @AfterEach + void tearDown() { + spanExporter.reset(); + } + + @Test + void withSpanAttributesTest_existingSpan() { + Span span = tracer.spanBuilder("withSpanAttributesTest").startSpan(); + String result; + try (Scope scope = span.makeCurrent()) { + result = helloRouter.withSpanAttributes( + "implicit", "explicit", null, "ignore"); + } finally { + span.end(); + } + assertEquals("hello!", result); + List spanItems = spanExporter.getFinishedSpanItems(1); + SpanData spanDataOut = spanItems.get(0); + assertEquals("withSpanAttributesTest", spanDataOut.getName()); + assertEquals(INTERNAL, spanDataOut.getKind()); + assertFalse(spanDataOut.getAttributes().isEmpty(), "No attributes found"); + assertEquals("implicit", getAttribute(spanDataOut, "implicitName")); + assertEquals("explicit", getAttribute(spanDataOut, "explicitName")); + } + + @Test + void withSpanAttributesTest_noActiveSpan() { + String resultWithoutSpan = helloRouter.withSpanAttributes( + "implicit", "explicit", null, "ignore"); + assertEquals("hello!", resultWithoutSpan); + + spanExporter.getFinishedSpanItems(0); + // No span created + + String resultWithSpan = helloRouter.withSpanTakesPrecedence( + "implicit", "explicit", null, "ignore"); + assertEquals("hello!", resultWithSpan); + + // we need 1 span to make sure we don't get a false positive. + // The previous call to getFinishedSpanItems might return too early. + + List spanItems = spanExporter.getFinishedSpanItems(1); + assertEquals(1, spanItems.size()); + SpanData spanDataOut = spanItems.get(0); + assertEquals("HelloRouter.withSpanTakesPrecedence", spanDataOut.getName()); + } + + @Test + void withSpanAttributesTest_newSpan() { + String result = helloRouter.withSpanTakesPrecedence( + "implicit", "explicit", null, "ignore"); + + assertEquals("hello!", result); + List spanItems = spanExporter.getFinishedSpanItems(1); + SpanData spanDataOut = spanItems.get(0); + assertEquals("HelloRouter.withSpanTakesPrecedence", spanDataOut.getName()); + assertEquals(INTERNAL, spanDataOut.getKind()); + assertEquals(2, spanDataOut.getAttributes().size()); + assertEquals("implicit", getAttribute(spanDataOut, "implicitName")); + assertEquals("explicit", getAttribute(spanDataOut, "explicitName")); + } + + @Test + void noAttributesAdded() { + Span span = tracer.spanBuilder("noAttributesAdded").startSpan(); + String result; + try (Scope scope = span.makeCurrent()) { + result = helloRouter.noAttributesAdded( + "implicit", "explicit", null, "ignore"); + } finally { + span.end(); + } + assertEquals("hello!", result); + List spanItems = spanExporter.getFinishedSpanItems(1); + SpanData spanDataOut = spanItems.get(0); + assertEquals("noAttributesAdded", spanDataOut.getName()); + assertEquals(INTERNAL, spanDataOut.getKind()); + assertTrue(spanDataOut.getAttributes().isEmpty(), "No attributes must be present"); + } + + private static Object getAttribute(SpanData spanDataOut, String attributeName) { + return spanDataOut.getAttributes().asMap().get(AttributeKey.stringKey(attributeName)); + } + + @ApplicationScoped + public static class HelloRouter { + // mast have already an active span + @AddingSpanAttributes + public String withSpanAttributes( + @SpanAttribute String implicitName, + @SpanAttribute("explicitName") String parameter, + @SpanAttribute("nullAttribute") String nullAttribute, + String notTraced) { + + return "hello!"; + } + + @WithSpan + @AddingSpanAttributes + public String withSpanTakesPrecedence( + @SpanAttribute String implicitName, + @SpanAttribute("explicitName") String parameter, + @SpanAttribute("nullAttribute") String nullAttribute, + String notTraced) { + + return "hello!"; + } + + public String noAttributesAdded( + @SpanAttribute String implicitName, + @SpanAttribute("explicitName") String parameter, + @SpanAttribute("nullAttribute") String nullAttribute, + String notTraced) { + + return "hello!"; + } + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanInterceptorTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanInterceptorTest.java similarity index 99% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanInterceptorTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanInterceptorTest.java index 52de43f905ee1..b12624fd36912 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanInterceptorTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanInterceptorTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.interceptor; import static io.opentelemetry.api.trace.SpanKind.CLIENT; import static io.opentelemetry.api.trace.SpanKind.INTERNAL; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanLegacyInterceptorTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanLegacyInterceptorTest.java similarity index 99% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanLegacyInterceptorTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanLegacyInterceptorTest.java index b6b2eb1f4f30e..a38c18c57f626 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanLegacyInterceptorTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanLegacyInterceptorTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.interceptor; import static io.opentelemetry.api.trace.SpanKind.CLIENT; import static io.opentelemetry.api.trace.SpanKind.INTERNAL; diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/AddingSpanAttributesInterceptor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/AddingSpanAttributesInterceptor.java new file mode 100644 index 0000000000000..3eecc0f3cade5 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/AddingSpanAttributesInterceptor.java @@ -0,0 +1,87 @@ +package io.quarkus.opentelemetry.runtime.tracing.cdi; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.annotations.AddingSpanAttributes; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.api.annotation.support.ParameterAttributeNamesExtractor; +import io.quarkus.arc.ArcInvocationContext; + +/** + * Will capture the arguments annotated with {@link SpanAttribute} on methods annotated with {@link AddingSpanAttributes}. + * Will not start a Span if one is not already started. + */ +@SuppressWarnings("CdiInterceptorInspection") +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE) +public class AddingSpanAttributesInterceptor { + + private final WithSpanParameterAttributeNamesExtractor extractor; + + public AddingSpanAttributesInterceptor() { + extractor = new WithSpanParameterAttributeNamesExtractor(); + } + + @AroundInvoke + public Object span(final ArcInvocationContext invocationContext) throws Exception { + String[] extractedParameterNames = extractor.extract(invocationContext.getMethod(), + invocationContext.getMethod().getParameters()); + Object[] parameterValues = invocationContext.getParameters(); + + Span span = Span.current(); + if (span.isRecording()) { + try (Scope scope = span.makeCurrent()) { + for (int i = 0; i < extractedParameterNames.length; i++) { + if (extractedParameterNames[i] == null || parameterValues[i] == null) { + continue; + } + span.setAttribute(extractedParameterNames[i], parameterValues[i].toString()); + } + } + } + return invocationContext.proceed(); + } + + private static final class WithSpanParameterAttributeNamesExtractor implements ParameterAttributeNamesExtractor { + @Override + public String[] extract(final Method method, final Parameter[] parameters) { + String[] attributeNames = new String[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + attributeNames[i] = attributeName(parameters[i]); + } + return attributeNames; + } + + private static String attributeName(Parameter parameter) { + String value; + SpanAttribute spanAttribute = parameter.getDeclaredAnnotation(SpanAttribute.class); + if (spanAttribute == null) { + // Needed because SpanAttribute cannot be transformed + io.opentelemetry.extension.annotations.SpanAttribute legacySpanAttribute = parameter.getDeclaredAnnotation( + io.opentelemetry.extension.annotations.SpanAttribute.class); + if (legacySpanAttribute == null) { + return null; + } else { + value = legacySpanAttribute.value(); + } + } else { + value = spanAttribute.value(); + } + + if (!value.isEmpty()) { + return value; + } else if (parameter.isNamePresent()) { + return parameter.getName(); + } else { + return null; + } + } + } +}