Reinstate Spring for GraphQL auto-configuration
This commit adds the Spring for GraphQL auto-configuration back into Spring Boot 3.0, now that a 1.1.0 release is scheduled with the required baseline. This release also needs GraphQL Java 19.0 as a baseline. Closes gh-31809
This commit is contained in:
@@ -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,166 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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);
|
||||
|
||||
}
|
||||
@@ -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,185 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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"));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user