Skip to content

Commit 692739a

Browse files
hannahchanlaurit
andauthored
Add GraphQL DataFetcher Instrumentation (#10998)
Co-authored-by: Lauri Tulmin <[email protected]>
1 parent 6b66434 commit 692739a

File tree

15 files changed

+714
-59
lines changed

15 files changed

+714
-59
lines changed
+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Settings for the GraphQL instrumentation
22

33
| System property | Type | Default | Description |
4-
| ------------------------------------------------------ | ------- | ------- | ------------------------------------------------------------------------------------------ |
4+
|--------------------------------------------------------|---------|---------|--------------------------------------------------------------------------------------------|
55
| `otel.instrumentation.graphql.query-sanitizer.enabled` | Boolean | `true` | Whether to remove sensitive information from query source that is added as span attribute. |
6+
7+
# Settings for the GraphQL 20 instrumentation
8+
9+
| System property | Type | Default | Description |
10+
|-------------------------------------------------------------|---------|---------|-----------------------------------------------------------------------------------------------------------------------------------|
11+
| `otel.instrumentation.graphql.data-fetcher.enabled` | Boolean | `false` | Whether to create spans for data fetchers. |
12+
| `otel.instrumentation.graphql.trivial-data-fetcher.enabled` | Boolean | `false` | Whether to create spans for trivial data fetchers. A trivial data fetcher is one that simply maps data from an object to a field. |

instrumentation/graphql-java/graphql-java-20.0/javaagent/build.gradle.kts

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ dependencies {
2323
testImplementation(project(":instrumentation:graphql-java:graphql-java-common:testing"))
2424
}
2525

26+
tasks.withType<Test>().configureEach {
27+
jvmArgs("-Dotel.instrumentation.graphql.data-fetcher.enabled=true")
28+
}
29+
2630
if (findProperty("testLatestDeps") as Boolean) {
2731
otelJava {
2832
minJavaVersionSupported.set(JavaVersion.VERSION_11)

instrumentation/graphql-java/graphql-java-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlSingletons.java

+8
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,18 @@ public final class GraphqlSingletons {
1616
private static final boolean QUERY_SANITIZATION_ENABLED =
1717
InstrumentationConfig.get()
1818
.getBoolean("otel.instrumentation.graphql.query-sanitizer.enabled", true);
19+
private static final boolean DATA_FETCHER_ENABLED =
20+
InstrumentationConfig.get()
21+
.getBoolean("otel.instrumentation.graphql.data-fetcher.enabled", false);
22+
private static final boolean TRIVIAL_DATA_FETCHER_ENABLED =
23+
InstrumentationConfig.get()
24+
.getBoolean("otel.instrumentation.graphql.trivial-data-fetcher.enabled", false);
1925

2026
private static final GraphQLTelemetry TELEMETRY =
2127
GraphQLTelemetry.builder(GlobalOpenTelemetry.get())
2228
.setSanitizeQuery(QUERY_SANITIZATION_ENABLED)
29+
.setDataFetcherInstrumentationEnabled(DATA_FETCHER_ENABLED)
30+
.setTrivialDataFetcherInstrumentationEnabled(TRIVIAL_DATA_FETCHER_ENABLED)
2331
.build();
2432

2533
private GraphqlSingletons() {}

instrumentation/graphql-java/graphql-java-20.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlTest.java

+5
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,9 @@ protected InstrumentationExtension getTesting() {
2323

2424
@Override
2525
protected void configure(GraphQL.Builder builder) {}
26+
27+
@Override
28+
protected boolean hasDataFetcherSpans() {
29+
return true;
30+
}
2631
}

instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetry.java

+15-7
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
package io.opentelemetry.instrumentation.graphql.v20_0;
77

88
import graphql.execution.instrumentation.Instrumentation;
9+
import graphql.schema.DataFetchingEnvironment;
910
import io.opentelemetry.api.OpenTelemetry;
11+
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
1012
import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper;
1113

1214
@SuppressWarnings("AbbreviationAsWordInName")
1315
public final class GraphQLTelemetry {
14-
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.graphql-java-20.0";
1516

1617
/** Returns a new {@link GraphQLTelemetry} configured with the given {@link OpenTelemetry}. */
1718
public static GraphQLTelemetry create(OpenTelemetry openTelemetry) {
@@ -26,17 +27,24 @@ public static GraphQLTelemetryBuilder builder(OpenTelemetry openTelemetry) {
2627
}
2728

2829
private final OpenTelemetryInstrumentationHelper helper;
29-
30-
GraphQLTelemetry(OpenTelemetry openTelemetry, boolean sanitizeQuery) {
31-
helper =
32-
OpenTelemetryInstrumentationHelper.create(
33-
openTelemetry, INSTRUMENTATION_NAME, sanitizeQuery);
30+
private final Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter;
31+
private final boolean createSpansForTrivialDataFetcher;
32+
33+
GraphQLTelemetry(
34+
OpenTelemetry openTelemetry,
35+
boolean sanitizeQuery,
36+
Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter,
37+
boolean createSpansForTrivialDataFetcher) {
38+
helper = GraphqlInstrumenterFactory.createInstrumentationHelper(openTelemetry, sanitizeQuery);
39+
this.dataFetcherInstrumenter = dataFetcherInstrumenter;
40+
this.createSpansForTrivialDataFetcher = createSpansForTrivialDataFetcher;
3441
}
3542

3643
/**
3744
* Returns a new {@link Instrumentation} that generates telemetry for received GraphQL requests.
3845
*/
3946
public Instrumentation newInstrumentation() {
40-
return new OpenTelemetryInstrumentation(helper);
47+
return new OpenTelemetryInstrumentation(
48+
helper, dataFetcherInstrumenter, createSpansForTrivialDataFetcher);
4149
}
4250
}

instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetryBuilder.java

+29-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ public final class GraphQLTelemetryBuilder {
1616

1717
private boolean sanitizeQuery = true;
1818

19+
private boolean dataFetcherInstrumentationEnabled = false;
20+
21+
private boolean trivialDataFetcherInstrumentationEnabled = false;
22+
1923
GraphQLTelemetryBuilder(OpenTelemetry openTelemetry) {
2024
this.openTelemetry = openTelemetry;
2125
}
@@ -27,11 +31,35 @@ public GraphQLTelemetryBuilder setSanitizeQuery(boolean sanitizeQuery) {
2731
return this;
2832
}
2933

34+
/** Sets whether spans are created for GraphQL Data Fetchers. Default is {@code false}. */
35+
@CanIgnoreReturnValue
36+
public GraphQLTelemetryBuilder setDataFetcherInstrumentationEnabled(
37+
boolean dataFetcherInstrumentationEnabled) {
38+
this.dataFetcherInstrumentationEnabled = dataFetcherInstrumentationEnabled;
39+
return this;
40+
}
41+
42+
/**
43+
* Sets whether spans are created for trivial GraphQL Data Fetchers. A trivial DataFetcher is one
44+
* that simply maps data from an object to a field. Default is {@code false}.
45+
*/
46+
@CanIgnoreReturnValue
47+
public GraphQLTelemetryBuilder setTrivialDataFetcherInstrumentationEnabled(
48+
boolean trivialDataFetcherInstrumentationEnabled) {
49+
this.trivialDataFetcherInstrumentationEnabled = trivialDataFetcherInstrumentationEnabled;
50+
return this;
51+
}
52+
3053
/**
3154
* Returns a new {@link GraphQLTelemetry} with the settings of this {@link
3255
* GraphQLTelemetryBuilder}.
3356
*/
3457
public GraphQLTelemetry build() {
35-
return new GraphQLTelemetry(openTelemetry, sanitizeQuery);
58+
return new GraphQLTelemetry(
59+
openTelemetry,
60+
sanitizeQuery,
61+
GraphqlInstrumenterFactory.createDataFetcherInstrumenter(
62+
openTelemetry, dataFetcherInstrumentationEnabled),
63+
trivialDataFetcherInstrumentationEnabled);
3664
}
3765
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.graphql.v20_0;
7+
8+
import graphql.execution.ResultPath;
9+
import io.opentelemetry.context.Context;
10+
import java.util.concurrent.ConcurrentHashMap;
11+
import java.util.concurrent.ConcurrentMap;
12+
13+
final class Graphql20OpenTelemetryInstrumentationState
14+
extends io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationState {
15+
private static final String ROOT_PATH = ResultPath.rootPath().toString();
16+
17+
private final ConcurrentMap<String, Context> contextStorage = new ConcurrentHashMap<>();
18+
19+
@Override
20+
public Context getContext() {
21+
return contextStorage.getOrDefault(ROOT_PATH, Context.current());
22+
}
23+
24+
@Override
25+
public void setContext(Context context) {
26+
this.contextStorage.put(ROOT_PATH, context);
27+
}
28+
29+
public Context setContextForPath(ResultPath resultPath, Context context) {
30+
return contextStorage.putIfAbsent(resultPath.toString(), context);
31+
}
32+
33+
public Context getParentContextForPath(ResultPath resultPath) {
34+
35+
// Navigate up the path until we find the closest parent context
36+
for (ResultPath currentPath = resultPath.getParent();
37+
currentPath != null;
38+
currentPath = currentPath.getParent()) {
39+
40+
Context parentContext = contextStorage.getOrDefault(currentPath.toString(), null);
41+
42+
if (parentContext != null) {
43+
return parentContext;
44+
}
45+
}
46+
47+
// Fallback to returning the context for ROOT_PATH
48+
return getContext();
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.graphql.v20_0;
7+
8+
import graphql.schema.DataFetchingEnvironment;
9+
import io.opentelemetry.api.common.AttributeKey;
10+
import io.opentelemetry.api.common.AttributesBuilder;
11+
import io.opentelemetry.context.Context;
12+
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
13+
import javax.annotation.Nullable;
14+
15+
final class GraphqlDataFetcherAttributesExtractor
16+
implements AttributesExtractor<DataFetchingEnvironment, Void> {
17+
18+
// NOTE: These are not part of the Semantic Convention and are subject to change
19+
private static final AttributeKey<String> GRAPHQL_FIELD_NAME =
20+
AttributeKey.stringKey("graphql.field.name");
21+
private static final AttributeKey<String> GRAPHQL_FIELD_PATH =
22+
AttributeKey.stringKey("graphql.field.path");
23+
24+
@Override
25+
public void onStart(
26+
AttributesBuilder attributes, Context parentContext, DataFetchingEnvironment environment) {
27+
attributes
28+
.put(GRAPHQL_FIELD_NAME, environment.getExecutionStepInfo().getField().getName())
29+
.put(GRAPHQL_FIELD_PATH, environment.getExecutionStepInfo().getPath().toString());
30+
}
31+
32+
@Override
33+
public void onEnd(
34+
AttributesBuilder attributes,
35+
Context context,
36+
DataFetchingEnvironment environment,
37+
@Nullable Void unused,
38+
@Nullable Throwable error) {}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.graphql.v20_0;
7+
8+
import graphql.schema.DataFetchingEnvironment;
9+
import io.opentelemetry.api.OpenTelemetry;
10+
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
11+
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
12+
import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper;
13+
14+
final class GraphqlInstrumenterFactory {
15+
16+
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.graphql-java-20.0";
17+
18+
static OpenTelemetryInstrumentationHelper createInstrumentationHelper(
19+
OpenTelemetry openTelemetry, boolean sanitizeQuery) {
20+
return OpenTelemetryInstrumentationHelper.create(
21+
openTelemetry, INSTRUMENTATION_NAME, sanitizeQuery);
22+
}
23+
24+
static Instrumenter<DataFetchingEnvironment, Void> createDataFetcherInstrumenter(
25+
OpenTelemetry openTelemetry, boolean enabled) {
26+
return Instrumenter.<DataFetchingEnvironment, Void>builder(
27+
openTelemetry,
28+
INSTRUMENTATION_NAME,
29+
environment -> environment.getExecutionStepInfo().getField().getName())
30+
.addAttributesExtractor(new GraphqlDataFetcherAttributesExtractor())
31+
.setSpanStatusExtractor(
32+
(spanStatusBuilder, environment, unused, error) ->
33+
SpanStatusExtractor.getDefault()
34+
.extract(spanStatusBuilder, environment, null, error))
35+
.setEnabled(enabled)
36+
.buildInstrumenter();
37+
}
38+
39+
private GraphqlInstrumenterFactory() {}
40+
}

instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentation.java

+59-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static graphql.execution.instrumentation.InstrumentationState.ofState;
99

1010
import graphql.ExecutionResult;
11+
import graphql.execution.ResultPath;
1112
import graphql.execution.instrumentation.InstrumentationContext;
1213
import graphql.execution.instrumentation.InstrumentationState;
1314
import graphql.execution.instrumentation.SimplePerformantInstrumentation;
@@ -16,19 +17,31 @@
1617
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
1718
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
1819
import graphql.schema.DataFetcher;
20+
import graphql.schema.DataFetchingEnvironment;
21+
import io.opentelemetry.context.Context;
22+
import io.opentelemetry.context.Scope;
23+
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
1924
import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper;
2025
import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationState;
26+
import java.util.concurrent.CompletionStage;
2127

2228
final class OpenTelemetryInstrumentation extends SimplePerformantInstrumentation {
2329
private final OpenTelemetryInstrumentationHelper helper;
30+
private final Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter;
31+
private final boolean createSpansForTrivialDataFetcher;
2432

25-
OpenTelemetryInstrumentation(OpenTelemetryInstrumentationHelper helper) {
33+
OpenTelemetryInstrumentation(
34+
OpenTelemetryInstrumentationHelper helper,
35+
Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter,
36+
boolean createSpansForTrivialDataFetcher) {
2637
this.helper = helper;
38+
this.dataFetcherInstrumenter = dataFetcherInstrumenter;
39+
this.createSpansForTrivialDataFetcher = createSpansForTrivialDataFetcher;
2740
}
2841

2942
@Override
3043
public InstrumentationState createState(InstrumentationCreateStateParameters parameters) {
31-
return new OpenTelemetryInstrumentationState();
44+
return new Graphql20OpenTelemetryInstrumentationState();
3245
}
3346

3447
@Override
@@ -50,7 +63,49 @@ public DataFetcher<?> instrumentDataFetcher(
5063
DataFetcher<?> dataFetcher,
5164
InstrumentationFieldFetchParameters parameters,
5265
InstrumentationState rawState) {
53-
OpenTelemetryInstrumentationState state = ofState(rawState);
54-
return helper.instrumentDataFetcher(dataFetcher, state);
66+
67+
Graphql20OpenTelemetryInstrumentationState state = ofState(rawState);
68+
69+
return environment -> {
70+
ResultPath path = environment.getExecutionStepInfo().getPath();
71+
Context parentContext = state.getParentContextForPath(path);
72+
73+
if (!dataFetcherInstrumenter.shouldStart(parentContext, environment)
74+
|| (parameters.isTrivialDataFetcher() && !createSpansForTrivialDataFetcher)) {
75+
// Propagate context only, do not create span
76+
try (Scope ignored = parentContext.makeCurrent()) {
77+
return dataFetcher.get(environment);
78+
}
79+
}
80+
81+
// Start span
82+
Context childContext = dataFetcherInstrumenter.start(parentContext, environment);
83+
state.setContextForPath(path, childContext);
84+
85+
boolean isCompletionStage = false;
86+
87+
try (Scope ignored = childContext.makeCurrent()) {
88+
Object fieldValue = dataFetcher.get(environment);
89+
90+
isCompletionStage = fieldValue instanceof CompletionStage;
91+
92+
if (isCompletionStage) {
93+
return ((CompletionStage<?>) fieldValue)
94+
.whenComplete(
95+
(result, throwable) ->
96+
dataFetcherInstrumenter.end(childContext, environment, null, throwable));
97+
}
98+
99+
return fieldValue;
100+
101+
} catch (Throwable throwable) {
102+
dataFetcherInstrumenter.end(childContext, environment, null, throwable);
103+
throw throwable;
104+
} finally {
105+
if (!isCompletionStage) {
106+
dataFetcherInstrumenter.end(childContext, environment, null, null);
107+
}
108+
}
109+
};
55110
}
56111
}

0 commit comments

Comments
 (0)