Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ public static GraphQLTelemetryBuilder builder(OpenTelemetry openTelemetry) {
}

private final OpenTelemetryInstrumentationHelper helper;
private final Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter;
private final Instrumenter<DataFetchingEnvironment, Object> dataFetcherInstrumenter;
private final boolean createSpansForTrivialDataFetcher;

GraphQLTelemetry(
OpenTelemetry openTelemetry,
boolean sanitizeQuery,
Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter,
Instrumenter<DataFetchingEnvironment, Object> dataFetcherInstrumenter,
boolean createSpansForTrivialDataFetcher,
boolean addOperationNameToSpanName) {
helper =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import javax.annotation.Nullable;

final class GraphqlDataFetcherAttributesExtractor
implements AttributesExtractor<DataFetchingEnvironment, Void> {
implements AttributesExtractor<DataFetchingEnvironment, Object> {

// NOTE: These are not part of the Semantic Convention and are subject to change
private static final AttributeKey<String> GRAPHQL_FIELD_NAME =
Expand All @@ -34,6 +34,6 @@ public void onEnd(
AttributesBuilder attributes,
Context context,
DataFetchingEnvironment environment,
@Nullable Void unused,
@Nullable Object unused,
@Nullable Throwable error) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -20,13 +23,23 @@ static OpenTelemetryInstrumentationHelper createInstrumentationHelper(
openTelemetry, INSTRUMENTATION_NAME, sanitizeQuery, addOperationNameToSpanName);
}

static Instrumenter<DataFetchingEnvironment, Void> createDataFetcherInstrumenter(
static Instrumenter<DataFetchingEnvironment, Object> createDataFetcherInstrumenter(
OpenTelemetry openTelemetry, boolean enabled) {
return Instrumenter.<DataFetchingEnvironment, Void>builder(
return Instrumenter.<DataFetchingEnvironment, Object>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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<DataFetchingEnvironment, Void> dataFetcherInstrumenter;
private final Instrumenter<DataFetchingEnvironment, Object> dataFetcherInstrumenter;
private final boolean createSpansForTrivialDataFetcher;

OpenTelemetryInstrumentation(
OpenTelemetryInstrumentationHelper helper,
Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter,
Instrumenter<DataFetchingEnvironment, Object> dataFetcherInstrumenter,
boolean createSpansForTrivialDataFetcher) {
this.helper = helper;
this.dataFetcherInstrumenter = dataFetcherInstrumenter;
Expand Down Expand Up @@ -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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -114,17 +116,32 @@ private RuntimeWiring buildWiring() {
.build();
}

private DataFetcher<Map<String, String>> getBookByIdDataFetcher() {
private DataFetcher<DataFetcherResult<Map<String, String>>> 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<Map<String, String>> 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();
});
}

Expand Down
Loading