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

AddingSpanAttributes annotation #7787

Merged
merged 12 commits into from
Apr 24, 2023
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
Comparing source compatibility of against
No changes.
+++ NEW ANNOTATION: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.annotations.AddingSpanAttributes (not serializable)
+++ CLASS FILE FORMAT VERSION: 52.0 <- n.a.
+++ NEW INTERFACE: java.lang.annotation.Annotation
+++ NEW SUPERCLASS: java.lang.Object
+++ NEW ANNOTATION: java.lang.annotation.Target
+++ NEW ELEMENT: value=java.lang.annotation.ElementType.METHOD,java.lang.annotation.ElementType.CONSTRUCTOR (+)
+++ NEW ANNOTATION: java.lang.annotation.Retention
+++ NEW ELEMENT: value=java.lang.annotation.RetentionPolicy.RUNTIME (+)
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@
package io.opentelemetry.instrumentation.api.annotation.support;

import io.opentelemetry.api.common.AttributesBuilder;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

/** Represents the bindings of method parameters to attributes of a traced method. */
interface AttributeBindings {
@@ -24,4 +26,42 @@ interface AttributeBindings {
* @param args the method arguments
*/
void apply(AttributesBuilder target, Object[] args);

/**
* Creates a binding of the parameters of the traced method to span attributes.
*
* @param method the traced method
* @return the bindings of the parameters
*/
static AttributeBindings bind(
Method method, ParameterAttributeNamesExtractor parameterAttributeNamesExtractor) {
AttributeBindings bindings = EmptyAttributeBindings.INSTANCE;

Parameter[] parameters = method.getParameters();
if (parameters.length == 0) {
return bindings;
}

String[] attributeNames = parameterAttributeNamesExtractor.extract(method, parameters);
if (attributeNames == null || attributeNames.length != parameters.length) {
return bindings;
}

for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
String attributeName = attributeNames[i];
if (attributeName == null || attributeName.isEmpty()) {
continue;
}

bindings =
new CombinedAttributeBindings(
bindings,
i,
AttributeBindingFactory.createBinding(
attributeName, parameter.getParameterizedType()));
}

return bindings;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.annotation.support;

import io.opentelemetry.api.common.AttributesBuilder;

/** AttributeBindings implementation that is able to chain multiple AttributeBindings. */
final class CombinedAttributeBindings implements AttributeBindings {
private final AttributeBindings parent;
private final int index;
private final AttributeBinding binding;

public CombinedAttributeBindings(AttributeBindings parent, int index, AttributeBinding binding) {
this.parent = parent;
this.index = index;
this.binding = binding;
}

@Override
public boolean isEmpty() {
return false;
}

@Override
public void apply(AttributesBuilder target, Object[] args) {
parent.apply(target, args);
if (args != null && args.length > index) {
Object arg = args[index];
if (arg != null) {
binding.apply(target, arg);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.annotation.support;

import io.opentelemetry.api.common.AttributesBuilder;

/** Singleton empty implementation of AttributeBindings. */
enum EmptyAttributeBindings implements AttributeBindings {
INSTANCE;

@Override
public boolean isEmpty() {
return true;
}

@Override
public void apply(AttributesBuilder target, Object[] args) {}
}
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import io.opentelemetry.instrumentation.api.internal.cache.Cache;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import javax.annotation.Nullable;

/** Extractor of {@link io.opentelemetry.api.common.Attributes} for a traced method. */
@@ -22,7 +21,7 @@ public final class MethodSpanAttributesExtractor<REQUEST, RESPONSE>
private final Cache<Method, AttributeBindings> cache;
private final ParameterAttributeNamesExtractor parameterAttributeNamesExtractor;

public static <REQUEST, RESPONSE> MethodSpanAttributesExtractor<REQUEST, RESPONSE> newInstance(
public static <REQUEST, RESPONSE> MethodSpanAttributesExtractor<REQUEST, RESPONSE> create(
MethodExtractor<REQUEST> methodExtractor,
ParameterAttributeNamesExtractor parameterAttributeNamesExtractor,
MethodArgumentsExtractor<REQUEST> methodArgumentsExtractor) {
@@ -48,7 +47,9 @@ public static <REQUEST, RESPONSE> MethodSpanAttributesExtractor<REQUEST, RESPONS
@Override
public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {
Method method = methodExtractor.extract(request);
AttributeBindings bindings = cache.computeIfAbsent(method, this::bind);
AttributeBindings bindings =
cache.computeIfAbsent(
method, (Method m) -> AttributeBindings.bind(m, parameterAttributeNamesExtractor));
if (!bindings.isEmpty()) {
Object[] args = methodArgumentsExtractor.extract(request);
bindings.apply(attributes, args);
@@ -62,82 +63,4 @@ public void onEnd(
REQUEST request,
@Nullable RESPONSE response,
@Nullable Throwable error) {}

/**
* Creates a binding of the parameters of the traced method to span attributes.
*
* @param method the traced method
* @return the bindings of the parameters
*/
private AttributeBindings bind(Method method) {
AttributeBindings bindings = EmptyAttributeBindings.INSTANCE;

Parameter[] parameters = method.getParameters();
if (parameters.length == 0) {
return bindings;
}

String[] attributeNames = parameterAttributeNamesExtractor.extract(method, parameters);
if (attributeNames.length != parameters.length) {
return bindings;
}

for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
String attributeName = attributeNames[i];
if (attributeName == null || attributeName.isEmpty()) {
continue;
}

bindings =
new CombinedAttributeBindings(
bindings,
i,
AttributeBindingFactory.createBinding(
attributeName, parameter.getParameterizedType()));
}

return bindings;
}

protected enum EmptyAttributeBindings implements AttributeBindings {
INSTANCE;

@Override
public boolean isEmpty() {
return true;
}

@Override
public void apply(AttributesBuilder target, Object[] args) {}
}

private static final class CombinedAttributeBindings implements AttributeBindings {
private final AttributeBindings parent;
private final int index;
private final AttributeBinding binding;

public CombinedAttributeBindings(
AttributeBindings parent, int index, AttributeBinding binding) {
this.parent = parent;
this.index = index;
this.binding = binding;
}

@Override
public boolean isEmpty() {
return false;
}

@Override
public void apply(AttributesBuilder target, Object[] args) {
parent.apply(target, args);
if (args != null && args.length > index) {
Object arg = args[index];
if (arg != null) {
binding.apply(target, arg);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.annotation.support;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.instrumentation.api.internal.cache.Cache;
import java.lang.reflect.Method;

/** Extractor of {@link io.opentelemetry.api.common.Attributes} for a traced method. */
public final class SpanAttributesExtractor {

private final Cache<Method, AttributeBindings> cache;
private final ParameterAttributeNamesExtractor parameterAttributeNamesExtractor;

public static SpanAttributesExtractor create(
ParameterAttributeNamesExtractor parameterAttributeNamesExtractor) {
return new SpanAttributesExtractor(parameterAttributeNamesExtractor, new MethodCache<>());
}

SpanAttributesExtractor(
ParameterAttributeNamesExtractor parameterAttributeNamesExtractor,
Cache<Method, AttributeBindings> cache) {
this.parameterAttributeNamesExtractor = parameterAttributeNamesExtractor;
this.cache = cache;
}

public Attributes extract(Method method, Object[] args) {
AttributesBuilder attributes = Attributes.builder();
AttributeBindings bindings =
cache.computeIfAbsent(
method, (Method m) -> AttributeBindings.bind(m, parameterAttributeNamesExtractor));
if (!bindings.isEmpty()) {
bindings.apply(attributes, args);
}
return attributes.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.annotations;

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

/**
* This annotation marks that an execution of this method or constructor is able to add attributes
* to the current span {@link io.opentelemetry.api.trace.Span}.
*
* <p>Application developers can use this annotation to signal OpenTelemetry auto-instrumentation
* that attributes annotated with the {@link
* io.opentelemetry.instrumentation.annotations.SpanAttribute} should be detected and added to the
* current span.
*
* <p>If no span is currently active no new span will be created, and no attributes will be
* extracted.
*
* <p>Similar to {@link
* io.opentelemetry.api.trace.Span#setAttribute(io.opentelemetry.api.common.AttributeKey,
* java.lang.Object) Span.setAttribute() } methods, if the key is already mapped to a value the old
* value is replaced by the extracted value.
*
* <p>If you are a library developer, then probably you should NOT use this annotation, because it
* is non-functional without some form of auto-instrumentation.
*/
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface AddingSpanAttributes {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.annotations;

import io.opentelemetry.api.trace.Span;

/**
* This class is not a classical test. It's just a demonstration of possible usages of {@link
* AddingSpanAttributes} annotation together with some explanations. The goal of this class is to
* serve as an early detection system for inconvenient API and unintended API breakage.
*/
@SuppressWarnings("unused")
public class AddingSpanAttributesUsageExamples {

/**
* The current {@link Span} will be updated to contain the annotated method parameters as
* attributes.
*
* @param attribute1 A span attribute with the default name of {@code attribute1}.
* @param value A span attribute with the name "attribute2".
*/
@AddingSpanAttributes
public void attributes(
@SpanAttribute String attribute1, @SpanAttribute("attribute2") long value) {}
}
Original file line number Diff line number Diff line change
@@ -23,6 +23,10 @@ dependencies {
// see the comment in opentelemetry-api-1.0.gradle for more details
compileOnly(project(":opentelemetry-ext-annotations-shaded-for-instrumenting", configuration = "shadow"))

// Used by byte-buddy but not brought in as a transitive dependency.
compileOnly("com.google.code.findbugs:annotations")
testCompileOnly("com.google.code.findbugs:annotations")

testImplementation("io.opentelemetry:opentelemetry-extension-annotations")
testImplementation(project(":instrumentation-annotations-support"))
testImplementation("net.bytebuddy:byte-buddy")
Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@ private static Instrumenter<MethodRequest, Object> createInstrumenterWithAttribu
.addAttributesExtractor(
CodeAttributesExtractor.create(MethodRequestCodeAttributesGetter.INSTANCE))
.addAttributesExtractor(
MethodSpanAttributesExtractor.newInstance(
MethodSpanAttributesExtractor.create(
MethodRequest::method,
WithSpanParameterAttributeNamesExtractor.INSTANCE,
MethodRequest::args))

This file was deleted.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -25,6 +25,10 @@ dependencies {
// see the comment in opentelemetry-api-1.0.gradle for more details
compileOnly(project(":opentelemetry-instrumentation-annotations-shaded-for-instrumenting", configuration = "shadow"))

// Used by byte-buddy but not brought in as a transitive dependency.
compileOnly("com.google.code.findbugs:annotations")
testCompileOnly("com.google.code.findbugs:annotations")

testImplementation(project(":instrumentation-annotations"))
testImplementation(project(":instrumentation-annotations-support"))
testImplementation("net.bytebuddy:byte-buddy")
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.instrumentationannotations;

import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.AnnotationSingletons.attributes;
import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
import static net.bytebuddy.matcher.ElementMatchers.hasParameters;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.whereAny;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import java.lang.reflect.Method;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.annotation.AnnotationSource;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.matcher.ElementMatcher;

public class AddingSpanAttributesInstrumentation implements TypeInstrumentation {

private final ElementMatcher.Junction<AnnotationSource> annotatedMethodMatcher;
private final ElementMatcher.Junction<MethodDescription> annotatedParametersMatcher;
// this matcher matches all methods that should be excluded from transformation
private final ElementMatcher.Junction<MethodDescription> excludedMethodsMatcher;

AddingSpanAttributesInstrumentation() {
annotatedMethodMatcher =
isAnnotatedWith(
named(
"application.io.opentelemetry.instrumentation.annotations.AddingSpanAttributes"))
// Avoid repeat extraction if method is already annotation with WithSpan
.and(
not(
isAnnotatedWith(
named(
"application.io.opentelemetry.instrumentation.annotations.WithSpan"))));
annotatedParametersMatcher =
hasParameters(
whereAny(
isAnnotatedWith(
named(
"application.io.opentelemetry.instrumentation.annotations.SpanAttribute"))));
excludedMethodsMatcher = AnnotationExcludedMethods.configureExcludedMethods();
}

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return declaresMethod(annotatedMethodMatcher);
}

@Override
public void transform(TypeTransformer transformer) {
ElementMatcher.Junction<MethodDescription> tracedMethodsWithParameters =
annotatedMethodMatcher.and(not(excludedMethodsMatcher)).and(annotatedParametersMatcher);

transformer.applyAdviceToMethod(
tracedMethodsWithParameters,
AddingSpanAttributesInstrumentation.class.getName() + "$AddingSpanAttributesAdvice");
}

public static class AddingSpanAttributesAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.Origin Method method,
@Advice.AllArguments(typing = Assigner.Typing.DYNAMIC) Object[] args) {
Span otelSpan = Java8BytecodeBridge.currentSpan();
if (otelSpan.isRecording() && otelSpan.getSpanContext().isValid()) {
otelSpan.setAllAttributes(attributes().extract(method, args));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.instrumentationannotations;

import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy;
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
import static net.bytebuddy.matcher.ElementMatchers.none;

import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig;
import io.opentelemetry.javaagent.tooling.config.MethodsConfigurationParser;
import java.util.Map;
import java.util.Set;
import net.bytebuddy.description.ByteCodeElement;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;

final class AnnotationExcludedMethods {

private static final String TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG =
"otel.instrumentation.opentelemetry-instrumentation-annotations.exclude-methods";

/*
Returns a matcher for all methods that should be excluded from auto-instrumentation by
annotation-based advices.
*/
static ElementMatcher.Junction<MethodDescription> configureExcludedMethods() {
ElementMatcher.Junction<MethodDescription> result = none();

Map<String, Set<String>> excludedMethods =
MethodsConfigurationParser.parse(
InstrumentationConfig.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG));
for (Map.Entry<String, Set<String>> entry : excludedMethods.entrySet()) {
String className = entry.getKey();
ElementMatcher.Junction<ByteCodeElement> matcher =
isDeclaredBy(ElementMatchers.named(className));

Set<String> methodNames = entry.getValue();
if (!methodNames.isEmpty()) {
matcher = matcher.and(namedOneOf(methodNames.toArray(new String[0])));
}

result = result.or(matcher);
}

return result;
}

private AnnotationExcludedMethods() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.instrumentationannotations;

import static java.util.Arrays.asList;

import application.io.opentelemetry.instrumentation.annotations.AddingSpanAttributes;
import application.io.opentelemetry.instrumentation.annotations.WithSpan;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;

/**
* Instrumentation for methods annotated with {@link WithSpan} and {@link AddingSpanAttributes}
* annotations.
*/
@AutoService(InstrumentationModule.class)
public class AnnotationInstrumentationModule extends InstrumentationModule {

public AnnotationInstrumentationModule() {
super("opentelemetry-instrumentation-annotations");
}

@Override
public int order() {
// Run first to ensure other automatic instrumentation is added after and therefore is executed
// earlier in the instrumented method and create the span to attach attributes to.
return -1000;
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(new WithSpanInstrumentation(), new AddingSpanAttributesInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -11,21 +11,23 @@
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.instrumentation.api.annotation.support.MethodSpanAttributesExtractor;
import io.opentelemetry.instrumentation.api.annotation.support.SpanAttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.util.SpanNames;
import java.lang.reflect.Method;
import java.util.logging.Logger;

public final class WithSpanSingletons {
public final class AnnotationSingletons {

private static final String INSTRUMENTATION_NAME =
"io.opentelemetry.opentelemetry-instrumentation-annotations-1.16";

private static final Logger logger = Logger.getLogger(WithSpanSingletons.class.getName());
private static final Logger logger = Logger.getLogger(AnnotationSingletons.class.getName());
private static final Instrumenter<Method, Object> INSTRUMENTER = createInstrumenter();
private static final Instrumenter<MethodRequest, Object> INSTRUMENTER_WITH_ATTRIBUTES =
createInstrumenterWithAttributes();
private static final SpanAttributesExtractor ATTRIBUTES = createAttributesExtractor();

public static Instrumenter<Method, Object> instrumenter() {
return INSTRUMENTER;
@@ -35,26 +37,36 @@ public static Instrumenter<MethodRequest, Object> instrumenterWithAttributes() {
return INSTRUMENTER_WITH_ATTRIBUTES;
}

public static SpanAttributesExtractor attributes() {
return ATTRIBUTES;
}

private static Instrumenter<Method, Object> createInstrumenter() {
return Instrumenter.builder(
GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, WithSpanSingletons::spanNameFromMethod)
GlobalOpenTelemetry.get(),
INSTRUMENTATION_NAME,
AnnotationSingletons::spanNameFromMethod)
.addAttributesExtractor(CodeAttributesExtractor.create(MethodCodeAttributesGetter.INSTANCE))
.buildInstrumenter(WithSpanSingletons::spanKindFromMethod);
.buildInstrumenter(AnnotationSingletons::spanKindFromMethod);
}

private static Instrumenter<MethodRequest, Object> createInstrumenterWithAttributes() {
return Instrumenter.builder(
GlobalOpenTelemetry.get(),
INSTRUMENTATION_NAME,
WithSpanSingletons::spanNameFromMethodRequest)
AnnotationSingletons::spanNameFromMethodRequest)
.addAttributesExtractor(
CodeAttributesExtractor.create(MethodRequestCodeAttributesGetter.INSTANCE))
.addAttributesExtractor(
MethodSpanAttributesExtractor.newInstance(
MethodSpanAttributesExtractor.create(
MethodRequest::method,
WithSpanParameterAttributeNamesExtractor.INSTANCE,
MethodRequest::args))
.buildInstrumenter(WithSpanSingletons::spanKindFromMethodRequest);
.buildInstrumenter(AnnotationSingletons::spanKindFromMethodRequest);
}

private static SpanAttributesExtractor createAttributesExtractor() {
return SpanAttributesExtractor.create(WithSpanParameterAttributeNamesExtractor.INSTANCE);
}

private static SpanKind spanKindFromMethodRequest(MethodRequest request) {
@@ -92,5 +104,5 @@ private static String spanNameFromMethod(Method method) {
return spanName;
}

private WithSpanSingletons() {}
private AnnotationSingletons() {}
}
Original file line number Diff line number Diff line change
@@ -6,15 +6,12 @@
package io.opentelemetry.javaagent.instrumentation.instrumentationannotations;

import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.WithSpanSingletons.instrumenter;
import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.WithSpanSingletons.instrumenterWithAttributes;
import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.AnnotationSingletons.instrumenter;
import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.AnnotationSingletons.instrumenterWithAttributes;
import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
import static net.bytebuddy.matcher.ElementMatchers.hasParameters;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
import static net.bytebuddy.matcher.ElementMatchers.none;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.whereAny;

@@ -23,27 +20,18 @@
import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge;
import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import io.opentelemetry.javaagent.tooling.config.MethodsConfigurationParser;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.ByteCodeElement;
import net.bytebuddy.description.annotation.AnnotationSource;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;

public class WithSpanInstrumentation implements TypeInstrumentation {

private static final String TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG =
"otel.instrumentation.opentelemetry-instrumentation-annotations.exclude-methods";

private final ElementMatcher.Junction<AnnotationSource> annotatedMethodMatcher;
private final ElementMatcher.Junction<MethodDescription> annotatedParametersMatcher;
// this matcher matches all methods that should be excluded from transformation
@@ -58,7 +46,7 @@ public class WithSpanInstrumentation implements TypeInstrumentation {
isAnnotatedWith(
named(
"application.io.opentelemetry.instrumentation.annotations.SpanAttribute"))));
excludedMethodsMatcher = configureExcludedMethods();
excludedMethodsMatcher = AnnotationExcludedMethods.configureExcludedMethods();
}

@Override
@@ -92,32 +80,6 @@ public void transform(TypeTransformer transformer) {
WithSpanInstrumentation.class.getName() + "$WithSpanAttributesAdvice");
}

/*
Returns a matcher for all methods that should be excluded from auto-instrumentation by
annotation-based advices.
*/
static ElementMatcher.Junction<MethodDescription> configureExcludedMethods() {
ElementMatcher.Junction<MethodDescription> result = none();

Map<String, Set<String>> excludedMethods =
MethodsConfigurationParser.parse(
InstrumentationConfig.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG));
for (Map.Entry<String, Set<String>> entry : excludedMethods.entrySet()) {
String className = entry.getKey();
ElementMatcher.Junction<ByteCodeElement> matcher =
isDeclaredBy(ElementMatchers.named(className));

Set<String> methodNames = entry.getValue();
if (!methodNames.isEmpty()) {
matcher = matcher.and(namedOneOf(methodNames.toArray(new String[0])));
}

result = result.or(matcher);
}

return result;
}

@SuppressWarnings("unused")
public static class WithSpanAdvice {

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.test.annotation;

import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static org.assertj.core.api.Assertions.entry;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanId;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

class AddingSpanAttributesInstrumentationTest {

@RegisterExtension
public static final AgentInstrumentationExtension testing =
AgentInstrumentationExtension.create();

@Test
void captureAttributesInNewSpan() throws Exception {

testing.runWithSpan(
"root",
() ->
new ExtractAttributesUsingAddingSpanAttributes()
.withSpanTakesPrecedence("foo", "bar", null, "baz"));

assertThat(testing.waitForTraces(1))
.satisfiesExactly(
trace ->
assertThat(trace)
.satisfiesExactly(
span ->
assertThat(span)
.hasName("root")
.hasKind(SpanKind.INTERNAL)
.hasParentSpanId(SpanId.getInvalid()),
span ->
assertThat(span)
.hasName(
"ExtractAttributesUsingAddingSpanAttributes.withSpanTakesPrecedence")
.hasKind(SpanKind.INTERNAL)
.hasParentSpanId(trace.get(0).getSpanId())
.hasAttributesSatisfying(
attributes ->
assertThat(attributes)
.containsOnly(
entry(
SemanticAttributes.CODE_NAMESPACE,
ExtractAttributesUsingAddingSpanAttributes.class
.getName()),
entry(
SemanticAttributes.CODE_FUNCTION,
"withSpanTakesPrecedence"),
entry(
AttributeKey.stringKey("implicitName"), "foo"),
entry(
AttributeKey.stringKey("explicitName"),
"bar")))));
}

@Test
void captureAttributesInCurrentSpan() throws Exception {

testing.runWithSpan(
"root",
() ->
new ExtractAttributesUsingAddingSpanAttributes()
.withSpanAttributes("foo", "bar", null, "baz"));

assertThat(testing.waitForTraces(1))
.satisfiesExactly(
trace ->
assertThat(trace)
.satisfiesExactly(
span ->
assertThat(span)
.hasName("root")
.hasKind(SpanKind.INTERNAL)
.hasParentSpanId(SpanId.getInvalid())
.hasAttributesSatisfying(
attributes ->
assertThat(attributes)
.containsOnly(
entry(
AttributeKey.stringKey("implicitName"), "foo"),
entry(
AttributeKey.stringKey("explicitName"),
"bar")))));
}

@Test
void noExistingSpan() throws Exception {

new ExtractAttributesUsingAddingSpanAttributes().withSpanAttributes("foo", "bar", null, "baz");

assertThat(testing.waitForTraces(0));
}

@Test
void overwriteAttributes() throws Exception {

testing.runWithSpan(
"root",
() -> {
Span.current().setAttribute("implicitName", "willbegone");
Span.current().setAttribute("keep", "willbekept");
new ExtractAttributesUsingAddingSpanAttributes()
.withSpanAttributes("foo", "bar", null, "baz");
});

assertThat(testing.waitForTraces(1))
.satisfiesExactly(
trace ->
assertThat(trace)
.satisfiesExactly(
span ->
assertThat(span)
.hasName("root")
.hasKind(SpanKind.INTERNAL)
.hasParentSpanId(SpanId.getInvalid())
.hasAttributesSatisfying(
attributes ->
assertThat(attributes)
.containsOnly(
entry(AttributeKey.stringKey("keep"), "willbekept"),
entry(
AttributeKey.stringKey("implicitName"), "foo"),
entry(
AttributeKey.stringKey("explicitName"),
"bar")))));
}

@Test
void multiMethodOverwriteAttributes() throws Exception {

testing.runWithSpan(
"root",
() -> {
Span.current().setAttribute("implicitName", "willbegone");
Span.current().setAttribute("keep", "willbekept");
new ExtractAttributesUsingAddingSpanAttributes()
.withSpanAttributesParent("parentbegone", "parentbegone", null, "parentbegone");
});

assertThat(testing.waitForTraces(1))
.satisfiesExactly(
trace ->
assertThat(trace)
.satisfiesExactly(
span ->
assertThat(span)
.hasName("root")
.hasKind(SpanKind.INTERNAL)
.hasParentSpanId(SpanId.getInvalid())
.hasAttributesSatisfying(
attributes ->
assertThat(attributes)
.containsOnly(
entry(AttributeKey.stringKey("keep"), "willbekept"),
entry(
AttributeKey.stringKey("implicitName"), "foo"),
entry(
AttributeKey.stringKey("explicitName"),
"bar")))));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.test.annotation;

import io.opentelemetry.instrumentation.annotations.AddingSpanAttributes;
import io.opentelemetry.instrumentation.annotations.SpanAttribute;
import io.opentelemetry.instrumentation.annotations.WithSpan;

public class ExtractAttributesUsingAddingSpanAttributes {

@AddingSpanAttributes
public String withSpanAttributes(
@SpanAttribute String implicitName,
@SpanAttribute("explicitName") String parameter,
@SpanAttribute("nullAttribute") String nullAttribute,
String notTraced) {

return "hello!";
}

@AddingSpanAttributes
public String withSpanAttributesParent(
@SpanAttribute String implicitName,
@SpanAttribute("explicitName") String parameter,
@SpanAttribute("nullAttribute") String nullAttribute,
String notTraced) {

return withSpanAttributes("foo", "bar", null, "baz");
}

@WithSpan
@AddingSpanAttributes
public String withSpanTakesPrecedence(
@SpanAttribute String implicitName,
@SpanAttribute("explicitName") String parameter,
@SpanAttribute("nullAttribute") String nullAttribute,
String notTraced) {

return "hello!";
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@ abstract class WithSpanAspect {
.addAttributesExtractor(
CodeAttributesExtractor.create(JointPointCodeAttributesExtractor.INSTANCE))
.addAttributesExtractor(
MethodSpanAttributesExtractor.newInstance(
MethodSpanAttributesExtractor.create(
JoinPointRequest::method,
parameterAttributeNamesExtractor,
JoinPointRequest::args))