Skip to content

Commit 95fa93c

Browse files
Feat: Add graphql-java instrumentation. (#1777)
* Feat: Add `graphql-java` instrumentation. Fixes #1755
1 parent 2d8a74a commit 95fa93c

File tree

21 files changed

+704
-5
lines changed

21 files changed

+704
-5
lines changed

.craft.yml

+1
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ targets:
3939
maven:io.sentry:sentry-openfeign:
4040
maven:io.sentry:sentry-apollo:
4141
maven:io.sentry:sentry-jdbc:
42+
maven:io.sentry:sentry-graphql-java:

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
* Feat: Add `graphql-java` instrumentation (#1777)
6+
57
## 5.3.0
68

79
* Feat: Add datasource tracing with P6Spy (#1784)

build.gradle.kts

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ apiValidation {
4949
"sentry-samples-servlet",
5050
"sentry-samples-spring",
5151
"sentry-samples-spring-boot",
52-
"sentry-samples-spring-boot-webflux"
52+
"sentry-samples-spring-boot-webflux",
53+
"sentry-samples-netflix-dgs",
5354
)
5455
)
5556
}

buildSrc/src/main/java/Config.kt

+4
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ object Config {
101101
val apolloCoroutines = "com.apollographql.apollo:apollo-coroutines-support:$apolloVersion"
102102

103103
val p6spy = "p6spy:p6spy:3.9.1"
104+
105+
val graphQlJava = "com.graphql-java:graphql-java:17.3"
106+
107+
val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect"
104108
}
105109

106110
object AnnotationProcessors {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
public final class io/sentry/graphql/SentryDataFetcherExceptionHandler : graphql/execution/DataFetcherExceptionHandler {
2+
public fun <init> (Lgraphql/execution/DataFetcherExceptionHandler;)V
3+
public fun <init> (Lio/sentry/IHub;Lgraphql/execution/DataFetcherExceptionHandler;)V
4+
public fun onException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Lgraphql/execution/DataFetcherExceptionHandlerResult;
5+
}
6+
7+
public final class io/sentry/graphql/SentryInstrumentation : graphql/execution/instrumentation/SimpleInstrumentation {
8+
public fun <init> ()V
9+
public fun <init> (Lio/sentry/IHub;)V
10+
public fun <init> (Lio/sentry/IHub;Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;)V
11+
public fun beginExecution (Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;)Lgraphql/execution/instrumentation/InstrumentationContext;
12+
public fun createState ()Lgraphql/execution/instrumentation/InstrumentationState;
13+
public fun instrumentDataFetcher (Lgraphql/schema/DataFetcher;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Lgraphql/schema/DataFetcher;
14+
}
15+
16+
public abstract interface class io/sentry/graphql/SentryInstrumentation$BeforeSpanCallback {
17+
public abstract fun execute (Lio/sentry/ISpan;Lgraphql/schema/DataFetchingEnvironment;Ljava/lang/Object;)Lio/sentry/ISpan;
18+
}
19+

sentry-graphql-java/build.gradle.kts

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import net.ltgt.gradle.errorprone.errorprone
2+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3+
4+
plugins {
5+
`java-library`
6+
kotlin("jvm")
7+
jacoco
8+
id(Config.QualityPlugins.errorProne)
9+
id(Config.QualityPlugins.gradleVersions)
10+
}
11+
12+
configure<JavaPluginExtension> {
13+
sourceCompatibility = JavaVersion.VERSION_1_8
14+
targetCompatibility = JavaVersion.VERSION_1_8
15+
}
16+
17+
tasks.withType<KotlinCompile>().configureEach {
18+
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
19+
kotlinOptions.languageVersion = Config.springKotlinCompatibleLanguageVersion
20+
}
21+
22+
dependencies {
23+
api(projects.sentry)
24+
implementation(Config.Libs.graphQlJava)
25+
26+
compileOnly(Config.CompileOnly.nopen)
27+
errorprone(Config.CompileOnly.nopenChecker)
28+
errorprone(Config.CompileOnly.errorprone)
29+
errorprone(Config.CompileOnly.errorProneNullAway)
30+
compileOnly(Config.CompileOnly.jetbrainsAnnotations)
31+
32+
// tests
33+
testImplementation(projects.sentry)
34+
testImplementation(projects.sentryTestSupport)
35+
testImplementation(kotlin(Config.kotlinStdLib))
36+
testImplementation(Config.TestLibs.kotlinTestJunit)
37+
testImplementation(Config.TestLibs.mockitoKotlin)
38+
testImplementation(Config.TestLibs.mockWebserver)
39+
testImplementation(Config.Libs.okhttp)
40+
}
41+
42+
configure<SourceSetContainer> {
43+
test {
44+
java.srcDir("src/test/java")
45+
}
46+
}
47+
48+
jacoco {
49+
toolVersion = Config.QualityPlugins.Jacoco.version
50+
}
51+
52+
tasks.jacocoTestReport {
53+
reports {
54+
xml.required.set(true)
55+
html.required.set(false)
56+
}
57+
}
58+
59+
tasks {
60+
jacocoTestCoverageVerification {
61+
violationRules {
62+
rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } }
63+
}
64+
}
65+
check {
66+
dependsOn(jacocoTestCoverageVerification)
67+
dependsOn(jacocoTestReport)
68+
}
69+
}
70+
71+
tasks.withType<JavaCompile>().configureEach {
72+
options.errorprone {
73+
check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR)
74+
option("NullAway:AnnotatedPackages", "io.sentry")
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.sentry.graphql;
2+
3+
import graphql.execution.DataFetcherExceptionHandler;
4+
import graphql.execution.DataFetcherExceptionHandlerParameters;
5+
import graphql.execution.DataFetcherExceptionHandlerResult;
6+
import io.sentry.HubAdapter;
7+
import io.sentry.IHub;
8+
import io.sentry.util.Objects;
9+
import org.jetbrains.annotations.NotNull;
10+
11+
/**
12+
* Captures exceptions that occur during data fetching, passes them to Sentry and invokes a delegate
13+
* exception handler.
14+
*/
15+
public final class SentryDataFetcherExceptionHandler implements DataFetcherExceptionHandler {
16+
private final @NotNull IHub hub;
17+
private final @NotNull DataFetcherExceptionHandler delegate;
18+
19+
public SentryDataFetcherExceptionHandler(
20+
final @NotNull IHub hub, final @NotNull DataFetcherExceptionHandler delegate) {
21+
this.hub = Objects.requireNonNull(hub, "hub is required");
22+
this.delegate = Objects.requireNonNull(delegate, "delegate is required");
23+
}
24+
25+
public SentryDataFetcherExceptionHandler(final @NotNull DataFetcherExceptionHandler delegate) {
26+
this(HubAdapter.getInstance(), delegate);
27+
}
28+
29+
@Override
30+
@SuppressWarnings("deprecation")
31+
public DataFetcherExceptionHandlerResult onException(
32+
final @NotNull DataFetcherExceptionHandlerParameters handlerParameters) {
33+
hub.captureException(handlerParameters.getException(), handlerParameters);
34+
return delegate.onException(handlerParameters);
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package io.sentry.graphql;
2+
3+
import graphql.ExecutionResult;
4+
import graphql.execution.instrumentation.InstrumentationContext;
5+
import graphql.execution.instrumentation.InstrumentationState;
6+
import graphql.execution.instrumentation.SimpleInstrumentation;
7+
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
8+
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
9+
import graphql.schema.DataFetcher;
10+
import graphql.schema.DataFetchingEnvironment;
11+
import graphql.schema.GraphQLNonNull;
12+
import graphql.schema.GraphQLObjectType;
13+
import graphql.schema.GraphQLOutputType;
14+
import io.sentry.HubAdapter;
15+
import io.sentry.IHub;
16+
import io.sentry.ISpan;
17+
import io.sentry.SpanStatus;
18+
import io.sentry.util.Objects;
19+
import java.util.concurrent.CompletableFuture;
20+
import org.jetbrains.annotations.NotNull;
21+
import org.jetbrains.annotations.Nullable;
22+
23+
public final class SentryInstrumentation extends SimpleInstrumentation {
24+
private final @NotNull IHub hub;
25+
private final @Nullable BeforeSpanCallback beforeSpan;
26+
27+
public SentryInstrumentation(
28+
final @NotNull IHub hub, final @Nullable BeforeSpanCallback beforeSpan) {
29+
this.hub = Objects.requireNonNull(hub, "hub is required");
30+
this.beforeSpan = beforeSpan;
31+
}
32+
33+
public SentryInstrumentation(final @NotNull IHub hub) {
34+
this(hub, null);
35+
}
36+
37+
public SentryInstrumentation() {
38+
this(HubAdapter.getInstance());
39+
}
40+
41+
@Override
42+
public @NotNull InstrumentationState createState() {
43+
return new TracingState();
44+
}
45+
46+
@Override
47+
public @NotNull InstrumentationContext<ExecutionResult> beginExecution(
48+
final @NotNull InstrumentationExecutionParameters parameters) {
49+
final TracingState tracingState = parameters.getInstrumentationState();
50+
tracingState.setTransaction(hub.getSpan());
51+
return super.beginExecution(parameters);
52+
}
53+
54+
@Override
55+
@SuppressWarnings("FutureReturnValueIgnored")
56+
public @NotNull DataFetcher<?> instrumentDataFetcher(
57+
final @NotNull DataFetcher<?> dataFetcher,
58+
final @NotNull InstrumentationFieldFetchParameters parameters) {
59+
// We only care about user code
60+
if (parameters.isTrivialDataFetcher()) {
61+
return dataFetcher;
62+
}
63+
64+
return environment -> {
65+
final TracingState tracingState = parameters.getInstrumentationState();
66+
final ISpan transaction = tracingState.getTransaction();
67+
if (transaction != null) {
68+
final ISpan span = transaction.startChild(findDataFetcherTag(parameters));
69+
try {
70+
final Object result = dataFetcher.get(environment);
71+
if (result instanceof CompletableFuture) {
72+
((CompletableFuture<?>) result)
73+
.whenComplete(
74+
(r, ex) -> {
75+
if (ex != null) {
76+
span.setThrowable(ex);
77+
span.setStatus(SpanStatus.INTERNAL_ERROR);
78+
} else {
79+
span.setStatus(SpanStatus.OK);
80+
}
81+
finish(span, environment, r);
82+
});
83+
} else {
84+
span.setStatus(SpanStatus.OK);
85+
finish(span, environment, result);
86+
}
87+
return result;
88+
} catch (Exception e) {
89+
span.setThrowable(e);
90+
span.setStatus(SpanStatus.INTERNAL_ERROR);
91+
finish(span, environment);
92+
throw e;
93+
}
94+
} else {
95+
return dataFetcher.get(environment);
96+
}
97+
};
98+
}
99+
100+
private void finish(
101+
final @NotNull ISpan span,
102+
final @NotNull DataFetchingEnvironment environment,
103+
final @Nullable Object result) {
104+
if (beforeSpan != null) {
105+
final ISpan newSpan = beforeSpan.execute(span, environment, result);
106+
if (newSpan == null) {
107+
// span is dropped
108+
span.getSpanContext().setSampled(false);
109+
} else {
110+
newSpan.finish();
111+
}
112+
} else {
113+
span.finish();
114+
}
115+
}
116+
117+
private void finish(
118+
final @NotNull ISpan span, final @NotNull DataFetchingEnvironment environment) {
119+
finish(span, environment, null);
120+
}
121+
122+
private @NotNull String findDataFetcherTag(
123+
final @NotNull InstrumentationFieldFetchParameters parameters) {
124+
final GraphQLOutputType type = parameters.getExecutionStepInfo().getParent().getType();
125+
GraphQLObjectType parent;
126+
if (type instanceof GraphQLNonNull) {
127+
parent = (GraphQLObjectType) ((GraphQLNonNull) type).getWrappedType();
128+
} else {
129+
parent = (GraphQLObjectType) type;
130+
}
131+
132+
return parent.getName() + "." + parameters.getExecutionStepInfo().getPath().getSegmentName();
133+
}
134+
135+
static final class TracingState implements InstrumentationState {
136+
private @Nullable ISpan transaction;
137+
138+
public @Nullable ISpan getTransaction() {
139+
return transaction;
140+
}
141+
142+
public void setTransaction(final @Nullable ISpan transaction) {
143+
this.transaction = transaction;
144+
}
145+
}
146+
147+
@FunctionalInterface
148+
public interface BeforeSpanCallback {
149+
@Nullable
150+
ISpan execute(
151+
@NotNull ISpan span, @NotNull DataFetchingEnvironment environment, @Nullable Object result);
152+
}
153+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.sentry.graphql
2+
3+
import com.nhaarman.mockitokotlin2.mock
4+
import com.nhaarman.mockitokotlin2.verify
5+
import graphql.execution.DataFetcherExceptionHandler
6+
import graphql.execution.DataFetcherExceptionHandlerParameters
7+
import io.sentry.IHub
8+
import kotlin.test.Test
9+
10+
class SentryDataFetcherExceptionHandlerTest {
11+
12+
@Test
13+
fun `passes exception to Sentry and invokes delegate`() {
14+
val hub = mock<IHub>()
15+
val delegate = mock<DataFetcherExceptionHandler>()
16+
val handler = SentryDataFetcherExceptionHandler(hub, delegate)
17+
18+
val exception = RuntimeException()
19+
val parameters = DataFetcherExceptionHandlerParameters.newExceptionParameters().exception(exception).build()
20+
handler.onException(parameters)
21+
22+
verify(hub).captureException(exception, parameters)
23+
verify(delegate).onException(parameters)
24+
}
25+
}

0 commit comments

Comments
 (0)