From d1b7356ffe668f9e3f773c05c5e7da96a41e2123 Mon Sep 17 00:00:00 2001 From: sfriberg Date: Mon, 24 Apr 2023 10:58:28 -0500 Subject: [PATCH] AddingSpanAttributes annotation (#7787) Co-authored-by: Mateusz Rzeszutek --- ...ntelemetry-instrumentation-annotations.txt | 9 +- .../annotation/support/AttributeBindings.java | 40 ++ .../support/CombinedAttributeBindings.java | 37 ++ .../support/EmptyAttributeBindings.java | 21 + .../MethodSpanAttributesExtractor.java | 85 +-- .../support/SpanAttributesExtractor.java | 41 ++ .../annotations/AddingSpanAttributes.java | 35 ++ .../AddingSpanAttributesUsageExamples.java | 28 + .../javaagent/build.gradle.kts | 4 + .../WithSpanSingletons.java | 2 +- .../groovy/WithSpanInstrumentationTest.groovy | 441 -------------- .../WithSpanInstrumentationTest.java | 565 ++++++++++++++++++ .../javaagent/build.gradle.kts | 4 + .../AddingSpanAttributesInstrumentation.java | 82 +++ .../AnnotationExcludedMethods.java | 53 ++ .../AnnotationInstrumentationModule.java | 39 ++ ...gletons.java => AnnotationSingletons.java} | 28 +- .../WithSpanInstrumentation.java | 44 +- .../WithSpanInstrumentationModule.java | 28 - .../groovy/WithSpanInstrumentationTest.groovy | 441 -------------- ...dingSpanAttributesInstrumentationTest.java | 174 ++++++ ...ctAttributesUsingAddingSpanAttributes.java | 44 ++ .../WithSpanInstrumentationTest.java | 562 +++++++++++++++++ .../autoconfigure/aspects/WithSpanAspect.java | 2 +- 24 files changed, 1766 insertions(+), 1043 deletions(-) create mode 100644 instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/CombinedAttributeBindings.java create mode 100644 instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/EmptyAttributeBindings.java create mode 100644 instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/SpanAttributesExtractor.java create mode 100644 instrumentation-annotations/src/main/java/io/opentelemetry/instrumentation/annotations/AddingSpanAttributes.java create mode 100644 instrumentation-annotations/src/test/java/io/opentelemetry/instrumentation/annotations/AddingSpanAttributesUsageExamples.java delete mode 100644 instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy create mode 100644 instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java create mode 100644 instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AddingSpanAttributesInstrumentation.java create mode 100644 instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationExcludedMethods.java create mode 100644 instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationInstrumentationModule.java rename instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/{WithSpanSingletons.java => AnnotationSingletons.java} (76%) delete mode 100644 instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentationModule.java delete mode 100644 instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy create mode 100644 instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/AddingSpanAttributesInstrumentationTest.java create mode 100644 instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/ExtractAttributesUsingAddingSpanAttributes.java create mode 100644 instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt index df26146497bf..df5fcf512827 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt @@ -1,2 +1,9 @@ Comparing source compatibility of against -No changes. \ No newline at end of file ++++ 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 (+) diff --git a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/AttributeBindings.java b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/AttributeBindings.java index aa309dc1a4f1..740555fe2406 100644 --- a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/AttributeBindings.java +++ b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/AttributeBindings.java @@ -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; + } } diff --git a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/CombinedAttributeBindings.java b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/CombinedAttributeBindings.java new file mode 100644 index 000000000000..961e85da677e --- /dev/null +++ b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/CombinedAttributeBindings.java @@ -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); + } + } + } +} diff --git a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/EmptyAttributeBindings.java b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/EmptyAttributeBindings.java new file mode 100644 index 000000000000..c7923814a5ad --- /dev/null +++ b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/EmptyAttributeBindings.java @@ -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) {} +} diff --git a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/MethodSpanAttributesExtractor.java b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/MethodSpanAttributesExtractor.java index 4e143eecf1c3..6f7760b08f51 100644 --- a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/MethodSpanAttributesExtractor.java +++ b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/MethodSpanAttributesExtractor.java @@ -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 private final Cache cache; private final ParameterAttributeNamesExtractor parameterAttributeNamesExtractor; - public static MethodSpanAttributesExtractor newInstance( + public static MethodSpanAttributesExtractor create( MethodExtractor methodExtractor, ParameterAttributeNamesExtractor parameterAttributeNamesExtractor, MethodArgumentsExtractor methodArgumentsExtractor) { @@ -48,7 +47,9 @@ public static MethodSpanAttributesExtractor 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); - } - } - } - } } diff --git a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/SpanAttributesExtractor.java b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/SpanAttributesExtractor.java new file mode 100644 index 000000000000..3e16e9c70ca7 --- /dev/null +++ b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/SpanAttributesExtractor.java @@ -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 cache; + private final ParameterAttributeNamesExtractor parameterAttributeNamesExtractor; + + public static SpanAttributesExtractor create( + ParameterAttributeNamesExtractor parameterAttributeNamesExtractor) { + return new SpanAttributesExtractor(parameterAttributeNamesExtractor, new MethodCache<>()); + } + + SpanAttributesExtractor( + ParameterAttributeNamesExtractor parameterAttributeNamesExtractor, + Cache 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(); + } +} diff --git a/instrumentation-annotations/src/main/java/io/opentelemetry/instrumentation/annotations/AddingSpanAttributes.java b/instrumentation-annotations/src/main/java/io/opentelemetry/instrumentation/annotations/AddingSpanAttributes.java new file mode 100644 index 000000000000..73fccf42df8f --- /dev/null +++ b/instrumentation-annotations/src/main/java/io/opentelemetry/instrumentation/annotations/AddingSpanAttributes.java @@ -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}. + * + *

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. + * + *

If no span is currently active no new span will be created, and no attributes will be + * extracted. + * + *

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. + * + *

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 {} diff --git a/instrumentation-annotations/src/test/java/io/opentelemetry/instrumentation/annotations/AddingSpanAttributesUsageExamples.java b/instrumentation-annotations/src/test/java/io/opentelemetry/instrumentation/annotations/AddingSpanAttributesUsageExamples.java new file mode 100644 index 000000000000..48a25dde72e3 --- /dev/null +++ b/instrumentation-annotations/src/test/java/io/opentelemetry/instrumentation/annotations/AddingSpanAttributesUsageExamples.java @@ -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) {} +} diff --git a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/build.gradle.kts b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/build.gradle.kts index 9abbae283ebb..173c3bfaa5af 100644 --- a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/build.gradle.kts +++ b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/build.gradle.kts @@ -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") diff --git a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanSingletons.java b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanSingletons.java index 836ae98f1344..86479423ce85 100644 --- a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanSingletons.java +++ b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanSingletons.java @@ -50,7 +50,7 @@ private static Instrumenter createInstrumenterWithAttribu .addAttributesExtractor( CodeAttributesExtractor.create(MethodRequestCodeAttributesGetter.INSTANCE)) .addAttributesExtractor( - MethodSpanAttributesExtractor.newInstance( + MethodSpanAttributesExtractor.create( MethodRequest::method, WithSpanParameterAttributeNamesExtractor.INSTANCE, MethodRequest::args)) diff --git a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy deleted file mode 100644 index fd5fd3264f0e..000000000000 --- a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy +++ /dev/null @@ -1,441 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import io.opentelemetry.test.annotation.TracedWithSpan -import net.bytebuddy.ByteBuddy -import net.bytebuddy.ClassFileVersion -import net.bytebuddy.asm.MemberAttributeExtension -import net.bytebuddy.description.annotation.AnnotationDescription -import net.bytebuddy.implementation.MethodDelegation -import net.bytebuddy.implementation.bind.annotation.RuntimeType -import net.bytebuddy.implementation.bind.annotation.This -import net.bytebuddy.matcher.ElementMatchers - -import java.lang.reflect.Modifier -import java.util.concurrent.CompletableFuture - -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.SpanKind.PRODUCER -import static io.opentelemetry.api.trace.SpanKind.SERVER -import static io.opentelemetry.api.trace.StatusCode.ERROR - -/** - * This test verifies that auto instrumentation supports the - * {@link io.opentelemetry.extension.annotations.WithSpan} annotation. - */ -@SuppressWarnings("deprecation") // testing instrumentation of deprecated class -class WithSpanInstrumentationTest extends AgentInstrumentationSpecification { - - def "should derive automatic name"() { - setup: - new TracedWithSpan().otel() - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.otel" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "otel" - } - } - } - } - } - - def "should take span name from annotation"() { - setup: - new TracedWithSpan().namedOtel() - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "manualName" - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "namedOtel" - } - } - } - } - } - - def "should take span kind from annotation"() { - setup: - new TracedWithSpan().someKind() - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.someKind" - kind PRODUCER - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "someKind" - } - } - } - } - } - - def "should capture multiple spans"() { - setup: - new TracedWithSpan().server() - - expect: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "TracedWithSpan.server" - kind SERVER - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "server" - } - } - span(1) { - name "TracedWithSpan.otel" - childOf span(0) - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "otel" - } - } - } - } - } - - def "should ignore method excluded by trace.annotated.methods.exclude configuration"() { - setup: - new TracedWithSpan().ignored() - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - } - - def "should capture span for already completed CompletionStage"() { - setup: - def future = CompletableFuture.completedFuture("Done") - new TracedWithSpan().completionStage(future) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completionStage" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completionStage" - } - } - } - } - } - - def "should capture span for eventually completed CompletionStage"() { - setup: - def future = new CompletableFuture() - new TracedWithSpan().completionStage(future) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - future.complete("Done") - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completionStage" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completionStage" - } - } - } - } - } - - def "should capture span for already exceptionally completed CompletionStage"() { - setup: - def future = new CompletableFuture() - future.completeExceptionally(new IllegalArgumentException("Boom")) - new TracedWithSpan().completionStage(future) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completionStage" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completionStage" - } - } - } - } - } - - def "should capture span for eventually exceptionally completed CompletionStage"() { - setup: - def future = new CompletableFuture() - new TracedWithSpan().completionStage(future) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - future.completeExceptionally(new IllegalArgumentException("Boom")) - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completionStage" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completionStage" - } - } - } - } - } - - def "should capture span for null CompletionStage"() { - setup: - new TracedWithSpan().completionStage(null) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completionStage" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completionStage" - } - } - } - } - } - - def "should capture span for already completed CompletableFuture"() { - setup: - def future = CompletableFuture.completedFuture("Done") - new TracedWithSpan().completableFuture(future) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completableFuture" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completableFuture" - } - } - } - } - } - - def "should capture span for eventually completed CompletableFuture"() { - setup: - def future = new CompletableFuture() - new TracedWithSpan().completableFuture(future) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - future.complete("Done") - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completableFuture" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completableFuture" - } - } - } - } - } - - def "should capture span for already exceptionally completed CompletableFuture"() { - setup: - def future = new CompletableFuture() - future.completeExceptionally(new IllegalArgumentException("Boom")) - new TracedWithSpan().completableFuture(future) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completableFuture" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completableFuture" - } - } - } - } - } - - def "should capture span for eventually exceptionally completed CompletableFuture"() { - setup: - def future = new CompletableFuture() - new TracedWithSpan().completableFuture(future) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - future.completeExceptionally(new IllegalArgumentException("Boom")) - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completableFuture" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completableFuture" - } - } - } - } - } - - def "should capture span for null CompletableFuture"() { - setup: - new TracedWithSpan().completableFuture(null) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completableFuture" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completableFuture" - } - } - } - } - } - - def "instrument java6 class"() { - setup: - /* - class GeneratedJava6TestClass implements Runnable { - @WithSpan - public void run() { - runWithSpan("intercept", {}) - } - } - */ - Class generatedClass = new ByteBuddy(ClassFileVersion.JAVA_V6) - .subclass(Object) - .name("GeneratedJava6TestClass") - .implement(Runnable) - .defineMethod("run", void.class, Modifier.PUBLIC).intercept(MethodDelegation.to(new Object() { - @RuntimeType - void intercept(@This Object o) { - runWithSpan("intercept", {}) - } - })) - .visit(new MemberAttributeExtension.ForMethod() - .annotateMethod(AnnotationDescription.Builder.ofType(io.opentelemetry.extension.annotations.WithSpan).build()) - .on(ElementMatchers.named("run"))) - .make() - .load(getClass().getClassLoader()) - .getLoaded() - - Runnable runnable = (Runnable) generatedClass.getConstructor().newInstance() - runnable.run() - - expect: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "GeneratedJava6TestClass.run" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" "GeneratedJava6TestClass" - "$SemanticAttributes.CODE_FUNCTION" "run" - } - } - span(1) { - name "intercept" - kind INTERNAL - childOf(span(0)) - attributes { - } - } - } - } - } - - def "should capture attributes"() { - setup: - new TracedWithSpan().withSpanAttributes("foo", "bar", null, "baz") - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.withSpanAttributes" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "withSpanAttributes" - "implicitName" "foo" - "explicitName" "bar" - } - } - } - } - } -} diff --git a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java new file mode 100644 index 000000000000..9736f67c387a --- /dev/null +++ b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java @@ -0,0 +1,565 @@ +/* + * 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.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.lang.reflect.Modifier; +import java.util.concurrent.CompletableFuture; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.ClassFileVersion; +import net.bytebuddy.asm.MemberAttributeExtension; +import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.RuntimeType; +import net.bytebuddy.implementation.bind.annotation.This; +import net.bytebuddy.matcher.ElementMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressWarnings("deprecation") // testing instrumentation of deprecated class +class WithSpanInstrumentationTest { + + @RegisterExtension + public static final AgentInstrumentationExtension testing = + AgentInstrumentationExtension.create(); + + @Test + void deriveAutomaticName() throws Exception { + + new TracedWithSpan().otel(); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.otel") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry(SemanticAttributes.CODE_FUNCTION, "otel"))))); + } + + @Test + void manualName() throws Exception { + + new TracedWithSpan().namedOtel(); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("manualName") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "namedOtel"))))); + } + + @Test + void manualKind() throws Exception { + + new TracedWithSpan().someKind(); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.someKind") + .hasKind(SpanKind.PRODUCER) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "someKind"))))); + } + + @Test + void multipleSpans() throws Exception { + + new TracedWithSpan().server(); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.server") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry(SemanticAttributes.CODE_FUNCTION, "server"))), + span -> + assertThat(span) + .hasName("TracedWithSpan.otel") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.get(0)) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry(SemanticAttributes.CODE_FUNCTION, "otel"))))); + } + + @Test + void excludedMethod() throws Exception { + + new TracedWithSpan().ignored(); + + Thread.sleep(500); // sleep a bit just to make sure no span is captured + assertThat(testing.waitForTraces(0)); + } + + @Test + void completedCompletionStage() throws Exception { + + CompletableFuture future = CompletableFuture.completedFuture("Done"); + new TracedWithSpan().completionStage(future); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completionStage") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completionStage"))))); + } + + @Test + void exceptionallyCompletedCompletionStage() throws Exception { + + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new IllegalArgumentException("Boom")); + new TracedWithSpan().completionStage(future); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completionStage") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.error()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completionStage"))))); + } + + @Test + void nullCompletionStage() throws Exception { + + new TracedWithSpan().completionStage(null); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completionStage") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completionStage"))))); + } + + @Test + void completingCompletionStage() throws Exception { + + CompletableFuture future = new CompletableFuture<>(); + new TracedWithSpan().completionStage(future); + + Thread.sleep(500); // sleep a bit just to make sure no span is captured + assertThat(testing.waitForTraces(0)); + + future.complete("Done"); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completionStage") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completionStage"))))); + } + + @Test + void exceptionallyCompletingCompletionStage() throws Exception { + + CompletableFuture future = new CompletableFuture<>(); + new TracedWithSpan().completionStage(future); + + Thread.sleep(500); // sleep a bit just to make sure no span is captured + assertThat(testing.waitForTraces(0)); + + future.completeExceptionally(new IllegalArgumentException("Boom")); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completionStage") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.error()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completionStage"))))); + } + + @Test + void completedCompletableFuture() throws Exception { + + CompletableFuture future = CompletableFuture.completedFuture("Done"); + new TracedWithSpan().completableFuture(future); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completableFuture") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completableFuture"))))); + } + + @Test + void exceptionallyCompletedCompletableFuture() throws Exception { + + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new IllegalArgumentException("Boom")); + new TracedWithSpan().completableFuture(future); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completableFuture") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.error()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completableFuture"))))); + } + + @Test + void nullCompletableFuture() throws Exception { + + new TracedWithSpan().completableFuture(null); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completableFuture") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completableFuture"))))); + } + + @Test + void completingCompletableFuture() throws Exception { + + CompletableFuture future = new CompletableFuture<>(); + new TracedWithSpan().completableFuture(future); + + Thread.sleep(500); // sleep a bit just to make sure no span is captured + assertThat(testing.waitForTraces(0)); + + future.complete("Done"); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completableFuture") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completableFuture"))))); + } + + @Test + void exceptionallyCompletingCompletableFuture() throws Exception { + + CompletableFuture future = new CompletableFuture<>(); + new TracedWithSpan().completableFuture(future); + + Thread.sleep(500); // sleep a bit just to make sure no span is captured + assertThat(testing.waitForTraces(0)); + + future.completeExceptionally(new IllegalArgumentException("Boom")); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completableFuture") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.error()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completableFuture"))))); + } + + @Test + void captureAttributes() throws Exception { + + new TracedWithSpan().withSpanAttributes("foo", "bar", null, "baz"); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.withSpanAttributes") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "withSpanAttributes"), + entry( + AttributeKey.stringKey("implicitName"), "foo"), + entry( + AttributeKey.stringKey("explicitName"), + "bar"))))); + } + + // Needs to be public for ByteBuddy + public static class Intercept { + @RuntimeType + public void intercept(@This Object o) { + testing.runWithSpan("intercept", () -> {}); + } + } + + @Test + void java6Class() throws Exception { + /* + class GeneratedJava6TestClass implements Runnable { + @WithSpan + public void run() { + testing.runWithSpan("intercept", () -> {}); + } + } + */ + Class generatedClass = + new ByteBuddy(ClassFileVersion.JAVA_V6) + .subclass(Object.class) + .name("GeneratedJava6TestClass") + .implement(Runnable.class) + .defineMethod("run", void.class, Modifier.PUBLIC) + .intercept(MethodDelegation.to(new Intercept())) + .visit( + new MemberAttributeExtension.ForMethod() + .annotateMethod( + AnnotationDescription.Builder.ofType( + io.opentelemetry.extension.annotations.WithSpan.class) + .build()) + .on(ElementMatchers.named("run"))) + .make() + .load(getClass().getClassLoader()) + .getLoaded(); + + Runnable runnable = (Runnable) generatedClass.getConstructor().newInstance(); + runnable.run(); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("GeneratedJava6TestClass.run") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + "GeneratedJava6TestClass"), + entry(SemanticAttributes.CODE_FUNCTION, "run"))), + span -> + assertThat(span) + .hasName("intercept") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(trace.get(0).getSpanId()) + .hasAttributesSatisfying( + attributes -> assertThat(attributes).isEmpty()))); + } +} diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/build.gradle.kts b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/build.gradle.kts index d6e18f44926f..6ea1548e4354 100644 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/build.gradle.kts +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/build.gradle.kts @@ -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") diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AddingSpanAttributesInstrumentation.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AddingSpanAttributesInstrumentation.java new file mode 100644 index 000000000000..e6b47276559a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AddingSpanAttributesInstrumentation.java @@ -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 annotatedMethodMatcher; + private final ElementMatcher.Junction annotatedParametersMatcher; + // this matcher matches all methods that should be excluded from transformation + private final ElementMatcher.Junction 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 typeMatcher() { + return declaresMethod(annotatedMethodMatcher); + } + + @Override + public void transform(TypeTransformer transformer) { + ElementMatcher.Junction 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)); + } + } + } +} diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationExcludedMethods.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationExcludedMethods.java new file mode 100644 index 000000000000..264d83390246 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationExcludedMethods.java @@ -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 configureExcludedMethods() { + ElementMatcher.Junction result = none(); + + Map> excludedMethods = + MethodsConfigurationParser.parse( + InstrumentationConfig.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG)); + for (Map.Entry> entry : excludedMethods.entrySet()) { + String className = entry.getKey(); + ElementMatcher.Junction matcher = + isDeclaredBy(ElementMatchers.named(className)); + + Set methodNames = entry.getValue(); + if (!methodNames.isEmpty()) { + matcher = matcher.and(namedOneOf(methodNames.toArray(new String[0]))); + } + + result = result.or(matcher); + } + + return result; + } + + private AnnotationExcludedMethods() {} +} diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationInstrumentationModule.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationInstrumentationModule.java new file mode 100644 index 000000000000..c497897d58d3 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationInstrumentationModule.java @@ -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 typeInstrumentations() { + return asList(new WithSpanInstrumentation(), new AddingSpanAttributesInstrumentation()); + } +} diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanSingletons.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationSingletons.java similarity index 76% rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanSingletons.java rename to instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationSingletons.java index c0fd5bd7f63b..ce2061c4e4bf 100644 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanSingletons.java +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationSingletons.java @@ -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 INSTRUMENTER = createInstrumenter(); private static final Instrumenter INSTRUMENTER_WITH_ATTRIBUTES = createInstrumenterWithAttributes(); + private static final SpanAttributesExtractor ATTRIBUTES = createAttributesExtractor(); public static Instrumenter instrumenter() { return INSTRUMENTER; @@ -35,26 +37,36 @@ public static Instrumenter instrumenterWithAttributes() { return INSTRUMENTER_WITH_ATTRIBUTES; } + public static SpanAttributesExtractor attributes() { + return ATTRIBUTES; + } + private static Instrumenter 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 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() {} } diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentation.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentation.java index 02b104beb96e..a0d5afde6c7e 100644 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentation.java +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentation.java @@ -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 annotatedMethodMatcher; private final ElementMatcher.Junction 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 configureExcludedMethods() { - ElementMatcher.Junction result = none(); - - Map> excludedMethods = - MethodsConfigurationParser.parse( - InstrumentationConfig.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG)); - for (Map.Entry> entry : excludedMethods.entrySet()) { - String className = entry.getKey(); - ElementMatcher.Junction matcher = - isDeclaredBy(ElementMatchers.named(className)); - - Set 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 { diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentationModule.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentationModule.java deleted file mode 100644 index 99d485452a6c..000000000000 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentationModule.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.instrumentationannotations; - -import static java.util.Collections.singletonList; - -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} annotation. */ -@AutoService(InstrumentationModule.class) -public class WithSpanInstrumentationModule extends InstrumentationModule { - - public WithSpanInstrumentationModule() { - super("opentelemetry-instrumentation-annotations"); - } - - @Override - public List typeInstrumentations() { - return singletonList(new WithSpanInstrumentation()); - } -} diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy deleted file mode 100644 index 8fb31a938889..000000000000 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy +++ /dev/null @@ -1,441 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.annotations.WithSpan -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import io.opentelemetry.test.annotation.TracedWithSpan -import net.bytebuddy.ByteBuddy -import net.bytebuddy.ClassFileVersion -import net.bytebuddy.asm.MemberAttributeExtension -import net.bytebuddy.description.annotation.AnnotationDescription -import net.bytebuddy.implementation.MethodDelegation -import net.bytebuddy.implementation.bind.annotation.RuntimeType -import net.bytebuddy.implementation.bind.annotation.This -import net.bytebuddy.matcher.ElementMatchers - -import java.lang.reflect.Modifier -import java.util.concurrent.CompletableFuture - -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.SpanKind.PRODUCER -import static io.opentelemetry.api.trace.SpanKind.SERVER -import static io.opentelemetry.api.trace.StatusCode.ERROR - -/** - * This test verifies that auto instrumentation supports the - * {@link io.opentelemetry.instrumentation.annotations.WithSpan} annotation. - */ -class WithSpanInstrumentationTest extends AgentInstrumentationSpecification { - - def "should derive automatic name"() { - setup: - new TracedWithSpan().otel() - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.otel" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "otel" - } - } - } - } - } - - def "should take span name from annotation"() { - setup: - new TracedWithSpan().namedOtel() - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "manualName" - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "namedOtel" - } - } - } - } - } - - def "should take span kind from annotation"() { - setup: - new TracedWithSpan().someKind() - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.someKind" - kind PRODUCER - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "someKind" - } - } - } - } - } - - def "should capture multiple spans"() { - setup: - new TracedWithSpan().server() - - expect: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "TracedWithSpan.server" - kind SERVER - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "server" - } - } - span(1) { - name "TracedWithSpan.otel" - childOf span(0) - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "otel" - } - } - } - } - } - - def "should ignore method excluded by trace.annotated.methods.exclude configuration"() { - setup: - new TracedWithSpan().ignored() - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - } - - def "should capture span for already completed CompletionStage"() { - setup: - def future = CompletableFuture.completedFuture("Done") - new TracedWithSpan().completionStage(future) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completionStage" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completionStage" - } - } - } - } - } - - def "should capture span for eventually completed CompletionStage"() { - setup: - def future = new CompletableFuture() - new TracedWithSpan().completionStage(future) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - future.complete("Done") - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completionStage" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completionStage" - } - } - } - } - } - - def "should capture span for already exceptionally completed CompletionStage"() { - setup: - def future = new CompletableFuture() - future.completeExceptionally(new IllegalArgumentException("Boom")) - new TracedWithSpan().completionStage(future) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completionStage" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completionStage" - } - } - } - } - } - - def "should capture span for eventually exceptionally completed CompletionStage"() { - setup: - def future = new CompletableFuture() - new TracedWithSpan().completionStage(future) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - future.completeExceptionally(new IllegalArgumentException("Boom")) - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completionStage" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completionStage" - } - } - } - } - } - - def "should capture span for null CompletionStage"() { - setup: - new TracedWithSpan().completionStage(null) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completionStage" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completionStage" - } - } - } - } - } - - def "should capture span for already completed CompletableFuture"() { - setup: - def future = CompletableFuture.completedFuture("Done") - new TracedWithSpan().completableFuture(future) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completableFuture" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completableFuture" - } - } - } - } - } - - def "should capture span for eventually completed CompletableFuture"() { - setup: - def future = new CompletableFuture() - new TracedWithSpan().completableFuture(future) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - future.complete("Done") - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completableFuture" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completableFuture" - } - } - } - } - } - - def "should capture span for already exceptionally completed CompletableFuture"() { - setup: - def future = new CompletableFuture() - future.completeExceptionally(new IllegalArgumentException("Boom")) - new TracedWithSpan().completableFuture(future) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completableFuture" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completableFuture" - } - } - } - } - } - - def "should capture span for eventually exceptionally completed CompletableFuture"() { - setup: - def future = new CompletableFuture() - new TracedWithSpan().completableFuture(future) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - future.completeExceptionally(new IllegalArgumentException("Boom")) - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completableFuture" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completableFuture" - } - } - } - } - } - - def "should capture span for null CompletableFuture"() { - setup: - new TracedWithSpan().completableFuture(null) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completableFuture" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "completableFuture" - } - } - } - } - } - - def "instrument java6 class"() { - setup: - /* - class GeneratedJava6TestClass implements Runnable { - @WithSpan - public void run() { - runWithSpan("intercept", {}) - } - } - */ - Class generatedClass = new ByteBuddy(ClassFileVersion.JAVA_V6) - .subclass(Object) - .name("GeneratedJava6TestClass") - .implement(Runnable) - .defineMethod("run", void.class, Modifier.PUBLIC).intercept(MethodDelegation.to(new Object() { - @RuntimeType - void intercept(@This Object o) { - runWithSpan("intercept", {}) - } - })) - .visit(new MemberAttributeExtension.ForMethod() - .annotateMethod(AnnotationDescription.Builder.ofType(WithSpan).build()) - .on(ElementMatchers.named("run"))) - .make() - .load(getClass().getClassLoader()) - .getLoaded() - - Runnable runnable = (Runnable) generatedClass.getConstructor().newInstance() - runnable.run() - - expect: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "GeneratedJava6TestClass.run" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" "GeneratedJava6TestClass" - "$SemanticAttributes.CODE_FUNCTION" "run" - } - } - span(1) { - name "intercept" - kind INTERNAL - childOf(span(0)) - attributes { - } - } - } - } - } - - def "should capture attributes"() { - setup: - new TracedWithSpan().withSpanAttributes("foo", "bar", null, "baz") - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.withSpanAttributes" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TracedWithSpan.name - "$SemanticAttributes.CODE_FUNCTION" "withSpanAttributes" - "implicitName" "foo" - "explicitName" "bar" - } - } - } - } - } -} diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/AddingSpanAttributesInstrumentationTest.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/AddingSpanAttributesInstrumentationTest.java new file mode 100644 index 000000000000..b52356e6bc8e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/AddingSpanAttributesInstrumentationTest.java @@ -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"))))); + } +} diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/ExtractAttributesUsingAddingSpanAttributes.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/ExtractAttributesUsingAddingSpanAttributes.java new file mode 100644 index 000000000000..9a8da88cdd36 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/ExtractAttributesUsingAddingSpanAttributes.java @@ -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!"; + } +} diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java new file mode 100644 index 000000000000..2ec26e4c5d07 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java @@ -0,0 +1,562 @@ +/* + * 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.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.lang.reflect.Modifier; +import java.util.concurrent.CompletableFuture; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.ClassFileVersion; +import net.bytebuddy.asm.MemberAttributeExtension; +import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.RuntimeType; +import net.bytebuddy.implementation.bind.annotation.This; +import net.bytebuddy.matcher.ElementMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class WithSpanInstrumentationTest { + + @RegisterExtension + public static final AgentInstrumentationExtension testing = + AgentInstrumentationExtension.create(); + + @Test + void deriveAutomaticName() throws Exception { + + new TracedWithSpan().otel(); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.otel") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry(SemanticAttributes.CODE_FUNCTION, "otel"))))); + } + + @Test + void manualName() throws Exception { + + new TracedWithSpan().namedOtel(); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("manualName") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "namedOtel"))))); + } + + @Test + void manualKind() throws Exception { + + new TracedWithSpan().someKind(); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.someKind") + .hasKind(SpanKind.PRODUCER) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "someKind"))))); + } + + @Test + void multipleSpans() throws Exception { + + new TracedWithSpan().server(); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.server") + .hasKind(SpanKind.SERVER) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry(SemanticAttributes.CODE_FUNCTION, "server"))), + span -> + assertThat(span) + .hasName("TracedWithSpan.otel") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(trace.get(0).getSpanId()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry(SemanticAttributes.CODE_FUNCTION, "otel"))))); + } + + @Test + void excludedMethod() throws Exception { + + new TracedWithSpan().ignored(); + + Thread.sleep(500); // sleep a bit just to make sure no span is captured + assertThat(testing.waitForTraces(0)); + } + + @Test + void completedCompletionStage() throws Exception { + + CompletableFuture future = CompletableFuture.completedFuture("Done"); + new TracedWithSpan().completionStage(future); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completionStage") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completionStage"))))); + } + + @Test + void exceptionallyCompletedCompletionStage() throws Exception { + + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new IllegalArgumentException("Boom")); + new TracedWithSpan().completionStage(future); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completionStage") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.error()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completionStage"))))); + } + + @Test + void nullCompletionStage() throws Exception { + + new TracedWithSpan().completionStage(null); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completionStage") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completionStage"))))); + } + + @Test + void completingCompletionStage() throws Exception { + + CompletableFuture future = new CompletableFuture<>(); + new TracedWithSpan().completionStage(future); + + Thread.sleep(500); // sleep a bit just to make sure no span is captured + assertThat(testing.waitForTraces(0)); + + future.complete("Done"); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completionStage") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completionStage"))))); + } + + @Test + void exceptionallyCompletingCompletionStage() throws Exception { + + CompletableFuture future = new CompletableFuture<>(); + new TracedWithSpan().completionStage(future); + + Thread.sleep(500); // sleep a bit just to make sure no span is captured + assertThat(testing.waitForTraces(0)); + + future.completeExceptionally(new IllegalArgumentException("Boom")); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completionStage") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.error()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completionStage"))))); + } + + @Test + void completedCompletableFuture() throws Exception { + + CompletableFuture future = CompletableFuture.completedFuture("Done"); + new TracedWithSpan().completableFuture(future); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completableFuture") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completableFuture"))))); + } + + @Test + void exceptionallyCompletedCompletableFuture() throws Exception { + + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new IllegalArgumentException("Boom")); + new TracedWithSpan().completableFuture(future); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completableFuture") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.error()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completableFuture"))))); + } + + @Test + void nullCompletableFuture() throws Exception { + + new TracedWithSpan().completableFuture(null); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completableFuture") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completableFuture"))))); + } + + @Test + void completingCompletableFuture() throws Exception { + + CompletableFuture future = new CompletableFuture<>(); + new TracedWithSpan().completableFuture(future); + + Thread.sleep(500); // sleep a bit just to make sure no span is captured + assertThat(testing.waitForTraces(0)); + + future.complete("Done"); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completableFuture") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completableFuture"))))); + } + + @Test + void exceptionallyCompletingCompletableFuture() throws Exception { + + CompletableFuture future = new CompletableFuture<>(); + new TracedWithSpan().completableFuture(future); + + Thread.sleep(500); // sleep a bit just to make sure no span is captured + assertThat(testing.waitForTraces(0)); + + future.completeExceptionally(new IllegalArgumentException("Boom")); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.completableFuture") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.error()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "completableFuture"))))); + } + + @Test + void captureAttributes() throws Exception { + + new TracedWithSpan().withSpanAttributes("foo", "bar", null, "baz"); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("TracedWithSpan.withSpanAttributes") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + TracedWithSpan.class.getName()), + entry( + SemanticAttributes.CODE_FUNCTION, + "withSpanAttributes"), + entry( + AttributeKey.stringKey("implicitName"), "foo"), + entry( + AttributeKey.stringKey("explicitName"), + "bar"))))); + } + + // Needs to be public for ByteBuddy + public static class Intercept { + @RuntimeType + public void intercept(@This Object o) { + testing.runWithSpan("intercept", () -> {}); + } + } + + @Test + void java6Class() throws Exception { + /* + class GeneratedJava6TestClass implements Runnable { + @WithSpan + public void run() { + testing.runWithSpan("intercept", () -> {}); + } + } + */ + Class generatedClass = + new ByteBuddy(ClassFileVersion.JAVA_V6) + .subclass(Object.class) + .name("GeneratedJava6TestClass") + .implement(Runnable.class) + .defineMethod("run", void.class, Modifier.PUBLIC) + .intercept(MethodDelegation.to(new Intercept())) + .visit( + new MemberAttributeExtension.ForMethod() + .annotateMethod(AnnotationDescription.Builder.ofType(WithSpan.class).build()) + .on(ElementMatchers.named("run"))) + .make() + .load(getClass().getClassLoader()) + .getLoaded(); + + Runnable runnable = (Runnable) generatedClass.getConstructor().newInstance(); + runnable.run(); + + assertThat(testing.waitForTraces(1)) + .satisfiesExactly( + trace -> + assertThat(trace) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("GeneratedJava6TestClass.run") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + entry( + SemanticAttributes.CODE_NAMESPACE, + "GeneratedJava6TestClass"), + entry(SemanticAttributes.CODE_FUNCTION, "run"))), + span -> + assertThat(span) + .hasName("intercept") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(trace.get(0).getSpanId()) + .hasAttributesSatisfying( + attributes -> assertThat(attributes).isEmpty()))); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java index b81acc6be9d1..bd508c212c5f 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java @@ -50,7 +50,7 @@ abstract class WithSpanAspect { .addAttributesExtractor( CodeAttributesExtractor.create(JointPointCodeAttributesExtractor.INSTANCE)) .addAttributesExtractor( - MethodSpanAttributesExtractor.newInstance( + MethodSpanAttributesExtractor.create( JoinPointRequest::method, parameterAttributeNamesExtractor, JoinPointRequest::args))