Skip to content
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

Merged
merged 97 commits into from
Sep 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
b66d56e
Initial build.gradle to get started on GraphQL client instrumentation
twcrone Jul 15, 2021
04684fb
Basic module directory for graphql work
twcrone Jul 20, 2021
d086b32
Base instrumentation
twcrone Jul 21, 2021
56792b8
Move module directory
twcrone Jul 22, 2021
df31d74
Add GraphQL test init
twcrone Jul 22, 2021
ea6ce43
Add Copyright comments
twcrone Jul 22, 2021
6bb31c4
Add genuine test of base GraphQL configuration
twcrone Jul 22, 2021
67bf14f
Fix trace
twcrone Jul 22, 2021
ccc45d7
Instrument Execution
twcrone Jul 23, 2021
8aa15bb
Minor tweak to test 'simple' named query transaction
twcrone Jul 23, 2021
ddb44b8
Add deepest unique path test
twcrone Jul 23, 2021
e3efad9
Add test excluding federated field names
twcrone Jul 23, 2021
7afcdb3
Ignore new use case for now with Fragments
twcrone Jul 23, 2021
62143b2
Add case with fragments that are not shown in name
twcrone Jul 23, 2021
f72ece9
Add validation errors test case
twcrone Jul 23, 2021
4f0666d
Add test file and ignored tests for batch queries
twcrone Jul 23, 2021
b09c3d0
enable distributed tracing for instrumentation test runner
XiXiaPdx Jul 28, 2021
987c68f
WIP trace entry point of graphql request
XiXiaPdx Jul 28, 2021
7df53e5
WIP trace resolvers
XiXiaPdx Jul 28, 2021
a6ada92
WIP rename span to tx name
XiXiaPdx Jul 28, 2021
a5a3c37
Add JUnit5 for parameterized test support
twcrone Jul 28, 2021
9f27b8f
Updating deps
twcrone Jul 28, 2021
b163a4f
Add beginnings of parameterized tests
twcrone Jul 28, 2021
35c5c56
Formatted test data file
twcrone Jul 28, 2021
4deecde
Move param name test files into their own directory
twcrone Jul 28, 2021
7d19a7e
Full refactored to data driven, parameterized tests
twcrone Jul 28, 2021
97684bb
Fix fragments for transaction name
twcrone Jul 28, 2021
5cbef22
add graphql agent Attributes to span
XiXiaPdx Jul 29, 2021
186c824
wip add agent attribute to resolver for testing span attribute
XiXiaPdx Jul 29, 2021
3923013
capture parse, validate, and execution result errors
XiXiaPdx Jul 29, 2021
64eaf63
two temp tests for exercising error testing
XiXiaPdx Jul 29, 2021
5c94c86
helper class for error reporting
XiXiaPdx Jul 29, 2021
6256767
report errors with helper class
XiXiaPdx Jul 29, 2021
faee5ca
create resolve spans from ExecutionStrategy
XiXiaPdx Jul 29, 2021
55a3648
clean up comments and add fixmes to parseAndValidate
XiXiaPdx Jul 29, 2021
fabff51
add todos for attributes on resolver and operation span
XiXiaPdx Jul 29, 2021
fb3c5cf
Remove tests for parse errors and batch queries
twcrone Jul 29, 2021
4a102dc
set parse error tx name and add todo for validation error
XiXiaPdx Jul 30, 2021
46ef385
add unscoped metrics for operation and resolver, need to fix operatio…
XiXiaPdx Jul 30, 2021
ceefaf0
Refactor safer extraction of OperationDefinition
twcrone Jul 30, 2021
06e7b03
add fixme and notes
XiXiaPdx Jul 30, 2021
28216b1
correct name for parse error
XiXiaPdx Jul 30, 2021
08f2b5a
add optional field arg to test
XiXiaPdx Jul 31, 2021
a9044de
add getAgentAttributes to introspector for spanEventts
XiXiaPdx Jul 31, 2021
f6d3236
add error class assertion for parseError test
XiXiaPdx Jul 31, 2021
7a5292c
refactor instrumentation test
XiXiaPdx Aug 2, 2021
c81702d
Disable test towards query string creation
twcrone Jul 30, 2021
2acd9f4
set tx name for validation errors
XiXiaPdx Aug 3, 2021
978226f
WIP build query from document
XiXiaPdx Aug 3, 2021
ed002b8
WIP refactor obfuscate code into helper
XiXiaPdx Aug 3, 2021
fff75d2
refactor obfuscate helper and pass testWithArg
XiXiaPdx Aug 3, 2021
a4cd672
cleanup
XiXiaPdx Aug 3, 2021
9f33148
setup parameterized test for obfuscate query string
XiXiaPdx Aug 3, 2021
6a41e54
refactor obfuscate helper for fragments and add more tests
XiXiaPdx Aug 3, 2021
b3e3985
move obfuscate query tests into own class
XiXiaPdx Aug 3, 2021
8244a00
add GraphQL to trimmable metric list
XiXiaPdx Aug 4, 2021
3bb47eb
add test for scoped and unscoped graphql metric
XiXiaPdx Aug 4, 2021
f6afeb9
add args as attributes to resolvers
XiXiaPdx Aug 4, 2021
2759f51
refactor class names
XiXiaPdx Aug 4, 2021
182a4f5
wip set arg attribute on resolver span
XiXiaPdx Aug 6, 2021
e156362
add clearSpans to clear of introspector
XiXiaPdx Aug 6, 2021
155e8bc
use sqlObfuscator pattern for graphqlObfuscator
XiXiaPdx Aug 6, 2021
0be6d65
refactor instrumentation test
XiXiaPdx Aug 6, 2021
c3b85dd
instrument execute
XiXiaPdx Aug 6, 2021
6b5777d
delete builder instrumentation
XiXiaPdx Aug 6, 2021
7a3c828
tighten up regex to avoid removing __ from query
XiXiaPdx Aug 6, 2021
6910ca4
prevent double reporting of errors at end of graphql request
XiXiaPdx Aug 16, 2021
7b883d7
notice error on resolver, with test
XiXiaPdx Aug 16, 2021
566d865
remove comment and remove query arg field attributes
XiXiaPdx Aug 16, 2021
c5a10b5
refactor set attributes on span
XiXiaPdx Aug 16, 2021
68c5234
refactor tests
XiXiaPdx Aug 19, 2021
c2151f1
check attributes not on other spans
XiXiaPdx Aug 19, 2021
523e564
need to fix error reporting
XiXiaPdx Aug 23, 2021
28e89a3
fix tx naming and error reporting
XiXiaPdx Aug 23, 2021
5f55e6f
error is reported only on correct span
XiXiaPdx Aug 24, 2021
21895bf
Removed passthru method
twcrone Aug 23, 2021
bfff5bd
Fix the 'null' suffix transaction name
twcrone Aug 23, 2021
35c2149
remove fixme and fix expected tx name
XiXiaPdx Aug 24, 2021
76f38aa
report exception and nonNullable exceptions
XiXiaPdx Aug 24, 2021
459fe8c
Make document parameter 'final'
twcrone Aug 24, 2021
366b235
Refactor to use document helper
twcrone Aug 24, 2021
65c5896
Add tests relying on PrivateApiStub
twcrone Aug 24, 2021
78f9fb3
Refactor fetching first operation def
twcrone Aug 26, 2021
3867bd5
Checkpoint to fixing transaction names for "multiple operations"
twcrone Aug 26, 2021
e8758f8
Account for multiple operations in transaction name
twcrone Aug 26, 2021
3197023
set tx name right after parsing is successful
XiXiaPdx Aug 26, 2021
eab51aa
Minor refactor
twcrone Aug 27, 2021
224e6f9
WIP remove instrumentation and reduce spans
XiXiaPdx Aug 27, 2021
bc2fdc8
start timing in executeAsync
XiXiaPdx Aug 30, 2021
ffca35f
refactor report error instrumentation
XiXiaPdx Aug 30, 2021
f7e4e9f
obfuscate query
XiXiaPdx Aug 30, 2021
004e798
Fix tests based on refactoring
twcrone Aug 30, 2021
5add1ee
Remove 'post' from transaction name on parse error
twcrone Aug 31, 2021
4a7cd91
Code cleanup
twcrone Aug 31, 2021
f5ef8f9
Add README.md
twcrone Aug 31, 2021
cc6eae4
Only support 16.x versions
twcrone Aug 31, 2021
5a035c1
Cleanup from review comments
twcrone Sep 1, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

package com.newrelic.agent.introspec;

import java.util.Map;

public interface SpanEvent {
String getName();

Expand All @@ -25,4 +27,6 @@ public interface SpanEvent {
String getHttpComponent();

String getTransactionId();

Map<String, Object> getAgentAttributes();
Copy link
Contributor

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?

Copy link
Contributor Author

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)

}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public void clear() {
statsService.clear();
IntrospectorTransactionTraceService traceService = (IntrospectorTransactionTraceService) ServiceFactory.getTransactionTraceService();
traceService.clear();
clearSpanEvents();
}

public void cleanup() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import com.newrelic.agent.introspec.SpanEvent;

import java.util.Map;

public class SpanEventImpl implements SpanEvent {

private final com.newrelic.agent.model.SpanEvent spanEvent;
Expand Down Expand Up @@ -61,4 +63,9 @@ public String getHttpComponent() {
public String getTransactionId() {
return (String) spanEvent.getIntrinsics().get("transactionId");
}

@Override
public Map<String, Object> getAgentAttributes() {
return spanEvent.getAgentAttributes();
}
}
92 changes: 92 additions & 0 deletions instrumentation/graphql-java-16.2/README.md
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.
31 changes: 31 additions & 0 deletions instrumentation/graphql-java-16.2/build.gradle
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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add something to the THIRD_PARTY_NOTICES.md file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for dependencies used in instrumentation modules.

Copy link
Contributor

Choose a reason for hiding this comment

The 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()
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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));
}
}
Loading