Skip to content

Commit 4d8208d

Browse files
committed
Fix high cardinality issue with "graphql.operation"
Prior to this commit, the GraphQL observability instrumentation would add a "graphql.operation" low cardinality keyvalue to the request observations. This value is extracted from the `ExecutionInput`. But this value can be effectively high cardinality as it is driven by user input and clients can set any name there. This commit fixes this cardinality issue by: * renaming "graphql.operation" to "graphql.operation.name" and making it high cardinality. * adding a new "graphql.operation.type" low cardinality keyvalue that is extracted from the `ExecutionContext`, after the request document has been parses and validated. This information is now used for the trace context name. Fixes gh-1322
1 parent db3aeda commit 4d8208d

File tree

6 files changed

+100
-30
lines changed

6 files changed

+100
-30
lines changed

spring-graphql-docs/modules/ROOT/pages/observability.adoc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ By default, the following KeyValues are created:
2727
[cols="a,a"]
2828
|===
2929
|Name | Description
30-
|`graphql.operation` _(required)_|GraphQL Operation name.
30+
|`graphql.operation.type` _(required)_|GraphQL Operation type.
3131
|`graphql.outcome` _(required)_|Outcome of the GraphQL request.
3232
|===
3333

34-
The `graphql.operation` KeyValue will use the custom name of the provided query, or http://spec.graphql.org/draft/#sec-Language.Operations[the standard name for the operation] if none (`"query"`, `"mutation"` or `"subscription"`).
34+
The `graphql.operation.type` KeyValue will use the http://spec.graphql.org/draft/#sec-Language.Operations[the standard name for the operation] (`"query"`, `"mutation"` or `"subscription"`) or `"operation"` if the request document could not be parsed.
35+
3536
The `graphql.outcome` KeyValue will be:
3637

3738
* `"SUCCESS"` if a valid GraphQL response has been sent and it contains no errors
@@ -43,8 +44,11 @@ The `graphql.outcome` KeyValue will be:
4344
|===
4445
|Name | Description
4546
|`graphql.execution.id` _(required)_|`graphql.execution.ExecutionId` of the GraphQL request.
47+
|`graphql.operation.name` _(required)_|GraphQL Operation name.
4648
|===
4749

50+
The `graphql.operation.name` KeyValue will be similar to `graphql.operation.name` will use the operation name provided by the client.
51+
4852
Spring for GraphQL also contributes Events for Server Request Observations.
4953
https://docs.micrometer.io/micrometer/reference/observation/components.html#micrometer-observation-events[Micrometer Observation Events] are usually handled as span annotations in traces.
5054
This instrumentation records errors listed in the GraphQL response as events.

spring-graphql/src/main/java/org/springframework/graphql/observation/DefaultExecutionRequestObservationConvention.java

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.graphql.observation;
1818

19+
import java.util.Locale;
20+
21+
import graphql.language.OperationDefinition;
1922
import io.micrometer.common.KeyValue;
2023
import io.micrometer.common.KeyValues;
2124

@@ -42,7 +45,9 @@ public class DefaultExecutionRequestObservationConvention implements ExecutionRe
4245

4346
private static final KeyValue OUTCOME_INTERNAL_ERROR = KeyValue.of(ExecutionRequestLowCardinalityKeyNames.OUTCOME, "INTERNAL_ERROR");
4447

45-
private static final KeyValue OPERATION_QUERY = KeyValue.of(ExecutionRequestLowCardinalityKeyNames.OPERATION, "query");
48+
private static final KeyValue OPERATION_NAME_QUERY = KeyValue.of(ExecutionRequestHighCardinalityKeyNames.OPERATION_NAME, "query");
49+
50+
private static final KeyValue OPERATION_TYPE_DEFAULT = KeyValue.of(ExecutionRequestLowCardinalityKeyNames.OPERATION_TYPE, "operation");
4651

4752
private final String name;
4853

@@ -61,13 +66,12 @@ public String getName() {
6166

6267
@Override
6368
public String getContextualName(ExecutionRequestObservationContext context) {
64-
String operationName = (context.getExecutionInput().getOperationName() != null) ? context.getExecutionInput().getOperationName() : "query";
65-
return BASE_CONTEXTUAL_NAME + operationName;
69+
return BASE_CONTEXTUAL_NAME + operationType(context).getValue();
6670
}
6771

6872
@Override
6973
public KeyValues getLowCardinalityKeyValues(ExecutionRequestObservationContext context) {
70-
return KeyValues.of(outcome(context), operation(context));
74+
return KeyValues.of(outcome(context), operationType(context));
7175
}
7276

7377
protected KeyValue outcome(ExecutionRequestObservationContext context) {
@@ -83,21 +87,29 @@ else if (!context.getExecutionResult().getErrors().isEmpty()) {
8387
return OUTCOME_SUCCESS;
8488
}
8589

86-
protected KeyValue operation(ExecutionRequestObservationContext context) {
87-
String operationName = context.getExecutionInput().getOperationName();
88-
if (operationName != null) {
89-
return KeyValue.of(ExecutionRequestLowCardinalityKeyNames.OPERATION, operationName);
90+
protected KeyValue operationType(ExecutionRequestObservationContext context) {
91+
if (context.getExecutionContext() != null) {
92+
OperationDefinition.Operation operation = context.getExecutionContext().getOperationDefinition().getOperation();
93+
return KeyValue.of(ExecutionRequestLowCardinalityKeyNames.OPERATION_TYPE, operation.name().toLowerCase(Locale.ROOT));
9094
}
91-
return OPERATION_QUERY;
95+
return OPERATION_TYPE_DEFAULT;
9296
}
9397

9498
@Override
9599
public KeyValues getHighCardinalityKeyValues(ExecutionRequestObservationContext context) {
96-
return KeyValues.of(executionId(context));
100+
return KeyValues.of(executionId(context), operationName(context));
97101
}
98102

99103
protected KeyValue executionId(ExecutionRequestObservationContext context) {
100104
return KeyValue.of(ExecutionRequestHighCardinalityKeyNames.EXECUTION_ID, context.getExecutionInput().getExecutionId().toString());
101105
}
102106

107+
protected KeyValue operationName(ExecutionRequestObservationContext context) {
108+
String operationName = context.getExecutionInput().getOperationName();
109+
if (operationName != null) {
110+
return KeyValue.of(ExecutionRequestHighCardinalityKeyNames.OPERATION_NAME, operationName);
111+
}
112+
return OPERATION_NAME_QUERY;
113+
}
114+
103115
}

spring-graphql/src/main/java/org/springframework/graphql/observation/ExecutionRequestObservationContext.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import graphql.ExecutionInput;
2020
import graphql.ExecutionResult;
21+
import graphql.execution.ExecutionContext;
2122
import io.micrometer.observation.Observation;
2223

2324
import org.springframework.lang.Nullable;
@@ -33,6 +34,8 @@ public class ExecutionRequestObservationContext extends Observation.Context {
3334

3435
private final ExecutionInput executionInput;
3536

37+
private ExecutionContext executionContext;
38+
3639
@Nullable
3740
private ExecutionResult executionResult;
3841

@@ -48,6 +51,24 @@ public ExecutionInput getExecutionInput() {
4851
return this.executionInput;
4952
}
5053

54+
/**
55+
* Return the {@link ExecutionContext context} for the request execution.
56+
* @since 1.3.7
57+
*/
58+
@Nullable
59+
public ExecutionContext getExecutionContext() {
60+
return this.executionContext;
61+
}
62+
63+
/**
64+
* Set the {@link ExecutionContext context} for the request execution.
65+
* @param executionContext the execution context
66+
* @since 1.3.7
67+
*/
68+
public void setExecutionContext(ExecutionContext executionContext) {
69+
this.executionContext = executionContext;
70+
}
71+
5172
/**
5273
* Return the {@link ExecutionResult result} for the request execution.
5374
* @since 1.1.4

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,18 +122,28 @@ public String asString() {
122122
},
123123

124124
/**
125-
* GraphQL Operation name.
125+
* GraphQL {@link graphql.language.OperationDefinition.Operation Operation type}.
126126
*/
127-
OPERATION {
127+
OPERATION_TYPE {
128128
@Override
129129
public String asString() {
130-
return "graphql.operation";
130+
return "graphql.operation.type";
131131
}
132132
}
133133
}
134134

135135
public enum ExecutionRequestHighCardinalityKeyNames implements KeyName {
136136

137+
/**
138+
* GraphQL Operation name.
139+
*/
140+
OPERATION_NAME {
141+
@Override
142+
public String asString() {
143+
return "graphql.operation.name";
144+
}
145+
},
146+
137147
/**
138148
* {@link graphql.execution.ExecutionId} of the GraphQL request.
139149
*/

spring-graphql/src/main/java/org/springframework/graphql/observation/GraphQlObservationInstrumentation.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import graphql.execution.instrumentation.SimpleInstrumentationContext;
3030
import graphql.execution.instrumentation.SimplePerformantInstrumentation;
3131
import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters;
32+
import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters;
3233
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
3334
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
3435
import graphql.schema.DataFetcher;
@@ -144,14 +145,15 @@ public GraphQlObservationInstrumentation(ObservationRegistry observationRegistry
144145

145146
@Override
146147
public CompletableFuture<InstrumentationState> createStateAsync(InstrumentationCreateStateParameters parameters) {
147-
return CompletableFuture.completedFuture(RequestObservationInstrumentationState.INSTANCE);
148+
ExecutionRequestObservationContext requestObservationContext = new ExecutionRequestObservationContext(parameters.getExecutionInput());
149+
return CompletableFuture.completedFuture(new RequestObservationInstrumentationState(requestObservationContext));
148150
}
149151

150152
@Override
151153
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters,
152154
InstrumentationState state) {
153-
if (state == RequestObservationInstrumentationState.INSTANCE) {
154-
ExecutionRequestObservationContext observationContext = new ExecutionRequestObservationContext(parameters.getExecutionInput());
155+
if (state instanceof RequestObservationInstrumentationState observationState) {
156+
ExecutionRequestObservationContext observationContext = observationState.requestObservationContext;
155157
Observation requestObservation = GraphQlObservationDocumentation.EXECUTION_REQUEST.observation(this.requestObservationConvention,
156158
DEFAULT_REQUEST_CONVENTION, () -> observationContext, this.observationRegistry);
157159
setCurrentObservation(requestObservation, parameters.getGraphQLContext());
@@ -180,11 +182,19 @@ private static void setCurrentObservation(Observation currentObservation, GraphQ
180182
graphQlContext.put(ObservationThreadLocalAccessor.KEY, currentObservation);
181183
}
182184

185+
@Override
186+
public InstrumentationContext<ExecutionResult> beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) {
187+
if (state instanceof RequestObservationInstrumentationState observationState) {
188+
observationState.requestObservationContext.setExecutionContext(parameters.getExecutionContext());
189+
}
190+
return super.beginExecuteOperation(parameters, state);
191+
}
192+
183193
@Override
184194
public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher,
185195
InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
186196
if (!parameters.isTrivialDataFetcher()
187-
&& state == RequestObservationInstrumentationState.INSTANCE) {
197+
&& state instanceof RequestObservationInstrumentationState) {
188198
// skip batch loading operations, already instrumented at the dataloader level
189199
if (dataFetcher instanceof SelfDescribingDataFetcher<?> selfDescribingDataFetcher
190200
&& selfDescribingDataFetcher.usesDataLoader()) {
@@ -266,7 +276,11 @@ private static DataFetchingEnvironment wrapDataFetchingEnvironment(DataFetchingE
266276

267277
static class RequestObservationInstrumentationState implements InstrumentationState {
268278

269-
static final RequestObservationInstrumentationState INSTANCE = new RequestObservationInstrumentationState();
279+
final ExecutionRequestObservationContext requestObservationContext;
280+
281+
RequestObservationInstrumentationState(ExecutionRequestObservationContext requestObservationContext) {
282+
this.requestObservationContext = requestObservationContext;
283+
}
270284

271285
}
272286

spring-graphql/src/test/java/org/springframework/graphql/observation/DefaultExecutionRequestObservationConventionTests.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
import graphql.ExecutionResult;
2323
import graphql.ExecutionResultImpl;
2424
import graphql.GraphQLError;
25+
import graphql.execution.ExecutionContext;
26+
import graphql.execution.ExecutionContextBuilder;
2527
import graphql.execution.ExecutionId;
28+
import graphql.language.OperationDefinition;
2629
import graphql.schema.idl.errors.QueryOperationMissingError;
2730
import io.micrometer.common.KeyValue;
2831
import org.junit.jupiter.api.Test;
@@ -61,21 +64,19 @@ void nameCanBeCustomized() {
6164
void hasContextualName() {
6265
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }")
6366
.operationName("mutation").build();
64-
ExecutionRequestObservationContext context = createObservationContext(input, builder -> { });
67+
ExecutionRequestObservationContext context = createObservationContext(input);
6568
assertThat(this.convention.getContextualName(context)).isEqualTo("graphql mutation");
6669
}
6770

6871
@Test
6972
void hasOperationKeyValueWhenSuccessfulOutput() {
70-
ExecutionRequestObservationContext context = createObservationContext(this.input, builder -> {
71-
});
72-
assertThat(this.convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("graphql.operation", "query"));
73+
ExecutionRequestObservationContext context = createObservationContext(this.input);
74+
assertThat(this.convention.getHighCardinalityKeyValues(context)).contains(KeyValue.of("graphql.operation.name", "query"));
7375
}
7476

7577
@Test
7678
void hasOutcomeKeyValueWhenSuccessfulOutput() {
77-
ExecutionRequestObservationContext context = createObservationContext(this.input, builder -> {
78-
});
79+
ExecutionRequestObservationContext context = createObservationContext(this.input);
7980
assertThat(this.convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("graphql.outcome", "SUCCESS"));
8081
}
8182

@@ -88,26 +89,34 @@ void hasOutcomeKeyValueWhenErrorOutput() {
8889

8990
@Test
9091
void hasOutcomeKeyValueWhenInternalError() {
91-
ExecutionRequestObservationContext context = createObservationContext(this.input, builder -> {
92-
});
92+
ExecutionRequestObservationContext context = createObservationContext(this.input);
9393
GraphQLError graphQLError = GraphQLError.newError().errorType(ErrorType.INTERNAL_ERROR).message(ErrorType.INTERNAL_ERROR + " for [executionId]").build();
9494
context.setExecutionResult(ExecutionResult.newExecutionResult().addError(graphQLError).build());
9595
assertThat(this.convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("graphql.outcome", "INTERNAL_ERROR"));
9696
}
9797

9898
@Test
9999
void hasExecutionIdKeyValue() {
100-
ExecutionRequestObservationContext context = createObservationContext(this.input, builder -> {
101-
});
100+
ExecutionRequestObservationContext context = createObservationContext(this.input);
102101
assertThat(this.convention.getHighCardinalityKeyValues(context)).contains(KeyValue.of("graphql.execution.id", "42"));
103102
}
104103

104+
private ExecutionRequestObservationContext createObservationContext(ExecutionInput executionInput) {
105+
return createObservationContext(executionInput, builder -> { });
106+
}
105107

106108
private ExecutionRequestObservationContext createObservationContext(ExecutionInput executionInput, Consumer<ExecutionResultImpl.Builder> resultConsumer) {
107109
ExecutionRequestObservationContext context = new ExecutionRequestObservationContext(executionInput);
108110
ExecutionResultImpl.Builder builder = ExecutionResultImpl.newExecutionResult();
109111
resultConsumer.accept(builder);
110112
context.setExecutionResult(builder.build());
113+
114+
ExecutionContext executionContext = ExecutionContextBuilder.newExecutionContextBuilder()
115+
.executionId(ExecutionId.from("123_456_789"))
116+
.executionInput(executionInput)
117+
.operationDefinition(OperationDefinition.newOperationDefinition().operation(OperationDefinition.Operation.MUTATION).build())
118+
.build();
119+
context.setExecutionContext(executionContext);
111120
return context;
112121
}
113122

0 commit comments

Comments
 (0)