Browse Source

AddingSpanAttributes annotation (#7787)

Co-authored-by: Mateusz Rzeszutek <mrzeszutek@splunk.com>
sfriberg 1 year ago
parent
commit
d1b7356ffe
24 changed files with 1766 additions and 1043 deletions
  1. 8 1
      docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt
  2. 40 0
      instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/AttributeBindings.java
  3. 37 0
      instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/CombinedAttributeBindings.java
  4. 21 0
      instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/EmptyAttributeBindings.java
  5. 4 81
      instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/MethodSpanAttributesExtractor.java
  6. 41 0
      instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/SpanAttributesExtractor.java
  7. 35 0
      instrumentation-annotations/src/main/java/io/opentelemetry/instrumentation/annotations/AddingSpanAttributes.java
  8. 28 0
      instrumentation-annotations/src/test/java/io/opentelemetry/instrumentation/annotations/AddingSpanAttributesUsageExamples.java
  9. 4 0
      instrumentation/opentelemetry-extension-annotations-1.0/javaagent/build.gradle.kts
  10. 1 1
      instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanSingletons.java
  11. 0 441
      instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy
  12. 565 0
      instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java
  13. 4 0
      instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/build.gradle.kts
  14. 82 0
      instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AddingSpanAttributesInstrumentation.java
  15. 53 0
      instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationExcludedMethods.java
  16. 39 0
      instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationInstrumentationModule.java
  17. 20 8
      instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationSingletons.java
  18. 3 41
      instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentation.java
  19. 0 28
      instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentationModule.java
  20. 0 441
      instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy
  21. 174 0
      instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/AddingSpanAttributesInstrumentationTest.java
  22. 44 0
      instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/ExtractAttributesUsingAddingSpanAttributes.java
  23. 562 0
      instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java
  24. 1 1
      instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java

+ 8 - 1
docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt

@@ -1,2 +1,9 @@
 Comparing source compatibility of  against 
-No changes.
++++  NEW ANNOTATION: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.annotations.AddingSpanAttributes  (not serializable)
+	+++  CLASS FILE FORMAT VERSION: 52.0 <- n.a.
+	+++  NEW INTERFACE: java.lang.annotation.Annotation
+	+++  NEW SUPERCLASS: java.lang.Object
+	+++  NEW ANNOTATION: java.lang.annotation.Target
+		+++  NEW ELEMENT: value=java.lang.annotation.ElementType.METHOD,java.lang.annotation.ElementType.CONSTRUCTOR (+)
+	+++  NEW ANNOTATION: java.lang.annotation.Retention
+		+++  NEW ELEMENT: value=java.lang.annotation.RetentionPolicy.RUNTIME (+)

+ 40 - 0
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;
+  }
 }

+ 37 - 0
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);
+      }
+    }
+  }
+}

+ 21 - 0
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) {}
+}

+ 4 - 81
instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/MethodSpanAttributesExtractor.java

@@ -10,7 +10,6 @@ import io.opentelemetry.context.Context;
 import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
 import io.opentelemetry.instrumentation.api.internal.cache.Cache;
 import java.lang.reflect.Method;
-import java.lang.reflect.Parameter;
 import javax.annotation.Nullable;
 
 /** Extractor of {@link io.opentelemetry.api.common.Attributes} for a traced method. */
@@ -22,7 +21,7 @@ public final class MethodSpanAttributesExtractor<REQUEST, RESPONSE>
   private final Cache<Method, AttributeBindings> cache;
   private final ParameterAttributeNamesExtractor parameterAttributeNamesExtractor;
 
-  public static <REQUEST, RESPONSE> MethodSpanAttributesExtractor<REQUEST, RESPONSE> newInstance(
+  public static <REQUEST, RESPONSE> MethodSpanAttributesExtractor<REQUEST, RESPONSE> create(
       MethodExtractor<REQUEST> methodExtractor,
       ParameterAttributeNamesExtractor parameterAttributeNamesExtractor,
       MethodArgumentsExtractor<REQUEST> methodArgumentsExtractor) {
@@ -48,7 +47,9 @@ public final class MethodSpanAttributesExtractor<REQUEST, RESPONSE>
   @Override
   public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {
     Method method = methodExtractor.extract(request);
-    AttributeBindings bindings = cache.computeIfAbsent(method, this::bind);
+    AttributeBindings bindings =
+        cache.computeIfAbsent(
+            method, (Method m) -> AttributeBindings.bind(m, parameterAttributeNamesExtractor));
     if (!bindings.isEmpty()) {
       Object[] args = methodArgumentsExtractor.extract(request);
       bindings.apply(attributes, args);
@@ -62,82 +63,4 @@ public final class MethodSpanAttributesExtractor<REQUEST, RESPONSE>
       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);
-        }
-      }
-    }
-  }
 }

+ 41 - 0
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<Method, AttributeBindings> cache;
+  private final ParameterAttributeNamesExtractor parameterAttributeNamesExtractor;
+
+  public static SpanAttributesExtractor create(
+      ParameterAttributeNamesExtractor parameterAttributeNamesExtractor) {
+    return new SpanAttributesExtractor(parameterAttributeNamesExtractor, new MethodCache<>());
+  }
+
+  SpanAttributesExtractor(
+      ParameterAttributeNamesExtractor parameterAttributeNamesExtractor,
+      Cache<Method, AttributeBindings> cache) {
+    this.parameterAttributeNamesExtractor = parameterAttributeNamesExtractor;
+    this.cache = cache;
+  }
+
+  public Attributes extract(Method method, Object[] args) {
+    AttributesBuilder attributes = Attributes.builder();
+    AttributeBindings bindings =
+        cache.computeIfAbsent(
+            method, (Method m) -> AttributeBindings.bind(m, parameterAttributeNamesExtractor));
+    if (!bindings.isEmpty()) {
+      bindings.apply(attributes, args);
+    }
+    return attributes.build();
+  }
+}

+ 35 - 0
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}.
+ *
+ * <p>Application developers can use this annotation to signal OpenTelemetry auto-instrumentation
+ * that attributes annotated with the {@link
+ * io.opentelemetry.instrumentation.annotations.SpanAttribute} should be detected and added to the
+ * current span.
+ *
+ * <p>If no span is currently active no new span will be created, and no attributes will be
+ * extracted.
+ *
+ * <p>Similar to {@link
+ * io.opentelemetry.api.trace.Span#setAttribute(io.opentelemetry.api.common.AttributeKey,
+ * java.lang.Object) Span.setAttribute() } methods, if the key is already mapped to a value the old
+ * value is replaced by the extracted value.
+ *
+ * <p>If you are a library developer, then probably you should NOT use this annotation, because it
+ * is non-functional without some form of auto-instrumentation.
+ */
+@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface AddingSpanAttributes {}

+ 28 - 0
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) {}
+}

+ 4 - 0
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")

+ 1 - 1
instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanSingletons.java

@@ -50,7 +50,7 @@ public final class WithSpanSingletons {
         .addAttributesExtractor(
             CodeAttributesExtractor.create(MethodRequestCodeAttributesGetter.INSTANCE))
         .addAttributesExtractor(
-            MethodSpanAttributesExtractor.newInstance(
+            MethodSpanAttributesExtractor.create(
                 MethodRequest::method,
                 WithSpanParameterAttributeNamesExtractor.INSTANCE,
                 MethodRequest::args))

+ 0 - 441
instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy

@@ -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<String>()
-    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<String>()
-    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<String>()
-    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<String>()
-    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<String>()
-    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<String>()
-    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"
-          }
-        }
-      }
-    }
-  }
-}

+ 565 - 0
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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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())));
+  }
+}

+ 4 - 0
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")

+ 82 - 0
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<AnnotationSource> annotatedMethodMatcher;
+  private final ElementMatcher.Junction<MethodDescription> annotatedParametersMatcher;
+  // this matcher matches all methods that should be excluded from transformation
+  private final ElementMatcher.Junction<MethodDescription> excludedMethodsMatcher;
+
+  AddingSpanAttributesInstrumentation() {
+    annotatedMethodMatcher =
+        isAnnotatedWith(
+                named(
+                    "application.io.opentelemetry.instrumentation.annotations.AddingSpanAttributes"))
+            // Avoid repeat extraction if method is already annotation with WithSpan
+            .and(
+                not(
+                    isAnnotatedWith(
+                        named(
+                            "application.io.opentelemetry.instrumentation.annotations.WithSpan"))));
+    annotatedParametersMatcher =
+        hasParameters(
+            whereAny(
+                isAnnotatedWith(
+                    named(
+                        "application.io.opentelemetry.instrumentation.annotations.SpanAttribute"))));
+    excludedMethodsMatcher = AnnotationExcludedMethods.configureExcludedMethods();
+  }
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return declaresMethod(annotatedMethodMatcher);
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    ElementMatcher.Junction<MethodDescription> tracedMethodsWithParameters =
+        annotatedMethodMatcher.and(not(excludedMethodsMatcher)).and(annotatedParametersMatcher);
+
+    transformer.applyAdviceToMethod(
+        tracedMethodsWithParameters,
+        AddingSpanAttributesInstrumentation.class.getName() + "$AddingSpanAttributesAdvice");
+  }
+
+  public static class AddingSpanAttributesAdvice {
+
+    @Advice.OnMethodEnter(suppress = Throwable.class)
+    public static void onEnter(
+        @Advice.Origin Method method,
+        @Advice.AllArguments(typing = Assigner.Typing.DYNAMIC) Object[] args) {
+      Span otelSpan = Java8BytecodeBridge.currentSpan();
+      if (otelSpan.isRecording() && otelSpan.getSpanContext().isValid()) {
+        otelSpan.setAllAttributes(attributes().extract(method, args));
+      }
+    }
+  }
+}

+ 53 - 0
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<MethodDescription> configureExcludedMethods() {
+    ElementMatcher.Junction<MethodDescription> result = none();
+
+    Map<String, Set<String>> excludedMethods =
+        MethodsConfigurationParser.parse(
+            InstrumentationConfig.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG));
+    for (Map.Entry<String, Set<String>> entry : excludedMethods.entrySet()) {
+      String className = entry.getKey();
+      ElementMatcher.Junction<ByteCodeElement> matcher =
+          isDeclaredBy(ElementMatchers.named(className));
+
+      Set<String> methodNames = entry.getValue();
+      if (!methodNames.isEmpty()) {
+        matcher = matcher.and(namedOneOf(methodNames.toArray(new String[0])));
+      }
+
+      result = result.or(matcher);
+    }
+
+    return result;
+  }
+
+  private AnnotationExcludedMethods() {}
+}

+ 39 - 0
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<TypeInstrumentation> typeInstrumentations() {
+    return asList(new WithSpanInstrumentation(), new AddingSpanAttributesInstrumentation());
+  }
+}

+ 20 - 8
instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanSingletons.java → instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationSingletons.java

@@ -11,21 +11,23 @@ import application.io.opentelemetry.instrumentation.annotations.WithSpan;
 import io.opentelemetry.api.GlobalOpenTelemetry;
 import io.opentelemetry.api.trace.SpanKind;
 import io.opentelemetry.instrumentation.api.annotation.support.MethodSpanAttributesExtractor;
+import io.opentelemetry.instrumentation.api.annotation.support.SpanAttributesExtractor;
 import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
 import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor;
 import io.opentelemetry.instrumentation.api.instrumenter.util.SpanNames;
 import java.lang.reflect.Method;
 import java.util.logging.Logger;
 
-public final class WithSpanSingletons {
+public final class AnnotationSingletons {
 
   private static final String INSTRUMENTATION_NAME =
       "io.opentelemetry.opentelemetry-instrumentation-annotations-1.16";
 
-  private static final Logger logger = Logger.getLogger(WithSpanSingletons.class.getName());
+  private static final Logger logger = Logger.getLogger(AnnotationSingletons.class.getName());
   private static final Instrumenter<Method, Object> INSTRUMENTER = createInstrumenter();
   private static final Instrumenter<MethodRequest, Object> INSTRUMENTER_WITH_ATTRIBUTES =
       createInstrumenterWithAttributes();
+  private static final SpanAttributesExtractor ATTRIBUTES = createAttributesExtractor();
 
   public static Instrumenter<Method, Object> instrumenter() {
     return INSTRUMENTER;
@@ -35,26 +37,36 @@ public final class WithSpanSingletons {
     return INSTRUMENTER_WITH_ATTRIBUTES;
   }
 
+  public static SpanAttributesExtractor attributes() {
+    return ATTRIBUTES;
+  }
+
   private static Instrumenter<Method, Object> createInstrumenter() {
     return Instrumenter.builder(
-            GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, WithSpanSingletons::spanNameFromMethod)
+            GlobalOpenTelemetry.get(),
+            INSTRUMENTATION_NAME,
+            AnnotationSingletons::spanNameFromMethod)
         .addAttributesExtractor(CodeAttributesExtractor.create(MethodCodeAttributesGetter.INSTANCE))
-        .buildInstrumenter(WithSpanSingletons::spanKindFromMethod);
+        .buildInstrumenter(AnnotationSingletons::spanKindFromMethod);
   }
 
   private static Instrumenter<MethodRequest, Object> createInstrumenterWithAttributes() {
     return Instrumenter.builder(
             GlobalOpenTelemetry.get(),
             INSTRUMENTATION_NAME,
-            WithSpanSingletons::spanNameFromMethodRequest)
+            AnnotationSingletons::spanNameFromMethodRequest)
         .addAttributesExtractor(
             CodeAttributesExtractor.create(MethodRequestCodeAttributesGetter.INSTANCE))
         .addAttributesExtractor(
-            MethodSpanAttributesExtractor.newInstance(
+            MethodSpanAttributesExtractor.create(
                 MethodRequest::method,
                 WithSpanParameterAttributeNamesExtractor.INSTANCE,
                 MethodRequest::args))
-        .buildInstrumenter(WithSpanSingletons::spanKindFromMethodRequest);
+        .buildInstrumenter(AnnotationSingletons::spanKindFromMethodRequest);
+  }
+
+  private static SpanAttributesExtractor createAttributesExtractor() {
+    return SpanAttributesExtractor.create(WithSpanParameterAttributeNamesExtractor.INSTANCE);
   }
 
   private static SpanKind spanKindFromMethodRequest(MethodRequest request) {
@@ -92,5 +104,5 @@ public final class WithSpanSingletons {
     return spanName;
   }
 
-  private WithSpanSingletons() {}
+  private AnnotationSingletons() {}
 }

+ 3 - 41
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.context.Scope;
 import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport;
 import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
 import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge;
-import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig;
 import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
 import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
-import io.opentelemetry.javaagent.tooling.config.MethodsConfigurationParser;
 import java.lang.reflect.Method;
-import java.util.Map;
-import java.util.Set;
 import net.bytebuddy.asm.Advice;
-import net.bytebuddy.description.ByteCodeElement;
 import net.bytebuddy.description.annotation.AnnotationSource;
 import net.bytebuddy.description.method.MethodDescription;
 import net.bytebuddy.description.type.TypeDescription;
 import net.bytebuddy.implementation.bytecode.assign.Assigner;
 import net.bytebuddy.matcher.ElementMatcher;
-import net.bytebuddy.matcher.ElementMatchers;
 
 public class WithSpanInstrumentation implements TypeInstrumentation {
 
-  private static final String TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG =
-      "otel.instrumentation.opentelemetry-instrumentation-annotations.exclude-methods";
-
   private final ElementMatcher.Junction<AnnotationSource> annotatedMethodMatcher;
   private final ElementMatcher.Junction<MethodDescription> annotatedParametersMatcher;
   // this matcher matches all methods that should be excluded from transformation
@@ -58,7 +46,7 @@ public class WithSpanInstrumentation implements TypeInstrumentation {
                 isAnnotatedWith(
                     named(
                         "application.io.opentelemetry.instrumentation.annotations.SpanAttribute"))));
-    excludedMethodsMatcher = configureExcludedMethods();
+    excludedMethodsMatcher = AnnotationExcludedMethods.configureExcludedMethods();
   }
 
   @Override
@@ -92,32 +80,6 @@ public class WithSpanInstrumentation implements TypeInstrumentation {
         WithSpanInstrumentation.class.getName() + "$WithSpanAttributesAdvice");
   }
 
-  /*
-  Returns a matcher for all methods that should be excluded from auto-instrumentation by
-  annotation-based advices.
-  */
-  static ElementMatcher.Junction<MethodDescription> configureExcludedMethods() {
-    ElementMatcher.Junction<MethodDescription> result = none();
-
-    Map<String, Set<String>> excludedMethods =
-        MethodsConfigurationParser.parse(
-            InstrumentationConfig.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG));
-    for (Map.Entry<String, Set<String>> entry : excludedMethods.entrySet()) {
-      String className = entry.getKey();
-      ElementMatcher.Junction<ByteCodeElement> matcher =
-          isDeclaredBy(ElementMatchers.named(className));
-
-      Set<String> methodNames = entry.getValue();
-      if (!methodNames.isEmpty()) {
-        matcher = matcher.and(namedOneOf(methodNames.toArray(new String[0])));
-      }
-
-      result = result.or(matcher);
-    }
-
-    return result;
-  }
-
   @SuppressWarnings("unused")
   public static class WithSpanAdvice {
 

+ 0 - 28
instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentationModule.java

@@ -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<TypeInstrumentation> typeInstrumentations() {
-    return singletonList(new WithSpanInstrumentation());
-  }
-}

+ 0 - 441
instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy

@@ -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<String>()
-    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<String>()
-    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<String>()
-    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<String>()
-    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<String>()
-    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<String>()
-    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"
-          }
-        }
-      }
-    }
-  }
-}

+ 174 - 0
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")))));
+  }
+}

+ 44 - 0
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!";
+  }
+}

+ 562 - 0
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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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())));
+  }
+}

+ 1 - 1
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))