From 1ef1707c44e413700d449281f690d76dba5072b1 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 15 Jan 2026 16:19:35 -0500 Subject: [PATCH 1/6] init --- ...ptionHandlerParametersInstrumentation.java | 49 +++++++ .../src/test/groovy/GraphQLTest.groovy | 126 ++++++++++++++++++ .../src/test/resources/schema.graphqls | 1 + ...ptionHandlerParametersInstrumentation.java | 39 ++++++ .../src/test/groovy/GraphQLTest.groovy | 124 +++++++++++++++++ .../src/test/resources/schema.graphqls | 1 + .../graphqljava/AsyncExceptionUnwrapper.java | 22 +++ .../graphqljava/InstrumentedDataFetcher.java | 2 +- 8 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/DataFetcherExceptionHandlerParametersInstrumentation.java create mode 100644 dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/DataFetcherExceptionHandlerParametersInstrumentation.java create mode 100644 dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/AsyncExceptionUnwrapper.java diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/DataFetcherExceptionHandlerParametersInstrumentation.java b/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/DataFetcherExceptionHandlerParametersInstrumentation.java new file mode 100644 index 00000000000..d89aa38b021 --- /dev/null +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/DataFetcherExceptionHandlerParametersInstrumentation.java @@ -0,0 +1,49 @@ +package datadog.trace.instrumentation.graphqljava14; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers.hasClassNamed; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.instrumentation.graphqljava.AsyncExceptionUnwrapper; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumenterModule.class) +public class DataFetcherExceptionHandlerParametersInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + + public DataFetcherExceptionHandlerParametersInstrumentation() { + super("graphql-java"); + } + + @Override + public String instrumentedType() { + return "graphql.execution.DataFetcherExceptionHandlerParameters"; + } + + // Safeguard copied from GraphQLJavaInstrumentation.java + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // introduced in 20.0 + return not(hasClassNamed("graphql.execution.instrumentation.SimplePerformantInstrumentation")); + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod().and(named("getException")).and(returns(Throwable.class)), + this.getClass().getName() + "$UnwrapGetExceptionAdvice"); + } + + public static class UnwrapGetExceptionAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Return(readOnly = false) Throwable throwable) { + throwable = AsyncExceptionUnwrapper.unwrap(throwable); + } + } +} diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/test/groovy/GraphQLTest.groovy b/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/test/groovy/GraphQLTest.groovy index e2d1bb2ca82..87bfa35a6f0 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/test/groovy/GraphQLTest.groovy +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/test/groovy/GraphQLTest.groovy @@ -3,6 +3,7 @@ import datadog.trace.api.DDSpanTypes import datadog.trace.api.Trace import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.test.util.Flaky +import graphql.ExceptionWhileDataFetching import graphql.ExecutionResult import graphql.GraphQL import graphql.schema.DataFetcher @@ -15,6 +16,7 @@ import spock.lang.Shared import java.nio.charset.StandardCharsets import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionException import java.util.concurrent.CompletionStage import java.util.concurrent.TimeUnit @@ -62,6 +64,18 @@ abstract class GraphQLTest extends VersionedNamingTestBase { throw new IllegalStateException("TEST") } })) + .type(newTypeWiring("Book").dataFetcher("asyncCover", new DataFetcher>() { + @Override + CompletionStage get(DataFetchingEnvironment environment) throws Exception { + // Simulate the "async resolver failed" shape seen in the wild: nested CompletionException wrappers. + // This avoids scheduling work on the common pool while still exercising graphql-java's unwrapping logic. + def future = new CompletableFuture() + future.completeExceptionally(new CompletionException( + new CompletionException(new CompletionException(new IllegalStateException("ASYNC_TEST"))) + )) + return future + } + })) .type(newTypeWiring("Book").dataFetcher("bookHash", new DataFetcher>() { @Override CompletableFuture get(DataFetchingEnvironment environment) throws Exception { @@ -546,6 +560,118 @@ abstract class GraphQLTest extends VersionedNamingTestBase { } } + def "query async fetch error unwraps nested CompletionException wrappers"() { + setup: + def query = 'query findBookById {\n' + + ' bookById(id: "book-1") {\n' + + ' id #test\n' + + ' asyncCover\n' + + ' }\n' + + '}' + def expectedQuery = 'query findBookById {\n' + + ' bookById(id: {String}) {\n' + + ' id\n' + + ' asyncCover\n' + + ' }\n' + + '}\n' + ExecutionResult result = graphql.execute(query) + + expect: + !result.getErrors().isEmpty() + result.getErrors().get(0).getMessage().contains("ASYNC_TEST") + !result.getErrors().get(0).getMessage().contains("CompletionException") + result.getErrors().get(0) instanceof ExceptionWhileDataFetching + ((ExceptionWhileDataFetching) result.getErrors().get(0)).getException() instanceof IllegalStateException + ((ExceptionWhileDataFetching) result.getErrors().get(0)).getException().getMessage() == "ASYNC_TEST" + + assertTraces(1) { + trace(6) { + span { + operationName operation() + resourceName "findBookById" + spanType DDSpanTypes.GRAPHQL + errored true + measured true + parent() + tags { + "$Tags.COMPONENT" "graphql-java" + "graphql.source" expectedQuery + "graphql.operation.name" "findBookById" + "error.message" { it.contains("ASYNC_TEST") } + defaultTags() + } + } + span { + operationName "graphql.field" + resourceName "Book.asyncCover" + childOf(span(0)) + spanType DDSpanTypes.GRAPHQL + errored true + measured true + tags { + "$Tags.COMPONENT" "graphql-java" + "graphql.type" "String" + "graphql.coordinates" "Book.asyncCover" + "error.type" "java.lang.IllegalStateException" + "error.message" "ASYNC_TEST" + "error.stack" String + defaultTags() + } + } + span { + operationName "graphql.field" + resourceName "Query.bookById" + childOf(span(0)) + spanType DDSpanTypes.GRAPHQL + errored false + measured true + tags { + "$Tags.COMPONENT" "graphql-java" + "graphql.type" "Book" + "graphql.coordinates" "Query.bookById" + defaultTags() + } + } + span { + operationName "getBookById" + resourceName "book" + childOf(span(2)) + spanType null + errored false + measured false + tags { + "$Tags.COMPONENT" "trace" + defaultTags() + } + } + span { + operationName "graphql.validation" + resourceName "graphql.validation" + childOf(span(0)) + spanType DDSpanTypes.GRAPHQL + errored false + measured true + tags { + "$Tags.COMPONENT" "graphql-java" + defaultTags() + } + } + span { + operationName "graphql.parsing" + resourceName "graphql.parsing" + childOf(span(0)) + spanType DDSpanTypes.GRAPHQL + errored false + measured true + tags { + "$Tags.COMPONENT" "graphql-java" + defaultTags() + } + } + } + } + } + def "fetch `year` returning a CompletedStage which is a MinimalStage with most methods throwing UnsupportedOperationException"() { setup: def query = 'query findBookById {\n' + diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/test/resources/schema.graphqls b/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/test/resources/schema.graphqls index 9315ac8e222..26bc6e57e46 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/test/resources/schema.graphqls +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/test/resources/schema.graphqls @@ -13,6 +13,7 @@ type Book { pageCount: Int author: Author cover: String + asyncCover: String isbn: ID! bookHash: Int! year: Int diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/DataFetcherExceptionHandlerParametersInstrumentation.java b/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/DataFetcherExceptionHandlerParametersInstrumentation.java new file mode 100644 index 00000000000..56428cd5601 --- /dev/null +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/DataFetcherExceptionHandlerParametersInstrumentation.java @@ -0,0 +1,39 @@ +package datadog.trace.instrumentation.graphqljava20; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.instrumentation.graphqljava.AsyncExceptionUnwrapper; +import net.bytebuddy.asm.Advice; + +@AutoService(InstrumenterModule.class) +public class DataFetcherExceptionHandlerParametersInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + + public DataFetcherExceptionHandlerParametersInstrumentation() { + super("graphql-java"); + } + + @Override + public String instrumentedType() { + return "graphql.execution.DataFetcherExceptionHandlerParameters"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod().and(named("getException")).and(returns(Throwable.class)), + this.getClass().getName() + "$UnwrapGetExceptionAdvice"); + } + + public static class UnwrapGetExceptionAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Return(readOnly = false) Throwable throwable) { + throwable = AsyncExceptionUnwrapper.unwrap(throwable); + } + } +} diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/test/groovy/GraphQLTest.groovy b/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/test/groovy/GraphQLTest.groovy index e2d1bb2ca82..c4426bcf473 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/test/groovy/GraphQLTest.groovy +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/test/groovy/GraphQLTest.groovy @@ -3,6 +3,7 @@ import datadog.trace.api.DDSpanTypes import datadog.trace.api.Trace import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.test.util.Flaky +import graphql.ExceptionWhileDataFetching import graphql.ExecutionResult import graphql.GraphQL import graphql.schema.DataFetcher @@ -15,6 +16,7 @@ import spock.lang.Shared import java.nio.charset.StandardCharsets import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionException import java.util.concurrent.CompletionStage import java.util.concurrent.TimeUnit @@ -62,6 +64,16 @@ abstract class GraphQLTest extends VersionedNamingTestBase { throw new IllegalStateException("TEST") } })) + .type(newTypeWiring("Book").dataFetcher("asyncCover", new DataFetcher>() { + @Override + CompletionStage get(DataFetchingEnvironment environment) throws Exception { + def future = new CompletableFuture() + future.completeExceptionally(new CompletionException( + new CompletionException(new CompletionException(new IllegalStateException("ASYNC_TEST"))) + )) + return future + } + })) .type(newTypeWiring("Book").dataFetcher("bookHash", new DataFetcher>() { @Override CompletableFuture get(DataFetchingEnvironment environment) throws Exception { @@ -546,6 +558,118 @@ abstract class GraphQLTest extends VersionedNamingTestBase { } } + def "query async fetch error unwraps nested CompletionException wrappers"() { + setup: + def query = 'query findBookById {\n' + + ' bookById(id: "book-1") {\n' + + ' id #test\n' + + ' asyncCover\n' + + ' }\n' + + '}' + def expectedQuery = 'query findBookById {\n' + + ' bookById(id: {String}) {\n' + + ' id\n' + + ' asyncCover\n' + + ' }\n' + + '}\n' + ExecutionResult result = graphql.execute(query) + + expect: + !result.getErrors().isEmpty() + result.getErrors().get(0).getMessage().contains("ASYNC_TEST") + !result.getErrors().get(0).getMessage().contains("CompletionException") + result.getErrors().get(0) instanceof ExceptionWhileDataFetching + ((ExceptionWhileDataFetching) result.getErrors().get(0)).getException() instanceof IllegalStateException + ((ExceptionWhileDataFetching) result.getErrors().get(0)).getException().getMessage() == "ASYNC_TEST" + + assertTraces(1) { + trace(6) { + span { + operationName operation() + resourceName "findBookById" + spanType DDSpanTypes.GRAPHQL + errored true + measured true + parent() + tags { + "$Tags.COMPONENT" "graphql-java" + "graphql.source" expectedQuery + "graphql.operation.name" "findBookById" + "error.message" { it.contains("ASYNC_TEST") } + defaultTags() + } + } + span { + operationName "graphql.field" + resourceName "Book.asyncCover" + childOf(span(0)) + spanType DDSpanTypes.GRAPHQL + errored true + measured true + tags { + "$Tags.COMPONENT" "graphql-java" + "graphql.type" "String" + "graphql.coordinates" "Book.asyncCover" + "error.type" "java.lang.IllegalStateException" + "error.message" "ASYNC_TEST" + "error.stack" String + defaultTags() + } + } + span { + operationName "graphql.field" + resourceName "Query.bookById" + childOf(span(0)) + spanType DDSpanTypes.GRAPHQL + errored false + measured true + tags { + "$Tags.COMPONENT" "graphql-java" + "graphql.type" "Book" + "graphql.coordinates" "Query.bookById" + defaultTags() + } + } + span { + operationName "getBookById" + resourceName "book" + childOf(span(2)) + spanType null + errored false + measured false + tags { + "$Tags.COMPONENT" "trace" + defaultTags() + } + } + span { + operationName "graphql.validation" + resourceName "graphql.validation" + childOf(span(0)) + spanType DDSpanTypes.GRAPHQL + errored false + measured true + tags { + "$Tags.COMPONENT" "graphql-java" + defaultTags() + } + } + span { + operationName "graphql.parsing" + resourceName "graphql.parsing" + childOf(span(0)) + spanType DDSpanTypes.GRAPHQL + errored false + measured true + tags { + "$Tags.COMPONENT" "graphql-java" + defaultTags() + } + } + } + } + } + def "fetch `year` returning a CompletedStage which is a MinimalStage with most methods throwing UnsupportedOperationException"() { setup: def query = 'query findBookById {\n' + diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/test/resources/schema.graphqls b/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/test/resources/schema.graphqls index 9315ac8e222..26bc6e57e46 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/test/resources/schema.graphqls +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/test/resources/schema.graphqls @@ -13,6 +13,7 @@ type Book { pageCount: Int author: Author cover: String + asyncCover: String isbn: ID! bookHash: Int! year: Int diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/AsyncExceptionUnwrapper.java b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/AsyncExceptionUnwrapper.java new file mode 100644 index 00000000000..504c49f82bf --- /dev/null +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/AsyncExceptionUnwrapper.java @@ -0,0 +1,22 @@ +package datadog.trace.instrumentation.graphqljava; + +import java.util.concurrent.CompletionException; + +public final class AsyncExceptionUnwrapper { + private static final int MAX_UNWRAP_DEPTH = 32; + + private AsyncExceptionUnwrapper() {} + + // Util function to unwrap CompletionException and expose underlying exception + public static Throwable unwrap(Throwable throwable) { + Throwable t = throwable; + int depth = 0; + while (t != null + && t.getCause() != null + && depth++ < MAX_UNWRAP_DEPTH + && (t instanceof CompletionException)) { + t = t.getCause(); + } + return t; + } +} diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/InstrumentedDataFetcher.java b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/InstrumentedDataFetcher.java index 582e6eab950..1c4ee127a72 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/InstrumentedDataFetcher.java +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/InstrumentedDataFetcher.java @@ -56,7 +56,7 @@ public Object get(DataFetchingEnvironment environment) throws Exception { return ((CompletionStage) dataValue) .whenComplete( (result, throwable) -> { - DECORATE.onError(fieldSpan, throwable); + DECORATE.onError(fieldSpan, AsyncExceptionUnwrapper.unwrap(throwable)); DECORATE.beforeFinish(fieldSpan); fieldSpan.finish(); }); From 557d566fa8fbd4b89236e4a2bfc000e239d190b9 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 15 Jan 2026 17:10:16 -0500 Subject: [PATCH 2/6] muzzle --- ...DataFetcherExceptionHandlerParametersInstrumentation.java | 5 +++++ .../graphqljava14/GraphQLJavaInstrumentation.java | 3 ++- ...DataFetcherExceptionHandlerParametersInstrumentation.java | 5 +++++ .../graphqljava20/GraphQLJavaInstrumentation.java | 3 ++- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/DataFetcherExceptionHandlerParametersInstrumentation.java b/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/DataFetcherExceptionHandlerParametersInstrumentation.java index d89aa38b021..a44f9e91e9e 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/DataFetcherExceptionHandlerParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/DataFetcherExceptionHandlerParametersInstrumentation.java @@ -40,6 +40,11 @@ public void methodAdvice(MethodTransformer transformer) { this.getClass().getName() + "$UnwrapGetExceptionAdvice"); } + @Override + public String[] helperClassNames() { + return new String[] {"datadog.trace.instrumentation.graphqljava.AsyncExceptionUnwrapper"}; + } + public static class UnwrapGetExceptionAdvice { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.Return(readOnly = false) Throwable throwable) { diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/GraphQLJavaInstrumentation.java b/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/GraphQLJavaInstrumentation.java index 6924e791b4c..4c33771e369 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/GraphQLJavaInstrumentation.java +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/GraphQLJavaInstrumentation.java @@ -38,7 +38,8 @@ public String[] helperClassNames() { "datadog.trace.instrumentation.graphqljava.State", packageName + ".GraphQLInstrumentation", "datadog.trace.instrumentation.graphqljava.GraphQLQuerySanitizer", - "datadog.trace.instrumentation.graphqljava.InstrumentedDataFetcher" + "datadog.trace.instrumentation.graphqljava.InstrumentedDataFetcher", + "datadog.trace.instrumentation.graphqljava.AsyncExceptionUnwrapper" }; } diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/DataFetcherExceptionHandlerParametersInstrumentation.java b/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/DataFetcherExceptionHandlerParametersInstrumentation.java index 56428cd5601..f077315eef5 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/DataFetcherExceptionHandlerParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/DataFetcherExceptionHandlerParametersInstrumentation.java @@ -30,6 +30,11 @@ public void methodAdvice(MethodTransformer transformer) { this.getClass().getName() + "$UnwrapGetExceptionAdvice"); } + @Override + public String[] helperClassNames() { + return new String[] {"datadog.trace.instrumentation.graphqljava.AsyncExceptionUnwrapper"}; + } + public static class UnwrapGetExceptionAdvice { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.Return(readOnly = false) Throwable throwable) { diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/GraphQLJavaInstrumentation.java b/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/GraphQLJavaInstrumentation.java index 7ac49f59feb..e4f357b696a 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/GraphQLJavaInstrumentation.java +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/GraphQLJavaInstrumentation.java @@ -33,7 +33,8 @@ public String[] helperClassNames() { "datadog.trace.instrumentation.graphqljava.State", packageName + ".GraphQLInstrumentation", "datadog.trace.instrumentation.graphqljava.GraphQLQuerySanitizer", - "datadog.trace.instrumentation.graphqljava.InstrumentedDataFetcher" + "datadog.trace.instrumentation.graphqljava.InstrumentedDataFetcher", + "datadog.trace.instrumentation.graphqljava.AsyncExceptionUnwrapper" }; } From 140011a9dc28d18ca31746e253aae7d552a1f200 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Tue, 20 Jan 2026 10:23:28 -0500 Subject: [PATCH 3/6] move exceptionunwrap instrumentation to graphql-java-common --- ...ptionHandlerParametersInstrumentation.java | 54 ------------------- ...raphQLUnwrapExceptionInstrumentation.java} | 7 ++- 2 files changed, 3 insertions(+), 58 deletions(-) delete mode 100644 dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/DataFetcherExceptionHandlerParametersInstrumentation.java rename dd-java-agent/instrumentation/graphql-java/{graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/DataFetcherExceptionHandlerParametersInstrumentation.java => graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/GraphQLUnwrapExceptionInstrumentation.java} (81%) diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/DataFetcherExceptionHandlerParametersInstrumentation.java b/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/DataFetcherExceptionHandlerParametersInstrumentation.java deleted file mode 100644 index a44f9e91e9e..00000000000 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava14/DataFetcherExceptionHandlerParametersInstrumentation.java +++ /dev/null @@ -1,54 +0,0 @@ -package datadog.trace.instrumentation.graphqljava14; - -import static datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers.hasClassNamed; -import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; -import static net.bytebuddy.matcher.ElementMatchers.isMethod; -import static net.bytebuddy.matcher.ElementMatchers.not; -import static net.bytebuddy.matcher.ElementMatchers.returns; - -import com.google.auto.service.AutoService; -import datadog.trace.agent.tooling.Instrumenter; -import datadog.trace.agent.tooling.InstrumenterModule; -import datadog.trace.instrumentation.graphqljava.AsyncExceptionUnwrapper; -import net.bytebuddy.asm.Advice; -import net.bytebuddy.matcher.ElementMatcher; - -@AutoService(InstrumenterModule.class) -public class DataFetcherExceptionHandlerParametersInstrumentation extends InstrumenterModule.Tracing - implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { - - public DataFetcherExceptionHandlerParametersInstrumentation() { - super("graphql-java"); - } - - @Override - public String instrumentedType() { - return "graphql.execution.DataFetcherExceptionHandlerParameters"; - } - - // Safeguard copied from GraphQLJavaInstrumentation.java - @Override - public ElementMatcher.Junction classLoaderMatcher() { - // introduced in 20.0 - return not(hasClassNamed("graphql.execution.instrumentation.SimplePerformantInstrumentation")); - } - - @Override - public void methodAdvice(MethodTransformer transformer) { - transformer.applyAdvice( - isMethod().and(named("getException")).and(returns(Throwable.class)), - this.getClass().getName() + "$UnwrapGetExceptionAdvice"); - } - - @Override - public String[] helperClassNames() { - return new String[] {"datadog.trace.instrumentation.graphqljava.AsyncExceptionUnwrapper"}; - } - - public static class UnwrapGetExceptionAdvice { - @Advice.OnMethodExit(suppress = Throwable.class) - public static void onExit(@Advice.Return(readOnly = false) Throwable throwable) { - throwable = AsyncExceptionUnwrapper.unwrap(throwable); - } - } -} diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/DataFetcherExceptionHandlerParametersInstrumentation.java b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/GraphQLUnwrapExceptionInstrumentation.java similarity index 81% rename from dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/DataFetcherExceptionHandlerParametersInstrumentation.java rename to dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/GraphQLUnwrapExceptionInstrumentation.java index f077315eef5..6bc2548a7f6 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/main/java/datadog/trace/instrumentation/graphqljava20/DataFetcherExceptionHandlerParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/GraphQLUnwrapExceptionInstrumentation.java @@ -1,4 +1,4 @@ -package datadog.trace.instrumentation.graphqljava20; +package datadog.trace.instrumentation.graphqljava; import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.isMethod; @@ -7,14 +7,13 @@ import com.google.auto.service.AutoService; import datadog.trace.agent.tooling.Instrumenter; import datadog.trace.agent.tooling.InstrumenterModule; -import datadog.trace.instrumentation.graphqljava.AsyncExceptionUnwrapper; import net.bytebuddy.asm.Advice; @AutoService(InstrumenterModule.class) -public class DataFetcherExceptionHandlerParametersInstrumentation extends InstrumenterModule.Tracing +public class GraphQLUnwrapExceptionInstrumentation extends InstrumenterModule.Tracing implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { - public DataFetcherExceptionHandlerParametersInstrumentation() { + public GraphQLUnwrapExceptionInstrumentation() { super("graphql-java"); } From eefd0c15681f9c2c5f9044046b47c84c1b493a6a Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Tue, 20 Jan 2026 11:55:33 -0500 Subject: [PATCH 4/6] removing 1 layer of CompletionException --- .../src/test/groovy/GraphQLTest.groovy | 15 +++++++-------- .../src/test/groovy/GraphQLTest.groovy | 8 +++----- .../graphqljava/AsyncExceptionUnwrapper.java | 11 +++-------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/test/groovy/GraphQLTest.groovy b/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/test/groovy/GraphQLTest.groovy index 87bfa35a6f0..aef20c01c31 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/test/groovy/GraphQLTest.groovy +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-14.0/src/test/groovy/GraphQLTest.groovy @@ -70,9 +70,7 @@ abstract class GraphQLTest extends VersionedNamingTestBase { // Simulate the "async resolver failed" shape seen in the wild: nested CompletionException wrappers. // This avoids scheduling work on the common pool while still exercising graphql-java's unwrapping logic. def future = new CompletableFuture() - future.completeExceptionally(new CompletionException( - new CompletionException(new CompletionException(new IllegalStateException("ASYNC_TEST"))) - )) + future.completeExceptionally(new CompletionException(new CompletionException(new IllegalStateException("ASYNC_TEST")))) return future } })) @@ -579,10 +577,11 @@ abstract class GraphQLTest extends VersionedNamingTestBase { expect: !result.getErrors().isEmpty() result.getErrors().get(0).getMessage().contains("ASYNC_TEST") - !result.getErrors().get(0).getMessage().contains("CompletionException") result.getErrors().get(0) instanceof ExceptionWhileDataFetching - ((ExceptionWhileDataFetching) result.getErrors().get(0)).getException() instanceof IllegalStateException - ((ExceptionWhileDataFetching) result.getErrors().get(0)).getException().getMessage() == "ASYNC_TEST" + // Note that GraphQL 14.0 does not do unwrapping of exceptions on their own, so nested CompletionExceptions will result in removing only one of them + ((ExceptionWhileDataFetching) result.getErrors().get(0)).getException() instanceof CompletionException + ((ExceptionWhileDataFetching) result.getErrors().get(0)).getException().getCause() instanceof IllegalStateException + ((ExceptionWhileDataFetching) result.getErrors().get(0)).getException().getMessage() == "java.lang.IllegalStateException: ASYNC_TEST" assertTraces(1) { trace(6) { @@ -612,8 +611,8 @@ abstract class GraphQLTest extends VersionedNamingTestBase { "$Tags.COMPONENT" "graphql-java" "graphql.type" "String" "graphql.coordinates" "Book.asyncCover" - "error.type" "java.lang.IllegalStateException" - "error.message" "ASYNC_TEST" + "error.type" "java.util.concurrent.CompletionException" + "error.message" "java.lang.IllegalStateException: ASYNC_TEST" "error.stack" String defaultTags() } diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/test/groovy/GraphQLTest.groovy b/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/test/groovy/GraphQLTest.groovy index c4426bcf473..7c25fb8dc54 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/test/groovy/GraphQLTest.groovy +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-20.0/src/test/groovy/GraphQLTest.groovy @@ -68,9 +68,7 @@ abstract class GraphQLTest extends VersionedNamingTestBase { @Override CompletionStage get(DataFetchingEnvironment environment) throws Exception { def future = new CompletableFuture() - future.completeExceptionally(new CompletionException( - new CompletionException(new CompletionException(new IllegalStateException("ASYNC_TEST"))) - )) + future.completeExceptionally(new CompletionException(new CompletionException(new IllegalStateException("ASYNC_TEST")))) return future } })) @@ -610,8 +608,8 @@ abstract class GraphQLTest extends VersionedNamingTestBase { "$Tags.COMPONENT" "graphql-java" "graphql.type" "String" "graphql.coordinates" "Book.asyncCover" - "error.type" "java.lang.IllegalStateException" - "error.message" "ASYNC_TEST" + "error.type" "java.util.concurrent.CompletionException" + "error.message" "java.lang.IllegalStateException: ASYNC_TEST" "error.stack" String defaultTags() } diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/AsyncExceptionUnwrapper.java b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/AsyncExceptionUnwrapper.java index 504c49f82bf..17e4871723b 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/AsyncExceptionUnwrapper.java +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/AsyncExceptionUnwrapper.java @@ -9,14 +9,9 @@ private AsyncExceptionUnwrapper() {} // Util function to unwrap CompletionException and expose underlying exception public static Throwable unwrap(Throwable throwable) { - Throwable t = throwable; - int depth = 0; - while (t != null - && t.getCause() != null - && depth++ < MAX_UNWRAP_DEPTH - && (t instanceof CompletionException)) { - t = t.getCause(); + if (throwable.getCause() != null && throwable instanceof CompletionException) { + return throwable.getCause(); } - return t; + return throwable; } } From 679bd73aa005a310d25a217a0122faad854a2f33 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Fri, 23 Jan 2026 14:27:22 -0500 Subject: [PATCH 5/6] pr comments --- .../instrumentation/graphqljava/AsyncExceptionUnwrapper.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/AsyncExceptionUnwrapper.java b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/AsyncExceptionUnwrapper.java index 17e4871723b..8d563ea77c2 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/AsyncExceptionUnwrapper.java +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/AsyncExceptionUnwrapper.java @@ -3,7 +3,6 @@ import java.util.concurrent.CompletionException; public final class AsyncExceptionUnwrapper { - private static final int MAX_UNWRAP_DEPTH = 32; private AsyncExceptionUnwrapper() {} From 59f02f1399906e8d340dac817fbb03c5fefbfbfd Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Mon, 26 Jan 2026 11:06:38 +0000 Subject: [PATCH 6/6] Use separate muzzle directive for graphql-java-common --- .../graphql-java/graphql-java-common/build.gradle | 1 + .../graphqljava/GraphQLUnwrapExceptionInstrumentation.java | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-common/build.gradle b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/build.gradle index 4ca31024a33..6da41ef0e0e 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-common/build.gradle +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/build.gradle @@ -1,6 +1,7 @@ muzzle { pass { + name = "graphql-java-common" group = "com.graphql-java" module = 'graphql-java' versions = '[14.0,)' diff --git a/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/GraphQLUnwrapExceptionInstrumentation.java b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/GraphQLUnwrapExceptionInstrumentation.java index 6bc2548a7f6..bfe8a907782 100644 --- a/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/GraphQLUnwrapExceptionInstrumentation.java +++ b/dd-java-agent/instrumentation/graphql-java/graphql-java-common/src/main/java/datadog/trace/instrumentation/graphqljava/GraphQLUnwrapExceptionInstrumentation.java @@ -17,6 +17,11 @@ public GraphQLUnwrapExceptionInstrumentation() { super("graphql-java"); } + @Override + public String muzzleDirective() { + return "graphql-java-common"; + } + @Override public String instrumentedType() { return "graphql.execution.DataFetcherExceptionHandlerParameters";