Add GraphQL DataFetcher Instrumentation (#10998)

Co-authored-by: Lauri Tulmin <ltulmin@splunk.com>
This commit is contained in:
Hannah Chan 2024-05-17 08:32:03 +10:00 committed by GitHub
parent 6b66434258
commit 692739a364
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 713 additions and 58 deletions

View File

@ -1,5 +1,12 @@
# Settings for the GraphQL instrumentation
| System property | Type | Default | Description |
| ------------------------------------------------------ | ------- | ------- | ------------------------------------------------------------------------------------------ |
|--------------------------------------------------------|---------|---------|--------------------------------------------------------------------------------------------|
| `otel.instrumentation.graphql.query-sanitizer.enabled` | Boolean | `true` | Whether to remove sensitive information from query source that is added as span attribute. |
# Settings for the GraphQL 20 instrumentation
| System property | Type | Default | Description |
|-------------------------------------------------------------|---------|---------|-----------------------------------------------------------------------------------------------------------------------------------|
| `otel.instrumentation.graphql.data-fetcher.enabled` | Boolean | `false` | Whether to create spans for data fetchers. |
| `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. |

View File

@ -23,6 +23,10 @@ dependencies {
testImplementation(project(":instrumentation:graphql-java:graphql-java-common:testing"))
}
tasks.withType<Test>().configureEach {
jvmArgs("-Dotel.instrumentation.graphql.data-fetcher.enabled=true")
}
if (findProperty("testLatestDeps") as Boolean) {
otelJava {
minJavaVersionSupported.set(JavaVersion.VERSION_11)

View File

@ -16,10 +16,18 @@ public final class GraphqlSingletons {
private static final boolean QUERY_SANITIZATION_ENABLED =
InstrumentationConfig.get()
.getBoolean("otel.instrumentation.graphql.query-sanitizer.enabled", true);
private static final boolean DATA_FETCHER_ENABLED =
InstrumentationConfig.get()
.getBoolean("otel.instrumentation.graphql.data-fetcher.enabled", false);
private static final boolean TRIVIAL_DATA_FETCHER_ENABLED =
InstrumentationConfig.get()
.getBoolean("otel.instrumentation.graphql.trivial-data-fetcher.enabled", false);
private static final GraphQLTelemetry TELEMETRY =
GraphQLTelemetry.builder(GlobalOpenTelemetry.get())
.setSanitizeQuery(QUERY_SANITIZATION_ENABLED)
.setDataFetcherInstrumentationEnabled(DATA_FETCHER_ENABLED)
.setTrivialDataFetcherInstrumentationEnabled(TRIVIAL_DATA_FETCHER_ENABLED)
.build();
private GraphqlSingletons() {}

View File

@ -23,4 +23,9 @@ public class GraphqlTest extends AbstractGraphqlTest {
@Override
protected void configure(GraphQL.Builder builder) {}
@Override
protected boolean hasDataFetcherSpans() {
return true;
}
}

View File

@ -6,12 +6,13 @@
package io.opentelemetry.instrumentation.graphql.v20_0;
import graphql.execution.instrumentation.Instrumentation;
import graphql.schema.DataFetchingEnvironment;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper;
@SuppressWarnings("AbbreviationAsWordInName")
public final class GraphQLTelemetry {
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.graphql-java-20.0";
/** Returns a new {@link GraphQLTelemetry} configured with the given {@link OpenTelemetry}. */
public static GraphQLTelemetry create(OpenTelemetry openTelemetry) {
@ -26,17 +27,24 @@ public final class GraphQLTelemetry {
}
private final OpenTelemetryInstrumentationHelper helper;
private final Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter;
private final boolean createSpansForTrivialDataFetcher;
GraphQLTelemetry(OpenTelemetry openTelemetry, boolean sanitizeQuery) {
helper =
OpenTelemetryInstrumentationHelper.create(
openTelemetry, INSTRUMENTATION_NAME, sanitizeQuery);
GraphQLTelemetry(
OpenTelemetry openTelemetry,
boolean sanitizeQuery,
Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter,
boolean createSpansForTrivialDataFetcher) {
helper = GraphqlInstrumenterFactory.createInstrumentationHelper(openTelemetry, sanitizeQuery);
this.dataFetcherInstrumenter = dataFetcherInstrumenter;
this.createSpansForTrivialDataFetcher = createSpansForTrivialDataFetcher;
}
/**
* Returns a new {@link Instrumentation} that generates telemetry for received GraphQL requests.
*/
public Instrumentation newInstrumentation() {
return new OpenTelemetryInstrumentation(helper);
return new OpenTelemetryInstrumentation(
helper, dataFetcherInstrumenter, createSpansForTrivialDataFetcher);
}
}

View File

@ -16,6 +16,10 @@ public final class GraphQLTelemetryBuilder {
private boolean sanitizeQuery = true;
private boolean dataFetcherInstrumentationEnabled = false;
private boolean trivialDataFetcherInstrumentationEnabled = false;
GraphQLTelemetryBuilder(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
@ -27,11 +31,35 @@ public final class GraphQLTelemetryBuilder {
return this;
}
/** Sets whether spans are created for GraphQL Data Fetchers. Default is {@code false}. */
@CanIgnoreReturnValue
public GraphQLTelemetryBuilder setDataFetcherInstrumentationEnabled(
boolean dataFetcherInstrumentationEnabled) {
this.dataFetcherInstrumentationEnabled = dataFetcherInstrumentationEnabled;
return this;
}
/**
* Sets whether spans are created for trivial GraphQL Data Fetchers. A trivial DataFetcher is one
* that simply maps data from an object to a field. Default is {@code false}.
*/
@CanIgnoreReturnValue
public GraphQLTelemetryBuilder setTrivialDataFetcherInstrumentationEnabled(
boolean trivialDataFetcherInstrumentationEnabled) {
this.trivialDataFetcherInstrumentationEnabled = trivialDataFetcherInstrumentationEnabled;
return this;
}
/**
* Returns a new {@link GraphQLTelemetry} with the settings of this {@link
* GraphQLTelemetryBuilder}.
*/
public GraphQLTelemetry build() {
return new GraphQLTelemetry(openTelemetry, sanitizeQuery);
return new GraphQLTelemetry(
openTelemetry,
sanitizeQuery,
GraphqlInstrumenterFactory.createDataFetcherInstrumenter(
openTelemetry, dataFetcherInstrumentationEnabled),
trivialDataFetcherInstrumentationEnabled);
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.graphql.v20_0;
import graphql.execution.ResultPath;
import io.opentelemetry.context.Context;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
final class Graphql20OpenTelemetryInstrumentationState
extends io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationState {
private static final String ROOT_PATH = ResultPath.rootPath().toString();
private final ConcurrentMap<String, Context> contextStorage = new ConcurrentHashMap<>();
@Override
public Context getContext() {
return contextStorage.getOrDefault(ROOT_PATH, Context.current());
}
@Override
public void setContext(Context context) {
this.contextStorage.put(ROOT_PATH, context);
}
public Context setContextForPath(ResultPath resultPath, Context context) {
return contextStorage.putIfAbsent(resultPath.toString(), context);
}
public Context getParentContextForPath(ResultPath resultPath) {
// Navigate up the path until we find the closest parent context
for (ResultPath currentPath = resultPath.getParent();
currentPath != null;
currentPath = currentPath.getParent()) {
Context parentContext = contextStorage.getOrDefault(currentPath.toString(), null);
if (parentContext != null) {
return parentContext;
}
}
// Fallback to returning the context for ROOT_PATH
return getContext();
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.graphql.v20_0;
import graphql.schema.DataFetchingEnvironment;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import javax.annotation.Nullable;
final class GraphqlDataFetcherAttributesExtractor
implements AttributesExtractor<DataFetchingEnvironment, Void> {
// NOTE: These are not part of the Semantic Convention and are subject to change
private static final AttributeKey<String> GRAPHQL_FIELD_NAME =
AttributeKey.stringKey("graphql.field.name");
private static final AttributeKey<String> GRAPHQL_FIELD_PATH =
AttributeKey.stringKey("graphql.field.path");
@Override
public void onStart(
AttributesBuilder attributes, Context parentContext, DataFetchingEnvironment environment) {
attributes
.put(GRAPHQL_FIELD_NAME, environment.getExecutionStepInfo().getField().getName())
.put(GRAPHQL_FIELD_PATH, environment.getExecutionStepInfo().getPath().toString());
}
@Override
public void onEnd(
AttributesBuilder attributes,
Context context,
DataFetchingEnvironment environment,
@Nullable Void unused,
@Nullable Throwable error) {}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.graphql.v20_0;
import graphql.schema.DataFetchingEnvironment;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper;
final class GraphqlInstrumenterFactory {
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.graphql-java-20.0";
static OpenTelemetryInstrumentationHelper createInstrumentationHelper(
OpenTelemetry openTelemetry, boolean sanitizeQuery) {
return OpenTelemetryInstrumentationHelper.create(
openTelemetry, INSTRUMENTATION_NAME, sanitizeQuery);
}
static Instrumenter<DataFetchingEnvironment, Void> createDataFetcherInstrumenter(
OpenTelemetry openTelemetry, boolean enabled) {
return Instrumenter.<DataFetchingEnvironment, Void>builder(
openTelemetry,
INSTRUMENTATION_NAME,
environment -> environment.getExecutionStepInfo().getField().getName())
.addAttributesExtractor(new GraphqlDataFetcherAttributesExtractor())
.setSpanStatusExtractor(
(spanStatusBuilder, environment, unused, error) ->
SpanStatusExtractor.getDefault()
.extract(spanStatusBuilder, environment, null, error))
.setEnabled(enabled)
.buildInstrumenter();
}
private GraphqlInstrumenterFactory() {}
}

View File

@ -8,6 +8,7 @@ package io.opentelemetry.instrumentation.graphql.v20_0;
import static graphql.execution.instrumentation.InstrumentationState.ofState;
import graphql.ExecutionResult;
import graphql.execution.ResultPath;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.execution.instrumentation.SimplePerformantInstrumentation;
@ -16,19 +17,31 @@ import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperat
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper;
import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationState;
import java.util.concurrent.CompletionStage;
final class OpenTelemetryInstrumentation extends SimplePerformantInstrumentation {
private final OpenTelemetryInstrumentationHelper helper;
private final Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter;
private final boolean createSpansForTrivialDataFetcher;
OpenTelemetryInstrumentation(OpenTelemetryInstrumentationHelper helper) {
OpenTelemetryInstrumentation(
OpenTelemetryInstrumentationHelper helper,
Instrumenter<DataFetchingEnvironment, Void> dataFetcherInstrumenter,
boolean createSpansForTrivialDataFetcher) {
this.helper = helper;
this.dataFetcherInstrumenter = dataFetcherInstrumenter;
this.createSpansForTrivialDataFetcher = createSpansForTrivialDataFetcher;
}
@Override
public InstrumentationState createState(InstrumentationCreateStateParameters parameters) {
return new OpenTelemetryInstrumentationState();
return new Graphql20OpenTelemetryInstrumentationState();
}
@Override
@ -50,7 +63,49 @@ final class OpenTelemetryInstrumentation extends SimplePerformantInstrumentation
DataFetcher<?> dataFetcher,
InstrumentationFieldFetchParameters parameters,
InstrumentationState rawState) {
OpenTelemetryInstrumentationState state = ofState(rawState);
return helper.instrumentDataFetcher(dataFetcher, state);
Graphql20OpenTelemetryInstrumentationState state = ofState(rawState);
return environment -> {
ResultPath path = environment.getExecutionStepInfo().getPath();
Context parentContext = state.getParentContextForPath(path);
if (!dataFetcherInstrumenter.shouldStart(parentContext, environment)
|| (parameters.isTrivialDataFetcher() && !createSpansForTrivialDataFetcher)) {
// Propagate context only, do not create span
try (Scope ignored = parentContext.makeCurrent()) {
return dataFetcher.get(environment);
}
}
// Start span
Context childContext = dataFetcherInstrumenter.start(parentContext, environment);
state.setContextForPath(path, childContext);
boolean isCompletionStage = false;
try (Scope ignored = childContext.makeCurrent()) {
Object fieldValue = dataFetcher.get(environment);
isCompletionStage = fieldValue instanceof CompletionStage;
if (isCompletionStage) {
return ((CompletionStage<?>) fieldValue)
.whenComplete(
(result, throwable) ->
dataFetcherInstrumenter.end(childContext, environment, null, throwable));
}
return fieldValue;
} catch (Throwable throwable) {
dataFetcherInstrumenter.end(childContext, environment, null, throwable);
throw throwable;
} finally {
if (!isCompletionStage) {
dataFetcherInstrumenter.end(childContext, environment, null, null);
}
}
};
}
}

View File

@ -5,10 +5,19 @@
package io.opentelemetry.instrumentation.graphql.v20_0;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import graphql.ExecutionResult;
import graphql.GraphQL;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.instrumentation.graphql.AbstractGraphqlTest;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.semconv.incubating.GraphqlIncubatingAttributes;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
public class GraphqlTest extends AbstractGraphqlTest {
@ -16,6 +25,12 @@ public class GraphqlTest extends AbstractGraphqlTest {
@RegisterExtension
private static final InstrumentationExtension testing = LibraryInstrumentationExtension.create();
private static final AttributeKey<String> GRAPHQL_FIELD_NAME =
AttributeKey.stringKey("graphql.field.name");
private static final AttributeKey<String> GRAPHQL_FIELD_PATH =
AttributeKey.stringKey("graphql.field.path");
@Override
protected InstrumentationExtension getTesting() {
return testing;
@ -23,7 +38,207 @@ public class GraphqlTest extends AbstractGraphqlTest {
@Override
protected void configure(GraphQL.Builder builder) {
GraphQLTelemetry telemetry = GraphQLTelemetry.builder(testing.getOpenTelemetry()).build();
GraphQLTelemetry telemetry =
GraphQLTelemetry.builder(testing.getOpenTelemetry())
.setDataFetcherInstrumentationEnabled(true)
.build();
builder.instrumentation(telemetry.newInstrumentation());
}
@Override
protected boolean hasDataFetcherSpans() {
return true;
}
@Test
void createSpansForDataFetchers() {
// Arrange
GraphQLTelemetry telemetry =
GraphQLTelemetry.builder(testing.getOpenTelemetry())
.setDataFetcherInstrumentationEnabled(true)
.build();
GraphQL graphql =
GraphQL.newGraphQL(graphqlSchema).instrumentation(telemetry.newInstrumentation()).build();
// Act
ExecutionResult result =
graphql.execute(
""
+ " query findBookById {\n"
+ " bookById(id: \"book-1\") {\n"
+ " name\n"
+ " author {\n"
+ " name\n"
+ " }\n"
+ " }\n"
+ " }");
// Assert
assertThat(result.getErrors()).isEmpty();
testing.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
span ->
span.hasName("query findBookById")
.hasKind(SpanKind.INTERNAL)
.hasNoParent()
.hasAttributesSatisfyingExactly(
equalTo(
GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME, "findBookById"),
equalTo(GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "query"),
normalizedQueryEqualsTo(
GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT,
"query findBookById { bookById(id: ?) { name author { name } } }")),
span ->
span.hasName("bookById")
.hasKind(SpanKind.INTERNAL)
.hasParent(spanWithName("query findBookById"))
.hasAttributesSatisfyingExactly(
equalTo(GRAPHQL_FIELD_NAME, "bookById"),
equalTo(GRAPHQL_FIELD_PATH, "/bookById")),
span ->
span.hasName("fetchBookById")
.hasKind(SpanKind.INTERNAL)
.hasParent(spanWithName("bookById")),
span ->
span.hasName("author")
.hasKind(SpanKind.INTERNAL)
.hasParent(spanWithName("bookById"))
.hasAttributesSatisfyingExactly(
equalTo(GRAPHQL_FIELD_NAME, "author"),
equalTo(GRAPHQL_FIELD_PATH, "/bookById/author"))));
}
@Test
void createSpanForTrivialDataFetchers() {
// Arrange
GraphQLTelemetry telemetry =
GraphQLTelemetry.builder(testing.getOpenTelemetry())
.setDataFetcherInstrumentationEnabled(true)
.setTrivialDataFetcherInstrumentationEnabled(true)
.build();
GraphQL graphql =
GraphQL.newGraphQL(graphqlSchema).instrumentation(telemetry.newInstrumentation()).build();
// Act
ExecutionResult result =
graphql.execute(
""
+ " query findBookById {\n"
+ " bookById(id: \"book-1\") {\n"
+ " name\n"
+ " author {\n"
+ " name\n"
+ " }\n"
+ " }\n"
+ " }");
// Assert
assertThat(result.getErrors()).isEmpty();
testing.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
span ->
span.hasName("query findBookById")
.hasKind(SpanKind.INTERNAL)
.hasNoParent()
.hasAttributesSatisfyingExactly(
equalTo(
GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME, "findBookById"),
equalTo(GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "query"),
normalizedQueryEqualsTo(
GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT,
"query findBookById { bookById(id: ?) { name author { name } } }")),
span ->
span.hasName("bookById")
.hasKind(SpanKind.INTERNAL)
.hasParent(spanWithName("query findBookById"))
.hasAttributesSatisfyingExactly(
equalTo(GRAPHQL_FIELD_NAME, "bookById"),
equalTo(GRAPHQL_FIELD_PATH, "/bookById")),
span ->
span.hasName("fetchBookById")
.hasKind(SpanKind.INTERNAL)
.hasParent(spanWithName("bookById")),
span ->
span.hasName("name")
.hasKind(SpanKind.INTERNAL)
.hasParent(spanWithName("bookById"))
.hasAttributesSatisfyingExactly(
equalTo(GRAPHQL_FIELD_NAME, "name"),
equalTo(GRAPHQL_FIELD_PATH, "/bookById/name")),
span ->
span.hasName("author")
.hasKind(SpanKind.INTERNAL)
.hasParent(spanWithName("bookById"))
.hasAttributesSatisfyingExactly(
equalTo(GRAPHQL_FIELD_NAME, "author"),
equalTo(GRAPHQL_FIELD_PATH, "/bookById/author")),
span ->
span.hasName("name")
.hasKind(SpanKind.INTERNAL)
.hasParent(spanWithName("author"))
.hasAttributesSatisfyingExactly(
equalTo(GRAPHQL_FIELD_NAME, "name"),
equalTo(GRAPHQL_FIELD_PATH, "/bookById/author/name"))));
}
@Test
void noDataFetcherSpansCreated() {
// Arrange
GraphQLTelemetry telemetry =
GraphQLTelemetry.builder(testing.getOpenTelemetry())
.setDataFetcherInstrumentationEnabled(false)
.setTrivialDataFetcherInstrumentationEnabled(true)
.build();
GraphQL graphql =
GraphQL.newGraphQL(graphqlSchema).instrumentation(telemetry.newInstrumentation()).build();
// Act
ExecutionResult result =
graphql.execute(
""
+ " query findBookById {\n"
+ " bookById(id: \"book-1\") {\n"
+ " name\n"
+ " author {\n"
+ " name\n"
+ " }\n"
+ " }\n"
+ " }");
// Assert
assertThat(result.getErrors()).isEmpty();
testing.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
span ->
span.hasName("query findBookById")
.hasKind(SpanKind.INTERNAL)
.hasNoParent()
.hasAttributesSatisfyingExactly(
equalTo(
GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME, "findBookById"),
equalTo(GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "query"),
normalizedQueryEqualsTo(
GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT,
"query findBookById { bookById(id: ?) { name author { name } } }")),
span ->
span.hasName("fetchBookById")
.hasKind(SpanKind.INTERNAL)
.hasParent(spanWithName("query findBookById"))));
}
private static SpanData spanWithName(String name) {
return testing.spans().stream()
.filter(span -> span.getName().equals(name))
.findFirst()
.orElse(null);
}
}

View File

@ -0,0 +1,155 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.graphql.v20_0;
import static org.junit.jupiter.api.Assertions.assertEquals;
import graphql.execution.ResultPath;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.ContextKey;
import io.opentelemetry.context.Scope;
import org.junit.jupiter.api.Test;
class OpenTelemetryInstrumentationStateTest {
@Test
void setContextSetsContextForRootPath() {
// Arrange
Graphql20OpenTelemetryInstrumentationState state =
new Graphql20OpenTelemetryInstrumentationState();
Context context = Context.root().with(ContextKey.named("New"), "Context");
// Act
state.setContext(context);
// Assert
assertEquals(context, state.getParentContextForPath(ResultPath.rootPath()));
}
@Test
void getContextGetsContextForRootPath() {
// Arrange
Graphql20OpenTelemetryInstrumentationState state =
new Graphql20OpenTelemetryInstrumentationState();
Context context = Context.root().with(ContextKey.named("New"), "Context");
// Act
state.setContextForPath(ResultPath.rootPath(), context);
// Assert
assertEquals(context, state.getContext());
}
@Test
void getContextReturnsCurrentContext() {
// Arrange
Graphql20OpenTelemetryInstrumentationState state =
new Graphql20OpenTelemetryInstrumentationState();
Context context = Context.root().with(ContextKey.named("New"), "Context");
Context result;
// Act
try (Scope ignored = context.makeCurrent()) {
result = state.getContext();
}
// Assert
assertEquals(context, result);
}
@Test
void getParentContextForPathReturnsCurrentContextForRootPath() {
// Arrange
Graphql20OpenTelemetryInstrumentationState state =
new Graphql20OpenTelemetryInstrumentationState();
Context context = Context.root().with(ContextKey.named("New"), "Context");
Context result;
// Act and Assert
try (Scope ignored = context.makeCurrent()) {
result = state.getParentContextForPath(ResultPath.rootPath());
}
// Assert
assertEquals(context, result);
}
@Test
void getParentContextForPathReturnsRootPathContextForRootPath() {
// Arrange
Graphql20OpenTelemetryInstrumentationState state =
new Graphql20OpenTelemetryInstrumentationState();
Context context = Context.root().with(ContextKey.named("New"), "Context");
state.setContextForPath(ResultPath.rootPath(), context);
// Act
Context result = state.getParentContextForPath(ResultPath.rootPath());
// Assert
assertEquals(context, result);
}
@Test
void getParentContextForPathReturnsCurrentContextForDeepPath() {
// Arrange
Graphql20OpenTelemetryInstrumentationState state =
new Graphql20OpenTelemetryInstrumentationState();
Context context = Context.root().with(ContextKey.named("New"), "Context");
ResultPath resultPath = ResultPath.parse("/segment1/segment2/segment3");
Context result;
// Act and Assert
try (Scope ignored = context.makeCurrent()) {
result = state.getParentContextForPath(resultPath);
}
// Assert
assertEquals(context, result);
}
@Test
void getParentContextForPathReturnsRootPathContextForDeepPath() {
// Arrange
Graphql20OpenTelemetryInstrumentationState state =
new Graphql20OpenTelemetryInstrumentationState();
Context context = Context.root().with(ContextKey.named("New"), "Context");
state.setContextForPath(ResultPath.rootPath(), context);
ResultPath resultPath = ResultPath.parse("/segment1/segment2/segment3");
// Act
Context result = state.getParentContextForPath(resultPath);
// Assert
assertEquals(context, result);
}
@Test
void getParentContextForPathReturnsParentContextForDeepPath() {
// Arrange
Graphql20OpenTelemetryInstrumentationState state =
new Graphql20OpenTelemetryInstrumentationState();
Context rootPathContext = Context.root().with(ContextKey.named("Name"), "RootPath");
Context childContext = Context.root().with(ContextKey.named("Name"), "Child");
state.setContextForPath(ResultPath.rootPath(), rootPathContext);
state.setContextForPath(ResultPath.parse("/segment1/segment2"), childContext);
// Act
Context result =
state.getParentContextForPath(
ResultPath.parse("/segment1/segment2/segment3/segment4/segment5"));
// Assert
assertEquals(childContext, result);
}
}

View File

@ -19,13 +19,13 @@ import javax.annotation.Nullable;
*/
final class GraphqlAttributesExtractor
implements AttributesExtractor<OpenTelemetryInstrumentationState, ExecutionResult> {
// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/graphql.md
private static final AttributeKey<String> OPERATION_NAME =
AttributeKey.stringKey("graphql.operation.name");
private static final AttributeKey<String> OPERATION_TYPE =
AttributeKey.stringKey("graphql.operation.type");
// copied from GraphqlIncubatingAttributes
private static final AttributeKey<String> GRAPHQL_DOCUMENT =
AttributeKey.stringKey("graphql.document");
private static final AttributeKey<String> GRAPHQL_OPERATION_NAME =
AttributeKey.stringKey("graphql.operation.name");
private static final AttributeKey<String> GRAPHQL_OPERATION_TYPE =
AttributeKey.stringKey("graphql.operation.type");
@Override
public void onStart(
@ -40,9 +40,10 @@ final class GraphqlAttributesExtractor
OpenTelemetryInstrumentationState request,
@Nullable ExecutionResult response,
@Nullable Throwable error) {
attributes.put(OPERATION_NAME, request.getOperationName());
attributes.put(GRAPHQL_OPERATION_NAME, request.getOperationName());
if (request.getOperation() != null) {
attributes.put(OPERATION_TYPE, request.getOperation().name().toLowerCase(Locale.ROOT));
attributes.put(
GRAPHQL_OPERATION_TYPE, request.getOperation().name().toLowerCase(Locale.ROOT));
}
attributes.put(GRAPHQL_DOCUMENT, request.getQuery());
}

View File

@ -13,41 +13,41 @@ import io.opentelemetry.context.Context;
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public final class OpenTelemetryInstrumentationState implements InstrumentationState {
public class OpenTelemetryInstrumentationState implements InstrumentationState {
private Context context;
private Operation operation;
private String operationName;
private String query;
Context getContext() {
public Context getContext() {
return context;
}
void setContext(Context context) {
public void setContext(Context context) {
this.context = context;
}
Operation getOperation() {
public Operation getOperation() {
return operation;
}
void setOperation(Operation operation) {
public void setOperation(Operation operation) {
this.operation = operation;
}
String getOperationName() {
public String getOperationName() {
return operationName;
}
void setOperationName(String operationName) {
public void setOperationName(String operationName) {
this.operationName = operationName;
}
String getQuery() {
public String getQuery() {
return query;
}
void setQuery(String query) {
public void setQuery(String query) {
this.query = query;
}
}

View File

@ -23,8 +23,10 @@ import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.sdk.testing.assertj.AttributeAssertion;
import io.opentelemetry.sdk.testing.assertj.SpanDataAssert;
import io.opentelemetry.sdk.trace.data.StatusData;
import io.opentelemetry.semconv.ExceptionAttributes;
import io.opentelemetry.semconv.incubating.GraphqlIncubatingAttributes;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
@ -33,6 +35,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
@ -43,12 +46,17 @@ public abstract class AbstractGraphqlTest {
private final List<Map<String, String>> books = new ArrayList<>();
private final List<Map<String, String>> authors = new ArrayList<>();
private GraphQL graphql;
protected GraphQL graphql;
protected GraphQLSchema graphqlSchema;
protected abstract InstrumentationExtension getTesting();
protected abstract void configure(GraphQL.Builder builder);
protected boolean hasDataFetcherSpans() {
return false;
}
@BeforeAll
void setup() throws IOException {
addAuthor("author-1", "John");
@ -62,7 +70,7 @@ public abstract class AbstractGraphqlTest {
new InputStreamReader(
this.getClass().getClassLoader().getResourceAsStream("schema.graphqls"),
StandardCharsets.UTF_8)) {
GraphQLSchema graphqlSchema = buildSchema(reader);
graphqlSchema = buildSchema(reader);
GraphQL.Builder graphqlBuilder = GraphQL.newGraphQL(graphqlSchema);
configure(graphqlBuilder);
this.graphql = graphqlBuilder.build();
@ -138,21 +146,37 @@ public abstract class AbstractGraphqlTest {
getTesting()
.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
trace -> {
List<Consumer<SpanDataAssert>> assertions = new ArrayList<>();
assertions.add(
span ->
span.hasName("query findBookById")
.hasKind(SpanKind.INTERNAL)
.hasNoParent()
.hasAttributesSatisfyingExactly(
equalTo(
GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME,
"findBookById"),
equalTo(GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "query"),
normalizedQueryEqualsTo(
GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT,
"query findBookById { bookById(id: ?) { name } }")));
if (hasDataFetcherSpans()) {
assertions.add(
span ->
span.hasName("query findBookById")
.hasKind(SpanKind.INTERNAL)
.hasNoParent()
span.hasName("bookById")
.hasParent(trace.getSpan(0))
.hasAttributesSatisfyingExactly(
equalTo(
AttributeKey.stringKey("graphql.operation.name"),
"findBookById"),
equalTo(AttributeKey.stringKey("graphql.operation.type"), "query"),
normalizedQueryEqualsTo(
AttributeKey.stringKey("graphql.document"),
"query findBookById { bookById(id: ?) { name } }")),
span -> span.hasName("fetchBookById").hasParent(trace.getSpan(0))));
equalTo(AttributeKey.stringKey("graphql.field.path"), "/bookById"),
equalTo(AttributeKey.stringKey("graphql.field.name"), "bookById")));
}
assertions.add(
span ->
span.hasName("fetchBookById")
.hasParent(trace.getSpan(hasDataFetcherSpans() ? 1 : 0)));
trace.hasSpansSatisfyingExactly(assertions);
});
}
@Test
@ -170,18 +194,33 @@ public abstract class AbstractGraphqlTest {
getTesting()
.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
trace -> {
List<Consumer<SpanDataAssert>> assertions = new ArrayList<>();
assertions.add(
span ->
span.hasName("query")
.hasKind(SpanKind.INTERNAL)
.hasNoParent()
.hasAttributesSatisfyingExactly(
equalTo(AttributeKey.stringKey("graphql.operation.type"), "query"),
normalizedQueryEqualsTo(
AttributeKey.stringKey("graphql.document"),
"{ bookById(id: ?) { name } }")));
if (hasDataFetcherSpans()) {
assertions.add(
span ->
span.hasName("query")
.hasKind(SpanKind.INTERNAL)
.hasNoParent()
span.hasName("bookById")
.hasParent(trace.getSpan(0))
.hasAttributesSatisfyingExactly(
equalTo(AttributeKey.stringKey("graphql.operation.type"), "query"),
normalizedQueryEqualsTo(
AttributeKey.stringKey("graphql.document"),
"{ bookById(id: ?) { name } }")),
span -> span.hasName("fetchBookById").hasParent(trace.getSpan(0))));
equalTo(AttributeKey.stringKey("graphql.field.path"), "/bookById"),
equalTo(AttributeKey.stringKey("graphql.field.name"), "bookById")));
}
assertions.add(
span ->
span.hasName("fetchBookById")
.hasParent(trace.getSpan(hasDataFetcherSpans() ? 1 : 0)));
trace.hasSpansSatisfyingExactly(assertions);
});
}
@Test
@ -280,15 +319,16 @@ public abstract class AbstractGraphqlTest {
.hasNoParent()
.hasAttributesSatisfyingExactly(
equalTo(
AttributeKey.stringKey("graphql.operation.name"), "addNewBook"),
GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME,
"addNewBook"),
equalTo(
AttributeKey.stringKey("graphql.operation.type"), "mutation"),
GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "mutation"),
normalizedQueryEqualsTo(
AttributeKey.stringKey("graphql.document"),
GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT,
"mutation addNewBook { addBook(id: ?, name: ?, author: ?) { id } }"))));
}
private static AttributeAssertion normalizedQueryEqualsTo(
protected static AttributeAssertion normalizedQueryEqualsTo(
AttributeKey<String> key, String value) {
return satisfies(
key,