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