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:
Brian Clozel
2022-07-20 13:57:17 +02:00
parent ab469e8b6a
commit 38f1bc9793
125 changed files with 7728 additions and 4 deletions

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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"));
}
}