Use GraphQL Observation instrumentation

This commit migrates the existing metrics support (added in #29140) to
the new `Observation` instrumentation contributed in
spring-projects/spring-graphql#501.

We cannot have a smoother migration path here as the instrumentation
does not use the same context information for metadata extraction.

Closes gh-32794
pull/32812/head
Brian Clozel 2 years ago
parent 09f3d459c6
commit 9948fc3e39

@ -63,8 +63,6 @@ public class MetricsProperties {
private final Data data = new Data();
private final Graphql graphql = new Graphql();
private final System system = new System();
private final Distribution distribution = new Distribution();
@ -93,10 +91,6 @@ public class MetricsProperties {
return this.data;
}
public Graphql getGraphql() {
return this.graphql;
}
public System getSystem() {
return this.system;
}
@ -249,20 +243,6 @@ public class MetricsProperties {
}
public static class Graphql {
/**
* Auto-timed queries settings.
*/
@NestedConfigurationProperty
private final AutoTimeProperties autotime = new AutoTimeProperties();
public AutoTimeProperties getAutotime() {
return this.autotime;
}
}
public static class System {
private final Diskspace diskspace = new Diskspace();

@ -1,68 +0,0 @@
/*
* Copyright 2012-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.boot.actuate.autoconfigure.metrics.graphql;
import graphql.GraphQL;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
import org.springframework.boot.actuate.autoconfigure.metrics.PropertiesAutoTimer;
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.metrics.graphql.DefaultGraphQlTagsProvider;
import org.springframework.boot.actuate.metrics.graphql.GraphQlMetricsInstrumentation;
import org.springframework.boot.actuate.metrics.graphql.GraphQlTagsContributor;
import org.springframework.boot.actuate.metrics.graphql.GraphQlTagsProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.execution.GraphQlSource;
/**
* {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring
* GraphQL endpoints.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class })
@ConditionalOnBean(MeterRegistry.class)
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class })
@EnableConfigurationProperties(MetricsProperties.class)
public class GraphQlMetricsAutoConfiguration {
@Bean
@ConditionalOnMissingBean(GraphQlTagsProvider.class)
public DefaultGraphQlTagsProvider graphQlTagsProvider(ObjectProvider<GraphQlTagsContributor> contributors) {
return new DefaultGraphQlTagsProvider(contributors.orderedStream().toList());
}
@Bean
public GraphQlMetricsInstrumentation graphQlMetricsInstrumentation(MeterRegistry meterRegistry,
GraphQlTagsProvider tagsProvider, MetricsProperties properties) {
return new GraphQlMetricsInstrumentation(meterRegistry, tagsProvider,
new PropertiesAutoTimer(properties.getGraphql().getAutotime()));
}
}

@ -0,0 +1,53 @@
/*
* Copyright 2012-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.boot.actuate.autoconfigure.observation.graphql;
import graphql.GraphQL;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.observation.GraphQlObservationInstrumentation;
/**
* {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring
* GraphQL endpoints.
*
* @author Brian Clozel
* @since 3.0.0
*/
@AutoConfiguration(after = ObservationAutoConfiguration.class)
@ConditionalOnBean(ObservationRegistry.class)
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class, Observation.class })
@SuppressWarnings("removal")
public class GraphQlObservationAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlObservationInstrumentation graphQlObservationInstrumentation(
ObservationRegistry observationRegistry) {
return new GraphQlObservationInstrumentation(observationRegistry);
}
}

@ -15,6 +15,6 @@
*/
/**
* Auto-configuration for Spring GraphQL metrics.
* Auto-configuration for Spring GraphQL observations.
*/
package org.springframework.boot.actuate.autoconfigure.metrics.graphql;
package org.springframework.boot.actuate.autoconfigure.observation.graphql;

@ -1885,6 +1885,32 @@
"replacement": "management.wavefront.uri"
}
},
{
"name": "management.metrics.graphql.autotime.enabled",
"description": "Whether to automatically time web client requests.",
"defaultValue": true,
"deprecation": {
"level": "error",
"reason": "Should be applied at the ObservationRegistry level."
}
},
{
"name": "management.metrics.graphql.autotime.percentiles",
"description": "Computed non-aggregable percentiles to publish.",
"deprecation": {
"level": "error",
"reason": "Should be applied at the ObservationRegistry level."
}
},
{
"name": "management.metrics.graphql.autotime.percentiles-histogram",
"description": "Whether percentile histograms should be published.",
"defaultValue": false,
"deprecation": {
"level": "error",
"reason": "Should be applied at the ObservationRegistry level."
}
},
{
"name": "management.metrics.mongo.command.enabled",
"description": "Whether to enable Mongo client command metrics.",

@ -67,7 +67,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetri
org.springframework.boot.actuate.autoconfigure.metrics.export.stackdriver.StackdriverMetricsExportAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.export.statsd.StatsdMetricsExportAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront.WavefrontMetricsExportAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.graphql.GraphQlMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.observation.graphql.GraphQlObservationAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.integration.IntegrationMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.jersey.JerseyServerMetricsAutoConfiguration

@ -1,98 +0,0 @@
/*
* Copyright 2012-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.boot.actuate.autoconfigure.metrics.graphql;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.DataFetcher;
import io.micrometer.core.instrument.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
import org.springframework.boot.actuate.metrics.graphql.DefaultGraphQlTagsProvider;
import org.springframework.boot.actuate.metrics.graphql.GraphQlMetricsInstrumentation;
import org.springframework.boot.actuate.metrics.graphql.GraphQlTagsProvider;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GraphQlMetricsAutoConfiguration}.
*
* @author Brian Clozel
*/
class GraphQlMetricsAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple())
.withConfiguration(AutoConfigurations.of(GraphQlMetricsAutoConfiguration.class));
@Test
void backsOffWhenMeterRegistryIsMissing() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(GraphQlMetricsAutoConfiguration.class))
.run((context) -> assertThat(context).doesNotHaveBean(DefaultGraphQlTagsProvider.class)
.doesNotHaveBean(GraphQlMetricsInstrumentation.class));
}
@Test
void definesTagsProviderAndInstrumentationWhenMeterRegistryIsPresent() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(DefaultGraphQlTagsProvider.class)
.hasSingleBean(GraphQlMetricsInstrumentation.class));
}
@Test
void tagsProviderBacksOffIfAlreadyPresent() {
this.contextRunner.withUserConfiguration(TagsProviderConfiguration.class).run((context) -> assertThat(context)
.doesNotHaveBean(DefaultGraphQlTagsProvider.class).hasSingleBean(TestGraphQlTagsProvider.class));
}
@Configuration(proxyBeanMethods = false)
static class TagsProviderConfiguration {
@Bean
TestGraphQlTagsProvider tagsProvider() {
return new TestGraphQlTagsProvider();
}
}
static class TestGraphQlTagsProvider implements GraphQlTagsProvider {
@Override
public Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
Throwable exception) {
return null;
}
@Override
public Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error) {
return null;
}
@Override
public Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher,
InstrumentationFieldFetchParameters parameters, Throwable exception) {
return null;
}
}
}

@ -0,0 +1,71 @@
/*
* Copyright 2012-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.boot.actuate.autoconfigure.observation.graphql;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.observation.GraphQlObservationInstrumentation;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GraphQlObservationAutoConfiguration}.
*
* @author Brian Clozel
*/
class GraphQlObservationAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withBean(TestObservationRegistry.class, TestObservationRegistry::create)
.withConfiguration(AutoConfigurations.of(GraphQlObservationAutoConfiguration.class));
@Test
void backsOffWhenObservationRegistryIsMissing() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GraphQlObservationAutoConfiguration.class))
.run((context) -> assertThat(context).doesNotHaveBean(GraphQlObservationInstrumentation.class));
}
@Test
void definesInstrumentationWhenObservationRegistryIsPresent() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlObservationInstrumentation.class));
}
@Test
void instrumentationBacksOffIfAlreadyPresent() {
this.contextRunner.withUserConfiguration(InstrumentationConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(GraphQlObservationInstrumentation.class)
.hasBean("customInstrumentation"));
}
@Configuration(proxyBeanMethods = false)
static class InstrumentationConfiguration {
@Bean
GraphQlObservationInstrumentation customInstrumentation(ObservationRegistry registry) {
return new GraphQlObservationInstrumentation(registry);
}
}
}

@ -1,77 +0,0 @@
/*
* 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.boot.actuate.metrics.graphql;
import java.util.Collections;
import java.util.List;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.DataFetcher;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
/**
* Default implementation for {@link GraphQlTagsProvider}.
*
* @author Brian Clozel
* @since 2.7.0
*/
public class DefaultGraphQlTagsProvider implements GraphQlTagsProvider {
private final List<GraphQlTagsContributor> contributors;
public DefaultGraphQlTagsProvider(List<GraphQlTagsContributor> contributors) {
this.contributors = contributors;
}
public DefaultGraphQlTagsProvider() {
this(Collections.emptyList());
}
@Override
public Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
Throwable exception) {
Tags tags = Tags.of(GraphQlTags.executionOutcome(result, exception));
for (GraphQlTagsContributor contributor : this.contributors) {
tags = tags.and(contributor.getExecutionTags(parameters, result, exception));
}
return tags;
}
@Override
public Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error) {
Tags tags = Tags.of(GraphQlTags.errorType(error), GraphQlTags.errorPath(error));
for (GraphQlTagsContributor contributor : this.contributors) {
tags = tags.and(contributor.getErrorTags(parameters, error));
}
return tags;
}
@Override
public Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
Throwable exception) {
Tags tags = Tags.of(GraphQlTags.dataFetchingOutcome(exception), GraphQlTags.dataFetchingPath(parameters));
for (GraphQlTagsContributor contributor : this.contributors) {
tags = tags.and(contributor.getDataFetchingTags(dataFetcher, parameters, exception));
}
return tags;
}
}

@ -1,166 +0,0 @@
/*
* 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.boot.actuate.metrics.graphql;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicLong;
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.InstrumentationCreateStateParameters;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.DataFetcher;
import io.micrometer.core.instrument.DistributionSummary;
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;
/**
* Micrometer-based {@link SimpleInstrumentation}.
*
* @author Brian Clozel
* @since 2.7.0
*/
public class GraphQlMetricsInstrumentation extends SimpleInstrumentation {
private final MeterRegistry registry;
private final GraphQlTagsProvider tagsProvider;
private final AutoTimer autoTimer;
private final DistributionSummary dataFetchingSummary;
public GraphQlMetricsInstrumentation(MeterRegistry registry, GraphQlTagsProvider tagsProvider,
AutoTimer autoTimer) {
this.registry = registry;
this.tagsProvider = tagsProvider;
this.autoTimer = autoTimer;
this.dataFetchingSummary = DistributionSummary.builder("graphql.request.datafetch.count").baseUnit("calls")
.description("Count of DataFetcher calls per request.").register(this.registry);
}
@Override
public InstrumentationState createState(InstrumentationCreateStateParameters parameters) {
return new RequestMetricsInstrumentationState(this.autoTimer, this.registry);
}
@Override
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters,
InstrumentationState state) {
if (this.autoTimer.isEnabled() && state instanceof RequestMetricsInstrumentationState instrumentationState) {
instrumentationState.startTimer();
return new SimpleInstrumentationContext<ExecutionResult>() {
@Override
public void onCompleted(ExecutionResult result, Throwable exc) {
Iterable<Tag> tags = GraphQlMetricsInstrumentation.this.tagsProvider.getExecutionTags(parameters,
result, exc);
instrumentationState.tags(tags).stopTimer();
if (!result.getErrors().isEmpty()) {
result.getErrors()
.forEach((error) -> GraphQlMetricsInstrumentation.this.registry.counter("graphql.error",
GraphQlMetricsInstrumentation.this.tagsProvider.getErrorTags(parameters, error))
.increment());
}
GraphQlMetricsInstrumentation.this.dataFetchingSummary
.record(instrumentationState.getDataFetchingCount());
}
};
}
return super.beginExecution(parameters);
}
@Override
public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher,
InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
if (this.autoTimer.isEnabled() && !parameters.isTrivialDataFetcher()
&& state instanceof RequestMetricsInstrumentationState instrumentationState) {
return (environment) -> {
Timer.Sample sample = Timer.start(this.registry);
try {
Object value = dataFetcher.get(environment);
if (value instanceof CompletionStage<?> completion) {
return completion.whenComplete((result, error) -> recordDataFetcherMetric(sample,
instrumentationState, dataFetcher, parameters, error));
}
else {
recordDataFetcherMetric(sample, instrumentationState, dataFetcher, parameters, null);
return value;
}
}
catch (Throwable throwable) {
recordDataFetcherMetric(sample, instrumentationState, dataFetcher, parameters, throwable);
throw throwable;
}
};
}
return super.instrumentDataFetcher(dataFetcher, parameters, state);
}
private void recordDataFetcherMetric(Timer.Sample sample, RequestMetricsInstrumentationState instrumentationState,
DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters, Throwable throwable) {
Timer.Builder timer = this.autoTimer.builder("graphql.datafetcher");
timer.tags(this.tagsProvider.getDataFetchingTags(dataFetcher, parameters, throwable));
sample.stop(timer.register(this.registry));
instrumentationState.incrementDataFetchingCount();
}
static class RequestMetricsInstrumentationState implements InstrumentationState {
private final MeterRegistry registry;
private final Timer.Builder timer;
private Timer.Sample sample;
private final AtomicLong dataFetchingCount = new AtomicLong();
RequestMetricsInstrumentationState(AutoTimer autoTimer, MeterRegistry registry) {
this.timer = autoTimer.builder("graphql.request");
this.registry = registry;
}
RequestMetricsInstrumentationState tags(Iterable<Tag> tags) {
this.timer.tags(tags);
return this;
}
void startTimer() {
this.sample = Timer.start(this.registry);
}
void stopTimer() {
this.sample.stop(this.timer.register(this.registry));
}
void incrementDataFetchingCount() {
this.dataFetchingCount.incrementAndGet();
}
long getDataFetchingCount() {
return this.dataFetchingCount.get();
}
}
}

@ -1,101 +0,0 @@
/*
* 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.boot.actuate.metrics.graphql;
import java.util.List;
import graphql.ErrorClassification;
import graphql.ErrorType;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import graphql.execution.ExecutionStepInfo;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.GraphQLObjectType;
import io.micrometer.core.instrument.Tag;
import org.springframework.util.CollectionUtils;
/**
* Factory methods for Tags associated with a GraphQL request.
*
* @author Brian Clozel
* @since 2.7.0
*/
public final class GraphQlTags {
private static final Tag OUTCOME_SUCCESS = Tag.of("outcome", "SUCCESS");
private static final Tag OUTCOME_ERROR = Tag.of("outcome", "ERROR");
private static final Tag UNKNOWN_ERROR_TYPE = Tag.of("error.type", "UNKNOWN");
private GraphQlTags() {
}
public static Tag executionOutcome(ExecutionResult result, Throwable exception) {
if (exception == null && result.getErrors().isEmpty()) {
return OUTCOME_SUCCESS;
}
else {
return OUTCOME_ERROR;
}
}
public static Tag errorType(GraphQLError error) {
ErrorClassification errorType = error.getErrorType();
if (errorType instanceof ErrorType) {
return Tag.of("error.type", ((ErrorType) errorType).name());
}
return UNKNOWN_ERROR_TYPE;
}
public static Tag errorPath(GraphQLError error) {
StringBuilder builder = new StringBuilder();
List<Object> pathSegments = error.getPath();
if (!CollectionUtils.isEmpty(pathSegments)) {
builder.append('$');
for (Object segment : pathSegments) {
try {
Integer.parseUnsignedInt(segment.toString());
builder.append("[*]");
}
catch (NumberFormatException exc) {
builder.append('.');
builder.append(segment);
}
}
}
return Tag.of("error.path", builder.toString());
}
public static Tag dataFetchingOutcome(Throwable exception) {
return (exception != null) ? OUTCOME_ERROR : OUTCOME_SUCCESS;
}
public static Tag dataFetchingPath(InstrumentationFieldFetchParameters parameters) {
ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo();
StringBuilder dataFetchingPath = new StringBuilder();
if (executionStepInfo.hasParent() && executionStepInfo.getParent().getType() instanceof GraphQLObjectType) {
dataFetchingPath.append(((GraphQLObjectType) executionStepInfo.getParent().getType()).getName());
dataFetchingPath.append('.');
}
dataFetchingPath.append(executionStepInfo.getPath().getSegmentName());
return Tag.of("path", dataFetchingPath.toString());
}
}

@ -1,43 +0,0 @@
/*
* 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.boot.actuate.metrics.graphql;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.DataFetcher;
import io.micrometer.core.instrument.Tag;
/**
* A contributor of {@link Tag Tags} for Spring GraphQL-based request handling. Typically,
* used by a {@link GraphQlTagsProvider} to provide tags in addition to its defaults.
*
* @author Brian Clozel
* @since 2.7.0
*/
public interface GraphQlTagsContributor {
Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
Throwable exception);
Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error);
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
Throwable exception);
}

@ -1,42 +0,0 @@
/*
* 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.boot.actuate.metrics.graphql;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.DataFetcher;
import io.micrometer.core.instrument.Tag;
/**
* Provides {@link Tag Tags} for Spring GraphQL-based request handling.
*
* @author Brian Clozel
* @since 2.7.0
*/
public interface GraphQlTagsProvider {
Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
Throwable exception);
Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error);
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
Throwable exception);
}

@ -1,20 +0,0 @@
/*
* 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.
*/
/**
* Provides instrumentation support for Spring GraphQL.
*/
package org.springframework.boot.actuate.metrics.graphql;

@ -1,185 +0,0 @@
/*
* Copyright 2012-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.boot.actuate.metrics.graphql;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.ExecutionResultImpl;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.DataFetchingEnvironmentImpl;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.SchemaGenerator;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.MockClock;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.simple.SimpleConfig;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.metrics.AutoTimer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlMetricsInstrumentation}.
*
* @author Brian Clozel
*/
@SuppressWarnings("unchecked")
class GraphQlMetricsInstrumentationTests {
private final ExecutionInput input = ExecutionInput.newExecutionInput("{greeting}").build();
private final GraphQLSchema schema = SchemaGenerator.createdMockedSchema("type Query { greeting: String }");
private MeterRegistry registry;
private GraphQlMetricsInstrumentation instrumentation;
private InstrumentationState state;
private InstrumentationExecutionParameters parameters;
@BeforeEach
void setup() {
this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock());
this.instrumentation = new GraphQlMetricsInstrumentation(this.registry, mock(GraphQlTagsProvider.class),
AutoTimer.ENABLED);
this.state = this.instrumentation
.createState(new InstrumentationCreateStateParameters(this.schema, this.input));
this.parameters = new InstrumentationExecutionParameters(this.input, this.schema, this.state);
}
@Test
void shouldRecordTimerWhenResult() {
InstrumentationContext<ExecutionResult> execution = this.instrumentation.beginExecution(this.parameters,
this.state);
ExecutionResult result = new ExecutionResultImpl("Hello", null);
execution.onCompleted(result, null);
Timer timer = this.registry.find("graphql.request").timer();
assertThat(timer).isNotNull();
assertThat(timer.count()).isEqualTo(1);
}
@Test
void shouldRecordDataFetchingCount() throws Exception {
InstrumentationContext<ExecutionResult> execution = this.instrumentation.beginExecution(this.parameters,
this.state);
ExecutionResult result = new ExecutionResultImpl("Hello", null);
DataFetcher<String> dataFetcher = mock(DataFetcher.class);
given(dataFetcher.get(any())).willReturn("Hello");
InstrumentationFieldFetchParameters fieldFetchParameters = mockFieldFetchParameters(false);
DataFetcher<?> instrumented = this.instrumentation.instrumentDataFetcher(dataFetcher, fieldFetchParameters,
this.state);
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
instrumented.get(environment);
execution.onCompleted(result, null);
DistributionSummary summary = this.registry.find("graphql.request.datafetch.count").summary();
assertThat(summary).isNotNull();
assertThat(summary.count()).isEqualTo(1);
}
@Test
void shouldRecordDataFetchingMetricWhenSuccess() throws Exception {
DataFetcher<String> dataFetcher = mock(DataFetcher.class);
given(dataFetcher.get(any())).willReturn("Hello");
InstrumentationFieldFetchParameters fieldFetchParameters = mockFieldFetchParameters(false);
DataFetcher<?> instrumented = this.instrumentation.instrumentDataFetcher(dataFetcher, fieldFetchParameters,
this.state);
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
instrumented.get(environment);
Timer timer = this.registry.find("graphql.datafetcher").timer();
assertThat(timer).isNotNull();
assertThat(timer.count()).isEqualTo(1);
}
@Test
void shouldRecordDataFetchingMetricWhenSuccessCompletionStage() throws Exception {
DataFetcher<CompletionStage<String>> dataFetcher = mock(DataFetcher.class);
given(dataFetcher.get(any())).willReturn(CompletableFuture.completedFuture("Hello"));
InstrumentationFieldFetchParameters fieldFetchParameters = mockFieldFetchParameters(false);
DataFetcher<?> instrumented = this.instrumentation.instrumentDataFetcher(dataFetcher, fieldFetchParameters,
this.state);
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
instrumented.get(environment);
Timer timer = this.registry.find("graphql.datafetcher").timer();
assertThat(timer).isNotNull();
assertThat(timer.count()).isEqualTo(1);
}
@Test
void shouldRecordDataFetchingMetricWhenError() throws Exception {
DataFetcher<CompletionStage<String>> dataFetcher = mock(DataFetcher.class);
given(dataFetcher.get(any())).willThrow(new IllegalStateException("test"));
InstrumentationFieldFetchParameters fieldFetchParameters = mockFieldFetchParameters(false);
DataFetcher<?> instrumented = this.instrumentation.instrumentDataFetcher(dataFetcher, fieldFetchParameters,
this.state);
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
assertThatThrownBy(() -> instrumented.get(environment)).isInstanceOf(IllegalStateException.class);
Timer timer = this.registry.find("graphql.datafetcher").timer();
assertThat(timer).isNotNull();
assertThat(timer.count()).isEqualTo(1);
}
@Test
void shouldNotRecordDataFetchingMetricWhenTrivial() throws Exception {
DataFetcher<String> dataFetcher = mock(DataFetcher.class);
given(dataFetcher.get(any())).willReturn("Hello");
InstrumentationFieldFetchParameters fieldFetchParameters = mockFieldFetchParameters(true);
DataFetcher<?> instrumented = this.instrumentation.instrumentDataFetcher(dataFetcher, fieldFetchParameters,
this.state);
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
instrumented.get(environment);
Timer timer = this.registry.find("graphql.datafetcher").timer();
assertThat(timer).isNull();
}
private InstrumentationFieldFetchParameters mockFieldFetchParameters(boolean isTrivial) {
InstrumentationFieldFetchParameters fieldFetchParameters = mock(InstrumentationFieldFetchParameters.class);
given(fieldFetchParameters.isTrivialDataFetcher()).willReturn(isTrivial);
return fieldFetchParameters;
}
}

@ -1,95 +0,0 @@
/*
* Copyright 2012-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.boot.actuate.metrics.graphql;
import java.util.Arrays;
import graphql.ErrorType;
import graphql.ExecutionResult;
import graphql.ExecutionResultImpl;
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import io.micrometer.core.instrument.Tag;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GraphQlTags}.
*
* @author Brian Clozel
*/
class GraphQlTagsTests {
@Test
void executionOutcomeShouldSucceed() {
ExecutionResult result = ExecutionResultImpl.newExecutionResult().build();
Tag outcomeTag = GraphQlTags.executionOutcome(result, null);
assertThat(outcomeTag).isEqualTo(Tag.of("outcome", "SUCCESS"));
}
@Test
void executionOutcomeShouldErrorWhenExceptionThrown() {
ExecutionResult result = ExecutionResultImpl.newExecutionResult().build();
Tag tag = GraphQlTags.executionOutcome(result, new IllegalArgumentException("test error"));
assertThat(tag).isEqualTo(Tag.of("outcome", "ERROR"));
}
@Test
void executionOutcomeShouldErrorWhenResponseErrors() {
GraphQLError error = GraphqlErrorBuilder.newError().message("Invalid query").build();
Tag tag = GraphQlTags.executionOutcome(ExecutionResultImpl.newExecutionResult().addError(error).build(), null);
assertThat(tag).isEqualTo(Tag.of("outcome", "ERROR"));
}
@Test
void errorTypeShouldBeDefinedIfPresent() {
GraphQLError error = GraphqlErrorBuilder.newError().errorType(ErrorType.DataFetchingException)
.message("test error").build();
Tag errorTypeTag = GraphQlTags.errorType(error);
assertThat(errorTypeTag).isEqualTo(Tag.of("error.type", "DataFetchingException"));
}
@Test
void errorPathShouldUseJsonPathFormat() {
GraphQLError error = GraphqlErrorBuilder.newError().path(Arrays.asList("project", "name")).message("test error")
.build();
Tag errorPathTag = GraphQlTags.errorPath(error);
assertThat(errorPathTag).isEqualTo(Tag.of("error.path", "$.project.name"));
}
@Test
void errorPathShouldUseJsonPathFormatForIndices() {
GraphQLError error = GraphqlErrorBuilder.newError().path(Arrays.asList("issues", "42", "title"))
.message("test error").build();
Tag errorPathTag = GraphQlTags.errorPath(error);
assertThat(errorPathTag).isEqualTo(Tag.of("error.path", "$.issues[*].title"));
}
@Test
void dataFetchingOutcomeShouldBeSuccessfulIfNoException() {
Tag fetchingOutcomeTag = GraphQlTags.dataFetchingOutcome(null);
assertThat(fetchingOutcomeTag).isEqualTo(Tag.of("outcome", "SUCCESS"));
}
@Test
void dataFetchingOutcomeShouldBeErrorIfException() {
Tag fetchingOutcomeTag = GraphQlTags.dataFetchingOutcome(new IllegalStateException("error state"));
assertThat(fetchingOutcomeTag).isEqualTo(Tag.of("outcome", "ERROR"));
}
}
Loading…
Cancel
Save