Skip to content

Commit ae5b7e7

Browse files
committed
Set current observation in data fetching environment
Prior to this commit, the Observability instrumentation would instrument `DataFetcher` instances and set the current observation in the local context of the value returned by the data fetcher itself. This allowed to properly build a parent/child chain of observations between the main request and the instrumented data fetching observations. Because the current observation was not set in the `DataFetchingEnvironment` given as a parameter to the data fetcher, any operation done in the data fetcher would not propagate using the current observation but instead the parent one. This commit revisits the implementation of the instrumentation to not wrap the result anymore, but to build a new local `GraphQLContext` that holds the current observation right before calling the data fetcher. Note that we cannot "just" set the current observation in that local context as this is shared mutable instance for all child data fetchers. Fixes gh-764
1 parent c5f1e5f commit ae5b7e7

File tree

2 files changed

+32
-46
lines changed

2 files changed

+32
-46
lines changed

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

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import graphql.ExecutionResult;
2020
import graphql.GraphQLContext;
21-
import graphql.execution.DataFetcherResult;
2221
import graphql.execution.instrumentation.InstrumentationContext;
2322
import graphql.execution.instrumentation.InstrumentationState;
2423
import graphql.execution.instrumentation.SimpleInstrumentation;
@@ -28,11 +27,10 @@
2827
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
2928
import graphql.schema.DataFetcher;
3029
import graphql.schema.DataFetchingEnvironment;
30+
import graphql.schema.DataFetchingEnvironmentImpl;
3131
import io.micrometer.observation.Observation;
3232
import io.micrometer.observation.ObservationRegistry;
3333
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
34-
import org.apache.commons.logging.Log;
35-
import org.apache.commons.logging.LogFactory;
3634
import org.springframework.lang.Nullable;
3735

3836
import java.util.concurrent.CompletionException;
@@ -57,8 +55,6 @@
5755
*/
5856
public class GraphQlObservationInstrumentation extends SimpleInstrumentation {
5957

60-
private static final Log logger = LogFactory.getLog(GraphQlObservationInstrumentation.class);
61-
6258
private static final ExecutionRequestObservationConvention DEFAULT_REQUEST_CONVENTION =
6359
new DefaultExecutionRequestObservationConvention();
6460

@@ -106,13 +102,11 @@ public InstrumentationState createState(InstrumentationCreateStateParameters par
106102
@Override
107103
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters,
108104
InstrumentationState state) {
109-
if (state instanceof RequestObservationInstrumentationState instrumentationState) {
105+
if (state == RequestObservationInstrumentationState.INSTANCE) {
110106
ExecutionRequestObservationContext observationContext = new ExecutionRequestObservationContext(parameters.getExecutionInput());
111107
Observation requestObservation = GraphQlObservationDocumentation.EXECUTION_REQUEST.observation(this.requestObservationConvention,
112108
DEFAULT_REQUEST_CONVENTION, () -> observationContext, this.observationRegistry);
113-
requestObservation.parentObservation(getCurrentObservation(parameters.getGraphQLContext()));
114-
GraphQLContext graphQLContext = parameters.getGraphQLContext();
115-
graphQLContext.put(ObservationThreadLocalAccessor.KEY, requestObservation);
109+
setCurrentObservation(requestObservation, parameters.getGraphQLContext());
116110
requestObservation.start();
117111
return new SimpleInstrumentationContext<>() {
118112
@Override
@@ -128,24 +122,27 @@ public void onCompleted(ExecutionResult result, Throwable exc) {
128122
return super.beginExecution(parameters, state);
129123
}
130124

131-
@Nullable
132-
private static Observation getCurrentObservation(GraphQLContext graphQLContext) {
133-
return graphQLContext.get(ObservationThreadLocalAccessor.KEY);
125+
private static void setCurrentObservation(Observation currentObservation, GraphQLContext graphQlContext) {
126+
Observation parentObservation = graphQlContext.get(ObservationThreadLocalAccessor.KEY);
127+
currentObservation.parentObservation(parentObservation);
128+
graphQlContext.put(ObservationThreadLocalAccessor.KEY, currentObservation);
134129
}
135130

136131
@Override
137132
public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher,
138133
InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
139134
if (!parameters.isTrivialDataFetcher()
140-
&& state instanceof RequestObservationInstrumentationState instrumentationState) {
135+
&& state == RequestObservationInstrumentationState.INSTANCE) {
141136
return (environment) -> {
142-
DataFetcherObservationContext observationContext = new DataFetcherObservationContext(parameters.getEnvironment());
137+
DataFetcherObservationContext observationContext = new DataFetcherObservationContext(environment);
143138
Observation dataFetcherObservation = GraphQlObservationDocumentation.DATA_FETCHER.observation(this.dataFetcherObservationConvention,
144139
DEFAULT_DATA_FETCHER_CONVENTION, () -> observationContext, this.observationRegistry);
145140
dataFetcherObservation.parentObservation(getCurrentObservation(environment));
146141
dataFetcherObservation.start();
142+
143+
DataFetchingEnvironment dataFetchingEnvironment = wrapDataFetchingEnvironment(environment, dataFetcherObservation);
147144
try {
148-
Object value = dataFetcher.get(environment);
145+
Object value = dataFetcher.get(dataFetchingEnvironment);
149146
if (value instanceof CompletionStage<?> completion) {
150147
return completion.handle((result, error) -> {
151148
observationContext.setValue(result);
@@ -155,13 +152,13 @@ public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher,
155152
throw new CompletionException(error);
156153
}
157154
dataFetcherObservation.stop();
158-
return wrapAsDataFetcherResult(result, dataFetcherObservation, environment.getLocalContext());
155+
return result;
159156
});
160157
}
161158
else {
162159
observationContext.setValue(value);
163160
dataFetcherObservation.stop();
164-
return wrapAsDataFetcherResult(value, dataFetcherObservation, environment.getLocalContext());
161+
return value;
165162
}
166163
}
167164
catch (Throwable throwable) {
@@ -177,39 +174,25 @@ public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher,
177174
@Nullable
178175
private static Observation getCurrentObservation(DataFetchingEnvironment environment) {
179176
Observation currentObservation = null;
180-
if (environment.getLocalContext() != null && environment.getLocalContext() instanceof GraphQLContext) {
181-
GraphQLContext localContext = environment.getLocalContext();
177+
if (environment.getLocalContext() instanceof GraphQLContext localContext) {
182178
currentObservation = localContext.get(ObservationThreadLocalAccessor.KEY);
183179
}
184180
if (currentObservation == null) {
185-
return environment.getGraphQlContext().get(ObservationThreadLocalAccessor.KEY);
181+
currentObservation = environment.getGraphQlContext().get(ObservationThreadLocalAccessor.KEY);
186182
}
187183
return currentObservation;
188184
}
189185

190-
private static DataFetcherResult<?> wrapAsDataFetcherResult(Object value, Observation dataFetcherObservation,
191-
@Nullable GraphQLContext dataFetcherLocalContext) {
192-
if (value instanceof DataFetcherResult<?> result) {
193-
if (result.getLocalContext() == null) {
194-
return result.transform(builder -> builder.localContext(GraphQLContext.newContext().of(ObservationThreadLocalAccessor.KEY, dataFetcherObservation).build()));
195-
}
196-
else if (result.getLocalContext() instanceof GraphQLContext) {
197-
((GraphQLContext) result.getLocalContext()).put(ObservationThreadLocalAccessor.KEY, dataFetcherObservation);
198-
} else {
199-
logger.debug("Cannot add observation to localContext as it is not a GraphQLContext but a "
200-
+ result.getLocalContext().getClass().toString());
201-
}
202-
return result;
186+
private static DataFetchingEnvironment wrapDataFetchingEnvironment(DataFetchingEnvironment environment, Observation dataFetcherObservation) {
187+
GraphQLContext.Builder localContextBuilder = GraphQLContext.newContext();
188+
if (environment.getLocalContext() instanceof GraphQLContext localContext) {
189+
localContextBuilder.of(localContext);
203190
}
204-
else {
205-
GraphQLContext localContext = dataFetcherLocalContext == null ?
206-
GraphQLContext.newContext().build() : GraphQLContext.newContext().of(dataFetcherLocalContext).build();
207-
return DataFetcherResult.newResult()
208-
.data(value)
209-
.localContext(localContext.put(ObservationThreadLocalAccessor.KEY, dataFetcherObservation))
210-
.build();
211-
}
212-
191+
localContextBuilder.of(ObservationThreadLocalAccessor.KEY, dataFetcherObservation);
192+
return DataFetchingEnvironmentImpl
193+
.newDataFetchingEnvironment(environment)
194+
.localContext(localContextBuilder.build())
195+
.build();
213196
}
214197

215198

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import graphql.execution.DataFetcherResult;
2222
import graphql.schema.AsyncDataFetcher;
2323
import graphql.schema.DataFetcher;
24+
import io.micrometer.common.KeyValue;
2425
import io.micrometer.observation.Observation;
2526
import io.micrometer.observation.ObservationRegistry;
2627
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
@@ -262,13 +263,15 @@ void currentObservationSetInDataFetcherContext() {
262263
}
263264
""";
264265
DataFetcher<Book> bookDataFetcher = environment -> {
265-
assertThat(observationRegistry.getCurrentObservation().getContext())
266-
.isInstanceOf(ExecutionRequestObservationContext.class);
266+
Observation.Context context = observationRegistry.getCurrentObservation().getContext();
267+
assertThat(context).isInstanceOf(DataFetcherObservationContext.class);
268+
assertThat(context.getLowCardinalityKeyValues()).contains(KeyValue.of("graphql.field.name", "bookById"));
267269
return BookSource.getBookWithoutAuthor(1L);
268270
};
269271
DataFetcher<Author> authorDataFetcher = environment -> {
270-
assertThat(observationRegistry.getCurrentObservation().getContext())
271-
.isInstanceOf(DataFetcherObservationContext.class);
272+
Observation.Context context = observationRegistry.getCurrentObservation().getContext();
273+
assertThat(context).isInstanceOf(DataFetcherObservationContext.class);
274+
assertThat(context.getLowCardinalityKeyValues()).contains(KeyValue.of("graphql.field.name", "author"));
272275
return BookSource.getAuthor(101L);
273276
};
274277

0 commit comments

Comments
 (0)