-
Notifications
You must be signed in to change notification settings - Fork 149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GraphQL Instrumentation #396
Changes from all commits
b66d56e
04684fb
d086b32
56792b8
df31d74
ea6ce43
6bb31c4
67bf14f
ccc45d7
8aa15bb
ddb44b8
e3efad9
7afcdb3
62143b2
f72ece9
4f0666d
b09c3d0
987c68f
7df53e5
a6ada92
a5a3c37
9f27b8f
b163a4f
35c5c56
4deecde
7d19a7e
97684bb
5cbef22
186c824
3923013
64eaf63
5c94c86
6256767
faee5ca
55a3648
fabff51
fb3c5cf
4a102dc
46ef385
ceefaf0
06e7b03
28216b1
08f2b5a
a9044de
f6d3236
7a5292c
c81702d
2acd9f4
978226f
ed002b8
fff75d2
a4cd672
9f33148
6a41e54
b3e3985
8244a00
3bb47eb
f6afeb9
2759f51
182a4f5
e156362
155e8bc
0be6d65
c3b85dd
6b5777d
7a3c828
6910ca4
7b883d7
566d865
c5a10b5
68c5234
c2151f1
523e564
28e89a3
5f55e6f
21895bf
bfff5bd
35c2149
76f38aa
459fe8c
366b235
65c5896
78f9fb3
3867bd5
e8758f8
3197023
eab51aa
224e6f9
bc2fdc8
ffca35f
f7e4e9f
004e798
5add1ee
4a7cd91
f5ef8f9
cc6eae4
5a035c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. |
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' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to add something to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ??? this is the library we are instrumenting. I don't know about this type of file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not for dependencies used in instrumentation modules. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typically it's just libraries that we directly use in development of the agent that need to be declared. |
||
|
||
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() | ||
} |
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(); | ||
} | ||
} |
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; | ||
} | ||
} | ||
|
||
|
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; | ||
} | ||
} |
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; | ||
jasonjkeller marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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)); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of this? Looks like testing only?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, so that we can get the attributes from span event through introspector. (channeling Xi Xia)