Skip to content

Commit 1b19189

Browse files
committed
Add Observability support
Prior to this commit, metrics instrumentation was provided by Spring Boot, using Micrometer metrics. The `Instrumentation` would publish two timers and a counter, but would not support tracing. This commit replaces the former with a dedicated support in Spring for GraphQL directly. This uses the new `Observation` API from Micrometer and publishes two observations: * a request execution observation, with timing and tracing included * a data fetching observation with the request execution as a parent observation Closes gh-501
1 parent 30bc70b commit 1b19189

15 files changed

+1134
-1
lines changed

platform/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ dependencies {
1010
api(platform("org.springframework:spring-framework-bom:${springFrameworkVersion}"))
1111
api(platform("com.fasterxml.jackson:jackson-bom:2.13.4"))
1212
api(platform("io.projectreactor:reactor-bom:2022.0.0-SNAPSHOT"))
13+
api(platform("io.micrometer:micrometer-bom:1.10.0-SNAPSHOT"))
1314
api(platform("org.springframework.data:spring-data-bom:2022.0.0-M6"))
1415
api(platform("org.springframework.security:spring-security-bom:6.0.0-M7"))
1516
api(platform("com.querydsl:querydsl-bom:5.0.0"))

spring-graphql/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ dependencies {
88
api 'org.springframework:spring-context'
99
implementation 'io.micrometer:context-propagation'
1010

11+
compileOnly 'io.micrometer:micrometer-observation'
1112
compileOnly 'jakarta.annotation:jakarta.annotation-api'
1213
compileOnly 'org.springframework:spring-webflux'
1314
compileOnly 'org.springframework:spring-webmvc'
@@ -42,7 +43,7 @@ dependencies {
4243
testImplementation 'org.springframework.data:spring-data-commons'
4344
testImplementation 'org.springframework.data:spring-data-keyvalue'
4445
testImplementation 'org.springframework.data:spring-data-jpa'
45-
testImplementation 'io.micrometer:context-propagation'
46+
testImplementation 'io.micrometer:micrometer-observation-test'
4647
testImplementation 'com.h2database:h2'
4748
testImplementation 'org.hibernate:hibernate-core'
4849
testImplementation 'org.hibernate.validator:hibernate-validator'
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2020-2022 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 graphql.schema.DataFetchingEnvironment;
20+
import io.micrometer.observation.Observation;
21+
22+
/**
23+
* Context that holds information for metadata collection during observations
24+
* for {@link GraphQlObservationDocumentation#DATA_FETCHER data fetching operations}.
25+
*
26+
* @author Brian Clozel
27+
* @since 1.1.0
28+
*/
29+
public class DataFetcherObservationContext extends Observation.Context {
30+
31+
private final DataFetchingEnvironment environment;
32+
33+
private Object value;
34+
35+
DataFetcherObservationContext(DataFetchingEnvironment environment) {
36+
this.environment = environment;
37+
}
38+
39+
/**
40+
* Return the data fetching environment provided as an input.
41+
*/
42+
public DataFetchingEnvironment getEnvironment() {
43+
return this.environment;
44+
}
45+
46+
/**
47+
* Return the value returned by the {@link graphql.schema.DataFetcher}, if any.
48+
* @see #getError() for the exception thrown by the data fetcher.
49+
*/
50+
public Object getValue() {
51+
return this.value;
52+
}
53+
54+
void setValue(Object value) {
55+
this.value = value;
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2020-2022 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_FETCHER data fetching observations}.
25+
*
26+
* @author Brian Clozel
27+
* @since 1.1.0
28+
*/
29+
public interface DataFetcherObservationConvention extends ObservationConvention<DataFetcherObservationContext> {
30+
31+
32+
@Override
33+
default boolean supportsContext(Observation.Context context) {
34+
return context instanceof DataFetcherObservationContext;
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2020-2022 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.common.KeyValue;
20+
import io.micrometer.common.KeyValues;
21+
22+
import org.springframework.graphql.observation.GraphQlObservationDocumentation.DataFetcherLowCardinalityKeyNames;
23+
24+
/**
25+
* Default implementation for a {@link DataFetcherObservationConvention}
26+
* extracting information from a {@link DataFetcherObservationContext}.
27+
*
28+
* @author Brian Clozel
29+
* @since 1.1.0
30+
*/
31+
public class DefaultDataFetcherObservationConvention implements DataFetcherObservationConvention {
32+
33+
private static final String DEFAULT_NAME = "graphql.datafetcher";
34+
35+
private static final KeyValue OUTCOME_SUCCESS = KeyValue.of(DataFetcherLowCardinalityKeyNames.OUTCOME, "SUCCESS");
36+
37+
private static final KeyValue OUTCOME_ERROR = KeyValue.of(DataFetcherLowCardinalityKeyNames.OUTCOME, "ERROR");
38+
39+
private static final KeyValue ERROR_TYPE_NONE = KeyValue.of(DataFetcherLowCardinalityKeyNames.ERROR_TYPE, "NONE");
40+
41+
private final String name;
42+
43+
public DefaultDataFetcherObservationConvention() {
44+
this(DEFAULT_NAME);
45+
}
46+
47+
public DefaultDataFetcherObservationConvention(String name) {
48+
this.name = name;
49+
}
50+
51+
@Override
52+
public String getName() {
53+
return this.name;
54+
}
55+
56+
@Override
57+
public String getContextualName(DataFetcherObservationContext context) {
58+
return "graphQL field " + context.getEnvironment().getField().getName();
59+
}
60+
61+
@Override
62+
public KeyValues getLowCardinalityKeyValues(DataFetcherObservationContext context) {
63+
return KeyValues.of(outcome(context), fieldName(context), errorType(context));
64+
}
65+
66+
protected KeyValue outcome(DataFetcherObservationContext context) {
67+
if (context.getError() != null) {
68+
return OUTCOME_ERROR;
69+
} return OUTCOME_SUCCESS;
70+
}
71+
72+
protected KeyValue fieldName(DataFetcherObservationContext context) {
73+
return KeyValue.of(DataFetcherLowCardinalityKeyNames.FIELD_NAME, context.getEnvironment().getField().getName());
74+
}
75+
76+
protected KeyValue errorType(DataFetcherObservationContext context) {
77+
if (context.getError() != null) {
78+
return KeyValue.of(DataFetcherLowCardinalityKeyNames.ERROR_TYPE, context.getError().getClass().getSimpleName());
79+
} return ERROR_TYPE_NONE;
80+
}
81+
82+
@Override
83+
public KeyValues getHighCardinalityKeyValues(DataFetcherObservationContext context) {
84+
return KeyValues.of(fieldPath(context));
85+
}
86+
87+
protected KeyValue fieldPath(DataFetcherObservationContext context) {
88+
return KeyValue.of(GraphQlObservationDocumentation.DataFetcherHighCardinalityKeyNames.FIELD_PATH,
89+
context.getEnvironment().getExecutionStepInfo().getPath().toString());
90+
}
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2020-2022 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.common.KeyValue;
20+
import io.micrometer.common.KeyValues;
21+
22+
import org.springframework.graphql.observation.GraphQlObservationDocumentation.ExecutionRequestHighCardinalityKeyNames;
23+
import org.springframework.graphql.observation.GraphQlObservationDocumentation.ExecutionRequestLowCardinalityKeyNames;
24+
25+
/**
26+
* Default implementation for a {@link ExecutionRequestObservationConvention}
27+
* extracting information from a {@link ExecutionRequestObservationContext}.
28+
*
29+
* @author Brian Clozel
30+
* @since 1.1.0
31+
*/
32+
public class DefaultExecutionRequestObservationConvention implements ExecutionRequestObservationConvention {
33+
34+
private static final String DEFAULT_NAME = "graphql.request";
35+
36+
private static final String BASE_CONTEXTUAL_NAME = "graphQL ";
37+
38+
private static final KeyValue OUTCOME_SUCCESS = KeyValue.of(ExecutionRequestLowCardinalityKeyNames.OUTCOME, "SUCCESS");
39+
40+
private static final KeyValue OUTCOME_REQUEST_ERROR = KeyValue.of(ExecutionRequestLowCardinalityKeyNames.OUTCOME, "REQUEST_ERROR");
41+
42+
private static final KeyValue OUTCOME_INTERNAL_ERROR = KeyValue.of(ExecutionRequestLowCardinalityKeyNames.OUTCOME, "INTERNAL_ERROR");
43+
44+
private static final KeyValue OPERATION_QUERY = KeyValue.of(ExecutionRequestLowCardinalityKeyNames.OPERATION, "query");
45+
46+
private final String name;
47+
48+
public DefaultExecutionRequestObservationConvention() {
49+
this(DEFAULT_NAME);
50+
}
51+
52+
public DefaultExecutionRequestObservationConvention(String name) {
53+
this.name = name;
54+
}
55+
56+
@Override
57+
public String getName() {
58+
return this.name;
59+
}
60+
61+
@Override
62+
public String getContextualName(ExecutionRequestObservationContext context) {
63+
return BASE_CONTEXTUAL_NAME + context.getCarrier().getOperationName();
64+
}
65+
66+
@Override
67+
public KeyValues getLowCardinalityKeyValues(ExecutionRequestObservationContext context) {
68+
return KeyValues.of(outcome(context), operation(context));
69+
}
70+
71+
protected KeyValue outcome(ExecutionRequestObservationContext context) {
72+
if (context.getError() != null || context.getResponse() == null) {
73+
return OUTCOME_INTERNAL_ERROR;
74+
}
75+
else if (context.getResponse().getErrors().size() > 0) {
76+
return OUTCOME_REQUEST_ERROR;
77+
}
78+
return OUTCOME_SUCCESS;
79+
}
80+
81+
protected KeyValue operation(ExecutionRequestObservationContext context) {
82+
String operationName = context.getCarrier().getOperationName();
83+
if (operationName != null) {
84+
return KeyValue.of(ExecutionRequestLowCardinalityKeyNames.OPERATION, operationName);
85+
}
86+
return OPERATION_QUERY;
87+
}
88+
89+
@Override
90+
public KeyValues getHighCardinalityKeyValues(ExecutionRequestObservationContext context) {
91+
return KeyValues.of(executionId(context));
92+
}
93+
94+
protected KeyValue executionId(ExecutionRequestObservationContext context) {
95+
return KeyValue.of(ExecutionRequestHighCardinalityKeyNames.EXECUTION_ID, context.getCarrier().getExecutionId().toString());
96+
}
97+
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2020-2022 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 graphql.ExecutionInput;
20+
import graphql.ExecutionResult;
21+
import io.micrometer.observation.transport.RequestReplyReceiverContext;
22+
23+
/**
24+
* Context that holds information for metadata collection during observations
25+
* for {@link GraphQlObservationDocumentation#EXECUTION_REQUEST GraphQL requests}.
26+
* <p>This context also extends {@link RequestReplyReceiverContext} for propagating
27+
* tracing information with the HTTP server exchange.
28+
*
29+
* @author Brian Clozel
30+
* @since 1.1.0
31+
*/
32+
public class ExecutionRequestObservationContext extends RequestReplyReceiverContext<ExecutionInput, ExecutionResult> {
33+
34+
public ExecutionRequestObservationContext(ExecutionInput executionInput) {
35+
super((input, key) -> executionInput.getExtensions().get(key).toString());
36+
setCarrier(executionInput);
37+
}
38+
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2020-2022 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} for {@link GraphQlObservationDocumentation#EXECUTION_REQUEST GraphQL requests}.
24+
*
25+
* @author Brian Clozel
26+
* @since 1.1.0
27+
*/
28+
public interface ExecutionRequestObservationConvention extends ObservationConvention<ExecutionRequestObservationContext> {
29+
30+
@Override
31+
default boolean supportsContext(Observation.Context context) {
32+
return context instanceof ExecutionRequestObservationContext;
33+
}
34+
35+
}

0 commit comments

Comments
 (0)