Skip to content

Commit

Permalink
Record response errors as events in Request Observations
Browse files Browse the repository at this point in the history
Prior to this commit, GraphQL Request Observations would not record
errors as Observation errors, because with GraphQL errors can partially
affect the response and there can be multiple. Instead, an invalid
request (for example) would lead to a `"graphql.outcome", "REQUEST_ERROR"`
low cardinality KeyValue. In this case, developers would not know what
type of error occured nor if there were multiple.

This commit records all errors listed in the GraphQL as
Observation.Event on the request Observation. Such events are usually
handled by the tracing handler and are recorded as span annotations for
traces. Other `ObservationHandler` annotations can leverage events in a
different fashion.

Closes gh-859
  • Loading branch information
bclozel committed Feb 14, 2024
1 parent 98d39bc commit c55de8d
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 6 deletions.
10 changes: 10 additions & 0 deletions spring-graphql-docs/modules/ROOT/pages/observability.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ The `graphql.outcome` KeyValue will be `"SUCCESS"` if a valid GraphQL response h
|`graphql.execution.id` _(required)_|`graphql.execution.ExecutionId` of the GraphQL request.
|===

Spring for GraphQL also contributes Events for Server Request Observations.
https://docs.micrometer.io/micrometer/reference/observation/components.html#micrometer-observation-events[Micrometer Observation Events] are usually handled as span annotations in traces.
This instrumentation records errors listed in the GraphQL response as events.

.Observation Events
[cols="a,a"]
|===
|Name | Contextual Name
|the GraphQL error type, e.g. `InvalidSyntax`|the full GraphQL error message, e.g. `"Invalid syntax with offending token 'invalid'..."`
|===


[[observability.server.datafetcher]]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020-2023 the original author or authors.
* Copyright 2020-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -63,8 +63,10 @@ public class GraphQlObservationInstrumentation extends SimplePerformantInstrumen

private final ObservationRegistry observationRegistry;

@Nullable
private final ExecutionRequestObservationConvention requestObservationConvention;

@Nullable
private final DataFetcherObservationConvention dataFetcherObservationConvention;

/**
Expand All @@ -74,9 +76,7 @@ public class GraphQlObservationInstrumentation extends SimplePerformantInstrumen
* @param observationRegistry the registry to use for recording observations
*/
public GraphQlObservationInstrumentation(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
this.requestObservationConvention = new DefaultExecutionRequestObservationConvention();
this.dataFetcherObservationConvention = new DefaultDataFetcherObservationConvention();
this(observationRegistry, null, null);
}

/**
Expand All @@ -87,8 +87,8 @@ public GraphQlObservationInstrumentation(ObservationRegistry observationRegistry
* @param dateFetcherObservationConvention the convention to use for data fetcher observations
*/
public GraphQlObservationInstrumentation(ObservationRegistry observationRegistry,
ExecutionRequestObservationConvention requestObservationConvention,
DataFetcherObservationConvention dateFetcherObservationConvention) {
@Nullable ExecutionRequestObservationConvention requestObservationConvention,
@Nullable DataFetcherObservationConvention dateFetcherObservationConvention) {
this.observationRegistry = observationRegistry;
this.requestObservationConvention = requestObservationConvention;
this.dataFetcherObservationConvention = dateFetcherObservationConvention;
Expand All @@ -112,6 +112,10 @@ public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExe
@Override
public void onCompleted(ExecutionResult result, Throwable exc) {
observationContext.setExecutionResult(result);
result.getErrors().forEach(graphQLError -> {
Observation.Event event = Observation.Event.of(graphQLError.getErrorType().toString(), graphQLError.getMessage());
requestObservation.event(event);
});
if (exc != null) {
requestObservation.error(exc);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import graphql.schema.DataFetcher;
import io.micrometer.common.KeyValue;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import io.micrometer.observation.tck.TestObservationRegistry;
Expand All @@ -36,6 +37,8 @@
import org.springframework.graphql.execution.ErrorType;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.stream.Stream;
Expand Down Expand Up @@ -319,4 +322,48 @@ void shouldNotOverrideExistingLocalContext() {
ResponseHelper.forResponse(responseMono);
}

@Test
void shouldRecordGraphQlErrorsAsTraceEvents() {
EventListeningObservationHandler observationHandler = new EventListeningObservationHandler();
this.observationRegistry.observationConfig().observationHandler(observationHandler);

String document = "invalid{}";
Mono<ExecutionGraphQlResponse> responseMono = graphQlSetup
.toGraphQlService()
.execute(document);
ResponseHelper response = ResponseHelper.forResponse(responseMono);

assertThat(response.errorCount()).isEqualTo(1);
assertThat(response.error(0).errorType()).isEqualTo("InvalidSyntax");
assertThat(response.error(0).message()).startsWith("Invalid syntax with offending token 'invalid'");

assertThat(observationHandler.getEvents()).hasSize(1);
Observation.Event errorEvent = observationHandler.getEvents().get(0);
assertThat(errorEvent.getName()).isEqualTo("InvalidSyntax");
assertThat(errorEvent.getContextualName()).startsWith("Invalid syntax with offending token 'invalid'");

TestObservationRegistryAssert.assertThat(this.observationRegistry).hasObservationWithNameEqualTo("graphql.request")
.that().hasLowCardinalityKeyValue("graphql.outcome", "REQUEST_ERROR")
.hasHighCardinalityKeyValueWithKey("graphql.execution.id");
}

static class EventListeningObservationHandler implements ObservationHandler<ExecutionRequestObservationContext> {

private final List<Observation.Event> events = new ArrayList<>();

@Override
public boolean supportsContext(Observation.Context context) {
return context instanceof ExecutionRequestObservationContext;
}

@Override
public void onEvent(Observation.Event event, ExecutionRequestObservationContext context) {
this.events.add(event);
}

public List<Observation.Event> getEvents() {
return this.events;
}
}

}

0 comments on commit c55de8d

Please sign in to comment.