From 59d75a948e8813bca3e2197a2eab80f77398189c Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 13 Nov 2020 11:51:33 +0100 Subject: [PATCH] Add initial Metrics support This commit provides an initial instrumentation for metrics support with micrometer. The graphQL `SimpleInstrumentation` is used to intercept query execution and collect execution time as well as specific tags. The set and names of metrics and tags are temporary and we should refine this infrastructure. Closes gh-16 --- spring-graphql-web/build.gradle | 3 + .../graphql/DefaultGraphQLTagsProvider.java | 50 +++++++++++ .../GraphQLMetricsAutoConfiguration.java | 54 ++++++++++++ .../GraphQLMetricsInstrumentation.java | 84 +++++++++++++++++++ .../graphql/GraphQLMetricsProperties.java | 38 +++++++++ .../metrics/graphql/GraphQLTagsProvider.java | 26 ++++++ .../graphql/GraphQLAutoConfiguration.java | 14 +++- .../boot/graphql/GraphQLProperties.java | 3 + .../main/resources/META-INF/spring.factories | 1 + 9 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/DefaultGraphQLTagsProvider.java create mode 100644 spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLMetricsAutoConfiguration.java create mode 100644 spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLMetricsInstrumentation.java create mode 100644 spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLMetricsProperties.java create mode 100644 spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLTagsProvider.java diff --git a/spring-graphql-web/build.gradle b/spring-graphql-web/build.gradle index 81c78545..7ccf50a1 100644 --- a/spring-graphql-web/build.gradle +++ b/spring-graphql-web/build.gradle @@ -10,6 +10,9 @@ dependencies { compileOnly 'org.springframework:spring-webmvc' compileOnly 'javax.servlet:javax.servlet-api' + compileOnly 'io.micrometer:micrometer-core' + compileOnly 'org.springframework.boot:spring-boot-actuator-autoconfigure' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' annotationProcessor 'org.springframework.boot:spring-boot-autoconfigure-processor' diff --git a/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/DefaultGraphQLTagsProvider.java b/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/DefaultGraphQLTagsProvider.java new file mode 100644 index 00000000..19e0d7a3 --- /dev/null +++ b/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/DefaultGraphQLTagsProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2020 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.boot.actuate.metrics.graphql; + +import graphql.ErrorClassification; +import graphql.ErrorType; +import graphql.ExecutionResult; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; + +public class DefaultGraphQLTagsProvider implements GraphQLTagsProvider { + + private static final Tag OUTCOME_SUCCESS = Tag.of("outcome", "SUCCESS"); + + private static final Tag OUTCOME_ERROR = Tag.of("outcome", "ERROR"); + + + @Override + public Iterable getTags(InstrumentationExecutionParameters parameters, ExecutionResult result, Throwable exception) { + Tags tags = Tags.of(Tag.of("query", parameters.getQuery())); + if (result.isDataPresent()) { + tags = tags.and(OUTCOME_SUCCESS); + } + else { + tags = tags.and(OUTCOME_ERROR); + if (!result.getErrors().isEmpty()) { + ErrorClassification errorClassification = result.getErrors().get(0).getErrorType(); + if (errorClassification instanceof ErrorType) { + tags = tags.and(Tag.of("errorType", ((ErrorType) errorClassification).name())); + } + } + } + return tags; + } +} diff --git a/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLMetricsAutoConfiguration.java b/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLMetricsAutoConfiguration.java new file mode 100644 index 00000000..c2a4fbab --- /dev/null +++ b/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLMetricsAutoConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2020 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.boot.actuate.metrics.graphql; + +import io.micrometer.core.instrument.MeterRegistry; + +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring GraphQL + * endpoints. + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter({MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class}) +@ConditionalOnBean(MeterRegistry.class) +@EnableConfigurationProperties(GraphQLMetricsProperties.class) +public class GraphQLMetricsAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(GraphQLTagsProvider.class) + public DefaultGraphQLTagsProvider graphQLTagsProvider() { + return new DefaultGraphQLTagsProvider(); + } + + @Bean + public GraphQLMetricsInstrumentation graphQLMetricsInstrumentation(MeterRegistry meterRegistry, + GraphQLTagsProvider tagsProvider, GraphQLMetricsProperties properties) { + return new GraphQLMetricsInstrumentation(meterRegistry, tagsProvider, properties.getAutotime()); + } +} diff --git a/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLMetricsInstrumentation.java b/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLMetricsInstrumentation.java new file mode 100644 index 00000000..caa3a227 --- /dev/null +++ b/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLMetricsInstrumentation.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2020 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.boot.actuate.metrics.graphql; + +import graphql.ExecutionResult; +import graphql.execution.instrumentation.InstrumentationContext; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.SimpleInstrumentationContext; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; + +import org.springframework.boot.actuate.metrics.AutoTimer; + +public class GraphQLMetricsInstrumentation extends SimpleInstrumentation { + + private final MeterRegistry registry; + + private final GraphQLTagsProvider tagsProvider; + + private final AutoTimer autoTimer; + + public GraphQLMetricsInstrumentation(MeterRegistry registry, GraphQLTagsProvider tagsProvider, AutoTimer autoTimer) { + this.registry = registry; + this.tagsProvider = tagsProvider; + this.autoTimer = autoTimer; + } + + @Override + public InstrumentationState createState() { + return new MetricsInstrumentationState(this.registry); + } + + @Override + public InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters) { + MetricsInstrumentationState state = parameters.getInstrumentationState(); + state.startTimer(); + + return new SimpleInstrumentationContext() { + @Override + public void onCompleted(ExecutionResult result, Throwable exc) { + Iterable tags = tagsProvider.getTags(parameters, result, exc); + state.stopTimer(autoTimer.builder("graphql.query").tags(tags)); + } + }; + } + + + static class MetricsInstrumentationState implements InstrumentationState { + + private final MeterRegistry registry; + + private Timer.Sample timerSample; + + MetricsInstrumentationState(MeterRegistry registry) { + this.registry = registry; + } + + public void startTimer() { + this.timerSample = Timer.start(this.registry); + } + + public void stopTimer(Timer.Builder timer) { + this.timerSample.stop(timer.register(this.registry)); + } + } + +} diff --git a/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLMetricsProperties.java b/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLMetricsProperties.java new file mode 100644 index 00000000..297448a3 --- /dev/null +++ b/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLMetricsProperties.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2020 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.boot.actuate.metrics.graphql; + +import org.springframework.boot.actuate.autoconfigure.metrics.AutoTimeProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +/** + * This class could be merged with {@link org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties} + */ +@ConfigurationProperties("management.metrics.graphql") +public class GraphQLMetricsProperties { + + /** + * Auto-timed queries settings. + */ + @NestedConfigurationProperty + private final AutoTimeProperties autotime = new AutoTimeProperties(); + + public AutoTimeProperties getAutotime() { + return this.autotime; + } +} diff --git a/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLTagsProvider.java b/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLTagsProvider.java new file mode 100644 index 00000000..9efc6998 --- /dev/null +++ b/spring-graphql-web/src/main/java/org/springframework/boot/actuate/metrics/graphql/GraphQLTagsProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2020 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.boot.actuate.metrics.graphql; + +import graphql.ExecutionResult; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; +import io.micrometer.core.instrument.Tag; + +public interface GraphQLTagsProvider { + + Iterable getTags(InstrumentationExecutionParameters parameters, ExecutionResult result, Throwable exception); +} diff --git a/spring-graphql-web/src/main/java/org/springframework/boot/graphql/GraphQLAutoConfiguration.java b/spring-graphql-web/src/main/java/org/springframework/boot/graphql/GraphQLAutoConfiguration.java index 315bdd69..6408ea51 100644 --- a/spring-graphql-web/src/main/java/org/springframework/boot/graphql/GraphQLAutoConfiguration.java +++ b/spring-graphql-web/src/main/java/org/springframework/boot/graphql/GraphQLAutoConfiguration.java @@ -17,8 +17,12 @@ package org.springframework.boot.graphql; import java.io.File; import java.io.FileNotFoundException; +import java.util.List; +import java.util.stream.Collectors; import graphql.GraphQL; +import graphql.execution.instrumentation.ChainedInstrumentation; +import graphql.execution.instrumentation.Instrumentation; import graphql.schema.GraphQLSchema; import graphql.schema.idl.RuntimeWiring; import graphql.schema.idl.SchemaGenerator; @@ -52,11 +56,17 @@ public class GraphQLAutoConfiguration { } @Bean - public GraphQL.Builder graphQLBuilder(GraphQLProperties properties, RuntimeWiring runtimeWiring) { + public GraphQL.Builder graphQLBuilder(GraphQLProperties properties, RuntimeWiring runtimeWiring, + ObjectProvider instrumentationsProvider) { try { File schemaFile = ResourceUtils.getFile(properties.getSchemaLocation()); GraphQLSchema schema = buildSchema(schemaFile, runtimeWiring); - return GraphQL.newGraphQL(schema); + GraphQL.Builder builder = GraphQL.newGraphQL(schema); + List instrumentations = instrumentationsProvider.orderedStream().collect(Collectors.toList()); + if (!instrumentations.isEmpty()) { + builder = builder.instrumentation(new ChainedInstrumentation(instrumentations)); + } + return builder; } catch (FileNotFoundException ex) { throw new MissingGraphQLSchemaException(properties.getSchemaLocation()); diff --git a/spring-graphql-web/src/main/java/org/springframework/boot/graphql/GraphQLProperties.java b/spring-graphql-web/src/main/java/org/springframework/boot/graphql/GraphQLProperties.java index 190cb220..5d1ddb06 100644 --- a/spring-graphql-web/src/main/java/org/springframework/boot/graphql/GraphQLProperties.java +++ b/spring-graphql-web/src/main/java/org/springframework/boot/graphql/GraphQLProperties.java @@ -15,7 +15,9 @@ */ package org.springframework.boot.graphql; +import org.springframework.boot.actuate.autoconfigure.metrics.AutoTimeProperties; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; @ConfigurationProperties(prefix = "spring.graphql") public class GraphQLProperties { @@ -45,4 +47,5 @@ public class GraphQLProperties { public void setSchemaLocation(String schemaLocation) { this.schemaLocation = schemaLocation; } + } diff --git a/spring-graphql-web/src/main/resources/META-INF/spring.factories b/spring-graphql-web/src/main/resources/META-INF/spring.factories index 867f3762..f190d798 100644 --- a/spring-graphql-web/src/main/resources/META-INF/spring.factories +++ b/spring-graphql-web/src/main/resources/META-INF/spring.factories @@ -1,4 +1,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.boot.actuate.metrics.graphql.GraphQLMetricsAutoConfiguration,\ org.springframework.boot.graphql.GraphQLAutoConfiguration,\ org.springframework.boot.graphql.WebFluxGraphQLAutoConfiguration,\ org.springframework.boot.graphql.WebMvcGraphQLAutoConfiguration