Skip to content

Commit a8d9bba

Browse files
authored
Record data fetcher errors in graphql instrumentation (#15289)
1 parent 76d4c6f commit a8d9bba

File tree

6 files changed

+229
-20
lines changed

6 files changed

+229
-20
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: 156 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,160 @@ void noDataFetcherSpansCreated() {
239241
.hasParent(spanWithName("query findBookById"))));
240242
}
241243

244+
// test data fetcher throwing an exception
245+
@Test
246+
void dataFetcherException() {
247+
// Arrange
248+
GraphQLTelemetry telemetry =
249+
GraphQLTelemetry.builder(testing.getOpenTelemetry())
250+
.setDataFetcherInstrumentationEnabled(true)
251+
.setAddOperationNameToSpanName(true)
252+
.build();
253+
254+
GraphQL graphql =
255+
GraphQL.newGraphQL(graphqlSchema).instrumentation(telemetry.newInstrumentation()).build();
256+
257+
// Act
258+
// book-exception triggers exception in data fetcher
259+
ExecutionResult result =
260+
graphql.execute(
261+
""
262+
+ " query findBookById {\n"
263+
+ " bookById(id: \"book-exception\") {\n"
264+
+ " name\n"
265+
+ " author {\n"
266+
+ " name\n"
267+
+ " }\n"
268+
+ " }\n"
269+
+ " }");
270+
271+
// Assert
272+
assertThat(result.getErrors()).isNotEmpty();
273+
274+
testing.waitAndAssertTraces(
275+
trace ->
276+
trace.hasSpansSatisfyingExactly(
277+
span ->
278+
span.hasName("query findBookById")
279+
.hasKind(SpanKind.INTERNAL)
280+
.hasNoParent()
281+
.hasAttributesSatisfyingExactly(
282+
equalTo(
283+
GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME, "findBookById"),
284+
equalTo(GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "query"),
285+
normalizedQueryEqualsTo(
286+
GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT,
287+
"query findBookById { bookById(id: ?) { name author { name } } }"))
288+
.hasStatus(StatusData.error())
289+
.hasEventsSatisfyingExactly(
290+
event ->
291+
event
292+
.hasName("exception")
293+
.hasAttributesSatisfyingExactly(
294+
equalTo(
295+
ExceptionAttributes.EXCEPTION_TYPE,
296+
"DataFetchingException"),
297+
equalTo(
298+
ExceptionAttributes.EXCEPTION_MESSAGE,
299+
"Exception while fetching data (/bookById) : fetching book failed"))),
300+
span ->
301+
span.hasName("bookById")
302+
.hasKind(SpanKind.INTERNAL)
303+
.hasParent(spanWithName("query findBookById"))
304+
.hasAttributesSatisfyingExactly(
305+
equalTo(GRAPHQL_FIELD_NAME, "bookById"),
306+
equalTo(GRAPHQL_FIELD_PATH, "/bookById"))
307+
.hasStatus(StatusData.error())
308+
.hasException(new IllegalStateException("fetching book failed")),
309+
span ->
310+
span.hasName("fetchBookById")
311+
.hasKind(SpanKind.INTERNAL)
312+
.hasParent(spanWithName("bookById"))
313+
.hasStatus(StatusData.error())
314+
.hasException(new IllegalStateException("fetching book failed"))));
315+
}
316+
317+
// test data fetcher returning an error
318+
@Test
319+
void dataFetcherError() {
320+
// Arrange
321+
GraphQLTelemetry telemetry =
322+
GraphQLTelemetry.builder(testing.getOpenTelemetry())
323+
.setDataFetcherInstrumentationEnabled(true)
324+
.setAddOperationNameToSpanName(true)
325+
.build();
326+
327+
GraphQL graphql =
328+
GraphQL.newGraphQL(graphqlSchema).instrumentation(telemetry.newInstrumentation()).build();
329+
330+
// Act
331+
// book-graphql-error triggers returning an error from data fetcher
332+
ExecutionResult result =
333+
graphql.execute(
334+
""
335+
+ " query findBookById {\n"
336+
+ " bookById(id: \"book-graphql-error\") {\n"
337+
+ " name\n"
338+
+ " author {\n"
339+
+ " name\n"
340+
+ " }\n"
341+
+ " }\n"
342+
+ " }");
343+
344+
// Assert
345+
assertThat(result.getErrors()).isNotEmpty();
346+
347+
testing.waitAndAssertTraces(
348+
trace ->
349+
trace.hasSpansSatisfyingExactly(
350+
span ->
351+
span.hasName("query findBookById")
352+
.hasKind(SpanKind.INTERNAL)
353+
.hasNoParent()
354+
.hasAttributesSatisfyingExactly(
355+
equalTo(
356+
GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME, "findBookById"),
357+
equalTo(GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "query"),
358+
normalizedQueryEqualsTo(
359+
GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT,
360+
"query findBookById { bookById(id: ?) { name author { name } } }"))
361+
.hasStatus(StatusData.error())
362+
.hasEventsSatisfyingExactly(
363+
event ->
364+
event
365+
.hasName("exception")
366+
.hasAttributesSatisfyingExactly(
367+
equalTo(
368+
ExceptionAttributes.EXCEPTION_TYPE,
369+
"DataFetchingException"),
370+
equalTo(
371+
ExceptionAttributes.EXCEPTION_MESSAGE,
372+
"failed to fetch book"))),
373+
span ->
374+
span.hasName("bookById")
375+
.hasKind(SpanKind.INTERNAL)
376+
.hasParent(spanWithName("query findBookById"))
377+
.hasAttributesSatisfyingExactly(
378+
equalTo(GRAPHQL_FIELD_NAME, "bookById"),
379+
equalTo(GRAPHQL_FIELD_PATH, "/bookById"))
380+
.hasStatus(StatusData.error())
381+
.hasEventsSatisfyingExactly(
382+
event ->
383+
event
384+
.hasName("exception")
385+
.hasAttributesSatisfyingExactly(
386+
equalTo(
387+
ExceptionAttributes.EXCEPTION_TYPE,
388+
"DataFetchingException"),
389+
equalTo(
390+
ExceptionAttributes.EXCEPTION_MESSAGE,
391+
"failed to fetch book"))),
392+
span ->
393+
span.hasName("fetchBookById")
394+
.hasKind(SpanKind.INTERNAL)
395+
.hasParent(spanWithName("bookById"))));
396+
}
397+
242398
private static SpanData spanWithName(String name) {
243399
return testing.spans().stream()
244400
.filter(span -> span.getName().equals(name))

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

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import graphql.ExecutionResult;
1616
import graphql.GraphQL;
17+
import graphql.GraphqlErrorBuilder;
18+
import graphql.execution.DataFetcherResult;
1719
import graphql.schema.DataFetcher;
1820
import graphql.schema.GraphQLSchema;
1921
import graphql.schema.idl.RuntimeWiring;
@@ -114,17 +116,32 @@ private RuntimeWiring buildWiring() {
114116
.build();
115117
}
116118

117-
private DataFetcher<Map<String, String>> getBookByIdDataFetcher() {
119+
private DataFetcher<DataFetcherResult<Map<String, String>>> getBookByIdDataFetcher() {
118120
return dataFetchingEnvironment ->
119121
getTesting()
120122
.runWithSpan(
121123
"fetchBookById",
122124
() -> {
123125
String bookId = dataFetchingEnvironment.getArgument("id");
124-
return books.stream()
125-
.filter(book -> book.get("id").equals(bookId))
126-
.findFirst()
127-
.orElse(null);
126+
DataFetcherResult.Builder<Map<String, String>> builder =
127+
DataFetcherResult.newResult();
128+
if ("book-exception".equals(bookId)) {
129+
throw new IllegalStateException("fetching book failed");
130+
} else if ("book-graphql-error".equals(bookId)) {
131+
return builder
132+
.error(
133+
GraphqlErrorBuilder.newError(dataFetchingEnvironment)
134+
.message("failed to fetch book")
135+
.build())
136+
.build();
137+
}
138+
return builder
139+
.data(
140+
books.stream()
141+
.filter(book -> book.get("id").equals(bookId))
142+
.findFirst()
143+
.orElse(null))
144+
.build();
128145
});
129146
}
130147

0 commit comments

Comments
 (0)