Skip to content

Commit 333fe99

Browse files
committed
Record data fetcher errors in graphql instrumentation
1 parent 83d1cad commit 333fe99

File tree

6 files changed

+125
-15
lines changed

6 files changed

+125
-15
lines changed

instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetry.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ public static GraphQLTelemetryBuilder builder(OpenTelemetry openTelemetry) {
2727
}
2828

2929
private final OpenTelemetryInstrumentationHelper helper;
30-
private final Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter;
30+
private final Instrumenter<DataFetchingEnvironment, Object> dataFetcherInstrumenter;
3131
private final boolean createSpansForTrivialDataFetcher;
3232

3333
GraphQLTelemetry(
3434
OpenTelemetry openTelemetry,
3535
boolean sanitizeQuery,
36-
Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter,
36+
Instrumenter<DataFetchingEnvironment, Object> dataFetcherInstrumenter,
3737
boolean createSpansForTrivialDataFetcher,
3838
boolean addOperationNameToSpanName) {
3939
helper =

instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlDataFetcherAttributesExtractor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import javax.annotation.Nullable;
1414

1515
final class GraphqlDataFetcherAttributesExtractor
16-
implements AttributesExtractor<DataFetchingEnvironment, Void> {
16+
implements AttributesExtractor<DataFetchingEnvironment, Object> {
1717

1818
// NOTE: These are not part of the Semantic Convention and are subject to change
1919
private static final AttributeKey<String> GRAPHQL_FIELD_NAME =
@@ -34,6 +34,6 @@ public void onEnd(
3434
AttributesBuilder attributes,
3535
Context context,
3636
DataFetchingEnvironment environment,
37-
@Nullable Void unused,
37+
@Nullable Object unused,
3838
@Nullable Throwable error) {}
3939
}

instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlInstrumenterFactory.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55

66
package io.opentelemetry.instrumentation.graphql.v20_0;
77

8+
import graphql.execution.DataFetcherResult;
89
import graphql.schema.DataFetchingEnvironment;
910
import io.opentelemetry.api.OpenTelemetry;
11+
import io.opentelemetry.api.trace.StatusCode;
1012
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
13+
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
1114
import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper;
1215

1316
final class GraphqlInstrumenterFactory {
@@ -20,13 +23,23 @@ static OpenTelemetryInstrumentationHelper createInstrumentationHelper(
2023
openTelemetry, INSTRUMENTATION_NAME, sanitizeQuery, addOperationNameToSpanName);
2124
}
2225

23-
static Instrumenter<DataFetchingEnvironment, Void> createDataFetcherInstrumenter(
26+
static Instrumenter<DataFetchingEnvironment, Object> createDataFetcherInstrumenter(
2427
OpenTelemetry openTelemetry, boolean enabled) {
25-
return Instrumenter.<DataFetchingEnvironment, Void>builder(
28+
return Instrumenter.<DataFetchingEnvironment, Object>builder(
2629
openTelemetry,
2730
INSTRUMENTATION_NAME,
2831
environment -> environment.getExecutionStepInfo().getField().getName())
2932
.addAttributesExtractor(new GraphqlDataFetcherAttributesExtractor())
33+
.setSpanStatusExtractor(
34+
(spanStatusBuilder, dataFetchingEnvironment, result, error) -> {
35+
if (result instanceof DataFetcherResult
36+
&& ((DataFetcherResult<?>) result).hasErrors()) {
37+
spanStatusBuilder.setStatus(StatusCode.ERROR);
38+
} else {
39+
SpanStatusExtractor.getDefault()
40+
.extract(spanStatusBuilder, dataFetchingEnvironment, result, error);
41+
}
42+
})
3043
.setEnabled(enabled)
3144
.buildInstrumenter();
3245
}

instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentation.java

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import static graphql.execution.instrumentation.InstrumentationState.ofState;
99

1010
import graphql.ExecutionResult;
11+
import graphql.GraphQLError;
12+
import graphql.execution.DataFetcherResult;
1113
import graphql.execution.ResultPath;
1214
import graphql.execution.instrumentation.InstrumentationContext;
1315
import graphql.execution.instrumentation.InstrumentationState;
@@ -18,21 +20,25 @@
1820
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
1921
import graphql.schema.DataFetcher;
2022
import graphql.schema.DataFetchingEnvironment;
23+
import io.opentelemetry.api.common.Attributes;
24+
import io.opentelemetry.api.common.AttributesBuilder;
25+
import io.opentelemetry.api.trace.Span;
2126
import io.opentelemetry.context.Context;
2227
import io.opentelemetry.context.Scope;
2328
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
2429
import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper;
2530
import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationState;
31+
import io.opentelemetry.semconv.ExceptionAttributes;
2632
import java.util.concurrent.CompletionStage;
2733

2834
final class OpenTelemetryInstrumentation extends SimplePerformantInstrumentation {
2935
private final OpenTelemetryInstrumentationHelper helper;
30-
private final Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter;
36+
private final Instrumenter<DataFetchingEnvironment, Object> dataFetcherInstrumenter;
3137
private final boolean createSpansForTrivialDataFetcher;
3238

3339
OpenTelemetryInstrumentation(
3440
OpenTelemetryInstrumentationHelper helper,
35-
Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter,
41+
Instrumenter<DataFetchingEnvironment, Object> dataFetcherInstrumenter,
3642
boolean createSpansForTrivialDataFetcher) {
3743
this.helper = helper;
3844
this.dataFetcherInstrumenter = dataFetcherInstrumenter;
@@ -84,28 +90,45 @@ public DataFetcher<?> instrumentDataFetcher(
8490

8591
boolean isCompletionStage = false;
8692

93+
Object fieldValue = null;
8794
try (Scope ignored = childContext.makeCurrent()) {
88-
Object fieldValue = dataFetcher.get(environment);
89-
95+
fieldValue = dataFetcher.get(environment);
9096
isCompletionStage = fieldValue instanceof CompletionStage;
9197

9298
if (isCompletionStage) {
9399
return ((CompletionStage<?>) fieldValue)
94100
.whenComplete(
95-
(result, throwable) ->
96-
dataFetcherInstrumenter.end(childContext, environment, null, throwable));
101+
(result, throwable) -> {
102+
handleDataFetcherResult(childContext, result);
103+
dataFetcherInstrumenter.end(childContext, environment, result, throwable);
104+
});
97105
}
98-
99106
return fieldValue;
100-
101107
} catch (Throwable throwable) {
102108
dataFetcherInstrumenter.end(childContext, environment, null, throwable);
103109
throw throwable;
104110
} finally {
105111
if (!isCompletionStage) {
106-
dataFetcherInstrumenter.end(childContext, environment, null, null);
112+
handleDataFetcherResult(childContext, fieldValue);
113+
dataFetcherInstrumenter.end(childContext, environment, fieldValue, null);
107114
}
108115
}
109116
};
110117
}
118+
119+
private static void handleDataFetcherResult(Context context, Object result) {
120+
if (!(result instanceof DataFetcherResult)) {
121+
return;
122+
}
123+
124+
DataFetcherResult<?> dataFetcherResult = (DataFetcherResult<?>) result;
125+
Span span = Span.fromContext(context);
126+
for (GraphQLError error : dataFetcherResult.getErrors()) {
127+
AttributesBuilder attributes = Attributes.builder();
128+
attributes.put(ExceptionAttributes.EXCEPTION_TYPE, String.valueOf(error.getErrorType()));
129+
attributes.put(ExceptionAttributes.EXCEPTION_MESSAGE, error.getMessage());
130+
131+
span.addEvent("exception", attributes.build());
132+
}
133+
}
111134
}

instrumentation/graphql-java/graphql-java-20.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
1717
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
1818
import io.opentelemetry.sdk.trace.data.SpanData;
19+
import io.opentelemetry.sdk.trace.data.StatusData;
20+
import io.opentelemetry.semconv.ExceptionAttributes;
1921
import io.opentelemetry.semconv.incubating.GraphqlIncubatingAttributes;
2022
import org.junit.jupiter.api.Test;
2123
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -239,6 +241,75 @@ void noDataFetcherSpansCreated() {
239241
.hasParent(spanWithName("query findBookById"))));
240242
}
241243

244+
@Test
245+
void dataFetcherError() {
246+
// Arrange
247+
GraphQLTelemetry telemetry =
248+
GraphQLTelemetry.builder(testing.getOpenTelemetry())
249+
.setDataFetcherInstrumentationEnabled(true)
250+
.setAddOperationNameToSpanName(true)
251+
.build();
252+
253+
GraphQL graphql =
254+
GraphQL.newGraphQL(graphqlSchema).instrumentation(telemetry.newInstrumentation()).build();
255+
256+
// Act
257+
ExecutionResult result =
258+
graphql.execute(
259+
""
260+
+ " query findBookById {\n"
261+
+ " bookById(id: \"book-failure\") {\n"
262+
+ " name\n"
263+
+ " author {\n"
264+
+ " name\n"
265+
+ " }\n"
266+
+ " }\n"
267+
+ " }");
268+
269+
// Assert
270+
assertThat(result.getErrors()).isNotEmpty();
271+
272+
testing.waitAndAssertTraces(
273+
trace ->
274+
trace.hasSpansSatisfyingExactly(
275+
span ->
276+
span.hasName("query findBookById")
277+
.hasKind(SpanKind.INTERNAL)
278+
.hasNoParent()
279+
.hasAttributesSatisfyingExactly(
280+
equalTo(
281+
GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME, "findBookById"),
282+
equalTo(GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "query"),
283+
normalizedQueryEqualsTo(
284+
GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT,
285+
"query findBookById { bookById(id: ?) { name author { name } } }"))
286+
.hasStatus(StatusData.error())
287+
.hasEventsSatisfyingExactly(
288+
event ->
289+
event
290+
.hasName("exception")
291+
.hasAttributesSatisfyingExactly(
292+
equalTo(
293+
ExceptionAttributes.EXCEPTION_TYPE,
294+
"DataFetchingException"),
295+
equalTo(
296+
ExceptionAttributes.EXCEPTION_MESSAGE,
297+
"Exception while fetching data (/bookById) : fetching book failed"))),
298+
span ->
299+
span.hasName("bookById")
300+
.hasKind(SpanKind.INTERNAL)
301+
.hasParent(spanWithName("query findBookById"))
302+
.hasAttributesSatisfyingExactly(
303+
equalTo(GRAPHQL_FIELD_NAME, "bookById"),
304+
equalTo(GRAPHQL_FIELD_PATH, "/bookById")),
305+
span ->
306+
span.hasName("fetchBookById")
307+
.hasKind(SpanKind.INTERNAL)
308+
.hasParent(spanWithName("bookById"))
309+
.hasStatus(StatusData.error())
310+
.hasException(new IllegalStateException("fetching book failed"))));
311+
}
312+
242313
private static SpanData spanWithName(String name) {
243314
return testing.spans().stream()
244315
.filter(span -> span.getName().equals(name))

instrumentation/graphql-java/graphql-java-common/testing/src/main/java/io/opentelemetry/instrumentation/graphql/AbstractGraphqlTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ private DataFetcher<Map<String, String>> getBookByIdDataFetcher() {
121121
"fetchBookById",
122122
() -> {
123123
String bookId = dataFetchingEnvironment.getArgument("id");
124+
if ("book-failure".equals(bookId)) {
125+
throw new IllegalStateException("fetching book failed");
126+
}
124127
return books.stream()
125128
.filter(book -> book.get("id").equals(bookId))
126129
.findFirst()

0 commit comments

Comments
 (0)