Skip to content

Commit

Permalink
Support for AddingSpanAttributes
Browse files Browse the repository at this point in the history
  • Loading branch information
brunobat committed Oct 3, 2023
1 parent 899389a commit 3d1d7ad
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 6 deletions.
11 changes: 10 additions & 1 deletion docs/src/main/asciidoc/opentelemetry.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -523,6 +527,11 @@ class SpanBean {
void spanArgs(@SpanAttribute(value = "arg") String arg) {
}
@AddingSpanAttributes
void addArgumentToExistingSpan(@SpanAttribute(value = "arg") String arg) {
}
}
----

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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<AnnotationInstance> 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
Expand Down Expand Up @@ -168,11 +182,15 @@ void registerWithSpan(
new InterceptorBindingRegistrar() {
@Override
public List<InterceptorBinding> 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
Expand Down Expand Up @@ -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();
}));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<SpanData> 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<SpanData> 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<SpanData> 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<SpanData> 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!";
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}

0 comments on commit 3d1d7ad

Please sign in to comment.