diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetry.java b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetry.java index bc2b1ceed1d0..98905483858d 100644 --- a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetry.java +++ b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetry.java @@ -27,13 +27,13 @@ public static GraphQLTelemetryBuilder builder(OpenTelemetry openTelemetry) { } private final OpenTelemetryInstrumentationHelper helper; - private final Instrumenter dataFetcherInstrumenter; + private final Instrumenter dataFetcherInstrumenter; private final boolean createSpansForTrivialDataFetcher; GraphQLTelemetry( OpenTelemetry openTelemetry, boolean sanitizeQuery, - Instrumenter dataFetcherInstrumenter, + Instrumenter dataFetcherInstrumenter, boolean createSpansForTrivialDataFetcher, boolean addOperationNameToSpanName) { helper = diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlDataFetcherAttributesExtractor.java b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlDataFetcherAttributesExtractor.java index 59cf72aa7c13..c5d4a4f6bf35 100644 --- a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlDataFetcherAttributesExtractor.java +++ b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlDataFetcherAttributesExtractor.java @@ -13,7 +13,7 @@ import javax.annotation.Nullable; final class GraphqlDataFetcherAttributesExtractor - implements AttributesExtractor { + implements AttributesExtractor { // NOTE: These are not part of the Semantic Convention and are subject to change private static final AttributeKey GRAPHQL_FIELD_NAME = @@ -34,6 +34,6 @@ public void onEnd( AttributesBuilder attributes, Context context, DataFetchingEnvironment environment, - @Nullable Void unused, + @Nullable Object unused, @Nullable Throwable error) {} } diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlInstrumenterFactory.java b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlInstrumenterFactory.java index 1c2cc153da04..76414aed3281 100644 --- a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlInstrumenterFactory.java +++ b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlInstrumenterFactory.java @@ -5,9 +5,12 @@ package io.opentelemetry.instrumentation.graphql.v20_0; +import graphql.execution.DataFetcherResult; import graphql.schema.DataFetchingEnvironment; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper; final class GraphqlInstrumenterFactory { @@ -20,13 +23,23 @@ static OpenTelemetryInstrumentationHelper createInstrumentationHelper( openTelemetry, INSTRUMENTATION_NAME, sanitizeQuery, addOperationNameToSpanName); } - static Instrumenter createDataFetcherInstrumenter( + static Instrumenter createDataFetcherInstrumenter( OpenTelemetry openTelemetry, boolean enabled) { - return Instrumenter.builder( + return Instrumenter.builder( openTelemetry, INSTRUMENTATION_NAME, environment -> environment.getExecutionStepInfo().getField().getName()) .addAttributesExtractor(new GraphqlDataFetcherAttributesExtractor()) + .setSpanStatusExtractor( + (spanStatusBuilder, dataFetchingEnvironment, result, error) -> { + if (result instanceof DataFetcherResult + && ((DataFetcherResult) result).hasErrors()) { + spanStatusBuilder.setStatus(StatusCode.ERROR); + } else { + SpanStatusExtractor.getDefault() + .extract(spanStatusBuilder, dataFetchingEnvironment, result, error); + } + }) .setEnabled(enabled) .buildInstrumenter(); } diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentation.java b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentation.java index 03dc358f1f0c..6529aaa1a0a4 100644 --- a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentation.java +++ b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentation.java @@ -8,6 +8,8 @@ import static graphql.execution.instrumentation.InstrumentationState.ofState; import graphql.ExecutionResult; +import graphql.GraphQLError; +import graphql.execution.DataFetcherResult; import graphql.execution.ResultPath; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; @@ -18,21 +20,25 @@ import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper; import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationState; +import io.opentelemetry.semconv.ExceptionAttributes; import java.util.concurrent.CompletionStage; final class OpenTelemetryInstrumentation extends SimplePerformantInstrumentation { private final OpenTelemetryInstrumentationHelper helper; - private final Instrumenter dataFetcherInstrumenter; + private final Instrumenter dataFetcherInstrumenter; private final boolean createSpansForTrivialDataFetcher; OpenTelemetryInstrumentation( OpenTelemetryInstrumentationHelper helper, - Instrumenter dataFetcherInstrumenter, + Instrumenter dataFetcherInstrumenter, boolean createSpansForTrivialDataFetcher) { this.helper = helper; this.dataFetcherInstrumenter = dataFetcherInstrumenter; @@ -84,28 +90,45 @@ public DataFetcher instrumentDataFetcher( boolean isCompletionStage = false; + Object fieldValue = null; try (Scope ignored = childContext.makeCurrent()) { - Object fieldValue = dataFetcher.get(environment); - + fieldValue = dataFetcher.get(environment); isCompletionStage = fieldValue instanceof CompletionStage; if (isCompletionStage) { return ((CompletionStage) fieldValue) .whenComplete( - (result, throwable) -> - dataFetcherInstrumenter.end(childContext, environment, null, throwable)); + (result, throwable) -> { + handleDataFetcherResult(childContext, result); + dataFetcherInstrumenter.end(childContext, environment, result, throwable); + }); } - return fieldValue; - } catch (Throwable throwable) { dataFetcherInstrumenter.end(childContext, environment, null, throwable); throw throwable; } finally { if (!isCompletionStage) { - dataFetcherInstrumenter.end(childContext, environment, null, null); + handleDataFetcherResult(childContext, fieldValue); + dataFetcherInstrumenter.end(childContext, environment, fieldValue, null); } } }; } + + private static void handleDataFetcherResult(Context context, Object result) { + if (!(result instanceof DataFetcherResult)) { + return; + } + + DataFetcherResult dataFetcherResult = (DataFetcherResult) result; + Span span = Span.fromContext(context); + for (GraphQLError error : dataFetcherResult.getErrors()) { + AttributesBuilder attributes = Attributes.builder(); + attributes.put(ExceptionAttributes.EXCEPTION_TYPE, String.valueOf(error.getErrorType())); + attributes.put(ExceptionAttributes.EXCEPTION_MESSAGE, error.getMessage()); + + span.addEvent("exception", attributes.build()); + } + } } diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlTest.java b/instrumentation/graphql-java/graphql-java-20.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlTest.java index 792235f51e40..09eaeee136b5 100644 --- a/instrumentation/graphql-java/graphql-java-20.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlTest.java +++ b/instrumentation/graphql-java/graphql-java-20.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlTest.java @@ -16,6 +16,8 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ExceptionAttributes; import io.opentelemetry.semconv.incubating.GraphqlIncubatingAttributes; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -239,6 +241,160 @@ void noDataFetcherSpansCreated() { .hasParent(spanWithName("query findBookById")))); } + // test data fetcher throwing an exception + @Test + void dataFetcherException() { + // Arrange + GraphQLTelemetry telemetry = + GraphQLTelemetry.builder(testing.getOpenTelemetry()) + .setDataFetcherInstrumentationEnabled(true) + .setAddOperationNameToSpanName(true) + .build(); + + GraphQL graphql = + GraphQL.newGraphQL(graphqlSchema).instrumentation(telemetry.newInstrumentation()).build(); + + // Act + // book-exception triggers exception in data fetcher + ExecutionResult result = + graphql.execute( + "" + + " query findBookById {\n" + + " bookById(id: \"book-exception\") {\n" + + " name\n" + + " author {\n" + + " name\n" + + " }\n" + + " }\n" + + " }"); + + // Assert + assertThat(result.getErrors()).isNotEmpty(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("query findBookById") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo( + GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME, "findBookById"), + equalTo(GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "query"), + normalizedQueryEqualsTo( + GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT, + "query findBookById { bookById(id: ?) { name author { name } } }")) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + "DataFetchingException"), + equalTo( + ExceptionAttributes.EXCEPTION_MESSAGE, + "Exception while fetching data (/bookById) : fetching book failed"))), + span -> + span.hasName("bookById") + .hasKind(SpanKind.INTERNAL) + .hasParent(spanWithName("query findBookById")) + .hasAttributesSatisfyingExactly( + equalTo(GRAPHQL_FIELD_NAME, "bookById"), + equalTo(GRAPHQL_FIELD_PATH, "/bookById")) + .hasStatus(StatusData.error()) + .hasException(new IllegalStateException("fetching book failed")), + span -> + span.hasName("fetchBookById") + .hasKind(SpanKind.INTERNAL) + .hasParent(spanWithName("bookById")) + .hasStatus(StatusData.error()) + .hasException(new IllegalStateException("fetching book failed")))); + } + + // test data fetcher returning an error + @Test + void dataFetcherError() { + // Arrange + GraphQLTelemetry telemetry = + GraphQLTelemetry.builder(testing.getOpenTelemetry()) + .setDataFetcherInstrumentationEnabled(true) + .setAddOperationNameToSpanName(true) + .build(); + + GraphQL graphql = + GraphQL.newGraphQL(graphqlSchema).instrumentation(telemetry.newInstrumentation()).build(); + + // Act + // book-graphql-error triggers returning an error from data fetcher + ExecutionResult result = + graphql.execute( + "" + + " query findBookById {\n" + + " bookById(id: \"book-graphql-error\") {\n" + + " name\n" + + " author {\n" + + " name\n" + + " }\n" + + " }\n" + + " }"); + + // Assert + assertThat(result.getErrors()).isNotEmpty(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("query findBookById") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo( + GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME, "findBookById"), + equalTo(GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "query"), + normalizedQueryEqualsTo( + GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT, + "query findBookById { bookById(id: ?) { name author { name } } }")) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + "DataFetchingException"), + equalTo( + ExceptionAttributes.EXCEPTION_MESSAGE, + "failed to fetch book"))), + span -> + span.hasName("bookById") + .hasKind(SpanKind.INTERNAL) + .hasParent(spanWithName("query findBookById")) + .hasAttributesSatisfyingExactly( + equalTo(GRAPHQL_FIELD_NAME, "bookById"), + equalTo(GRAPHQL_FIELD_PATH, "/bookById")) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + "DataFetchingException"), + equalTo( + ExceptionAttributes.EXCEPTION_MESSAGE, + "failed to fetch book"))), + span -> + span.hasName("fetchBookById") + .hasKind(SpanKind.INTERNAL) + .hasParent(spanWithName("bookById")))); + } + private static SpanData spanWithName(String name) { return testing.spans().stream() .filter(span -> span.getName().equals(name)) diff --git a/instrumentation/graphql-java/graphql-java-common/testing/src/main/java/io/opentelemetry/instrumentation/graphql/AbstractGraphqlTest.java b/instrumentation/graphql-java/graphql-java-common/testing/src/main/java/io/opentelemetry/instrumentation/graphql/AbstractGraphqlTest.java index 133610aa75dc..e0b40616d7ae 100644 --- a/instrumentation/graphql-java/graphql-java-common/testing/src/main/java/io/opentelemetry/instrumentation/graphql/AbstractGraphqlTest.java +++ b/instrumentation/graphql-java/graphql-java-common/testing/src/main/java/io/opentelemetry/instrumentation/graphql/AbstractGraphqlTest.java @@ -14,6 +14,8 @@ import graphql.ExecutionResult; import graphql.GraphQL; +import graphql.GraphqlErrorBuilder; +import graphql.execution.DataFetcherResult; import graphql.schema.DataFetcher; import graphql.schema.GraphQLSchema; import graphql.schema.idl.RuntimeWiring; @@ -114,17 +116,32 @@ private RuntimeWiring buildWiring() { .build(); } - private DataFetcher> getBookByIdDataFetcher() { + private DataFetcher>> getBookByIdDataFetcher() { return dataFetchingEnvironment -> getTesting() .runWithSpan( "fetchBookById", () -> { String bookId = dataFetchingEnvironment.getArgument("id"); - return books.stream() - .filter(book -> book.get("id").equals(bookId)) - .findFirst() - .orElse(null); + DataFetcherResult.Builder> builder = + DataFetcherResult.newResult(); + if ("book-exception".equals(bookId)) { + throw new IllegalStateException("fetching book failed"); + } else if ("book-graphql-error".equals(bookId)) { + return builder + .error( + GraphqlErrorBuilder.newError(dataFetchingEnvironment) + .message("failed to fetch book") + .build()) + .build(); + } + return builder + .data( + books.stream() + .filter(book -> book.get("id").equals(bookId)) + .findFirst() + .orElse(null)) + .build(); }); }