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
This commit is contained in:
parent
a34308e5f7
commit
a7839bc9b9
|
@ -146,6 +146,7 @@ dependencies {
|
|||
optional("org.springframework.data:spring-data-elasticsearch") {
|
||||
exclude group: "commons-logging", module: "commons-logging"
|
||||
}
|
||||
optional("org.springframework.graphql:spring-graphql")
|
||||
optional("org.springframework.integration:spring-integration-core")
|
||||
optional("org.springframework.kafka:spring-kafka")
|
||||
optional("org.springframework.security:spring-security-config")
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoCo
|
|||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.test.util.ApplicationContextTestUtils;
|
||||
|
@ -69,7 +70,7 @@ class SpringApplicationHierarchyTests {
|
|||
@Configuration
|
||||
@EnableAutoConfiguration(exclude = { ElasticsearchDataAutoConfiguration.class,
|
||||
ElasticsearchRepositoriesAutoConfiguration.class, CassandraAutoConfiguration.class,
|
||||
CassandraDataAutoConfiguration.class, MongoDataAutoConfiguration.class,
|
||||
CassandraDataAutoConfiguration.class, GraphQlAutoConfiguration.class, MongoDataAutoConfiguration.class,
|
||||
MongoReactiveDataAutoConfiguration.class, Neo4jAutoConfiguration.class, Neo4jDataAutoConfiguration.class,
|
||||
Neo4jRepositoriesAutoConfiguration.class, RedisAutoConfiguration.class,
|
||||
RedisRepositoriesAutoConfiguration.class, FlywayAutoConfiguration.class, MetricsAutoConfiguration.class })
|
||||
|
@ -80,7 +81,7 @@ class SpringApplicationHierarchyTests {
|
|||
@Configuration
|
||||
@EnableAutoConfiguration(exclude = { ElasticsearchDataAutoConfiguration.class,
|
||||
ElasticsearchRepositoriesAutoConfiguration.class, CassandraAutoConfiguration.class,
|
||||
CassandraDataAutoConfiguration.class, MongoDataAutoConfiguration.class,
|
||||
CassandraDataAutoConfiguration.class, GraphQlAutoConfiguration.class, MongoDataAutoConfiguration.class,
|
||||
MongoReactiveDataAutoConfiguration.class, Neo4jAutoConfiguration.class, Neo4jDataAutoConfiguration.class,
|
||||
Neo4jRepositoriesAutoConfiguration.class, RedisAutoConfiguration.class,
|
||||
RedisRepositoriesAutoConfiguration.class, FlywayAutoConfiguration.class, MetricsAutoConfiguration.class })
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
|||
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
|
||||
|
@ -75,12 +76,12 @@ class WebEndpointsAutoConfigurationIntegrationTests {
|
|||
}
|
||||
|
||||
@EnableAutoConfiguration(exclude = { FlywayAutoConfiguration.class, LiquibaseAutoConfiguration.class,
|
||||
CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class, Neo4jDataAutoConfiguration.class,
|
||||
Neo4jRepositoriesAutoConfiguration.class, MongoAutoConfiguration.class, MongoDataAutoConfiguration.class,
|
||||
MongoReactiveAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class,
|
||||
RepositoryRestMvcAutoConfiguration.class, HazelcastAutoConfiguration.class,
|
||||
ElasticsearchDataAutoConfiguration.class, SolrAutoConfiguration.class, RedisAutoConfiguration.class,
|
||||
RedisRepositoriesAutoConfiguration.class, MetricsAutoConfiguration.class })
|
||||
CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class, GraphQlAutoConfiguration.class,
|
||||
Neo4jDataAutoConfiguration.class, Neo4jRepositoriesAutoConfiguration.class, MongoAutoConfiguration.class,
|
||||
MongoDataAutoConfiguration.class, MongoReactiveAutoConfiguration.class,
|
||||
MongoReactiveDataAutoConfiguration.class, RepositoryRestMvcAutoConfiguration.class,
|
||||
HazelcastAutoConfiguration.class, ElasticsearchDataAutoConfiguration.class, SolrAutoConfiguration.class,
|
||||
RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class, MetricsAutoConfiguration.class })
|
||||
@SpringBootConfiguration
|
||||
static class WebEndpointTestApplication {
|
||||
|
||||
|
|
|
@ -78,6 +78,7 @@ dependencies {
|
|||
optional("org.springframework.data:spring-data-mongodb")
|
||||
optional("org.springframework.data:spring-data-redis")
|
||||
optional("org.springframework.data:spring-data-rest-webmvc")
|
||||
optional("org.springframework.graphql:spring-graphql")
|
||||
optional("org.springframework.integration:spring-integration-core")
|
||||
optional("org.springframework.security:spring-security-core")
|
||||
optional("org.springframework.security:spring-security-web")
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright 2020-2021 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Copyright 2020-2021 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.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;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
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() {
|
||||
return new RequestMetricsInstrumentationState(this.autoTimer, this.registry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
|
||||
if (this.autoTimer.isEnabled()) {
|
||||
RequestMetricsInstrumentationState state = parameters.getInstrumentationState();
|
||||
state.startTimer();
|
||||
return new SimpleInstrumentationContext<ExecutionResult>() {
|
||||
@Override
|
||||
public void onCompleted(ExecutionResult result, Throwable exc) {
|
||||
Iterable<Tag> tags = GraphQlMetricsInstrumentation.this.tagsProvider.getExecutionTags(parameters,
|
||||
result, exc);
|
||||
state.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(state.getDataFetchingCount());
|
||||
}
|
||||
};
|
||||
}
|
||||
return super.beginExecution(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher,
|
||||
InstrumentationFieldFetchParameters parameters) {
|
||||
if (this.autoTimer.isEnabled() && !parameters.isTrivialDataFetcher()) {
|
||||
return (environment) -> {
|
||||
Timer.Sample sample = Timer.start(this.registry);
|
||||
try {
|
||||
Object value = dataFetcher.get(environment);
|
||||
if (value instanceof CompletionStage<?>) {
|
||||
CompletionStage<?> completion = (CompletionStage<?>) value;
|
||||
return completion.whenComplete(
|
||||
(result, error) -> recordDataFetcherMetric(sample, dataFetcher, parameters, error));
|
||||
}
|
||||
else {
|
||||
recordDataFetcherMetric(sample, dataFetcher, parameters, null);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
catch (Throwable throwable) {
|
||||
recordDataFetcherMetric(sample, dataFetcher, parameters, throwable);
|
||||
throw throwable;
|
||||
}
|
||||
};
|
||||
}
|
||||
return super.instrumentDataFetcher(dataFetcher, parameters);
|
||||
}
|
||||
|
||||
private void recordDataFetcherMetric(Timer.Sample sample, DataFetcher<?> dataFetcher,
|
||||
InstrumentationFieldFetchParameters parameters, @Nullable Throwable throwable) {
|
||||
Timer.Builder timer = this.autoTimer.builder("graphql.datafetcher");
|
||||
timer.tags(this.tagsProvider.getDataFetchingTags(dataFetcher, parameters, throwable));
|
||||
sample.stop(timer.register(this.registry));
|
||||
RequestMetricsInstrumentationState state = parameters.getInstrumentationState();
|
||||
state.incrementDataFetchingCount();
|
||||
}
|
||||
|
||||
static class RequestMetricsInstrumentationState implements InstrumentationState {
|
||||
|
||||
private final MeterRegistry registry;
|
||||
|
||||
private final Timer.Builder timer;
|
||||
|
||||
private Timer.Sample sample;
|
||||
|
||||
private AtomicLong dataFetchingCount = new AtomicLong(0L);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright 2020-2021 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.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Factory methods for Tags associated with a GraphQL request.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 1.0.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_ERRORTYPE = Tag.of("errorType", "UNKNOWN");
|
||||
|
||||
private GraphQlTags() {
|
||||
|
||||
}
|
||||
|
||||
public static Tag executionOutcome(ExecutionResult result, @Nullable 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("errorType", ((ErrorType) errorType).name());
|
||||
}
|
||||
return UNKNOWN_ERRORTYPE;
|
||||
}
|
||||
|
||||
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 {
|
||||
int index = Integer.parseUnsignedInt(segment.toString());
|
||||
builder.append("[*]");
|
||||
}
|
||||
catch (NumberFormatException exc) {
|
||||
builder.append('.');
|
||||
builder.append(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Tag.of("errorPath", builder.toString());
|
||||
}
|
||||
|
||||
public static Tag dataFetchingOutcome(@Nullable Throwable exception) {
|
||||
return (exception != null) ? OUTCOME_ERROR : OUTCOME_SUCCESS;
|
||||
}
|
||||
|
||||
public static Tag dataFetchingPath(InstrumentationFieldFetchParameters parameters) {
|
||||
ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo();
|
||||
StringBuilder dataFetchingType = new StringBuilder();
|
||||
if (executionStepInfo.hasParent() && executionStepInfo.getParent().getType() instanceof GraphQLObjectType) {
|
||||
dataFetchingType.append(((GraphQLObjectType) executionStepInfo.getParent().getType()).getName());
|
||||
dataFetchingType.append('.');
|
||||
}
|
||||
dataFetchingType.append(executionStepInfo.getPath().getSegmentName());
|
||||
return Tag.of("path", dataFetchingType.toString());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2020-2021 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;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@Nullable Throwable exception);
|
||||
|
||||
Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error);
|
||||
|
||||
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
|
||||
@Nullable Throwable exception);
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2020-2021 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;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@Nullable Throwable exception);
|
||||
|
||||
Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error);
|
||||
|
||||
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
|
||||
@Nullable Throwable exception);
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright 2020-2021 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;
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* Copyright 2012-2021 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.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
|
||||
*/
|
||||
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();
|
||||
this.parameters = new InstrumentationExecutionParameters(this.input, this.schema, this.state);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRecordTimerWhenResult() {
|
||||
InstrumentationContext<ExecutionResult> execution = this.instrumentation.beginExecution(this.parameters);
|
||||
ExecutionResult result = new ExecutionResultImpl("Hello", null);
|
||||
execution.onCompleted(result, null);
|
||||
|
||||
Timer timer = this.registry.find("graphql.request").timer();
|
||||
assertThat(timer).isNotNull();
|
||||
assertThat(timer.takeSnapshot().count()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRecordDataFetchingCount() throws Exception {
|
||||
InstrumentationContext<ExecutionResult> execution = this.instrumentation.beginExecution(this.parameters);
|
||||
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);
|
||||
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);
|
||||
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
|
||||
instrumented.get(environment);
|
||||
|
||||
Timer timer = this.registry.find("graphql.datafetcher").timer();
|
||||
assertThat(timer).isNotNull();
|
||||
assertThat(timer.takeSnapshot().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);
|
||||
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
|
||||
instrumented.get(environment);
|
||||
|
||||
Timer timer = this.registry.find("graphql.datafetcher").timer();
|
||||
assertThat(timer).isNotNull();
|
||||
assertThat(timer.takeSnapshot().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);
|
||||
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.takeSnapshot().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);
|
||||
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);
|
||||
given(fieldFetchParameters.getInstrumentationState()).willReturn(this.state);
|
||||
return fieldFetchParameters;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright 2012-2021 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.getValue()).isEqualTo("SUCCESS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void executionOutcomeShouldErrorWhenExceptionThrown() {
|
||||
ExecutionResult result = ExecutionResultImpl.newExecutionResult().build();
|
||||
Tag tag = GraphQlTags.executionOutcome(result, new IllegalArgumentException("test error"));
|
||||
assertThat(tag.getValue()).isEqualTo("ERROR");
|
||||
}
|
||||
|
||||
@Test
|
||||
void executionOutcomeShouldErrorWhenResponseErrors() {
|
||||
GraphQLError error = GraphqlErrorBuilder.newError().message("Invalid query").build();
|
||||
Tag tag = GraphQlTags.executionOutcome(ExecutionResultImpl.newExecutionResult().addError(error).build(), null);
|
||||
assertThat(tag.getValue()).isEqualTo("ERROR");
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorTypeShouldBeDefinedIfPresent() {
|
||||
GraphQLError error = GraphqlErrorBuilder.newError().errorType(ErrorType.DataFetchingException)
|
||||
.message("test error").build();
|
||||
Tag errorTypeTag = GraphQlTags.errorType(error);
|
||||
assertThat(errorTypeTag.getValue()).isEqualTo("DataFetchingException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorPathShouldUseJsonPathFormat() {
|
||||
GraphQLError error = GraphqlErrorBuilder.newError().path(Arrays.asList("project", "name")).message("test error")
|
||||
.build();
|
||||
Tag errorPathTag = GraphQlTags.errorPath(error);
|
||||
assertThat(errorPathTag.getValue()).isEqualTo("$.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.getValue()).isEqualTo("$.issues[*].title");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dataFetchingOutcomeShouldBeSuccessfulIfNoException() {
|
||||
Tag fetchingOutcomeTag = GraphQlTags.dataFetchingOutcome(null);
|
||||
assertThat(fetchingOutcomeTag.getValue()).isEqualTo("SUCCESS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dataFetchingOutcomeShouldBeErrorIfException() {
|
||||
Tag fetchingOutcomeTag = GraphQlTags.dataFetchingOutcome(new IllegalStateException("error state"));
|
||||
assertThat(fetchingOutcomeTag.getValue()).isEqualTo("ERROR");
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue