Skip to content

Commit 4e5aaed

Browse files
committed
Revisit request observation context type
Prior to this commit, the request execution observation would have a context of type `RequestReplyReceiverContext` to directly deal with tracing propagation at the transport level. This approach doesn't work anymore as the parent observation is not properly set on the resulting trace, even if it is manually set in the instrumentation. This commit revisits the request observation setup and turns its context into a regular `Observation.Context`. Tracing propagation should be dealt with directly at the transport level by an underlying observation. This is the case already for Spring Framework HTTP server observations. As a result, the `PropagationWebGraphQlInterceptor` is deprecated with no replacement and should not be used anymore. Fixes gh-675
1 parent c650db8 commit 4e5aaed

File tree

9 files changed

+80
-176
lines changed

9 files changed

+80
-176
lines changed

spring-graphql-docs/src/docs/asciidoc/includes/observability.adoc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ If your application is using Spring Boot, contributing the custom convention as
1313
[[observability.server.request]]
1414
== Server Requests instrumentation
1515

16-
GraphQL Server Requests observations are created with the name `"graphql.request"` for Servlet and Reactive applications and above all supported transports.
16+
GraphQL Server Requests observations are created with the name `"graphql.request"` for traditional and Reactive applications and above all supported transports.
17+
This instrumentation assumes that any parent observation must be set as the current one on the GraphQL context with the well-known `"micrometer.observation"` key.
18+
For trace propagation across network boundaries, a separate instrumentation at the transport level must be in charge.
19+
In the case of HTTP, Spring Framework {spring-framework-ref-docs}/integration.html#integration.observability.http-server[has dedicated instrumentation that takes care of trace propagation].
20+
1721
Applications need to configure the `org.springframework.graphql.observation.GraphQlObservationInstrumentation` instrumentation in their application.
1822
It is using the `org.springframework.graphql.observation.DefaultExecutionRequestObservationConvention` by default, backed by the `ExecutionRequestObservationContext`.
1923

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

Lines changed: 6 additions & 6 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.
@@ -60,7 +60,7 @@ public String getName() {
6060

6161
@Override
6262
public String getContextualName(ExecutionRequestObservationContext context) {
63-
String operationName = (context.getCarrier().getOperationName() != null) ? context.getCarrier().getOperationName() : "query";
63+
String operationName = (context.getExecutionInput().getOperationName() != null) ? context.getExecutionInput().getOperationName() : "query";
6464
return BASE_CONTEXTUAL_NAME + operationName;
6565
}
6666

@@ -70,17 +70,17 @@ public KeyValues getLowCardinalityKeyValues(ExecutionRequestObservationContext c
7070
}
7171

7272
protected KeyValue outcome(ExecutionRequestObservationContext context) {
73-
if (context.getError() != null || context.getResponse() == null) {
73+
if (context.getError() != null || context.getExecutionResult() == null) {
7474
return OUTCOME_INTERNAL_ERROR;
7575
}
76-
else if (context.getResponse().getErrors().size() > 0) {
76+
else if (context.getExecutionResult().getErrors().size() > 0) {
7777
return OUTCOME_REQUEST_ERROR;
7878
}
7979
return OUTCOME_SUCCESS;
8080
}
8181

8282
protected KeyValue operation(ExecutionRequestObservationContext context) {
83-
String operationName = context.getCarrier().getOperationName();
83+
String operationName = context.getExecutionInput().getOperationName();
8484
if (operationName != null) {
8585
return KeyValue.of(ExecutionRequestLowCardinalityKeyNames.OPERATION, operationName);
8686
}
@@ -93,7 +93,7 @@ public KeyValues getHighCardinalityKeyValues(ExecutionRequestObservationContext
9393
}
9494

9595
protected KeyValue executionId(ExecutionRequestObservationContext context) {
96-
return KeyValue.of(ExecutionRequestHighCardinalityKeyNames.EXECUTION_ID, context.getCarrier().getExecutionId().toString());
96+
return KeyValue.of(ExecutionRequestHighCardinalityKeyNames.EXECUTION_ID, context.getExecutionInput().getExecutionId().toString());
9797
}
9898

9999
}
Lines changed: 58 additions & 28 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.
@@ -16,42 +16,72 @@
1616

1717
package org.springframework.graphql.observation;
1818

19-
import java.util.Map;
20-
2119
import graphql.ExecutionInput;
2220
import graphql.ExecutionResult;
23-
import io.micrometer.observation.transport.RequestReplyReceiverContext;
21+
import io.micrometer.observation.Observation;
22+
import org.springframework.lang.Nullable;
2423

2524
/**
2625
* Context that holds information for metadata collection during observations
2726
* for {@link GraphQlObservationDocumentation#EXECUTION_REQUEST GraphQL requests}.
28-
* <p>This context also extends {@link RequestReplyReceiverContext} for propagating
29-
* tracing information from the {@link graphql.GraphQLContext}
30-
* or the {@link ExecutionInput#getExtensions() input extensions}.
3127
*
3228
* @author Brian Clozel
3329
* @since 1.1.0
3430
*/
35-
public class ExecutionRequestObservationContext extends RequestReplyReceiverContext<ExecutionInput, ExecutionResult> {
36-
37-
public ExecutionRequestObservationContext(ExecutionInput executionInput) {
38-
super(ExecutionRequestObservationContext::getContextValue);
39-
setCarrier(executionInput);
40-
}
41-
42-
/**
43-
* Read propagation field from the {@link graphql.GraphQLContext},
44-
* or the {@link ExecutionInput#getExtensions() input extensions} as a fallback.
45-
*/
46-
private static String getContextValue(ExecutionInput executionInput, String key) {
47-
String value = executionInput.getGraphQLContext().get(key);
48-
if (value == null) {
49-
Map<String, Object> extensions = executionInput.getExtensions();
50-
if (extensions != null) {
51-
value = (String) extensions.get(key);
52-
}
53-
}
54-
return value;
55-
}
31+
public class ExecutionRequestObservationContext extends Observation.Context {
32+
33+
private final ExecutionInput executionInput;
34+
35+
@Nullable
36+
private ExecutionResult executionResult;
37+
38+
public ExecutionRequestObservationContext(ExecutionInput executionInput) {
39+
this.executionInput = executionInput;
40+
}
41+
42+
/**
43+
* Return the {@link ExecutionInput input} for the request execution.
44+
* @since 1.1.4
45+
*/
46+
public ExecutionInput getExecutionInput() {
47+
return this.executionInput;
48+
}
49+
50+
/**
51+
* Return the {@link ExecutionInput input} for the request execution.
52+
* @deprecated since 1.1.4 in favor of {@link #getExecutionInput()}
53+
*/
54+
@Deprecated(since = "1.1.4", forRemoval = true)
55+
public ExecutionInput getCarrier() {
56+
return this.executionInput;
57+
}
58+
59+
/**
60+
* Return the {@link ExecutionResult result} for the request execution.
61+
* @since 1.1.4
62+
*/
63+
@Nullable
64+
public ExecutionResult getExecutionResult() {
65+
return this.executionResult;
66+
}
67+
68+
/**
69+
* Set the {@link ExecutionResult result} for the request execution.
70+
* @param executionResult the execution result
71+
* @since 1.1.4
72+
*/
73+
public void setExecutionResult(ExecutionResult executionResult) {
74+
this.executionResult = executionResult;
75+
}
76+
77+
/**
78+
* Return the {@link ExecutionResult result} for the request execution.
79+
* @deprecated since 1.1.4 in favor of {@link #getExecutionResult()}
80+
*/
81+
@Nullable
82+
@Deprecated(since = "1.1.4", forRemoval = true)
83+
public ExecutionResult getResponse() {
84+
return this.executionResult;
85+
}
5686

5787
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExe
110110
return new SimpleInstrumentationContext<>() {
111111
@Override
112112
public void onCompleted(ExecutionResult result, Throwable exc) {
113-
observationContext.setResponse(result);
113+
observationContext.setExecutionResult(result);
114114
if (exc != null) {
115115
requestObservation.error(exc);
116116
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
*
3333
* @author Brian Clozel
3434
* @since 1.1.1
35+
* @deprecated since 1.1.4 with no replacement.
3536
*/
37+
@Deprecated(since = "1.1.4", forRemoval = true)
3638
public class PropagationWebGraphQlInterceptor implements WebGraphQlInterceptor {
3739

3840
private final Propagator propagator;

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

Lines changed: 2 additions & 2 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.
@@ -102,7 +102,7 @@ private ExecutionRequestObservationContext createObservationContext(ExecutionInp
102102
ExecutionRequestObservationContext context = new ExecutionRequestObservationContext(executionInput);
103103
ExecutionResultImpl.Builder builder = ExecutionResultImpl.newExecutionResult();
104104
resultConsumer.accept(builder);
105-
context.setResponse(builder.build());
105+
context.setExecutionResult(builder.build());
106106
return context;
107107
}
108108

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

Lines changed: 0 additions & 62 deletions
This file was deleted.

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

Lines changed: 5 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,12 @@
1616

1717
package org.springframework.graphql.observation;
1818

19-
import java.util.List;
20-
import java.util.concurrent.CompletableFuture;
21-
2219
import graphql.GraphqlErrorBuilder;
2320
import io.micrometer.observation.Observation;
2421
import io.micrometer.observation.ObservationRegistry;
22+
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
2523
import io.micrometer.observation.tck.TestObservationRegistry;
2624
import io.micrometer.observation.tck.TestObservationRegistryAssert;
27-
import io.micrometer.observation.transport.ReceiverContext;
28-
import io.micrometer.tracing.Span;
29-
import io.micrometer.tracing.TraceContext;
30-
import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler;
31-
import io.micrometer.tracing.handler.TracingObservationHandler;
32-
import io.micrometer.tracing.propagation.Propagator;
33-
import io.micrometer.tracing.test.simple.SimpleSpanBuilder;
34-
import io.micrometer.tracing.test.simple.SimpleTracer;
35-
import io.micrometer.tracing.test.simple.TracerAssert;
3625
import org.junit.jupiter.api.Test;
3726
import reactor.core.publisher.Mono;
3827

@@ -45,7 +34,7 @@
4534
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
4635
import org.springframework.graphql.execution.ErrorType;
4736

48-
import static org.assertj.core.api.Assertions.assertThat;
37+
import java.util.concurrent.CompletableFuture;
4938

5039
/**
5140
* Tests for {@link GraphQlObservationInstrumentation}.
@@ -134,7 +123,8 @@ void instrumentMultipleDataFetcherOperations() {
134123
.that()
135124
.hasLowCardinalityKeyValue("graphql.outcome", "SUCCESS")
136125
.hasLowCardinalityKeyValue("graphql.field.name", "bookById")
137-
.hasHighCardinalityKeyValue("graphql.field.path", "/bookById");
126+
.hasHighCardinalityKeyValue("graphql.field.path", "/bookById")
127+
.hasParentObservationContextMatching(context -> context instanceof ExecutionRequestObservationContext);
138128

139129
TestObservationRegistryAssert.assertThat(this.observationRegistry)
140130
.hasAnObservationWithAKeyValue("graphql.field.name", "author")
@@ -213,7 +203,7 @@ void setIncomingObservationAsParent() {
213203
ExecutionGraphQlRequest graphQlRequest = TestExecutionRequest.forDocument(document);
214204
Observation incoming = Observation.start("incoming", ObservationRegistry.create());
215205
graphQlRequest.configureExecutionInput((input, builder) ->
216-
builder.graphQLContext(contextBuilder -> contextBuilder.of("micrometer.observation", incoming)).build());
206+
builder.graphQLContext(contextBuilder -> contextBuilder.of(ObservationThreadLocalAccessor.KEY, incoming)).build());
217207
Mono<ExecutionGraphQlResponse> responseMono = graphQlSetup
218208
.queryFetcher("bookById", env -> BookSource.getBookWithoutAuthor(1L))
219209
.toGraphQlService()
@@ -225,65 +215,4 @@ void setIncomingObservationAsParent() {
225215
incoming.stop();
226216
}
227217

228-
@Test
229-
void inboundTracingInformationIsPropagated() {
230-
SimpleTracer simpleTracer = new SimpleTracer();
231-
String traceId = "traceId";
232-
TracingObservationHandler<ReceiverContext> tracingHandler = new PropagatingReceiverTracingObservationHandler<>(simpleTracer, new TestPropagator(simpleTracer, traceId));
233-
this.observationRegistry.observationConfig().observationHandler(tracingHandler);
234-
String document = """
235-
{
236-
bookById(id: 1) {
237-
name
238-
}
239-
}
240-
""";
241-
ExecutionGraphQlRequest executionRequest = TestExecutionRequest.forDocument(document);
242-
executionRequest.configureExecutionInput((input, builder) ->
243-
builder.graphQLContext(context -> context.of(TestPropagator.TRACING_HEADER_NAME, traceId)).build());
244-
Mono<ExecutionGraphQlResponse> responseMono = graphQlSetup
245-
.queryFetcher("bookById", env -> BookSource.getBookWithoutAuthor(1L))
246-
.toGraphQlService()
247-
.execute(executionRequest);
248-
ResponseHelper response = ResponseHelper.forResponse(responseMono);
249-
250-
TracerAssert.assertThat(simpleTracer)
251-
.onlySpan()
252-
.hasNameEqualTo("graphql query")
253-
.hasKindEqualTo(Span.Kind.SERVER)
254-
.hasTag("graphql.operation", "query")
255-
.hasTag("graphql.outcome", "SUCCESS")
256-
.hasTagWithKey("graphql.execution.id");
257-
}
258-
259-
static class TestPropagator implements Propagator {
260-
261-
public static String TRACING_HEADER_NAME = "X-Test-Tracing";
262-
263-
private final SimpleTracer tracer;
264-
265-
private final String traceId;
266-
267-
TestPropagator(SimpleTracer tracer, String traceId) {
268-
this.tracer = tracer;
269-
this.traceId = traceId;
270-
}
271-
272-
@Override
273-
public List<String> fields() {
274-
return List.of(TRACING_HEADER_NAME);
275-
}
276-
277-
@Override
278-
public <C> void inject(TraceContext context, C carrier, Setter<C> setter) {
279-
setter.set(carrier, TRACING_HEADER_NAME, "traceId");
280-
}
281-
282-
@Override
283-
public <C> Span.Builder extract(C carrier, Getter<C> getter) {
284-
String foo = getter.get(carrier, TRACING_HEADER_NAME);
285-
assertThat(foo).isEqualTo(this.traceId);
286-
return new SimpleSpanBuilder(this.tracer);
287-
}
288-
}
289218
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
*
4848
* @author Brian Clozel
4949
*/
50+
@SuppressWarnings("removal")
5051
class PropagationWebGraphQlInterceptorTests {
5152

5253
PropagationWebGraphQlInterceptor interceptor = new PropagationWebGraphQlInterceptor(new TestPropagator());

0 commit comments

Comments
 (0)