-
Notifications
You must be signed in to change notification settings - Fork 149
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #396 from newrelic/graphql-java
GraphQL Instrumentation
- Loading branch information
Showing
54 changed files
with
1,476 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
GraphQL Instrumentation | ||
======================= | ||
|
||
A graphQL request can be made by any httpclient. | ||
|
||
So the instrumentation work is really focused on the graphql-core library that: | ||
|
||
* Parses the query for syntax errors | ||
* Validates the query for validation errors | ||
* Checks to make sure fields that require a non-null value to be returned, gets a non-null value. | ||
* Executes the query, resolving each field, which will activate DataFetchers for those fields. | ||
|
||
## Transaction name | ||
|
||
Change the transaction name from something like | ||
|
||
`WebDispatcher/SpringController/web.#graphql….` | ||
|
||
To | ||
|
||
`/QUERY/<anonymous>/shows` | ||
|
||
![](transactionView.png) | ||
|
||
## Spans | ||
|
||
One request can create hundreds of spans. Generate Spans with helpful names and that have certain attributes on those spans | ||
|
||
Report errors that occur during a request. There can be many errors. We try to capture as many as we can. | ||
|
||
![](distributedTraceView.png) | ||
|
||
### GraphQL (start timing) | ||
|
||
Our chosen entry for sake of timing staring `GraphQL.executeAsync()`. All the `GraphQL` public methods eventually go | ||
through this method. Even the `sync` ones call it and block. | ||
|
||
`public CompletableFuture<ExecutionResult> executeAsync(ExecutionInput executionInput)` | ||
|
||
### ParseAndValidate Instrumentation | ||
|
||
Pretty quickly in executeAsync, we start instrumentation at `ParseAndValidate_Instrumentation` | ||
|
||
`parse()` | ||
|
||
Allows us to create: | ||
|
||
* transaction name | ||
* set span name (e.g. `GraphQL/operation/<transactionName>`) | ||
* set span attributes | ||
|
||
The reason instrumentation starts here is that we have access to graphQL objects that allow us to create the proper | ||
transaction name, span Name, and attribute values for spans without writing our own parse code. | ||
|
||
`validate()` | ||
|
||
If the GraphQL Document does not pass validation, we report the error. | ||
The specification says not to change the transaction name. | ||
|
||
### ExecutionStrategy_instrumentation | ||
|
||
So if parsing and validation were both error free, the query can be fully resolved. | ||
|
||
* starting timing on resolving the fields | ||
* set the span name (e.g. `GraphQL/resolve/<fieldName>`) | ||
* set span attributes | ||
|
||
`protected CompletableFuture<FieldValueInfo> resolveFieldWithInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters)` | ||
|
||
## GraphQL Errors | ||
|
||
There can be errors during resolve. GraphQL seems to handle two groups of exceptions with two different paths. | ||
|
||
1. Parsing and validation errors captured above in `ParseAndValidate_Instrumentation` | ||
2. `completeValue()` is to capture the `graphql.execution.NonNullableFieldWasNullException` | ||
3. `handleException()` is for everything else | ||
|
||
## Obfuscator | ||
|
||
There are utility classes in the instrumentation module: | ||
|
||
The obfuscator is to obfuscate the GraphQL query, according to spec. | ||
The code is copied from the agent SqlObfuscator, with a slight tweak to a regex. | ||
|
||
## Gotchas | ||
|
||
* `TracerToSpanEvent` class in the agent. | ||
* `maybeSetGraphQLAttributes()` | ||
* `SimpleStatsEngine` in agent | ||
* For every metric created, the agent creates a scoped and unscoped version. | ||
An unscoped metric could get filtered out from being sent by the agent if it doesn't meet certain criteria. | ||
This prevents the GraphQL metrics from being filtered. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
dependencies { | ||
implementation(project(":agent-bridge")) | ||
|
||
implementation 'com.graphql-java:graphql-java:16.2' | ||
|
||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2' | ||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.2' | ||
|
||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2' | ||
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.7.2'} | ||
|
||
repositories { | ||
mavenCentral() | ||
} | ||
|
||
jar { | ||
manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.graphql-java-16.2' } | ||
} | ||
|
||
verifyInstrumentation { | ||
passes 'com.graphql-java:graphql-java:[16.0,16.2]' | ||
} | ||
|
||
site { | ||
title 'GraphQL Java' | ||
type 'Framework' | ||
} | ||
|
||
test { | ||
useJUnitPlatform() | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions
56
...n/graphql-java-16.2/src/main/java/com/nr/instrumentation/graphql/GraphQLErrorHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/* | ||
* | ||
* * Copyright 2020 New Relic Corporation. All rights reserved. | ||
* * SPDX-License-Identifier: Apache-2.0 | ||
* | ||
*/ | ||
|
||
package com.nr.instrumentation.graphql; | ||
|
||
import com.newrelic.api.agent.NewRelic; | ||
import graphql.ExecutionResult; | ||
import graphql.GraphQLError; | ||
import graphql.GraphQLException; | ||
import graphql.GraphqlErrorException; | ||
import graphql.execution.FieldValueInfo; | ||
|
||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.ExecutionException; | ||
import java.util.logging.Level; | ||
|
||
public class GraphQLErrorHandler { | ||
public static void reportNonNullableExceptionToNR(FieldValueInfo result) { | ||
CompletableFuture<ExecutionResult> exceptionResult = result.getFieldValue(); | ||
if (resultHasException(exceptionResult)) { | ||
reportExceptionFromCompletedExceptionally(exceptionResult); | ||
} | ||
} | ||
|
||
public static void reportGraphQLException(GraphQLException exception) { | ||
NewRelic.noticeError(exception); | ||
} | ||
|
||
public static void reportGraphQLError(GraphQLError error) { | ||
NewRelic.noticeError(throwableFromGraphQLError(error)); | ||
} | ||
|
||
private static boolean resultHasException(CompletableFuture<ExecutionResult> exceptionResult) { | ||
return exceptionResult != null && exceptionResult.isCompletedExceptionally(); | ||
} | ||
|
||
private static void reportExceptionFromCompletedExceptionally(CompletableFuture<ExecutionResult> exceptionResult) { | ||
try { | ||
exceptionResult.get(); | ||
} catch (InterruptedException e) { | ||
NewRelic.getAgent().getLogger().log(Level.FINEST, "Could not report GraphQL exception."); | ||
} catch (ExecutionException e) { | ||
NewRelic.noticeError(e.getCause()); | ||
} | ||
} | ||
|
||
private static Throwable throwableFromGraphQLError(GraphQLError error) { | ||
return GraphqlErrorException.newErrorException() | ||
.message(error.getMessage()) | ||
.build(); | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
...ion/graphql-java-16.2/src/main/java/com/nr/instrumentation/graphql/GraphQLObfuscator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* | ||
* | ||
* * Copyright 2020 New Relic Corporation. All rights reserved. | ||
* * SPDX-License-Identifier: Apache-2.0 | ||
* | ||
*/ | ||
|
||
package com.nr.instrumentation.graphql; | ||
|
||
import graphql.com.google.common.base.Joiner; | ||
|
||
import java.util.regex.Pattern; | ||
|
||
public class GraphQLObfuscator { | ||
private static final String SINGLE_QUOTE = "'(?:[^']|'')*?(?:\\\\'.*|'(?!'))"; | ||
private static final String DOUBLE_QUOTE = "\"(?:[^\"]|\"\")*?(?:\\\\\".*|\"(?!\"))"; | ||
private static final String COMMENT = "(?:#|--).*?(?=\\r|\\n|$)"; | ||
private static final String MULTILINE_COMMENT = "/\\*(?:[^/]|/[^*])*?(?:\\*/|/\\*.*)"; | ||
private static final String UUID = "\\{?(?:[0-9a-f]\\-*){32}\\}?"; | ||
private static final String HEX = "0x[0-9a-f]+"; | ||
private static final String BOOLEAN = "\\b(?:true|false|null)\\b"; | ||
private static final String NUMBER = "-?\\b(?:[0-9]+\\.)?[0-9]+([eE][+-]?[0-9]+)?"; | ||
|
||
private static final Pattern ALL_DIALECTS_PATTERN; | ||
private static final Pattern ALL_UNMATCHED_PATTERN; | ||
|
||
static { | ||
String allDialectsPattern = Joiner.on("|").join(SINGLE_QUOTE, DOUBLE_QUOTE, UUID, HEX, | ||
MULTILINE_COMMENT, COMMENT, NUMBER, BOOLEAN); | ||
|
||
ALL_DIALECTS_PATTERN = Pattern.compile(allDialectsPattern, Pattern.DOTALL | Pattern.CASE_INSENSITIVE); | ||
ALL_UNMATCHED_PATTERN = Pattern.compile("'|\"|/\\*|\\*/|\\$", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); | ||
} | ||
|
||
public static String obfuscate(final String query) { | ||
if (query == null || query.length() == 0) { | ||
return query; | ||
} | ||
String obfuscatedQuery = ALL_DIALECTS_PATTERN.matcher(query).replaceAll("***"); | ||
return checkForUnmatchedPairs(obfuscatedQuery); | ||
} | ||
|
||
private static String checkForUnmatchedPairs(final String obfuscatedQuery) { | ||
return GraphQLObfuscator.ALL_UNMATCHED_PATTERN.matcher(obfuscatedQuery).find() ? "***" : obfuscatedQuery; | ||
} | ||
} | ||
|
||
|
34 changes: 34 additions & 0 deletions
34
...ql-java-16.2/src/main/java/com/nr/instrumentation/graphql/GraphQLOperationDefinition.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* | ||
* | ||
* * Copyright 2020 New Relic Corporation. All rights reserved. | ||
* * SPDX-License-Identifier: Apache-2.0 | ||
* | ||
*/ | ||
|
||
package com.nr.instrumentation.graphql; | ||
|
||
import graphql.language.Document; | ||
import graphql.language.OperationDefinition; | ||
|
||
import java.util.List; | ||
|
||
public class GraphQLOperationDefinition { | ||
private final static String DEFAULT_OPERATION_DEFINITION_NAME = "<anonymous>"; | ||
private final static String DEFAULT_OPERATION_NAME = ""; | ||
|
||
// Multiple operations are supported for transaction name only | ||
// The underlying library does not seem to support multiple operations at time of this instrumentation | ||
public static OperationDefinition firstFrom(final Document document) { | ||
List<OperationDefinition> operationDefinitions = document.getDefinitionsOfType(OperationDefinition.class); | ||
return operationDefinitions.isEmpty() ? null : operationDefinitions.get(0); | ||
} | ||
|
||
public static String getOperationNameFrom(final OperationDefinition operationDefinition) { | ||
return operationDefinition.getName() != null ? operationDefinition.getName() : DEFAULT_OPERATION_DEFINITION_NAME; | ||
} | ||
|
||
public static String getOperationTypeFrom(final OperationDefinition operationDefinition) { | ||
OperationDefinition.Operation operation = operationDefinition.getOperation(); | ||
return operation != null ? operation.name() : DEFAULT_OPERATION_NAME; | ||
} | ||
} |
57 changes: 57 additions & 0 deletions
57
...ation/graphql-java-16.2/src/main/java/com/nr/instrumentation/graphql/GraphQLSpanUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
/* | ||
* | ||
* * Copyright 2020 New Relic Corporation. All rights reserved. | ||
* * SPDX-License-Identifier: Apache-2.0 | ||
* | ||
*/ | ||
|
||
package com.nr.instrumentation.graphql; | ||
|
||
import com.newrelic.agent.bridge.AgentBridge; | ||
import graphql.execution.ExecutionStrategyParameters; | ||
import graphql.language.Document; | ||
import graphql.language.OperationDefinition; | ||
import graphql.schema.GraphQLObjectType; | ||
|
||
import static com.nr.instrumentation.graphql.GraphQLObfuscator.obfuscate; | ||
import static com.nr.instrumentation.graphql.GraphQLOperationDefinition.getOperationTypeFrom; | ||
import static com.nr.instrumentation.graphql.Utils.getValueOrDefault; | ||
|
||
public class GraphQLSpanUtil { | ||
|
||
private final static String DEFAULT_OPERATION_TYPE = "Unavailable"; | ||
private final static String DEFAULT_OPERATION_NAME = "<anonymous>"; | ||
|
||
public static void setOperationAttributes(final Document document, final String query) { | ||
String nonNullQuery = getValueOrDefault(query, ""); | ||
if (document == null) { | ||
setDefaultOperationAttributes(nonNullQuery); | ||
return; | ||
} | ||
OperationDefinition definition = GraphQLOperationDefinition.firstFrom(document); | ||
if (definition == null) { | ||
setDefaultOperationAttributes(nonNullQuery); | ||
} else { | ||
setOperationAttributes(getOperationTypeFrom(definition), definition.getName(), nonNullQuery); | ||
} | ||
} | ||
|
||
public static void setResolverAttributes(ExecutionStrategyParameters parameters) { | ||
AgentBridge.privateApi.addTracerParameter("graphql.field.path", parameters.getPath().getSegmentName()); | ||
GraphQLObjectType type = (GraphQLObjectType) parameters.getExecutionStepInfo().getType(); | ||
AgentBridge.privateApi.addTracerParameter("graphql.field.parentType", type.getName()); | ||
AgentBridge.privateApi.addTracerParameter("graphql.field.name", parameters.getField().getName()); | ||
} | ||
|
||
private static void setOperationAttributes(String type, String name, String query) { | ||
AgentBridge.privateApi.addTracerParameter("graphql.operation.type", getValueOrDefault(type, DEFAULT_OPERATION_TYPE)); | ||
AgentBridge.privateApi.addTracerParameter("graphql.operation.name", getValueOrDefault(name, DEFAULT_OPERATION_NAME)); | ||
AgentBridge.privateApi.addTracerParameter("graphql.operation.query", obfuscate(query)); | ||
} | ||
|
||
private static void setDefaultOperationAttributes(String query) { | ||
AgentBridge.privateApi.addTracerParameter("graphql.operation.type", DEFAULT_OPERATION_TYPE); | ||
AgentBridge.privateApi.addTracerParameter("graphql.operation.name", DEFAULT_OPERATION_NAME); | ||
AgentBridge.privateApi.addTracerParameter("graphql.operation.query", obfuscate(query)); | ||
} | ||
} |
Oops, something went wrong.