Add asynchronous tracing to Spring Boot Autoconfigure WithSpanAspect
ADD
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/AsyncSpanEndStrategies.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.tracer.async;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Registry of {@link AsyncSpanEndStrategy} implementations for tracing the asynchronous operations
+ * represented by the return type of a traced method.
+ */
+public class AsyncSpanEndStrategies {
+ private static final AsyncSpanEndStrategies instance = new AsyncSpanEndStrategies();
+
+ public static AsyncSpanEndStrategies getInstance() {
+ return instance;
+ }
+
+ private final List<AsyncSpanEndStrategy> strategies = new CopyOnWriteArrayList<>();
+
+ private AsyncSpanEndStrategies() {
+ strategies.add(Jdk8AsyncEndStrategy.INSTANCE);
+ }
+
+ public void registerStrategy(AsyncSpanEndStrategy strategy) {
+ Objects.requireNonNull(strategy);
+ strategies.add(strategy);
+ }
+
+ @Nullable
+ public AsyncSpanEndStrategy resolveStrategy(Class<?> returnType) {
+ for (AsyncSpanEndStrategy strategy : strategies) {
+ if (strategy.supports(returnType)) {
+ return strategy;
+ }
+ }
+ return null;
+ }
+}

RENAME
instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/async/MethodSpanStrategy.java
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/AsyncSpanEndStrategy.java
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.javaagent.instrumentation.otelannotations.async;
+package io.opentelemetry.instrumentation.api.tracer.async;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.tracer.BaseTracer;
/**
- * Represents an implementation of a strategy for composing over the return value of a traced
- * method. If the return value represents the result of an asynchronous operation the implementation
- * can compose or register for notification of completion at which point the span representing the
- * invocation of the method will be ended.
+ * Represents an implementation of a strategy for composing over the return value of an asynchronous
+ * traced method which can compose or register for notification of completion at which point the
+ * span representing the invocation of the method will be ended.
*/
-public interface MethodSpanStrategy {
+public interface AsyncSpanEndStrategy {
boolean supports(Class<?> returnType);
/**
* Denotes the end of the invocation of the traced method with a successful result which will end
- * the span stored in the passed {@code context}. If the method returned a value representing an
- * asynchronous operation then the span will remain open until the asynchronous operation has
- * completed.
+ * the span stored in the passed {@code context}. The span will remain open until the asynchronous
+ * operation has completed.
*
* @param tracer {@link BaseTracer} tracer to be used to end the span stored in the {@code
* context}.
@@ -32,12 +30,4 @@ public interface MethodSpanStrategy {
* notification of completion.
*/
Object end(BaseTracer tracer, Context context, Object returnValue);
-
- /**
- * Returns a {@link MethodSpanStrategy} for tracing synchronous methods where the return value
- * does not represent the completion of an asynchronous operation.
- */
- static MethodSpanStrategy synchronous() {
- return SynchronousMethodSpanStrategy.INSTANCE;
- }
}

RENAME
instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/async/Jdk8MethodStrategy.java
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/Jdk8AsyncEndStrategy.java
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.javaagent.instrumentation.otelannotations.async;
+package io.opentelemetry.instrumentation.api.tracer.async;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.tracer.BaseTracer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
-enum Jdk8MethodStrategy implements MethodSpanStrategy {
+enum Jdk8AsyncEndStrategy implements AsyncSpanEndStrategy {
INSTANCE;
@Override

RENAME
instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/async/package-info.java
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/package-info.java
* Provides implementations of strategies for tracing methods that return asynchronous and reactive
* values so that the span can be ended when the asynchronous operation completes.
*/
-package io.opentelemetry.javaagent.instrumentation.otelannotations.async;
+package io.opentelemetry.instrumentation.api.tracer.async;

ADD
instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/async/Jdk8MethodStrategyTest.groovy
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.tracer.async
+
+import io.opentelemetry.context.Context
+import io.opentelemetry.instrumentation.api.tracer.BaseTracer
+import spock.lang.Specification
+
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.CompletionException
+
+class Jdk8MethodStrategyTest extends Specification {
+ BaseTracer tracer
+
+ Context context
+
+ def underTest = Jdk8AsyncEndStrategy.INSTANCE
+
+ void setup() {
+ tracer = Mock()
+ context = Mock()
+ }
+
+ def "ends span on completed future"() {
+ when:
+ underTest.end(tracer, context, CompletableFuture.completedFuture("completed"))
+
+ then:
+ 1 * tracer.end(context)
+ }
+
+ def "ends span exceptionally on failed future"() {
+ given:
+ def exception = new CompletionException()
+ def future = new CompletableFuture<String>()
+ future.completeExceptionally(exception)
+
+ when:
+ underTest.end(tracer, context, future)
+
+ then:
+ 1 * tracer.endExceptionally(context, exception)
+ }
+
+ def "ends span on future when complete"() {
+ def future = new CompletableFuture<String>()
+
+ when:
+ underTest.end(tracer, context, future)
+
+ then:
+ 0 * tracer._
+
+ when:
+ future.complete("completed")
+
+ then:
+ 1 * tracer.end(context)
+ }
+
+ def "ends span exceptionally on future when completed exceptionally"() {
+ def future = new CompletableFuture<String>()
+ def exception = new Exception()
+
+ when:
+ underTest.end(tracer, context, future)
+
+ then:
+ 0 * tracer._
+
+ when:
+ future.completeExceptionally(exception)
+
+ then:
+ 1 * tracer.endExceptionally(context, exception)
+ }
+}

MODIFY
instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/WithSpanTracer.java
instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/WithSpanTracer.java
@@ -10,7 +10,8 @@ import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.tracer.BaseTracer;
-import io.opentelemetry.javaagent.instrumentation.otelannotations.async.MethodSpanStrategies;
+import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategies;
+import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategy;
import java.lang.reflect.Method;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -24,7 +25,8 @@ public class WithSpanTracer extends BaseTracer {
private static final Logger log = LoggerFactory.getLogger(WithSpanTracer.class);
- private final MethodSpanStrategies methodSpanStrategies = MethodSpanStrategies.getInstance();
+ private final AsyncSpanEndStrategies asyncSpanEndStrategies =
+ AsyncSpanEndStrategies.getInstance();
public Context startSpan(
Context parentContext, WithSpan applicationAnnotation, Method method, SpanKind kind) {
@@ -85,12 +87,16 @@ public class WithSpanTracer extends BaseTracer {
* @throws ClassCastException if returnValue is not an instance of returnType
*/
public Object end(Context context, Class<?> returnType, Object returnValue) {
- if (!returnType.isInstance(returnValue)) {
+ if (returnType.isInstance(returnValue)) {
+ AsyncSpanEndStrategy asyncSpanEndStrategy =
+ asyncSpanEndStrategies.resolveStrategy(returnType);
+ if (asyncSpanEndStrategy != null) {
+ return asyncSpanEndStrategy.end(this, context, returnValue);
+ }
+ }
end(context);
return returnValue;
}
- return methodSpanStrategies.resolveStrategy(returnType).end(this, context, returnValue);
- }
@Override
protected String getInstrumentationName() {

DELETE
instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/async/MethodSpanStrategies.java
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.javaagent.instrumentation.otelannotations.async;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-/**
- * Registry of {@link MethodSpanStrategy} implementations for tracing the asynchronous operations
- * represented by the return type of a traced method.
- */
-public class MethodSpanStrategies {
- private static final MethodSpanStrategies instance = new MethodSpanStrategies();
-
- public static MethodSpanStrategies getInstance() {
- return instance;
- }
-
- private final List<MethodSpanStrategy> strategies = new CopyOnWriteArrayList<>();
-
- private MethodSpanStrategies() {
- strategies.add(Jdk8MethodStrategy.INSTANCE);
- }
-
- public void registerStrategy(MethodSpanStrategy strategy) {
- Objects.requireNonNull(strategy);
- strategies.add(strategy);
- }
-
- public MethodSpanStrategy resolveStrategy(Class<?> returnType) {
- for (MethodSpanStrategy strategy : strategies) {
- if (strategy.supports(returnType)) {
- return strategy;
- }
- }
- return MethodSpanStrategy.synchronous();
- }
-}

DELETE
instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/async/SynchronousMethodSpanStrategy.java
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.javaagent.instrumentation.otelannotations.async;
-
-import io.opentelemetry.context.Context;
-import io.opentelemetry.instrumentation.api.tracer.BaseTracer;
-
-enum SynchronousMethodSpanStrategy implements MethodSpanStrategy {
- INSTANCE;
-
- @Override
- public boolean supports(Class<?> returnType) {
- return true;
- }
-
- @Override
- public Object end(BaseTracer tracer, Context context, Object returnValue) {
- tracer.end(context);
- return returnValue;
- }
-}

MODIFY
instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java
instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java
@@ -47,8 +47,7 @@ public class WithSpanAspect {
Context context = tracer.startSpan(parentContext, withSpan, method);
try (Scope ignored = context.makeCurrent()) {
Object result = pjp.proceed();
- tracer.end(context);
- return result;
+ return tracer.end(context, method.getReturnType(), result);
} catch (Throwable t) {
tracer.endExceptionally(context, t);
throw t;

MODIFY
instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTracer.java
instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTracer.java
@@ -10,9 +10,15 @@ import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.extension.annotations.WithSpan;
import io.opentelemetry.instrumentation.api.tracer.BaseTracer;
+import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategies;
+import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategy;
import java.lang.reflect.Method;
class WithSpanAspectTracer extends BaseTracer {
+
+ private final AsyncSpanEndStrategies asyncSpanEndStrategies =
+ AsyncSpanEndStrategies.getInstance();
+
WithSpanAspectTracer(OpenTelemetry openTelemetry) {
super(openTelemetry);
}
@@ -42,4 +48,27 @@ class WithSpanAspectTracer extends BaseTracer {
}
return spanName;
}
+
+ /**
+ * Denotes the end of the invocation of the traced method with a successful result which will end
+ * the span stored in the passed {@code context}. If the method returned a value representing an
+ * asynchronous operation then the span will not be finished until the asynchronous operation has
+ * completed.
+ *
+ * @param returnType Return type of the traced method.
+ * @param returnValue Return value from the traced method.
+ * @return Either {@code returnValue} or a value composing over {@code returnValue} for
+ * notification of completion.
+ */
+ public Object end(Context context, Class<?> returnType, Object returnValue) {
+ if (returnType.isInstance(returnValue)) {
+ AsyncSpanEndStrategy asyncSpanEndStrategy =
+ asyncSpanEndStrategies.resolveStrategy(returnType);
+ if (asyncSpanEndStrategy != null) {
+ return asyncSpanEndStrategy.end(this, context, returnValue);
+ }
+ }
+ end(context);
+ return returnValue;
+ }
}

MODIFY
instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTest.java
instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTest.java
@@ -21,9 +21,12 @@ import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExte
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.data.StatusData;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
@@ -59,6 +62,16 @@ public class WithSpanAspectTest {
public String testWithServerSpan() {
return "Span with name testWithServerSpan and SpanKind.SERVER was created";
}
+
+ @WithSpan
+ public CompletionStage<String> testAsyncCompletionStage(CompletionStage<String> stage) {
+ return stage;
+ }
+
+ @WithSpan
+ public CompletableFuture<String> testAsyncCompletableFuture(CompletableFuture<String> stage) {
+ return stage;
+ }
}
private WithSpanTester withSpanTester;
@@ -187,4 +200,226 @@ public class WithSpanAspectTest {
trace.hasSpansSatisfyingExactly(
parentSpan -> parentSpan.hasName("parent").hasKind(SERVER)));
}
+
+ @Nested
+ @DisplayName("with a method annotated with @WithSpan returns CompletionStage")
+ class WithCompletionStage {
+
+ @Test
+ @DisplayName("should end Span on complete")
+ void onComplete() throws Throwable {
+ CompletableFuture<String> future = new CompletableFuture<>();
+
+ // when
+ withSpan("parent", () -> withSpanTester.testAsyncCompletionStage(future));
+
+ // then
+ assertThat(instrumentation.waitForTraces(1))
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace
+ .hasSize(1)
+ .hasSpansSatisfyingExactly(span -> span.hasName("parent").hasKind(INTERNAL)));
+
+ // when
+ future.complete("DONE");
+
+ // then
+ List<List<SpanData>> traces = instrumentation.waitForTraces(1);
+ assertThat(traces)
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL),
+ span ->
+ span.hasName("WithSpanTester.testAsyncCompletionStage")
+ .hasKind(INTERNAL)
+ .hasParentSpanId(traces.get(0).get(0).getSpanId())));
+ }
+
+ @Test
+ @DisplayName("should end Span on completeException AND should record the exception")
+ void onCompleteExceptionally() throws Throwable {
+ CompletableFuture<String> future = new CompletableFuture<>();
+
+ // when
+ withSpan("parent", () -> withSpanTester.testAsyncCompletionStage(future));
+
+ // then
+ assertThat(instrumentation.waitForTraces(1))
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace
+ .hasSize(1)
+ .hasSpansSatisfyingExactly(span -> span.hasName("parent").hasKind(INTERNAL)));
+
+ // when
+ future.completeExceptionally(new Exception("Test @WithSpan With completeExceptionally"));
+
+ // then
+ List<List<SpanData>> traces = instrumentation.waitForTraces(1);
+ assertThat(traces)
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL),
+ span ->
+ span.hasName("WithSpanTester.testAsyncCompletionStage")
+ .hasKind(INTERNAL)
+ .hasStatus(StatusData.error())
+ .hasParentSpanId(traces.get(0).get(0).getSpanId())));
+ }
+
+ @Test
+ @DisplayName("should end Span on incompatible return value")
+ void onIncompatibleReturnValue() throws Throwable {
+ // when
+ withSpan("parent", () -> withSpanTester.testAsyncCompletionStage(null));
+
+ // then
+ List<List<SpanData>> traces = instrumentation.waitForTraces(1);
+ assertThat(traces)
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL),
+ span ->
+ span.hasName("WithSpanTester.testAsyncCompletionStage")
+ .hasKind(INTERNAL)
+ .hasParentSpanId(traces.get(0).get(0).getSpanId())));
+ }
+ }
+
+ @Nested
+ @DisplayName("with a method annotated with @WithSpan returns CompletableFuture")
+ class WithCompletableFuture {
+
+ @Test
+ @DisplayName("should end Span on complete")
+ void onComplete() throws Throwable {
+ CompletableFuture<String> future = new CompletableFuture<>();
+
+ // when
+ withSpan("parent", () -> withSpanTester.testAsyncCompletableFuture(future));
+
+ // then
+ assertThat(instrumentation.waitForTraces(1))
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace
+ .hasSize(1)
+ .hasSpansSatisfyingExactly(span -> span.hasName("parent").hasKind(INTERNAL)));
+
+ // when
+ future.complete("DONE");
+
+ // then
+ List<List<SpanData>> traces = instrumentation.waitForTraces(1);
+ assertThat(traces)
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL),
+ span ->
+ span.hasName("WithSpanTester.testAsyncCompletableFuture")
+ .hasKind(INTERNAL)
+ .hasParentSpanId(traces.get(0).get(0).getSpanId())));
+ }
+
+ @Test
+ @DisplayName("should end Span on completeException AND should record the exception")
+ void onCompleteExceptionally() throws Throwable {
+ CompletableFuture<String> future = new CompletableFuture<>();
+
+ // when
+ withSpan("parent", () -> withSpanTester.testAsyncCompletableFuture(future));
+
+ // then
+ assertThat(instrumentation.waitForTraces(1))
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace
+ .hasSize(1)
+ .hasSpansSatisfyingExactly(span -> span.hasName("parent").hasKind(INTERNAL)));
+
+ // when
+ future.completeExceptionally(new Exception("Test @WithSpan With completeExceptionally"));
+
+ // then
+ List<List<SpanData>> traces = instrumentation.waitForTraces(1);
+ assertThat(traces)
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL),
+ span ->
+ span.hasName("WithSpanTester.testAsyncCompletableFuture")
+ .hasKind(INTERNAL)
+ .hasStatus(StatusData.error())
+ .hasParentSpanId(traces.get(0).get(0).getSpanId())));
+ }
+
+ @Test
+ @DisplayName("should end the Span when already complete")
+ void onCompletedFuture() throws Throwable {
+ CompletableFuture<String> future = CompletableFuture.completedFuture("Done");
+
+ // when
+ withSpan("parent", () -> withSpanTester.testAsyncCompletableFuture(future));
+
+ // then
+ List<List<SpanData>> traces = instrumentation.waitForTraces(1);
+ assertThat(traces)
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL),
+ span ->
+ span.hasName("WithSpanTester.testAsyncCompletableFuture")
+ .hasKind(INTERNAL)
+ .hasParentSpanId(traces.get(0).get(0).getSpanId())));
+ }
+
+ @Test
+ @DisplayName("should end the Span when already failed")
+ void onFailedFuture() throws Throwable {
+ CompletableFuture<String> future = new CompletableFuture<>();
+ future.completeExceptionally(new Exception("Test @WithSpan With completeExceptionally"));
+
+ // when
+ withSpan("parent", () -> withSpanTester.testAsyncCompletableFuture(future));
+
+ // then
+ List<List<SpanData>> traces = instrumentation.waitForTraces(1);
+ assertThat(traces)
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL),
+ span ->
+ span.hasName("WithSpanTester.testAsyncCompletableFuture")
+ .hasKind(INTERNAL)
+ .hasStatus(StatusData.error())
+ .hasParentSpanId(traces.get(0).get(0).getSpanId())));
+ }
+
+ @Test
+ @DisplayName("should end Span on incompatible return value")
+ void onIncompatibleReturnValue() throws Throwable {
+ // when
+ withSpan("parent", () -> withSpanTester.testAsyncCompletableFuture(null));
+
+ // then
+ List<List<SpanData>> traces = instrumentation.waitForTraces(1);
+ assertThat(traces)
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL),
+ span ->
+ span.hasName("WithSpanTester.testAsyncCompletableFuture")
+ .hasKind(INTERNAL)
+ .hasParentSpanId(traces.get(0).get(0).getSpanId())));
+ }
+ }
}

Add asynchronous tracing to Spring Boot Autoconfigure WithSpanAspect (#2618)