Skip to content

Commit

Permalink
Merge pull request #396 from newrelic/graphql-java
Browse files Browse the repository at this point in the history
GraphQL Instrumentation
  • Loading branch information
twcrone authored Sep 2, 2021
2 parents 02b0c57 + 5a035c1 commit 1be967c
Show file tree
Hide file tree
Showing 54 changed files with 1,476 additions and 1 deletion.
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();
}
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'

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;

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

0 comments on commit 1be967c

Please sign in to comment.