diff --git a/platform/build.gradle b/platform/build.gradle
index 6dbbc008..e864be82 100644
--- a/platform/build.gradle
+++ b/platform/build.gradle
@@ -11,6 +11,7 @@ dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.14.1"))
api(platform("io.projectreactor:reactor-bom:2022.0.0"))
api(platform("io.micrometer:micrometer-bom:1.10.2"))
+ api(platform("io.micrometer:micrometer-tracing-bom:1.0.0"))
api(platform("org.springframework.data:spring-data-bom:2022.0.0"))
api(platform("org.springframework.security:spring-security-bom:6.0.0"))
api(platform("com.querydsl:querydsl-bom:5.0.0"))
diff --git a/spring-graphql/build.gradle b/spring-graphql/build.gradle
index e49ea7b3..8f45ef75 100644
--- a/spring-graphql/build.gradle
+++ b/spring-graphql/build.gradle
@@ -9,6 +9,7 @@ dependencies {
implementation 'io.micrometer:context-propagation'
compileOnly 'io.micrometer:micrometer-observation'
+ compileOnly 'io.micrometer:micrometer-tracing'
compileOnly 'jakarta.annotation:jakarta.annotation-api'
compileOnly 'org.springframework:spring-webflux'
compileOnly 'org.springframework:spring-webmvc'
@@ -45,6 +46,7 @@ dependencies {
testImplementation 'org.springframework.data:spring-data-keyvalue'
testImplementation 'org.springframework.data:spring-data-jpa'
testImplementation 'io.micrometer:micrometer-observation-test'
+ testImplementation 'io.micrometer:micrometer-tracing-test'
testImplementation 'com.h2database:h2'
testImplementation 'org.hibernate:hibernate-core'
testImplementation 'org.hibernate.validator:hibernate-validator'
diff --git a/spring-graphql/src/main/java/org/springframework/graphql/observation/ExecutionRequestObservationContext.java b/spring-graphql/src/main/java/org/springframework/graphql/observation/ExecutionRequestObservationContext.java
index c05b069c..fff84209 100644
--- a/spring-graphql/src/main/java/org/springframework/graphql/observation/ExecutionRequestObservationContext.java
+++ b/spring-graphql/src/main/java/org/springframework/graphql/observation/ExecutionRequestObservationContext.java
@@ -16,6 +16,8 @@
package org.springframework.graphql.observation;
+import java.util.Map;
+
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import io.micrometer.observation.transport.RequestReplyReceiverContext;
@@ -24,7 +26,8 @@ import io.micrometer.observation.transport.RequestReplyReceiverContext;
* Context that holds information for metadata collection during observations
* for {@link GraphQlObservationDocumentation#EXECUTION_REQUEST GraphQL requests}.
*
This context also extends {@link RequestReplyReceiverContext} for propagating
- * tracing information with the HTTP server exchange.
+ * tracing information from the {@link graphql.GraphQLContext}
+ * or the {@link ExecutionInput#getExtensions() input extensions}.
*
* @author Brian Clozel
* @since 1.1.0
@@ -32,8 +35,23 @@ import io.micrometer.observation.transport.RequestReplyReceiverContext;
public class ExecutionRequestObservationContext extends RequestReplyReceiverContext {
public ExecutionRequestObservationContext(ExecutionInput executionInput) {
- super((input, key) -> executionInput.getExtensions().get(key).toString());
+ super(ExecutionRequestObservationContext::getContextValue);
setCarrier(executionInput);
}
+ /**
+ * Read propagation field from the {@link graphql.GraphQLContext},
+ * or the {@link ExecutionInput#getExtensions() input extensions} as a fallback.
+ */
+ private static String getContextValue(ExecutionInput executionInput, String key) {
+ String value = executionInput.getGraphQLContext().get(key);
+ if (value == null) {
+ Map extensions = executionInput.getExtensions();
+ if (extensions != null) {
+ value = (String) extensions.get(key);
+ }
+ }
+ return value;
+ }
+
}
diff --git a/spring-graphql/src/main/java/org/springframework/graphql/observation/PropagationWebGraphQlInterceptor.java b/spring-graphql/src/main/java/org/springframework/graphql/observation/PropagationWebGraphQlInterceptor.java
new file mode 100644
index 00000000..c883d0d7
--- /dev/null
+++ b/spring-graphql/src/main/java/org/springframework/graphql/observation/PropagationWebGraphQlInterceptor.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2020-2022 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.graphql.observation;
+
+import io.micrometer.tracing.propagation.Propagator;
+import reactor.core.publisher.Mono;
+
+import org.springframework.graphql.server.WebGraphQlInterceptor;
+import org.springframework.graphql.server.WebGraphQlRequest;
+import org.springframework.graphql.server.WebGraphQlResponse;
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.Assert;
+
+/**
+ * {@link WebGraphQlInterceptor} that copies {@link Propagator propagation} headers
+ * from the HTTP request to the {@link graphql.GraphQLContext}.
+ * This makes it possible to propagate tracing information sent by HTTP clients.
+ *
+ * @author Brian Clozel
+ * @since 1.1.1
+ */
+public class PropagationWebGraphQlInterceptor implements WebGraphQlInterceptor {
+
+ private final Propagator propagator;
+
+ /**
+ * Create an interceptor that leverages the field names used by the given
+ * {@link Propagator} instance.
+ *
+ * @param propagator the propagator that will be used for tracing support
+ */
+ public PropagationWebGraphQlInterceptor(Propagator propagator) {
+ Assert.notNull(propagator, "propagator should not be null");
+ this.propagator = propagator;
+ }
+
+ @Override
+ public Mono intercept(WebGraphQlRequest request, Chain chain) {
+ request.configureExecutionInput((input, inputBuilder) -> {
+ HttpHeaders headers = request.getHeaders();
+ for (String field : this.propagator.fields()) {
+ if (headers.containsKey(field)) {
+ inputBuilder.graphQLContext(contextBuilder -> contextBuilder.of(field, headers.getFirst(field)));
+ }
+ }
+ return inputBuilder.build();
+ });
+ return chain.next(request);
+ }
+
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/observation/ExecutionRequestObservationContextTests.java b/spring-graphql/src/test/java/org/springframework/graphql/observation/ExecutionRequestObservationContextTests.java
new file mode 100644
index 00000000..6e5df645
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/observation/ExecutionRequestObservationContextTests.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020-2022 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.graphql.observation;
+
+
+import java.util.Map;
+
+import graphql.ExecutionInput;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ExecutionRequestObservationContext}.
+ *
+ * @author Brian Clozel
+ */
+class ExecutionRequestObservationContextTests {
+
+ @Test
+ void readPropagationFieldFromGraphQlContext() {
+ ExecutionInput executionInput = ExecutionInput
+ .newExecutionInput("{ notUsed }")
+ .graphQLContext(builder -> builder.of("X-Tracing-Test", "traceId"))
+ .build();
+ ExecutionRequestObservationContext context = new ExecutionRequestObservationContext(executionInput);
+ assertThat(context.getGetter().get(executionInput, "X-Tracing-Test")).isEqualTo("traceId");
+ }
+
+ @Test
+ void readPropagationFieldFromExtensions() {
+ ExecutionInput executionInput = ExecutionInput
+ .newExecutionInput("{ notUsed }")
+ .extensions(Map.of("X-Tracing-Test", "traceId"))
+ .build();
+ ExecutionRequestObservationContext context = new ExecutionRequestObservationContext(executionInput);
+ assertThat(context.getGetter().get(executionInput, "X-Tracing-Test")).isEqualTo("traceId");
+ }
+
+ @Test
+ void doesNotFailIsMissingPropagationField() {
+ ExecutionInput executionInput = ExecutionInput
+ .newExecutionInput("{ notUsed }")
+ .build();
+ ExecutionRequestObservationContext context = new ExecutionRequestObservationContext(executionInput);
+ assertThat(context.getGetter().get(executionInput, "X-Tracing-Test")).isNull();
+ }
+}
\ No newline at end of file
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/observation/GraphQlObservationInstrumentationTests.java b/spring-graphql/src/test/java/org/springframework/graphql/observation/GraphQlObservationInstrumentationTests.java
index 7f4edce2..cf6e6cd4 100644
--- a/spring-graphql/src/test/java/org/springframework/graphql/observation/GraphQlObservationInstrumentationTests.java
+++ b/spring-graphql/src/test/java/org/springframework/graphql/observation/GraphQlObservationInstrumentationTests.java
@@ -16,15 +16,26 @@
package org.springframework.graphql.observation;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
import graphql.GraphqlErrorBuilder;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
+import io.micrometer.observation.transport.ReceiverContext;
+import io.micrometer.tracing.Span;
+import io.micrometer.tracing.TraceContext;
+import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler;
+import io.micrometer.tracing.handler.TracingObservationHandler;
+import io.micrometer.tracing.propagation.Propagator;
+import io.micrometer.tracing.test.simple.SimpleSpanBuilder;
+import io.micrometer.tracing.test.simple.SimpleTracer;
+import io.micrometer.tracing.test.simple.TracerAssert;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.graphql.BookSource;
+import org.springframework.graphql.ExecutionGraphQlRequest;
import org.springframework.graphql.ExecutionGraphQlResponse;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.ResponseHelper;
@@ -32,6 +43,8 @@ import org.springframework.graphql.TestExecutionRequest;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.ErrorType;
+import static org.assertj.core.api.Assertions.assertThat;
+
/**
* Tests for {@link GraphQlObservationInstrumentation}.
*
@@ -186,6 +199,65 @@ class GraphQlObservationInstrumentationTests {
.hasParentObservationContextMatching(context -> context instanceof ExecutionRequestObservationContext);
}
+ @Test
+ void inboundTracingInformationIsPropagated() {
+ SimpleTracer simpleTracer = new SimpleTracer();
+ String traceId = "traceId";
+ TracingObservationHandler tracingHandler = new PropagatingReceiverTracingObservationHandler<>(simpleTracer, new TestPropagator(simpleTracer, traceId));
+ this.observationRegistry.observationConfig().observationHandler(tracingHandler);
+ String document = """
+ {
+ bookById(id: 1) {
+ name
+ }
+ }
+ """;
+ ExecutionGraphQlRequest executionRequest = TestExecutionRequest.forDocument(document);
+ executionRequest.configureExecutionInput((input, builder) ->
+ builder.graphQLContext(context -> context.of(TestPropagator.TRACING_HEADER_NAME, traceId)).build());
+ Mono responseMono = graphQlSetup
+ .queryFetcher("bookById", env -> BookSource.getBookWithoutAuthor(1L))
+ .toGraphQlService()
+ .execute(executionRequest);
+ ResponseHelper response = ResponseHelper.forResponse(responseMono);
+ TracerAssert.assertThat(simpleTracer)
+ .onlySpan()
+ .hasNameEqualTo("graphql query")
+ .hasKindEqualTo(Span.Kind.SERVER)
+ .hasTag("graphql.operation", "query")
+ .hasTag("graphql.outcome", "SUCCESS")
+ .hasTagWithKey("graphql.execution.id");
+ }
-}
\ No newline at end of file
+ static class TestPropagator implements Propagator {
+
+ public static String TRACING_HEADER_NAME = "X-Test-Tracing";
+
+ private final SimpleTracer tracer;
+
+ private final String traceId;
+
+ TestPropagator(SimpleTracer tracer, String traceId) {
+ this.tracer = tracer;
+ this.traceId = traceId;
+ }
+
+ @Override
+ public List fields() {
+ return List.of(TRACING_HEADER_NAME);
+ }
+
+ @Override
+ public void inject(TraceContext context, C carrier, Setter setter) {
+ setter.set(carrier, TRACING_HEADER_NAME, "traceId");
+ }
+
+ @Override
+ public Span.Builder extract(C carrier, Getter getter) {
+ String foo = getter.get(carrier, TRACING_HEADER_NAME);
+ assertThat(foo).isEqualTo(this.traceId);
+ return new SimpleSpanBuilder(this.tracer);
+ }
+ }
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/observation/PropagationWebGraphQlInterceptorTests.java b/spring-graphql/src/test/java/org/springframework/graphql/observation/PropagationWebGraphQlInterceptorTests.java
new file mode 100644
index 00000000..95364356
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/observation/PropagationWebGraphQlInterceptorTests.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2020-2022 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.graphql.observation;
+
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import graphql.ExecutionInput;
+import graphql.ExecutionResultImpl;
+import graphql.GraphQLContext;
+import io.micrometer.tracing.Span;
+import io.micrometer.tracing.TraceContext;
+import io.micrometer.tracing.propagation.Propagator;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Mono;
+
+import org.springframework.graphql.ExecutionGraphQlRequest;
+import org.springframework.graphql.ExecutionGraphQlResponse;
+import org.springframework.graphql.server.WebGraphQlHandler;
+import org.springframework.graphql.server.WebGraphQlRequest;
+import org.springframework.graphql.support.DefaultExecutionGraphQlResponse;
+import org.springframework.http.HttpHeaders;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for {@link PropagationWebGraphQlInterceptor}.
+ *
+ * @author Brian Clozel
+ */
+class PropagationWebGraphQlInterceptorTests {
+
+ PropagationWebGraphQlInterceptor interceptor = new PropagationWebGraphQlInterceptor(new TestPropagator());
+
+ @Test
+ void rejectsNullPropagator() {
+ assertThatThrownBy(() -> new PropagationWebGraphQlInterceptor(null))
+ .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("propagator should not be null");
+ }
+
+ @Test
+ void copiesPropagationHeadersWhenPresent() {
+ Map tracingHeaders = Map.of("X-Test-TraceId", "traceId", "baggage", "project=spring");
+ Map httpHeaders = new HashMap<>();
+ httpHeaders.put("Accept", "application/graphql+json");
+ httpHeaders.putAll(tracingHeaders);
+ WebGraphQlRequest webRequest = createRequest(httpHeaders);
+
+ WebGraphQlHandler handler = WebGraphQlHandler.builder(request -> {
+ GraphQLContext context = request.toExecutionInput().getGraphQLContext();
+ assertThatContextContains(context, tracingHeaders);
+ return emptyExecutionResult(request);
+ }).interceptor(this.interceptor).build();
+ handler.handleRequest(webRequest).block();
+ }
+
+ private void assertThatContextContains(GraphQLContext context, Map tracingHeaders) {
+ tracingHeaders.forEach((key, value) -> {
+ String actual = context.get(key);
+ assertThat(actual).isEqualTo(value);
+ });
+ }
+
+ WebGraphQlRequest createRequest(Map headers) {
+ HttpHeaders httpHeaders = new HttpHeaders();
+ headers.forEach(httpHeaders::set);
+ return new WebGraphQlRequest(URI.create("https://example.org/graphql"), httpHeaders, Map.of("query", "{ notUsed }"), "1", null);
+ }
+
+ private Mono emptyExecutionResult(ExecutionGraphQlRequest request) {
+ return Mono.just(new DefaultExecutionGraphQlResponse(
+ ExecutionInput.newExecutionInput("{}").build(),
+ ExecutionResultImpl.newExecutionResult().build()));
+ }
+
+
+ static class TestPropagator implements Propagator {
+
+ @Override
+ public List fields() {
+ return List.of("X-Test-TraceId", "baggage");
+ }
+
+ @Override
+ public void inject(TraceContext context, C carrier, Setter setter) {
+
+ }
+
+ @Override
+ public Span.Builder extract(C carrier, Getter getter) {
+ return null;
+ }
+ }
+
+}
\ No newline at end of file