Add @WithSpan option to break from existing context (#13112)

Signed-off-by: xiepuhuan <xiepuhuan@didachuxing.com>
Co-authored-by: Lauri Tulmin <ltulmin@splunk.com>
Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
This commit is contained in:
xiepuhuan 2025-03-14 03:15:44 +08:00 committed by GitHub
parent b54b2000e7
commit 0477d38897
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 137 additions and 6 deletions

View File

@ -1,2 +1,4 @@
Comparing source compatibility of opentelemetry-instrumentation-annotations-2.14.0-SNAPSHOT.jar against opentelemetry-instrumentation-annotations-2.13.3.jar
No changes.
**** MODIFIED ANNOTATION: PUBLIC ABSTRACT io.opentelemetry.instrumentation.annotations.WithSpan (not serializable)
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
+++* NEW METHOD: PUBLIC(+) ABSTRACT(+) boolean inheritContext()

View File

@ -6,6 +6,7 @@
package io.opentelemetry.instrumentation.annotations;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -34,4 +35,15 @@ public @interface WithSpan {
/** Specify the {@link SpanKind} of span to be created. Defaults to {@link SpanKind#INTERNAL}. */
SpanKind kind() default SpanKind.INTERNAL;
/**
* Specifies whether to inherit the current context when creating a span.
*
* <p>If set to {@code true} (default), the created span will use the current context as its
* parent, remaining within the same trace.
*
* <p>If set to {@code false}, the created span will use {@link Context#root()} as its parent,
* starting a new, independent trace.
*/
boolean inheritContext() default true;
}

View File

@ -10,11 +10,15 @@ import static java.util.logging.Level.FINE;
import application.io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.annotation.support.MethodSpanAttributesExtractor;
import io.opentelemetry.instrumentation.api.annotation.support.SpanAttributesExtractor;
import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.semconv.util.SpanNames;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.logging.Logger;
@ -29,6 +33,21 @@ public final class AnnotationSingletons {
createInstrumenterWithAttributes();
private static final SpanAttributesExtractor ATTRIBUTES = createAttributesExtractor();
// The reason for using reflection here is that it needs to be compatible with the old version of
// @WithSpan annotation that does not include the inheritContext option to avoid failing the
// muzzle check.
private static MethodHandle inheritContextMethodHandle = null;
static {
try {
inheritContextMethodHandle =
MethodHandles.publicLookup()
.findVirtual(WithSpan.class, "inheritContext", MethodType.methodType(boolean.class));
} catch (NoSuchMethodException | IllegalAccessException ignore) {
// ignore
}
}
public static Instrumenter<Method, Object> instrumenter() {
return INSTRUMENTER;
}
@ -104,5 +123,24 @@ public final class AnnotationSingletons {
return spanName;
}
public static Context getContextForMethod(Method method) {
return inheritContextFromMethod(method) ? Context.current() : Context.root();
}
private static boolean inheritContextFromMethod(Method method) {
if (inheritContextMethodHandle == null) {
return true;
}
WithSpan annotation = method.getDeclaredAnnotation(WithSpan.class);
try {
return (boolean) inheritContextMethodHandle.invoke(annotation);
} catch (Throwable ignore) {
// ignore
}
return true;
}
private AnnotationSingletons() {}
}

View File

@ -19,7 +19,6 @@ import io.opentelemetry.context.Context;
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.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import java.lang.reflect.Method;
@ -91,7 +90,7 @@ class WithSpanInstrumentation implements TypeInstrumentation {
method = originMethod;
Instrumenter<Method, Object> instrumenter = instrumenter();
Context current = Java8BytecodeBridge.currentContext();
Context current = AnnotationSingletons.getContextForMethod(method);
if (instrumenter.shouldStart(current, method)) {
context = instrumenter.start(current, method);
@ -134,7 +133,7 @@ class WithSpanInstrumentation implements TypeInstrumentation {
method = originMethod;
Instrumenter<MethodRequest, Object> instrumenter = instrumenterWithAttributes();
Context current = Java8BytecodeBridge.currentContext();
Context current = AnnotationSingletons.getContextForMethod(method);
request = new MethodRequest(method, args);
if (instrumenter.shouldStart(current, request)) {

View File

@ -57,4 +57,14 @@ public class TracedWithSpan {
public CompletableFuture<String> completableFuture(CompletableFuture<String> future) {
return future;
}
@WithSpan(inheritContext = false)
public String withoutParent() {
return "hello!";
}
@WithSpan(kind = SpanKind.CONSUMER)
public String consumer() {
return withoutParent();
}
}

View File

@ -108,6 +108,31 @@ class WithSpanInstrumentationTest {
equalTo(CODE_FUNCTION, "otel"))));
}
@Test
void multipleSpansWithoutParent() {
new TracedWithSpan().consumer();
testing.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
span ->
span.hasName("TracedWithSpan.consumer")
.hasKind(SpanKind.CONSUMER)
.hasNoParent()
.hasAttributesSatisfyingExactly(
equalTo(CODE_NAMESPACE, TracedWithSpan.class.getName()),
equalTo(CODE_FUNCTION, "consumer"))),
trace ->
trace.hasSpansSatisfyingExactly(
span ->
span.hasName("TracedWithSpan.withoutParent")
.hasKind(SpanKind.INTERNAL)
.hasNoParent()
.hasAttributesSatisfyingExactly(
equalTo(CODE_NAMESPACE, TracedWithSpan.class.getName()),
equalTo(CODE_FUNCTION, "withoutParent"))));
}
@Test
void excludedMethod() throws Exception {
new TracedWithSpan().ignored();

View File

@ -18,8 +18,14 @@ final class JoinPointRequest {
private final Method method;
private final String spanName;
private final SpanKind spanKind;
private final boolean inheritContext;
private JoinPointRequest(JoinPoint joinPoint, Method method, String spanName, SpanKind spanKind) {
private JoinPointRequest(
JoinPoint joinPoint,
Method method,
String spanName,
SpanKind spanKind,
boolean inheritContext) {
if (spanName.isEmpty()) {
spanName = SpanNames.fromMethod(method);
}
@ -28,6 +34,7 @@ final class JoinPointRequest {
this.method = method;
this.spanName = spanName;
this.spanKind = spanKind;
this.inheritContext = inheritContext;
}
String spanName() {
@ -46,6 +53,10 @@ final class JoinPointRequest {
return joinPoint.getArgs();
}
boolean inheritContext() {
return inheritContext;
}
interface Factory {
JoinPointRequest create(JoinPoint joinPoint);
@ -65,8 +76,9 @@ final class JoinPointRequest {
WithSpan annotation = method.getDeclaredAnnotation(WithSpan.class);
String spanName = annotation != null ? annotation.value() : "";
SpanKind spanKind = annotation != null ? annotation.kind() : SpanKind.INTERNAL;
boolean inheritContext = annotation == null || annotation.inheritContext();
return new JoinPointRequest(joinPoint, method, spanName, spanKind);
return new JoinPointRequest(joinPoint, method, spanName, spanKind, inheritContext);
}
}
}

View File

@ -6,6 +6,7 @@
package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
@ -53,10 +54,16 @@ abstract class WithSpanAspect {
JoinPointRequest::method,
parameterAttributeNamesExtractor,
JoinPointRequest::args))
.addContextCustomizer(WithSpanAspect::parentContext)
.buildInstrumenter(JoinPointRequest::spanKind);
this.requestFactory = requestFactory;
}
private static Context parentContext(
Context parentContext, JoinPointRequest request, Attributes unused) {
return request.inheritContext() ? parentContext : Context.root();
}
public Object traceMethod(ProceedingJoinPoint pjp) throws Throwable {
JoinPointRequest request = requestFactory.create(pjp);
Context parentContext = Context.current();

View File

@ -181,6 +181,27 @@ class InstrumentationWithSpanAspectTest {
equalTo(stringKey("explicitName"), "baz"))));
}
@Test
@DisplayName(
"when method is annotated with @WithSpan(inheritContext=false) should build span without parent")
void withSpanWithoutParent() {
// when
testing.runWithSpan("parent", withSpanTester::testWithoutParentSpan);
// then
testing.waitAndAssertTraces(
trace -> trace.hasSpansSatisfyingExactly(span -> span.hasName("parent").hasKind(INTERNAL)),
trace ->
trace.hasSpansSatisfyingExactly(
span ->
span.hasName(unproxiedTesterSimpleClassName + ".testWithoutParentSpan")
.hasKind(INTERNAL)
.hasNoParent()
.hasAttributesSatisfyingExactly(
equalTo(CODE_NAMESPACE, unproxiedTesterClassName),
equalTo(CODE_FUNCTION, "testWithoutParentSpan"))));
}
static class InstrumentationWithSpanTester {
@WithSpan
public String testWithSpan() {
@ -222,6 +243,11 @@ class InstrumentationWithSpanAspectTest {
return "hello!";
}
@WithSpan(inheritContext = false)
public String testWithoutParentSpan() {
return "Span without parent span was created";
}
}
@Nested