Skip to content

Commit a7839bc

Browse files
committed
Add Metrics support for Spring GraphQL
This commit adds the required infrastructure for instrumenting the GraphQL engine and datafetchers in order to collect metrics. With this infrastructure, we can collect metrics such as: * "graphql.request", a timer for GraphQL query * "graphql.datafetcher", a timer for GraphQL datafetcher calls * "graphql.request.datafetch.count", a distribution summary of datafetcher count per query * "graphql.error", an error counter See gh-29140
1 parent a34308e commit a7839bc

File tree

12 files changed

+731
-8
lines changed

12 files changed

+731
-8
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ dependencies {
146146
optional("org.springframework.data:spring-data-elasticsearch") {
147147
exclude group: "commons-logging", module: "commons-logging"
148148
}
149+
optional("org.springframework.graphql:spring-graphql")
149150
optional("org.springframework.integration:spring-integration-core")
150151
optional("org.springframework.kafka:spring-kafka")
151152
optional("org.springframework.security:spring-security-config")

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
3333
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
3434
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
35+
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
3536
import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration;
3637
import org.springframework.boot.builder.SpringApplicationBuilder;
3738
import org.springframework.boot.test.util.ApplicationContextTestUtils;
@@ -69,7 +70,7 @@ void testChild() {
6970
@Configuration
7071
@EnableAutoConfiguration(exclude = { ElasticsearchDataAutoConfiguration.class,
7172
ElasticsearchRepositoriesAutoConfiguration.class, CassandraAutoConfiguration.class,
72-
CassandraDataAutoConfiguration.class, MongoDataAutoConfiguration.class,
73+
CassandraDataAutoConfiguration.class, GraphQlAutoConfiguration.class, MongoDataAutoConfiguration.class,
7374
MongoReactiveDataAutoConfiguration.class, Neo4jAutoConfiguration.class, Neo4jDataAutoConfiguration.class,
7475
Neo4jRepositoriesAutoConfiguration.class, RedisAutoConfiguration.class,
7576
RedisRepositoriesAutoConfiguration.class, FlywayAutoConfiguration.class, MetricsAutoConfiguration.class })
@@ -80,7 +81,7 @@ static class Parent {
8081
@Configuration
8182
@EnableAutoConfiguration(exclude = { ElasticsearchDataAutoConfiguration.class,
8283
ElasticsearchRepositoriesAutoConfiguration.class, CassandraAutoConfiguration.class,
83-
CassandraDataAutoConfiguration.class, MongoDataAutoConfiguration.class,
84+
CassandraDataAutoConfiguration.class, GraphQlAutoConfiguration.class, MongoDataAutoConfiguration.class,
8485
MongoReactiveDataAutoConfiguration.class, Neo4jAutoConfiguration.class, Neo4jDataAutoConfiguration.class,
8586
Neo4jRepositoriesAutoConfiguration.class, RedisAutoConfiguration.class,
8687
RedisRepositoriesAutoConfiguration.class, FlywayAutoConfiguration.class, MetricsAutoConfiguration.class })

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebEndpointsAutoConfigurationIntegrationTests.java

+7-6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
3535
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
3636
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
37+
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
3738
import org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration;
3839
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
3940
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
@@ -75,12 +76,12 @@ private ReactiveWebApplicationContextRunner reactiveWebRunner() {
7576
}
7677

7778
@EnableAutoConfiguration(exclude = { FlywayAutoConfiguration.class, LiquibaseAutoConfiguration.class,
78-
CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class, Neo4jDataAutoConfiguration.class,
79-
Neo4jRepositoriesAutoConfiguration.class, MongoAutoConfiguration.class, MongoDataAutoConfiguration.class,
80-
MongoReactiveAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class,
81-
RepositoryRestMvcAutoConfiguration.class, HazelcastAutoConfiguration.class,
82-
ElasticsearchDataAutoConfiguration.class, SolrAutoConfiguration.class, RedisAutoConfiguration.class,
83-
RedisRepositoriesAutoConfiguration.class, MetricsAutoConfiguration.class })
79+
CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class, GraphQlAutoConfiguration.class,
80+
Neo4jDataAutoConfiguration.class, Neo4jRepositoriesAutoConfiguration.class, MongoAutoConfiguration.class,
81+
MongoDataAutoConfiguration.class, MongoReactiveAutoConfiguration.class,
82+
MongoReactiveDataAutoConfiguration.class, RepositoryRestMvcAutoConfiguration.class,
83+
HazelcastAutoConfiguration.class, ElasticsearchDataAutoConfiguration.class, SolrAutoConfiguration.class,
84+
RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class, MetricsAutoConfiguration.class })
8485
@SpringBootConfiguration
8586
static class WebEndpointTestApplication {
8687

spring-boot-project/spring-boot-actuator/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ dependencies {
7878
optional("org.springframework.data:spring-data-mongodb")
7979
optional("org.springframework.data:spring-data-redis")
8080
optional("org.springframework.data:spring-data-rest-webmvc")
81+
optional("org.springframework.graphql:spring-graphql")
8182
optional("org.springframework.integration:spring-integration-core")
8283
optional("org.springframework.security:spring-security-core")
8384
optional("org.springframework.security:spring-security-web")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2020-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.metrics.graphql;
18+
19+
import java.util.Collections;
20+
import java.util.List;
21+
22+
import graphql.ExecutionResult;
23+
import graphql.GraphQLError;
24+
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
25+
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
26+
import graphql.schema.DataFetcher;
27+
import io.micrometer.core.instrument.Tag;
28+
import io.micrometer.core.instrument.Tags;
29+
30+
/**
31+
* Default implementation for {@link GraphQlTagsProvider}.
32+
*
33+
* @author Brian Clozel
34+
* @since 2.7.0
35+
*/
36+
public class DefaultGraphQlTagsProvider implements GraphQlTagsProvider {
37+
38+
private final List<GraphQlTagsContributor> contributors;
39+
40+
public DefaultGraphQlTagsProvider(List<GraphQlTagsContributor> contributors) {
41+
this.contributors = contributors;
42+
}
43+
44+
public DefaultGraphQlTagsProvider() {
45+
this(Collections.emptyList());
46+
}
47+
48+
@Override
49+
public Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
50+
Throwable exception) {
51+
Tags tags = Tags.of(GraphQlTags.executionOutcome(result, exception));
52+
for (GraphQlTagsContributor contributor : this.contributors) {
53+
tags = tags.and(contributor.getExecutionTags(parameters, result, exception));
54+
}
55+
return tags;
56+
}
57+
58+
@Override
59+
public Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error) {
60+
Tags tags = Tags.of(GraphQlTags.errorType(error), GraphQlTags.errorPath(error));
61+
for (GraphQlTagsContributor contributor : this.contributors) {
62+
tags = tags.and(contributor.getErrorTags(parameters, error));
63+
}
64+
return tags;
65+
}
66+
67+
@Override
68+
public Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
69+
Throwable exception) {
70+
Tags tags = Tags.of(GraphQlTags.dataFetchingOutcome(exception), GraphQlTags.dataFetchingPath(parameters));
71+
for (GraphQlTagsContributor contributor : this.contributors) {
72+
tags = tags.and(contributor.getDataFetchingTags(dataFetcher, parameters, exception));
73+
}
74+
return tags;
75+
}
76+
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright 2020-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.metrics.graphql;
18+
19+
import java.util.concurrent.CompletionStage;
20+
import java.util.concurrent.atomic.AtomicLong;
21+
22+
import graphql.ExecutionResult;
23+
import graphql.execution.instrumentation.InstrumentationContext;
24+
import graphql.execution.instrumentation.InstrumentationState;
25+
import graphql.execution.instrumentation.SimpleInstrumentation;
26+
import graphql.execution.instrumentation.SimpleInstrumentationContext;
27+
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
28+
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
29+
import graphql.schema.DataFetcher;
30+
import io.micrometer.core.instrument.DistributionSummary;
31+
import io.micrometer.core.instrument.MeterRegistry;
32+
import io.micrometer.core.instrument.Tag;
33+
import io.micrometer.core.instrument.Timer;
34+
35+
import org.springframework.boot.actuate.metrics.AutoTimer;
36+
import org.springframework.lang.Nullable;
37+
38+
public class GraphQlMetricsInstrumentation extends SimpleInstrumentation {
39+
40+
private final MeterRegistry registry;
41+
42+
private final GraphQlTagsProvider tagsProvider;
43+
44+
private final AutoTimer autoTimer;
45+
46+
private final DistributionSummary dataFetchingSummary;
47+
48+
public GraphQlMetricsInstrumentation(MeterRegistry registry, GraphQlTagsProvider tagsProvider,
49+
AutoTimer autoTimer) {
50+
this.registry = registry;
51+
this.tagsProvider = tagsProvider;
52+
this.autoTimer = autoTimer;
53+
this.dataFetchingSummary = DistributionSummary.builder("graphql.request.datafetch.count").baseUnit("calls")
54+
.description("Count of DataFetcher calls per request.").register(this.registry);
55+
}
56+
57+
@Override
58+
public InstrumentationState createState() {
59+
return new RequestMetricsInstrumentationState(this.autoTimer, this.registry);
60+
}
61+
62+
@Override
63+
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
64+
if (this.autoTimer.isEnabled()) {
65+
RequestMetricsInstrumentationState state = parameters.getInstrumentationState();
66+
state.startTimer();
67+
return new SimpleInstrumentationContext<ExecutionResult>() {
68+
@Override
69+
public void onCompleted(ExecutionResult result, Throwable exc) {
70+
Iterable<Tag> tags = GraphQlMetricsInstrumentation.this.tagsProvider.getExecutionTags(parameters,
71+
result, exc);
72+
state.tags(tags).stopTimer();
73+
if (!result.getErrors().isEmpty()) {
74+
result.getErrors()
75+
.forEach((error) -> GraphQlMetricsInstrumentation.this.registry.counter("graphql.error",
76+
GraphQlMetricsInstrumentation.this.tagsProvider.getErrorTags(parameters, error))
77+
.increment());
78+
}
79+
GraphQlMetricsInstrumentation.this.dataFetchingSummary.record(state.getDataFetchingCount());
80+
}
81+
};
82+
}
83+
return super.beginExecution(parameters);
84+
}
85+
86+
@Override
87+
public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher,
88+
InstrumentationFieldFetchParameters parameters) {
89+
if (this.autoTimer.isEnabled() && !parameters.isTrivialDataFetcher()) {
90+
return (environment) -> {
91+
Timer.Sample sample = Timer.start(this.registry);
92+
try {
93+
Object value = dataFetcher.get(environment);
94+
if (value instanceof CompletionStage<?>) {
95+
CompletionStage<?> completion = (CompletionStage<?>) value;
96+
return completion.whenComplete(
97+
(result, error) -> recordDataFetcherMetric(sample, dataFetcher, parameters, error));
98+
}
99+
else {
100+
recordDataFetcherMetric(sample, dataFetcher, parameters, null);
101+
return value;
102+
}
103+
}
104+
catch (Throwable throwable) {
105+
recordDataFetcherMetric(sample, dataFetcher, parameters, throwable);
106+
throw throwable;
107+
}
108+
};
109+
}
110+
return super.instrumentDataFetcher(dataFetcher, parameters);
111+
}
112+
113+
private void recordDataFetcherMetric(Timer.Sample sample, DataFetcher<?> dataFetcher,
114+
InstrumentationFieldFetchParameters parameters, @Nullable Throwable throwable) {
115+
Timer.Builder timer = this.autoTimer.builder("graphql.datafetcher");
116+
timer.tags(this.tagsProvider.getDataFetchingTags(dataFetcher, parameters, throwable));
117+
sample.stop(timer.register(this.registry));
118+
RequestMetricsInstrumentationState state = parameters.getInstrumentationState();
119+
state.incrementDataFetchingCount();
120+
}
121+
122+
static class RequestMetricsInstrumentationState implements InstrumentationState {
123+
124+
private final MeterRegistry registry;
125+
126+
private final Timer.Builder timer;
127+
128+
private Timer.Sample sample;
129+
130+
private AtomicLong dataFetchingCount = new AtomicLong(0L);
131+
132+
RequestMetricsInstrumentationState(AutoTimer autoTimer, MeterRegistry registry) {
133+
this.timer = autoTimer.builder("graphql.request");
134+
this.registry = registry;
135+
}
136+
137+
RequestMetricsInstrumentationState tags(Iterable<Tag> tags) {
138+
this.timer.tags(tags);
139+
return this;
140+
}
141+
142+
void startTimer() {
143+
this.sample = Timer.start(this.registry);
144+
}
145+
146+
void stopTimer() {
147+
this.sample.stop(this.timer.register(this.registry));
148+
}
149+
150+
void incrementDataFetchingCount() {
151+
this.dataFetchingCount.incrementAndGet();
152+
}
153+
154+
long getDataFetchingCount() {
155+
return this.dataFetchingCount.get();
156+
}
157+
158+
}
159+
160+
}

0 commit comments

Comments
 (0)