Skip to content

Commit

Permalink
Feat: Add graphql-java instrumentation. (#1777)
Browse files Browse the repository at this point in the history
* Feat: Add `graphql-java` instrumentation.

Fixes #1755
  • Loading branch information
maciejwalkowiak authored Nov 8, 2021
1 parent 2d8a74a commit 95fa93c
Show file tree
Hide file tree
Showing 21 changed files with 704 additions and 5 deletions.
1 change: 1 addition & 0 deletions .craft.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ targets:
maven:io.sentry:sentry-openfeign:
maven:io.sentry:sentry-apollo:
maven:io.sentry:sentry-jdbc:
maven:io.sentry:sentry-graphql-java:
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

* Feat: Add `graphql-java` instrumentation (#1777)

## 5.3.0

* Feat: Add datasource tracing with P6Spy (#1784)
Expand Down
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ apiValidation {
"sentry-samples-servlet",
"sentry-samples-spring",
"sentry-samples-spring-boot",
"sentry-samples-spring-boot-webflux"
"sentry-samples-spring-boot-webflux",
"sentry-samples-netflix-dgs",
)
)
}
Expand Down
4 changes: 4 additions & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ object Config {
val apolloCoroutines = "com.apollographql.apollo:apollo-coroutines-support:$apolloVersion"

val p6spy = "p6spy:p6spy:3.9.1"

val graphQlJava = "com.graphql-java:graphql-java:17.3"

val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect"
}

object AnnotationProcessors {
Expand Down
19 changes: 19 additions & 0 deletions sentry-graphql-java/api/sentry-graphql-java.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
public final class io/sentry/graphql/SentryDataFetcherExceptionHandler : graphql/execution/DataFetcherExceptionHandler {
public fun <init> (Lgraphql/execution/DataFetcherExceptionHandler;)V
public fun <init> (Lio/sentry/IHub;Lgraphql/execution/DataFetcherExceptionHandler;)V
public fun onException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Lgraphql/execution/DataFetcherExceptionHandlerResult;
}

public final class io/sentry/graphql/SentryInstrumentation : graphql/execution/instrumentation/SimpleInstrumentation {
public fun <init> ()V
public fun <init> (Lio/sentry/IHub;)V
public fun <init> (Lio/sentry/IHub;Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;)V
public fun beginExecution (Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;)Lgraphql/execution/instrumentation/InstrumentationContext;
public fun createState ()Lgraphql/execution/instrumentation/InstrumentationState;
public fun instrumentDataFetcher (Lgraphql/schema/DataFetcher;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Lgraphql/schema/DataFetcher;
}

public abstract interface class io/sentry/graphql/SentryInstrumentation$BeforeSpanCallback {
public abstract fun execute (Lio/sentry/ISpan;Lgraphql/schema/DataFetchingEnvironment;Ljava/lang/Object;)Lio/sentry/ISpan;
}

76 changes: 76 additions & 0 deletions sentry-graphql-java/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import net.ltgt.gradle.errorprone.errorprone
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
`java-library`
kotlin("jvm")
jacoco
id(Config.QualityPlugins.errorProne)
id(Config.QualityPlugins.gradleVersions)
}

configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
kotlinOptions.languageVersion = Config.springKotlinCompatibleLanguageVersion
}

dependencies {
api(projects.sentry)
implementation(Config.Libs.graphQlJava)

compileOnly(Config.CompileOnly.nopen)
errorprone(Config.CompileOnly.nopenChecker)
errorprone(Config.CompileOnly.errorprone)
errorprone(Config.CompileOnly.errorProneNullAway)
compileOnly(Config.CompileOnly.jetbrainsAnnotations)

// tests
testImplementation(projects.sentry)
testImplementation(projects.sentryTestSupport)
testImplementation(kotlin(Config.kotlinStdLib))
testImplementation(Config.TestLibs.kotlinTestJunit)
testImplementation(Config.TestLibs.mockitoKotlin)
testImplementation(Config.TestLibs.mockWebserver)
testImplementation(Config.Libs.okhttp)
}

configure<SourceSetContainer> {
test {
java.srcDir("src/test/java")
}
}

jacoco {
toolVersion = Config.QualityPlugins.Jacoco.version
}

tasks.jacocoTestReport {
reports {
xml.required.set(true)
html.required.set(false)
}
}

tasks {
jacocoTestCoverageVerification {
violationRules {
rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } }
}
}
check {
dependsOn(jacocoTestCoverageVerification)
dependsOn(jacocoTestReport)
}
}

tasks.withType<JavaCompile>().configureEach {
options.errorprone {
check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR)
option("NullAway:AnnotatedPackages", "io.sentry")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.sentry.graphql;

import graphql.execution.DataFetcherExceptionHandler;
import graphql.execution.DataFetcherExceptionHandlerParameters;
import graphql.execution.DataFetcherExceptionHandlerResult;
import io.sentry.HubAdapter;
import io.sentry.IHub;
import io.sentry.util.Objects;
import org.jetbrains.annotations.NotNull;

/**
* Captures exceptions that occur during data fetching, passes them to Sentry and invokes a delegate
* exception handler.
*/
public final class SentryDataFetcherExceptionHandler implements DataFetcherExceptionHandler {
private final @NotNull IHub hub;
private final @NotNull DataFetcherExceptionHandler delegate;

public SentryDataFetcherExceptionHandler(
final @NotNull IHub hub, final @NotNull DataFetcherExceptionHandler delegate) {
this.hub = Objects.requireNonNull(hub, "hub is required");
this.delegate = Objects.requireNonNull(delegate, "delegate is required");
}

public SentryDataFetcherExceptionHandler(final @NotNull DataFetcherExceptionHandler delegate) {
this(HubAdapter.getInstance(), delegate);
}

@Override
@SuppressWarnings("deprecation")
public DataFetcherExceptionHandlerResult onException(
final @NotNull DataFetcherExceptionHandlerParameters handlerParameters) {
hub.captureException(handlerParameters.getException(), handlerParameters);
return delegate.onException(handlerParameters);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package io.sentry.graphql;

import graphql.ExecutionResult;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.execution.instrumentation.SimpleInstrumentation;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.GraphQLNonNull;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLOutputType;
import io.sentry.HubAdapter;
import io.sentry.IHub;
import io.sentry.ISpan;
import io.sentry.SpanStatus;
import io.sentry.util.Objects;
import java.util.concurrent.CompletableFuture;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class SentryInstrumentation extends SimpleInstrumentation {
private final @NotNull IHub hub;
private final @Nullable BeforeSpanCallback beforeSpan;

public SentryInstrumentation(
final @NotNull IHub hub, final @Nullable BeforeSpanCallback beforeSpan) {
this.hub = Objects.requireNonNull(hub, "hub is required");
this.beforeSpan = beforeSpan;
}

public SentryInstrumentation(final @NotNull IHub hub) {
this(hub, null);
}

public SentryInstrumentation() {
this(HubAdapter.getInstance());
}

@Override
public @NotNull InstrumentationState createState() {
return new TracingState();
}

@Override
public @NotNull InstrumentationContext<ExecutionResult> beginExecution(
final @NotNull InstrumentationExecutionParameters parameters) {
final TracingState tracingState = parameters.getInstrumentationState();
tracingState.setTransaction(hub.getSpan());
return super.beginExecution(parameters);
}

@Override
@SuppressWarnings("FutureReturnValueIgnored")
public @NotNull DataFetcher<?> instrumentDataFetcher(
final @NotNull DataFetcher<?> dataFetcher,
final @NotNull InstrumentationFieldFetchParameters parameters) {
// We only care about user code
if (parameters.isTrivialDataFetcher()) {
return dataFetcher;
}

return environment -> {
final TracingState tracingState = parameters.getInstrumentationState();
final ISpan transaction = tracingState.getTransaction();
if (transaction != null) {
final ISpan span = transaction.startChild(findDataFetcherTag(parameters));
try {
final Object result = dataFetcher.get(environment);
if (result instanceof CompletableFuture) {
((CompletableFuture<?>) result)
.whenComplete(
(r, ex) -> {
if (ex != null) {
span.setThrowable(ex);
span.setStatus(SpanStatus.INTERNAL_ERROR);
} else {
span.setStatus(SpanStatus.OK);
}
finish(span, environment, r);
});
} else {
span.setStatus(SpanStatus.OK);
finish(span, environment, result);
}
return result;
} catch (Exception e) {
span.setThrowable(e);
span.setStatus(SpanStatus.INTERNAL_ERROR);
finish(span, environment);
throw e;
}
} else {
return dataFetcher.get(environment);
}
};
}

private void finish(
final @NotNull ISpan span,
final @NotNull DataFetchingEnvironment environment,
final @Nullable Object result) {
if (beforeSpan != null) {
final ISpan newSpan = beforeSpan.execute(span, environment, result);
if (newSpan == null) {
// span is dropped
span.getSpanContext().setSampled(false);
} else {
newSpan.finish();
}
} else {
span.finish();
}
}

private void finish(
final @NotNull ISpan span, final @NotNull DataFetchingEnvironment environment) {
finish(span, environment, null);
}

private @NotNull String findDataFetcherTag(
final @NotNull InstrumentationFieldFetchParameters parameters) {
final GraphQLOutputType type = parameters.getExecutionStepInfo().getParent().getType();
GraphQLObjectType parent;
if (type instanceof GraphQLNonNull) {
parent = (GraphQLObjectType) ((GraphQLNonNull) type).getWrappedType();
} else {
parent = (GraphQLObjectType) type;
}

return parent.getName() + "." + parameters.getExecutionStepInfo().getPath().getSegmentName();
}

static final class TracingState implements InstrumentationState {
private @Nullable ISpan transaction;

public @Nullable ISpan getTransaction() {
return transaction;
}

public void setTransaction(final @Nullable ISpan transaction) {
this.transaction = transaction;
}
}

@FunctionalInterface
public interface BeforeSpanCallback {
@Nullable
ISpan execute(
@NotNull ISpan span, @NotNull DataFetchingEnvironment environment, @Nullable Object result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.sentry.graphql

import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import graphql.execution.DataFetcherExceptionHandler
import graphql.execution.DataFetcherExceptionHandlerParameters
import io.sentry.IHub
import kotlin.test.Test

class SentryDataFetcherExceptionHandlerTest {

@Test
fun `passes exception to Sentry and invokes delegate`() {
val hub = mock<IHub>()
val delegate = mock<DataFetcherExceptionHandler>()
val handler = SentryDataFetcherExceptionHandler(hub, delegate)

val exception = RuntimeException()
val parameters = DataFetcherExceptionHandlerParameters.newExceptionParameters().exception(exception).build()
handler.onException(parameters)

verify(hub).captureException(exception, parameters)
verify(delegate).onException(parameters)
}
}
Loading

0 comments on commit 95fa93c

Please sign in to comment.