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-java 17.0+ instrumentation #487

Merged
merged 2 commits into from
Oct 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions instrumentation/graphql-java-17.0/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:17.0'

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-17.0' }
}

verifyInstrumentation {
passes 'com.graphql-java:graphql-java:[17.0,18.0)'
}

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;

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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
*
* * Copyright 2020 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.instrumentation.graphql;

import graphql.language.*;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import static com.nr.instrumentation.graphql.Utils.isNullOrEmpty;

/**
* Generates GraphQL transaction names based on details referenced in Node instrumentation.
*
* @see <a href="https://github.com/newrelic/newrelic-node-apollo-server-plugin/blob/main/docs/transactions.md">
* NewRelic Node Apollo Server Plugin - Transactions
* </a>
* <p>
* Batch queries are not supported by GraphQL Java implementation at this time
* and transaction names for parse errors must be set elsewhere because this class
* relies on the GraphQL Document that is the artifact of a successful parse.
*/
public class GraphQLTransactionName {

private final static String DEFAULT_TRANSACTION_NAME = "";

// federated field names to exclude from path calculations
private final static String TYPENAME = "__typename";
private final static String ID = "id";

/**
* Generates a transaction name based on a valid, parsed GraphQL Document
*
* @param document parsed GraphQL Document
* @return a transaction name based on given document
*/
public static String from(final Document document) {
if (document == null) return DEFAULT_TRANSACTION_NAME;
List<OperationDefinition> operationDefinitions = document.getDefinitionsOfType(OperationDefinition.class);
if (isNullOrEmpty(operationDefinitions)) return DEFAULT_TRANSACTION_NAME;
if (operationDefinitions.size() == 1) {
return getTransactionNameFor(operationDefinitions.get(0));
}
return "/batch" + operationDefinitions.stream()
.map(GraphQLTransactionName::getTransactionNameFor)
.collect(Collectors.joining());
}

private static String getTransactionNameFor(OperationDefinition operationDefinition) {
if (operationDefinition == null) return DEFAULT_TRANSACTION_NAME;
return createBeginningOfTransactionNameFrom(operationDefinition) +
createEndOfTransactionNameFrom(operationDefinition.getSelectionSet());
}

private static String createBeginningOfTransactionNameFrom(final OperationDefinition operationDefinition) {
String operationType = GraphQLOperationDefinition.getOperationTypeFrom(operationDefinition);
String operationName = GraphQLOperationDefinition.getOperationNameFrom(operationDefinition);
return String.format("/%s/%s", operationType, operationName);
}

private static String createEndOfTransactionNameFrom(final SelectionSet selectionSet) {
Selection selection = onlyNonFederatedSelectionOrNoneFrom(selectionSet);
if (selection == null) return "";
List<Selection> selections = new ArrayList<>();
while (selection != null) {
selections.add(selection);
selection = nextNonFederatedSelectionChildFrom(selection);
}
return createPathSuffixFrom(selections);
}

private static String createPathSuffixFrom(final List<Selection> selections) {
if (selections == null || selections.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder("/").append(getNameFrom(selections.get(0)));
int length = selections.size();
// skip first element, it is already added without extra formatting
for (int i = 1; i < length; i++) {
sb.append(getFormattedNameFor(selections.get(i)));
}
return sb.toString();
}

private static String getFormattedNameFor(Selection selection) {
if (selection instanceof Field) {
return String.format(".%s", getNameFrom((Field) selection));
}
if (selection instanceof InlineFragment) {
return String.format("<%s>", getNameFrom((InlineFragment) selection));
}
return "";
}

private static Selection onlyNonFederatedSelectionOrNoneFrom(final SelectionSet selectionSet) {
if (selectionSet == null) {
return null;
}
List<Selection> selections = selectionSet.getSelections();
if (isNullOrEmpty(selections)) {
return null;
}
List<Selection> selection = selections.stream()
.filter(namedNode -> notFederatedFieldName(getNameFrom(namedNode)))
.collect(Collectors.toList());
// there can be only one, or we stop digging into query
return selection.size() == 1 ? selection.get(0) : null;
}

private static String getNameFrom(final Selection selection) {
if (selection instanceof Field) {
return getNameFrom((Field) selection);
}
if (selection instanceof InlineFragment) {
return getNameFrom((InlineFragment) selection);
}
// FragmentSpread also implements Selection but not sure how that might apply here
return null;
}

private static String getNameFrom(final Field field) {
return field.getName();
}

private static String getNameFrom(final InlineFragment inlineFragment) {
TypeName typeCondition = inlineFragment.getTypeCondition();
if (typeCondition != null) {
return typeCondition.getName();
}
return "";
}

private static Selection nextNonFederatedSelectionChildFrom(final Selection selection) {
if (!(selection instanceof SelectionSetContainer)) {
return null;
}
SelectionSet selectionSet = ((SelectionSetContainer<?>) selection).getSelectionSet();
return onlyNonFederatedSelectionOrNoneFrom(selectionSet);
}

private static boolean notFederatedFieldName(final String fieldName) {
return !(TYPENAME.equals(fieldName) || ID.equals(fieldName));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.nr.instrumentation.graphql;

import java.util.Collection;

// instead of adding dependencies, just add some utility methods
public class Utils {
public static <T> T getValueOrDefault(T value, T defaultValue) {
return value == null ? defaultValue : value;
}

public static boolean isNullOrEmpty(final Collection<?> c) {
return c == null || c.isEmpty();
}
}
Loading