Skip to content

Commit 1d2fc21

Browse files
committed
Fix parent Observation management in instrumentation
Prior to this commit, the `GraphQlObservationInstrumentation` was incorrectly setting up the parent observation for both request execution and data fetching observations. In the case of the request execution, we were not looking into the `GraphQLContext` for an existing observation - this commit ensures that if such an observation exists, it is set as the parent. As for the data fetching observation, we were incorrectly assuming that the parent of all data fetching operations was the request execution one, whereas data fetching operations can be nested. This commit ensures that we only rely on the current observation in the GraphQL context. Fixes gh-611
1 parent bcd6bad commit 1d2fc21

File tree

2 files changed

+54
-18
lines changed

2 files changed

+54
-18
lines changed

spring-graphql/src/main/java/org/springframework/graphql/observation/GraphQlObservationInstrumentation.java

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.util.concurrent.CompletionStage;
2020

2121
import graphql.ExecutionResult;
22+
import graphql.GraphQLContext;
2223
import graphql.execution.instrumentation.InstrumentationContext;
2324
import graphql.execution.instrumentation.InstrumentationState;
2425
import graphql.execution.instrumentation.SimpleInstrumentation;
@@ -100,20 +101,21 @@ public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExe
100101
InstrumentationState state) {
101102
if (state instanceof RequestObservationInstrumentationState instrumentationState) {
102103
ExecutionRequestObservationContext observationContext = new ExecutionRequestObservationContext(parameters.getExecutionInput());
104+
Observation parentObservation = parameters.getGraphQLContext().get(OBSERVATION_KEY);
103105
Observation requestObservation = instrumentationState.createRequestObservation(this.requestObservationConvention,
104106
observationContext, this.observationRegistry);
107+
requestObservation.parentObservation(parentObservation);
108+
parameters.getGraphQLContext().put(OBSERVATION_KEY, requestObservation);
105109
requestObservation.start();
106110
return new SimpleInstrumentationContext<>() {
107111
@Override
108112
public void onCompleted(ExecutionResult result, Throwable exc) {
109113
observationContext.setResponse(result);
110114
if (exc != null) {
111-
observationContext.setError(exc);
112115
requestObservation.error(exc);
113116
}
114-
else {
115-
requestObservation.stop();
116-
}
117+
requestObservation.stop();
118+
instrumentationState.restoreParentObservation(parameters.getGraphQLContext(), parentObservation);
117119
}
118120
};
119121
}
@@ -126,58 +128,66 @@ public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher,
126128
if (!parameters.isTrivialDataFetcher()
127129
&& state instanceof RequestObservationInstrumentationState instrumentationState) {
128130
return (environment) -> {
131+
GraphQLContext graphQLContext = parameters.getExecutionContext().getGraphQLContext();
132+
Observation parentObservation = graphQLContext.get(OBSERVATION_KEY);
129133
DataFetcherObservationContext observationContext = new DataFetcherObservationContext(parameters.getEnvironment());
130134
Observation dataFetcherObservation = instrumentationState.createDataFetcherObservation(
131135
this.dataFetcherObservationConvention, observationContext, this.observationRegistry);
132-
parameters.getExecutionContext().getGraphQLContext().put(OBSERVATION_KEY, dataFetcherObservation);
136+
dataFetcherObservation.parentObservation(parentObservation);
137+
graphQLContext.put(OBSERVATION_KEY, dataFetcherObservation);
133138
dataFetcherObservation.start();
134139
try {
135140
Object value = dataFetcher.get(environment);
136141
if (value instanceof CompletionStage<?> completion) {
137142
return completion.whenComplete((result, error) -> {
143+
observationContext.setValue(result);
138144
if (error != null) {
139145
dataFetcherObservation.error(error);
140146
}
141-
observationContext.setValue(result);
142147
dataFetcherObservation.stop();
148+
instrumentationState.restoreParentObservation(graphQLContext, parentObservation);
143149
});
144150
}
145151
else {
146152
observationContext.setValue(value);
147153
dataFetcherObservation.stop();
154+
instrumentationState.restoreParentObservation(graphQLContext, parentObservation);
148155
return value;
149156
}
150157
}
151158
catch (Throwable throwable) {
152159
dataFetcherObservation.error(throwable);
153160
dataFetcherObservation.stop();
161+
instrumentationState.restoreParentObservation(graphQLContext, parentObservation);
154162
throw throwable;
155163
}
156164
};
157165
}
158-
return super.instrumentDataFetcher(dataFetcher, parameters, state);
166+
return dataFetcher;
159167
}
160168

161169

162170
static class RequestObservationInstrumentationState implements InstrumentationState {
163171

164-
private Observation requestObservation;
165-
166-
167172
Observation createRequestObservation(ExecutionRequestObservationConvention convention,
168173
ExecutionRequestObservationContext context, ObservationRegistry registry) {
169-
Observation observation = GraphQlObservationDocumentation.EXECUTION_REQUEST.observation(convention,
174+
return GraphQlObservationDocumentation.EXECUTION_REQUEST.observation(convention,
170175
DEFAULT_REQUEST_CONVENTION, () -> context, registry);
171-
this.requestObservation = observation;
172-
return observation;
173176
}
174177

175178
Observation createDataFetcherObservation(DataFetcherObservationConvention convention,
176179
DataFetcherObservationContext context, ObservationRegistry registry) {
177-
Observation dataFetcherObservation = GraphQlObservationDocumentation.DATA_FETCHER.observation(convention,
180+
return GraphQlObservationDocumentation.DATA_FETCHER.observation(convention,
178181
DEFAULT_DATA_FETCHER_CONVENTION, () -> context, registry);
179-
dataFetcherObservation.parentObservation(requestObservation);
180-
return dataFetcherObservation;
182+
}
183+
184+
void restoreParentObservation(GraphQLContext context, Observation parentObservation) {
185+
if (parentObservation != null) {
186+
context.put(OBSERVATION_KEY, parentObservation);
187+
}
188+
else {
189+
context.delete(OBSERVATION_KEY);
190+
}
181191
}
182192

183193
}

spring-graphql/src/test/java/org/springframework/graphql/observation/GraphQlObservationInstrumentationTests.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@
2020
import java.util.concurrent.CompletableFuture;
2121

2222
import graphql.GraphqlErrorBuilder;
23+
import io.micrometer.observation.Observation;
24+
import io.micrometer.observation.ObservationRegistry;
2325
import io.micrometer.observation.tck.TestObservationRegistry;
2426
import io.micrometer.observation.tck.TestObservationRegistryAssert;
2527
import io.micrometer.observation.transport.ReceiverContext;
@@ -199,6 +201,30 @@ void propagatesContextBetweenObservations() {
199201
.hasParentObservationContextMatching(context -> context instanceof ExecutionRequestObservationContext);
200202
}
201203

204+
@Test
205+
void setIncomingObservationAsParent() {
206+
String document = """
207+
{
208+
bookById(id: 1) {
209+
name
210+
}
211+
}
212+
""";
213+
ExecutionGraphQlRequest graphQlRequest = TestExecutionRequest.forDocument(document);
214+
Observation incoming = Observation.start("incoming", ObservationRegistry.create());
215+
graphQlRequest.configureExecutionInput((input, builder) ->
216+
builder.graphQLContext(contextBuilder -> contextBuilder.of("micrometer.observation", incoming)).build());
217+
Mono<ExecutionGraphQlResponse> responseMono = graphQlSetup
218+
.queryFetcher("bookById", env -> BookSource.getBookWithoutAuthor(1L))
219+
.toGraphQlService()
220+
.execute(graphQlRequest);
221+
ResponseHelper response = ResponseHelper.forResponse(responseMono);
222+
223+
TestObservationRegistryAssert.assertThat(this.observationRegistry).hasObservationWithNameEqualTo("graphql.request")
224+
.that().hasParentObservationEqualTo(incoming);
225+
incoming.stop();
226+
}
227+
202228
@Test
203229
void inboundTracingInformationIsPropagated() {
204230
SimpleTracer simpleTracer = new SimpleTracer();

0 commit comments

Comments
 (0)