Add DataLoader observability support
Prior to this commit, the `GraphQlObservationInstrumentation` would instrument the following operations: * GraphQL requests * GraphQL data fetching operations In the case of batch loading operations, the instrumentation would consider each load call as a separate data fetching operation. This would significantly clutter recorded traces and would make it look like "N+1 problems" would still be present. This commit adds a new "graphql.dataloader" observation for such operations and avoids recording data fetching observations when `SelfDescribingDataFetcher` declare that they call batch loading operations. Closes gh-1034
This commit is contained in:
@@ -81,3 +81,29 @@ By default, the following KeyValues are created:
|
||||
|Name | Description
|
||||
|`graphql.field.path` _(required)_|Path to the field being fetched (for example, "/bookById").
|
||||
|===
|
||||
|
||||
[[observability.server.dataloader]]
|
||||
== DataLoader instrumentation
|
||||
|
||||
GraphQL DataLoader observations are created with the name `"graphql.dataloader"`, observing calls to `@BatchMapping` controller methods and manually registered `DataLoader` instances.
|
||||
Applications need to configure the `org.springframework.graphql.observation.GraphQlObservationInstrumentation` instrumentation in their application.
|
||||
It is using the `org.springframework.graphql.observation.DefaultDataLoaderObservationConvention` by default, backed by the `DataLoaderObservationContext`.
|
||||
|
||||
By default, the following KeyValues are created:
|
||||
|
||||
.Low cardinality Keys
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`graphql.error.type` _(required)_|Class name of the data fetching error
|
||||
|`graphql.loader.type` _(required)_|Class name of the elements being fetched.
|
||||
|`graphql.outcome` _(required)_|Outcome of the GraphQL data fetching operation, "SUCCESS" or "ERROR".
|
||||
|===
|
||||
|
||||
|
||||
.High cardinality Keys
|
||||
|===
|
||||
|Name | Description
|
||||
|`graphql.loader.size` _(required)_|Size of the list of loaded elements.
|
||||
|===
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2020-2025 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.graphql.observation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import org.dataloader.BatchLoaderEnvironment;
|
||||
|
||||
/**
|
||||
* Context that holds information for metadata collection during observations
|
||||
* for {@link GraphQlObservationDocumentation#DATA_LOADER data loader operations}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public class DataLoaderObservationContext extends Observation.Context {
|
||||
|
||||
private final List<?> keys;
|
||||
|
||||
private final BatchLoaderEnvironment environment;
|
||||
|
||||
private List<?> result = List.of();
|
||||
|
||||
DataLoaderObservationContext(List<?> keys, BatchLoaderEnvironment environment) {
|
||||
this.keys = keys;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the keys for loading by the {@link org.dataloader.DataLoader}.
|
||||
*/
|
||||
public List<?> getKeys() {
|
||||
return this.keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of values resolved by the {@link org.dataloader.DataLoader},
|
||||
* or an empty list if none were resolved.
|
||||
*/
|
||||
public List<?> getResult() {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of resolved values by the {@link org.dataloader.DataLoader}.
|
||||
*/
|
||||
public void setResult(List<?> result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link BatchLoaderEnvironment environment} given to the batch loading function.
|
||||
*/
|
||||
public BatchLoaderEnvironment getEnvironment() {
|
||||
return this.environment;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2020-2025 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.graphql.observation;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationConvention;
|
||||
|
||||
/**
|
||||
* Interface for an {@link ObservationConvention}
|
||||
* for {@link GraphQlObservationDocumentation#DATA_LOADER data loading observations}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public interface DataLoaderObservationConvention extends ObservationConvention<DataLoaderObservationContext> {
|
||||
|
||||
@Override
|
||||
default boolean supportsContext(Observation.Context context) {
|
||||
return context instanceof DataLoaderObservationContext;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2020-2025 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.graphql.observation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import io.micrometer.common.KeyValue;
|
||||
import io.micrometer.common.KeyValues;
|
||||
|
||||
import org.springframework.graphql.observation.GraphQlObservationDocumentation.DataLoaderHighCardinalityKeyNames;
|
||||
import org.springframework.graphql.observation.GraphQlObservationDocumentation.DataLoaderLowCardinalityKeyNames;
|
||||
|
||||
/**
|
||||
* Default implementation for a {@link DataLoaderObservationConvention}
|
||||
* extracting information from a {@link DataLoaderObservationContext}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public class DefaultDataLoaderObservationConvention implements DataLoaderObservationConvention {
|
||||
|
||||
private static final String DEFAULT_NAME = "graphql.dataloader";
|
||||
|
||||
private static final KeyValue ERROR_TYPE_NONE = KeyValue.of(DataLoaderLowCardinalityKeyNames.ERROR_TYPE, "NONE");
|
||||
|
||||
private static final KeyValue LOADER_TYPE_UNKNOWN = KeyValue.of(DataLoaderLowCardinalityKeyNames.LOADER_TYPE, "unknown");
|
||||
|
||||
private static final KeyValue OUTCOME_SUCCESS = KeyValue.of(DataLoaderLowCardinalityKeyNames.OUTCOME, "SUCCESS");
|
||||
|
||||
private static final KeyValue OUTCOME_ERROR = KeyValue.of(DataLoaderLowCardinalityKeyNames.OUTCOME, "ERROR");
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return DEFAULT_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContextualName(DataLoaderObservationContext context) {
|
||||
List<?> result = context.getResult();
|
||||
if (result.isEmpty()) {
|
||||
return "graphql dataloader";
|
||||
}
|
||||
else {
|
||||
return "graphql dataloader " + result.get(0).getClass().getSimpleName().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyValues getLowCardinalityKeyValues(DataLoaderObservationContext context) {
|
||||
return KeyValues.of(errorType(context), loaderType(context), outcome(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyValues getHighCardinalityKeyValues(DataLoaderObservationContext context) {
|
||||
return KeyValues.of(loaderSize(context));
|
||||
}
|
||||
|
||||
protected KeyValue errorType(DataLoaderObservationContext context) {
|
||||
if (context.getError() != null) {
|
||||
return KeyValue.of(DataLoaderLowCardinalityKeyNames.ERROR_TYPE, context.getError().getClass().getSimpleName());
|
||||
}
|
||||
return ERROR_TYPE_NONE;
|
||||
}
|
||||
|
||||
protected KeyValue loaderType(DataLoaderObservationContext context) {
|
||||
if (context.getResult().isEmpty()) {
|
||||
return LOADER_TYPE_UNKNOWN;
|
||||
}
|
||||
return KeyValue.of(DataLoaderLowCardinalityKeyNames.LOADER_TYPE, context.getResult().get(0).getClass().getSimpleName());
|
||||
}
|
||||
|
||||
protected KeyValue outcome(DataLoaderObservationContext context) {
|
||||
if (context.getError() != null) {
|
||||
return OUTCOME_ERROR;
|
||||
}
|
||||
return OUTCOME_SUCCESS;
|
||||
}
|
||||
|
||||
protected KeyValue loaderSize(DataLoaderObservationContext context) {
|
||||
return KeyValue.of(DataLoaderHighCardinalityKeyNames.LOADER_SIZE, String.valueOf(context.getResult().size()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -79,6 +79,33 @@ public enum GraphQlObservationDocumentation implements ObservationDocumentation
|
||||
public KeyName[] getLowCardinalityKeyNames() {
|
||||
return DataFetcherLowCardinalityKeyNames.values();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Observation created for {@link org.dataloader.DataLoader} operations.
|
||||
* @since 1.4.0
|
||||
*/
|
||||
DATA_LOADER {
|
||||
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
return "graphql";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
|
||||
return DefaultDataLoaderObservationConvention.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyName[] getLowCardinalityKeyNames() {
|
||||
return DataLoaderLowCardinalityKeyNames.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyName[] getHighCardinalityKeyNames() {
|
||||
return DataLoaderHighCardinalityKeyNames.values();
|
||||
}
|
||||
};
|
||||
|
||||
public enum ExecutionRequestLowCardinalityKeyNames implements KeyName {
|
||||
@@ -165,4 +192,52 @@ public enum GraphQlObservationDocumentation implements ObservationDocumentation
|
||||
|
||||
}
|
||||
|
||||
public enum DataLoaderLowCardinalityKeyNames implements KeyName {
|
||||
|
||||
/**
|
||||
* Class name of the data fetching error.
|
||||
*/
|
||||
ERROR_TYPE {
|
||||
@Override
|
||||
public String asString() {
|
||||
return "graphql.error.type";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* {@link Class#getSimpleName()} of the returned elements.
|
||||
*/
|
||||
LOADER_TYPE {
|
||||
@Override
|
||||
public String asString() {
|
||||
return "graphql.loader.type";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Outcome of the GraphQL data fetching operation.
|
||||
*/
|
||||
OUTCOME {
|
||||
@Override
|
||||
public String asString() {
|
||||
return "graphql.outcome";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum DataLoaderHighCardinalityKeyNames implements KeyName {
|
||||
|
||||
/**
|
||||
* Size of the list of elements returned by the data loading operation.
|
||||
*/
|
||||
LOADER_SIZE {
|
||||
@Override
|
||||
public String asString() {
|
||||
return "graphql.loader.size";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -16,10 +16,12 @@
|
||||
|
||||
package org.springframework.graphql.observation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
import graphql.ExecutionInput;
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.GraphQLContext;
|
||||
import graphql.execution.instrumentation.InstrumentationContext;
|
||||
@@ -35,7 +37,14 @@ import graphql.schema.DataFetchingEnvironmentImpl;
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
|
||||
import org.dataloader.BatchLoaderEnvironment;
|
||||
import org.dataloader.DataLoader;
|
||||
import org.dataloader.DataLoaderRegistry;
|
||||
import org.dataloader.instrumentation.DataLoaderInstrumentation;
|
||||
import org.dataloader.instrumentation.DataLoaderInstrumentationContext;
|
||||
|
||||
import org.springframework.graphql.execution.SelfDescribingDataFetcher;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
@@ -63,6 +72,9 @@ public class GraphQlObservationInstrumentation extends SimplePerformantInstrumen
|
||||
private static final DataFetcherObservationConvention DEFAULT_DATA_FETCHER_CONVENTION =
|
||||
new DefaultDataFetcherObservationConvention();
|
||||
|
||||
private static final DefaultDataLoaderObservationConvention DEFAULT_DATA_LOADER_CONVENTION =
|
||||
new DefaultDataLoaderObservationConvention();
|
||||
|
||||
private final ObservationRegistry observationRegistry;
|
||||
|
||||
@Nullable
|
||||
@@ -71,6 +83,9 @@ public class GraphQlObservationInstrumentation extends SimplePerformantInstrumen
|
||||
@Nullable
|
||||
private final DataFetcherObservationConvention dataFetcherObservationConvention;
|
||||
|
||||
@Nullable
|
||||
private final DataLoaderObservationConvention dataLoaderObservationConvention;
|
||||
|
||||
/**
|
||||
* Create an {@code GraphQlObservationInstrumentation} that records observations
|
||||
* against the given {@link ObservationRegistry}. The default observation
|
||||
@@ -78,7 +93,7 @@ public class GraphQlObservationInstrumentation extends SimplePerformantInstrumen
|
||||
* @param observationRegistry the registry to use for recording observations
|
||||
*/
|
||||
public GraphQlObservationInstrumentation(ObservationRegistry observationRegistry) {
|
||||
this(observationRegistry, null, null);
|
||||
this(observationRegistry, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,13 +102,44 @@ public class GraphQlObservationInstrumentation extends SimplePerformantInstrumen
|
||||
* @param observationRegistry the registry to use for recording observations
|
||||
* @param requestObservationConvention the convention to use for request observations
|
||||
* @param dateFetcherObservationConvention the convention to use for data fetcher observations
|
||||
* @deprecated since 1.4.0 in favor of {@link #GraphQlObservationInstrumentation(ObservationRegistry,
|
||||
* ExecutionRequestObservationConvention, DataFetcherObservationConvention, DataLoaderObservationConvention)}
|
||||
*/
|
||||
@Deprecated(since = "1.4.0", forRemoval = true)
|
||||
public GraphQlObservationInstrumentation(ObservationRegistry observationRegistry,
|
||||
@Nullable ExecutionRequestObservationConvention requestObservationConvention,
|
||||
@Nullable DataFetcherObservationConvention dateFetcherObservationConvention) {
|
||||
this(observationRegistry, requestObservationConvention, dateFetcherObservationConvention, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@code GraphQlObservationInstrumentation} that records observations
|
||||
* against the given {@link ObservationRegistry} with a custom convention.
|
||||
* @param observationRegistry the registry to use for recording observations
|
||||
* @param requestObservationConvention the convention to use for request observations
|
||||
* @param dateFetcherObservationConvention the convention to use for data fetcher observations
|
||||
* @param dataLoaderObservationConvention the convention to use for data loader observations
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public GraphQlObservationInstrumentation(ObservationRegistry observationRegistry,
|
||||
@Nullable ExecutionRequestObservationConvention requestObservationConvention,
|
||||
@Nullable DataFetcherObservationConvention dateFetcherObservationConvention,
|
||||
@Nullable DataLoaderObservationConvention dataLoaderObservationConvention) {
|
||||
this.observationRegistry = observationRegistry;
|
||||
this.requestObservationConvention = requestObservationConvention;
|
||||
this.dataFetcherObservationConvention = dateFetcherObservationConvention;
|
||||
this.dataLoaderObservationConvention = dataLoaderObservationConvention;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters, InstrumentationState state) {
|
||||
return executionInput.transform((builder) -> {
|
||||
DataLoaderRegistry dataLoaderRegistry = DataLoaderRegistry.newRegistry()
|
||||
.registerAll(executionInput.getDataLoaderRegistry())
|
||||
.instrumentation(new ObservationDataLoaderInstrumentation())
|
||||
.build();
|
||||
builder.dataLoaderRegistry(dataLoaderRegistry);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -112,7 +158,7 @@ public class GraphQlObservationInstrumentation extends SimplePerformantInstrumen
|
||||
requestObservation.start();
|
||||
return new SimpleInstrumentationContext<>() {
|
||||
@Override
|
||||
public void onCompleted(ExecutionResult result, Throwable exc) {
|
||||
public void onCompleted(ExecutionResult result, @Nullable Throwable exc) {
|
||||
observationContext.setExecutionResult(result);
|
||||
result.getErrors().forEach((graphQLError) -> {
|
||||
Observation.Event event = Observation.Event.of(graphQLError.getErrorType().toString(), graphQLError.getMessage());
|
||||
@@ -139,6 +185,11 @@ public class GraphQlObservationInstrumentation extends SimplePerformantInstrumen
|
||||
InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
|
||||
if (!parameters.isTrivialDataFetcher()
|
||||
&& state == RequestObservationInstrumentationState.INSTANCE) {
|
||||
// skip batch loading operations, already instrumented at the dataloader level
|
||||
if (dataFetcher instanceof SelfDescribingDataFetcher<?> selfDescribingDataFetcher
|
||||
&& selfDescribingDataFetcher.isBatchLoading()) {
|
||||
return dataFetcher;
|
||||
}
|
||||
return (environment) -> {
|
||||
DataFetcherObservationContext observationContext = new DataFetcherObservationContext(environment);
|
||||
Observation dataFetcherObservation = GraphQlObservationDocumentation.DATA_FETCHER.observation(this.dataFetcherObservationConvention,
|
||||
@@ -219,4 +270,37 @@ public class GraphQlObservationInstrumentation extends SimplePerformantInstrumen
|
||||
|
||||
}
|
||||
|
||||
class ObservationDataLoaderInstrumentation implements DataLoaderInstrumentation {
|
||||
|
||||
@Override
|
||||
public DataLoaderInstrumentationContext<List<?>> beginBatchLoader(DataLoader<?, ?> dataLoader, List<?> keys, BatchLoaderEnvironment environment) {
|
||||
|
||||
Observation observation = GraphQlObservationDocumentation.DATA_LOADER
|
||||
.observation(GraphQlObservationInstrumentation.this.dataLoaderObservationConvention,
|
||||
DEFAULT_DATA_LOADER_CONVENTION,
|
||||
() -> new DataLoaderObservationContext(keys, environment),
|
||||
GraphQlObservationInstrumentation.this.observationRegistry);
|
||||
if (environment.getContext() instanceof GraphQLContext graphQLContext) {
|
||||
Observation parentObservation = graphQLContext.get(ObservationThreadLocalAccessor.KEY);
|
||||
observation.parentObservation(parentObservation);
|
||||
}
|
||||
return new DataLoaderInstrumentationContext<List<?>>() {
|
||||
@Override
|
||||
public void onDispatched() {
|
||||
observation.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted(List<?> result, @Nullable Throwable t) {
|
||||
DataLoaderObservationContext context = (DataLoaderObservationContext) observation.getContext();
|
||||
context.setResult(result);
|
||||
if (t != null) {
|
||||
observation.error(t);
|
||||
}
|
||||
observation.stop();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -27,6 +27,7 @@ import graphql.GraphqlErrorBuilder;
|
||||
import graphql.execution.DataFetcherResult;
|
||||
import graphql.schema.AsyncDataFetcher;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import io.micrometer.common.KeyValue;
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationHandler;
|
||||
@@ -34,12 +35,15 @@ import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
|
||||
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||
import io.micrometer.observation.tck.TestObservationRegistryAssert;
|
||||
import org.dataloader.DataLoader;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.graphql.Author;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.BookSource;
|
||||
@@ -48,8 +52,11 @@ import org.springframework.graphql.ExecutionGraphQlResponse;
|
||||
import org.springframework.graphql.GraphQlSetup;
|
||||
import org.springframework.graphql.ResponseHelper;
|
||||
import org.springframework.graphql.TestExecutionRequest;
|
||||
import org.springframework.graphql.execution.BatchLoaderRegistry;
|
||||
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
|
||||
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
|
||||
import org.springframework.graphql.execution.ErrorType;
|
||||
import org.springframework.graphql.execution.SelfDescribingDataFetcher;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -357,6 +364,83 @@ class GraphQlObservationInstrumentationTests {
|
||||
.hasHighCardinalityKeyValueWithKey("graphql.execution.id");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRecordBatchLoadingAsSingleObservation() {
|
||||
String document = """
|
||||
{
|
||||
booksById(id: [1,2,3,4,5]) {
|
||||
name
|
||||
author {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
BatchLoaderRegistry registry = new DefaultBatchLoaderRegistry();
|
||||
registry.forTypePair(Long.class, Author.class)
|
||||
.registerBatchLoader((ids, env) -> Flux.fromIterable(ids).map(BookSource::getAuthor));
|
||||
|
||||
Mono<ExecutionGraphQlResponse> responseMono = graphQlSetup
|
||||
.queryFetcher("booksById", (environment) -> {
|
||||
List<String> id = environment.getArgument("id");
|
||||
return Flux.fromIterable(id).map(Long::parseLong).map(BookSource::getBookWithoutAuthor).collectList();
|
||||
})
|
||||
.dataFetcher("Book", "author", new AuthorBatchLoadingDataFetcher())
|
||||
.dataLoaders(registry)
|
||||
.toGraphQlService()
|
||||
.execute(document);
|
||||
ResponseHelper response = ResponseHelper.forResponse(responseMono);
|
||||
|
||||
List<Book> booksById = response.toList("booksById", Book.class);
|
||||
assertThat(booksById).hasSize(5);
|
||||
|
||||
TestObservationRegistryAssert.assertThat(this.observationRegistry).hasObservationWithNameEqualTo("graphql.request")
|
||||
.that().hasLowCardinalityKeyValue("graphql.outcome", "SUCCESS")
|
||||
.hasHighCardinalityKeyValueWithKey("graphql.execution.id");
|
||||
|
||||
TestObservationRegistryAssert.assertThat(this.observationRegistry)
|
||||
.hasNumberOfObservationsWithNameEqualTo("graphql.datafetcher", 1)
|
||||
.hasObservationWithNameEqualTo("graphql.datafetcher")
|
||||
.that()
|
||||
.hasLowCardinalityKeyValue("graphql.outcome", "SUCCESS")
|
||||
.hasLowCardinalityKeyValue("graphql.field.name", "booksById")
|
||||
.hasHighCardinalityKeyValue("graphql.field.path", "/booksById");
|
||||
|
||||
TestObservationRegistryAssert.assertThat(this.observationRegistry)
|
||||
.hasNumberOfObservationsWithNameEqualTo("graphql.dataloader", 1)
|
||||
.hasObservationWithNameEqualTo("graphql.dataloader")
|
||||
.that()
|
||||
.hasLowCardinalityKeyValue("graphql.outcome", "SUCCESS")
|
||||
.hasLowCardinalityKeyValue("graphql.loader.type", "Author")
|
||||
.hasHighCardinalityKeyValue("graphql.loader.size", "4")
|
||||
.hasContextualNameEqualTo("graphql dataloader author");
|
||||
}
|
||||
|
||||
static class AuthorBatchLoadingDataFetcher implements SelfDescribingDataFetcher<CompletableFuture<Author>> {
|
||||
|
||||
@Override
|
||||
public boolean isBatchLoading() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "BatchLoading authors";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvableType getReturnType() {
|
||||
return ResolvableType.forClass(Author.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Author> get(DataFetchingEnvironment env) throws Exception {
|
||||
Book book = env.getSource();
|
||||
DataLoader<Long, Author> dataLoader = env.getDataLoader(Author.class.getName());
|
||||
return dataLoader.load(book.getAuthorId());
|
||||
}
|
||||
}
|
||||
|
||||
static class EventListeningObservationHandler implements ObservationHandler<ExecutionRequestObservationContext> {
|
||||
|
||||
private final List<Observation.Event> events = new ArrayList<>();
|
||||
|
||||
Reference in New Issue
Block a user