Skip to content

Commit a50b418

Browse files
dariuszkuctapaderster
authored andcommitted
[spring-server] enable support for specifying instrumentation order (ExpediaGroup#395)
`graphql-java` allows for applying various `Instrumentations` to the GraphQL execution. If multilple instrumentations are to be applied they need to be wrapped in an instance of `ChainedInstrumentation` that has an explicit order of the instrumentations. In order to simplify Spring configuration we should allow users to either create a single instrumentation bean (could be a chained one) or multiple instrumentation beans and then automatically wrap them in chained instrumentation if necessary. Unfortunately, by default `graphql-java` `Instrumentation` does not specify order in which it should be applied (as it is generally explicitly configured). In order to support proper ordering of the instrumentations we can simply check if they implement `Ordered` interface and use their order value OR default to order of `0` otherwise (unordered behavior).
1 parent 89487d7 commit a50b418

File tree

2 files changed

+95
-3
lines changed

2 files changed

+95
-3
lines changed

graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/GraphQLAutoConfiguration.kt

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import graphql.execution.AsyncSerialExecutionStrategy
2828
import graphql.execution.DataFetcherExceptionHandler
2929
import graphql.execution.ExecutionIdProvider
3030
import graphql.execution.SubscriptionExecutionStrategy
31+
import graphql.execution.instrumentation.ChainedInstrumentation
3132
import graphql.execution.instrumentation.Instrumentation
3233
import graphql.execution.preparsed.PreparsedDocumentProvider
3334
import graphql.schema.GraphQLSchema
@@ -36,9 +37,15 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
3637
import org.springframework.context.annotation.Bean
3738
import org.springframework.context.annotation.Configuration
3839
import org.springframework.context.annotation.Import
40+
import org.springframework.core.Ordered
3941
import org.springframework.web.server.WebFilter
4042
import java.util.Optional
4143

44+
/**
45+
* Default order applied to Instrumentation beans.
46+
*/
47+
const val DEFAULT_INSTRUMENTATION_ORDER = 0
48+
4249
/**
4350
* SpringBoot auto-configuration that creates all beans required to start up reactive GraphQL web app.
4451
*/
@@ -62,7 +69,7 @@ class GraphQLAutoConfiguration {
6269
fun graphQL(
6370
schema: GraphQLSchema,
6471
dataFetcherExceptionHandler: DataFetcherExceptionHandler,
65-
instrumentation: Optional<Instrumentation>,
72+
instrumentations: Optional<List<Instrumentation>>,
6673
executionIdProvider: Optional<ExecutionIdProvider>,
6774
preparsedDocumentProvider: Optional<PreparsedDocumentProvider>
6875
): GraphQL {
@@ -71,8 +78,19 @@ class GraphQLAutoConfiguration {
7178
.mutationExecutionStrategy(AsyncSerialExecutionStrategy(dataFetcherExceptionHandler))
7279
.subscriptionExecutionStrategy(SubscriptionExecutionStrategy(dataFetcherExceptionHandler))
7380

74-
instrumentation.ifPresent {
75-
graphQL.instrumentation(it)
81+
instrumentations.ifPresent { unordered ->
82+
if (unordered.size == 1) {
83+
graphQL.instrumentation(unordered.first())
84+
} else {
85+
val sorted = unordered.sortedBy {
86+
if (it is Ordered) {
87+
it.order
88+
} else {
89+
DEFAULT_INSTRUMENTATION_ORDER
90+
}
91+
}
92+
graphQL.instrumentation(ChainedInstrumentation(sorted))
93+
}
7694
}
7795
executionIdProvider.ifPresent {
7896
graphQL.executionIdProvider(it)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.expediagroup.graphql.spring.instrumentation
2+
3+
import com.expediagroup.graphql.spring.DEFAULT_INSTRUMENTATION_ORDER
4+
import com.expediagroup.graphql.spring.model.GraphQLRequest
5+
import com.expediagroup.graphql.spring.operations.Query
6+
import graphql.ExecutionResult
7+
import graphql.ExecutionResultImpl
8+
import graphql.execution.instrumentation.Instrumentation
9+
import graphql.execution.instrumentation.SimpleInstrumentation
10+
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters
11+
import org.junit.jupiter.api.Test
12+
import org.springframework.beans.factory.annotation.Autowired
13+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
14+
import org.springframework.boot.test.context.SpringBootTest
15+
import org.springframework.context.annotation.Bean
16+
import org.springframework.context.annotation.Configuration
17+
import org.springframework.core.Ordered
18+
import org.springframework.http.MediaType
19+
import org.springframework.test.web.reactive.server.WebTestClient
20+
import java.util.concurrent.CompletableFuture
21+
import java.util.concurrent.atomic.AtomicInteger
22+
23+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = ["graphql.packages=com.expediagroup.graphql.spring.instrumentation"])
24+
@EnableAutoConfiguration
25+
class InstrumentationIT(@Autowired private val testClient: WebTestClient) {
26+
27+
@Configuration
28+
class TestConfiguration {
29+
private val atomicCounter = AtomicInteger()
30+
31+
@Bean
32+
fun query(): Query = BasicQuery()
33+
34+
@Bean
35+
fun firstInstrumentation(): Instrumentation = OrderedInstrumentation(DEFAULT_INSTRUMENTATION_ORDER, atomicCounter)
36+
37+
@Bean
38+
fun secondInstrumentation(): Instrumentation = OrderedInstrumentation(DEFAULT_INSTRUMENTATION_ORDER + 1, atomicCounter)
39+
}
40+
41+
class BasicQuery : Query {
42+
fun helloWorld(name: String) = "Hello $name!"
43+
}
44+
45+
class OrderedInstrumentation(private val instrumentationOrder: Int, private val counter: AtomicInteger) : SimpleInstrumentation(), Ordered {
46+
override fun instrumentExecutionResult(executionResult: ExecutionResult, parameters: InstrumentationExecutionParameters): CompletableFuture<ExecutionResult> {
47+
val extensions = mutableMapOf<Any, Any>()
48+
extensions[instrumentationOrder] = counter.getAndIncrement()
49+
val currentExt: Map<Any, Any>? = executionResult.extensions
50+
if (currentExt != null) {
51+
extensions.putAll(currentExt)
52+
}
53+
return CompletableFuture.completedFuture(ExecutionResultImpl(executionResult.getData(), executionResult.errors, extensions))
54+
}
55+
56+
override fun getOrder(): Int = instrumentationOrder
57+
}
58+
59+
@Test
60+
fun `verify instrumentations are applied in the specified order`() {
61+
testClient.post()
62+
.uri("/graphql")
63+
.accept(MediaType.APPLICATION_JSON)
64+
.contentType(MediaType.APPLICATION_JSON)
65+
.bodyValue(GraphQLRequest("query { helloWorld(name: \"World\") }"))
66+
.exchange()
67+
.expectBody()
68+
.jsonPath("$.data.helloWorld").isEqualTo("Hello World!")
69+
.jsonPath("$.errors").doesNotExist()
70+
.jsonPath("$.extensions").exists()
71+
.jsonPath("$.extensions.0").isEqualTo(0)
72+
.jsonPath("$.extensions.1").isEqualTo(1)
73+
}
74+
}

0 commit comments

Comments
 (0)