Skip to content

Commit 594c9df

Browse files
committed
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
1 parent 19a935f commit 594c9df

File tree

7 files changed

+481
-5
lines changed

7 files changed

+481
-5
lines changed

Diff for: spring-graphql-docs/modules/ROOT/pages/observability.adoc

+26
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,29 @@ By default, the following KeyValues are created:
8181
|Name | Description
8282
|`graphql.field.path` _(required)_|Path to the field being fetched (for example, "/bookById").
8383
|===
84+
85+
[[observability.server.dataloader]]
86+
== DataLoader instrumentation
87+
88+
GraphQL DataLoader observations are created with the name `"graphql.dataloader"`, observing calls to `@BatchMapping` controller methods and manually registered `DataLoader` instances.
89+
Applications need to configure the `org.springframework.graphql.observation.GraphQlObservationInstrumentation` instrumentation in their application.
90+
It is using the `org.springframework.graphql.observation.DefaultDataLoaderObservationConvention` by default, backed by the `DataLoaderObservationContext`.
91+
92+
By default, the following KeyValues are created:
93+
94+
.Low cardinality Keys
95+
[cols="a,a"]
96+
|===
97+
|Name | Description
98+
|`graphql.error.type` _(required)_|Class name of the data fetching error
99+
|`graphql.loader.type` _(required)_|Class name of the elements being fetched.
100+
|`graphql.outcome` _(required)_|Outcome of the GraphQL data fetching operation, "SUCCESS" or "ERROR".
101+
|===
102+
103+
104+
.High cardinality Keys
105+
|===
106+
|Name | Description
107+
|`graphql.loader.size` _(required)_|Size of the list of loaded elements.
108+
|===
109+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.observation;
18+
19+
import java.util.List;
20+
21+
import io.micrometer.observation.Observation;
22+
import org.dataloader.BatchLoaderEnvironment;
23+
24+
/**
25+
* Context that holds information for metadata collection during observations
26+
* for {@link GraphQlObservationDocumentation#DATA_LOADER data loader operations}.
27+
*
28+
* @author Brian Clozel
29+
* @since 1.4.0
30+
*/
31+
public class DataLoaderObservationContext extends Observation.Context {
32+
33+
private final List<?> keys;
34+
35+
private final BatchLoaderEnvironment environment;
36+
37+
private List<?> result = List.of();
38+
39+
DataLoaderObservationContext(List<?> keys, BatchLoaderEnvironment environment) {
40+
this.keys = keys;
41+
this.environment = environment;
42+
}
43+
44+
/**
45+
* Return the keys for loading by the {@link org.dataloader.DataLoader}.
46+
*/
47+
public List<?> getKeys() {
48+
return this.keys;
49+
}
50+
51+
/**
52+
* Return the list of values resolved by the {@link org.dataloader.DataLoader},
53+
* or an empty list if none were resolved.
54+
*/
55+
public List<?> getResult() {
56+
return this.result;
57+
}
58+
59+
/**
60+
* Set the list of resolved values by the {@link org.dataloader.DataLoader}.
61+
*/
62+
public void setResult(List<?> result) {
63+
this.result = result;
64+
}
65+
66+
/**
67+
* Return the {@link BatchLoaderEnvironment environment} given to the batch loading function.
68+
*/
69+
public BatchLoaderEnvironment getEnvironment() {
70+
return this.environment;
71+
}
72+
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.observation;
18+
19+
import io.micrometer.observation.Observation;
20+
import io.micrometer.observation.ObservationConvention;
21+
22+
/**
23+
* Interface for an {@link ObservationConvention}
24+
* for {@link GraphQlObservationDocumentation#DATA_LOADER data loading observations}.
25+
*
26+
* @author Brian Clozel
27+
* @since 1.4.0
28+
*/
29+
public interface DataLoaderObservationConvention extends ObservationConvention<DataLoaderObservationContext> {
30+
31+
@Override
32+
default boolean supportsContext(Observation.Context context) {
33+
return context instanceof DataLoaderObservationContext;
34+
}
35+
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.observation;
18+
19+
import java.util.List;
20+
import java.util.Locale;
21+
22+
import io.micrometer.common.KeyValue;
23+
import io.micrometer.common.KeyValues;
24+
25+
import org.springframework.graphql.observation.GraphQlObservationDocumentation.DataLoaderHighCardinalityKeyNames;
26+
import org.springframework.graphql.observation.GraphQlObservationDocumentation.DataLoaderLowCardinalityKeyNames;
27+
28+
/**
29+
* Default implementation for a {@link DataLoaderObservationConvention}
30+
* extracting information from a {@link DataLoaderObservationContext}.
31+
*
32+
* @author Brian Clozel
33+
* @since 1.4.0
34+
*/
35+
public class DefaultDataLoaderObservationConvention implements DataLoaderObservationConvention {
36+
37+
private static final String DEFAULT_NAME = "graphql.dataloader";
38+
39+
private static final KeyValue ERROR_TYPE_NONE = KeyValue.of(DataLoaderLowCardinalityKeyNames.ERROR_TYPE, "NONE");
40+
41+
private static final KeyValue LOADER_TYPE_UNKNOWN = KeyValue.of(DataLoaderLowCardinalityKeyNames.LOADER_TYPE, "unknown");
42+
43+
private static final KeyValue OUTCOME_SUCCESS = KeyValue.of(DataLoaderLowCardinalityKeyNames.OUTCOME, "SUCCESS");
44+
45+
private static final KeyValue OUTCOME_ERROR = KeyValue.of(DataLoaderLowCardinalityKeyNames.OUTCOME, "ERROR");
46+
47+
@Override
48+
public String getName() {
49+
return DEFAULT_NAME;
50+
}
51+
52+
@Override
53+
public String getContextualName(DataLoaderObservationContext context) {
54+
List<?> result = context.getResult();
55+
if (result.isEmpty()) {
56+
return "graphql dataloader";
57+
}
58+
else {
59+
return "graphql dataloader " + result.get(0).getClass().getSimpleName().toLowerCase(Locale.ROOT);
60+
}
61+
}
62+
63+
@Override
64+
public KeyValues getLowCardinalityKeyValues(DataLoaderObservationContext context) {
65+
return KeyValues.of(errorType(context), loaderType(context), outcome(context));
66+
}
67+
68+
@Override
69+
public KeyValues getHighCardinalityKeyValues(DataLoaderObservationContext context) {
70+
return KeyValues.of(loaderSize(context));
71+
}
72+
73+
protected KeyValue errorType(DataLoaderObservationContext context) {
74+
if (context.getError() != null) {
75+
return KeyValue.of(DataLoaderLowCardinalityKeyNames.ERROR_TYPE, context.getError().getClass().getSimpleName());
76+
}
77+
return ERROR_TYPE_NONE;
78+
}
79+
80+
protected KeyValue loaderType(DataLoaderObservationContext context) {
81+
if (context.getResult().isEmpty()) {
82+
return LOADER_TYPE_UNKNOWN;
83+
}
84+
return KeyValue.of(DataLoaderLowCardinalityKeyNames.LOADER_TYPE, context.getResult().get(0).getClass().getSimpleName());
85+
}
86+
87+
protected KeyValue outcome(DataLoaderObservationContext context) {
88+
if (context.getError() != null) {
89+
return OUTCOME_ERROR;
90+
}
91+
return OUTCOME_SUCCESS;
92+
}
93+
94+
protected KeyValue loaderSize(DataLoaderObservationContext context) {
95+
return KeyValue.of(DataLoaderHighCardinalityKeyNames.LOADER_SIZE, String.valueOf(context.getResult().size()));
96+
}
97+
98+
}

Diff for: spring-graphql/src/main/java/org/springframework/graphql/observation/GraphQlObservationDocumentation.java

+76-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -79,6 +79,33 @@ public Class<? extends ObservationConvention<? extends Observation.Context>> get
7979
public KeyName[] getLowCardinalityKeyNames() {
8080
return DataFetcherLowCardinalityKeyNames.values();
8181
}
82+
},
83+
84+
/**
85+
* Observation created for {@link org.dataloader.DataLoader} operations.
86+
* @since 1.4.0
87+
*/
88+
DATA_LOADER {
89+
90+
@Override
91+
public String getPrefix() {
92+
return "graphql";
93+
}
94+
95+
@Override
96+
public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
97+
return DefaultDataLoaderObservationConvention.class;
98+
}
99+
100+
@Override
101+
public KeyName[] getLowCardinalityKeyNames() {
102+
return DataLoaderLowCardinalityKeyNames.values();
103+
}
104+
105+
@Override
106+
public KeyName[] getHighCardinalityKeyNames() {
107+
return DataLoaderHighCardinalityKeyNames.values();
108+
}
82109
};
83110

84111
public enum ExecutionRequestLowCardinalityKeyNames implements KeyName {
@@ -165,4 +192,52 @@ public String asString() {
165192

166193
}
167194

195+
public enum DataLoaderLowCardinalityKeyNames implements KeyName {
196+
197+
/**
198+
* Class name of the data fetching error.
199+
*/
200+
ERROR_TYPE {
201+
@Override
202+
public String asString() {
203+
return "graphql.error.type";
204+
}
205+
},
206+
207+
/**
208+
* {@link Class#getSimpleName()} of the returned elements.
209+
*/
210+
LOADER_TYPE {
211+
@Override
212+
public String asString() {
213+
return "graphql.loader.type";
214+
}
215+
},
216+
217+
/**
218+
* Outcome of the GraphQL data fetching operation.
219+
*/
220+
OUTCOME {
221+
@Override
222+
public String asString() {
223+
return "graphql.outcome";
224+
}
225+
}
226+
227+
}
228+
229+
public enum DataLoaderHighCardinalityKeyNames implements KeyName {
230+
231+
/**
232+
* Size of the list of elements returned by the data loading operation.
233+
*/
234+
LOADER_SIZE {
235+
@Override
236+
public String asString() {
237+
return "graphql.loader.size";
238+
}
239+
}
240+
241+
}
242+
168243
}

0 commit comments

Comments
 (0)