From 67297e7a7ca84df41c333d03d5d2952426d442a5 Mon Sep 17 00:00:00 2001 From: xxia Date: Wed, 13 Oct 2021 18:41:27 -0700 Subject: [PATCH 1/2] graphql-java 17.0+ instrumentation --- .../graphql-java-17.0/build.gradle | 31 +++ .../graphql/GraphQLErrorHandler.java | 56 ++++ .../graphql/GraphQLObfuscator.java | 48 ++++ .../graphql/GraphQLOperationDefinition.java | 34 +++ .../graphql/GraphQLSpanUtil.java | 57 ++++ .../graphql/GraphQLTransactionName.java | 150 +++++++++++ .../com/nr/instrumentation/graphql/Utils.java | 14 + .../ExecutionStrategy_Instrumentation.java | 48 ++++ .../java/graphql/GraphQL_Instrumentation.java | 24 ++ .../ParseAndValidate_Instrumentation.java | 51 ++++ .../graphql/GraphQLObfuscatorTest.java | 37 +++ .../graphql/GraphQLSpanUtilTest.java | 83 ++++++ .../graphql/GraphQLTransactionNameTest.java | 34 +++ .../graphql/helper/GraphQLTestHelper.java | 34 +++ .../graphql/helper/PrivateApiStub.java | 99 +++++++ .../graphql/GraphQL_InstrumentationTest.java | 245 ++++++++++++++++++ .../test/resources/distributed_tracing.yml | 3 + .../federatedSubGraphQuery.gql | 7 + .../federatedSubGraphQueryObfuscated.gql | 7 + .../obfuscate-query-test-data.csv | 8 + .../queryMultiLevelAliasArg.gql | 24 ++ .../queryMultiLevelAliasArgObfuscated.gql | 24 ++ .../queryWithNameAndArg.gql | 5 + .../queryWithNameAndArgObfuscated.gql | 5 + .../obfuscateQueryTestData/simpleMutation.gql | 13 + .../simpleMutationObfuscated.gql | 13 + .../unionTypesInlineFragmentsQuery.gql | 11 + ...ionTypesInlineFragmentsQueryObfuscated.gql | 11 + .../deepestUniquePathQuery.gql | 14 + .../deepestUniqueSinglePathQuery.gql | 7 + .../federatedSubGraphQuery.gql | 7 + .../transactionNameTestData/fragments.gql | 16 ++ .../transactionNameTestData/inputTypes.gql | 6 + .../multipleOperations.gql | 18 ++ .../transactionNameTestData/schemaQuery.gql | 7 + .../simpleAnonymousQuery.gql | 10 + .../simpleMutation.gql | 13 + .../transactionNameTestData/simpleQuery.gql | 10 + .../transaction-name-test-data.csv | 17 ++ .../twoTopLevelNames.gql | 8 + .../unionTypesAndInlineFragmentQuery.gql | 8 + .../unionTypesAndInlineFragmentsQuery.gql | 11 + .../validationErrors.gql | 9 + .../variablesInsideFragments.gql | 20 ++ settings.gradle | 1 + 45 files changed, 1358 insertions(+) create mode 100644 instrumentation/graphql-java-17.0/build.gradle create mode 100644 instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLErrorHandler.java create mode 100644 instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLObfuscator.java create mode 100644 instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLOperationDefinition.java create mode 100644 instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLSpanUtil.java create mode 100644 instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLTransactionName.java create mode 100644 instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/Utils.java create mode 100644 instrumentation/graphql-java-17.0/src/main/java/graphql/ExecutionStrategy_Instrumentation.java create mode 100644 instrumentation/graphql-java-17.0/src/main/java/graphql/GraphQL_Instrumentation.java create mode 100644 instrumentation/graphql-java-17.0/src/main/java/graphql/ParseAndValidate_Instrumentation.java create mode 100644 instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/GraphQLObfuscatorTest.java create mode 100644 instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/GraphQLSpanUtilTest.java create mode 100644 instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/GraphQLTransactionNameTest.java create mode 100644 instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/helper/GraphQLTestHelper.java create mode 100644 instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/helper/PrivateApiStub.java create mode 100644 instrumentation/graphql-java-17.0/src/test/java/graphql/GraphQL_InstrumentationTest.java create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/distributed_tracing.yml create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/federatedSubGraphQuery.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/federatedSubGraphQueryObfuscated.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/obfuscate-query-test-data.csv create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryMultiLevelAliasArg.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryMultiLevelAliasArgObfuscated.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryWithNameAndArg.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryWithNameAndArgObfuscated.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/simpleMutation.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/simpleMutationObfuscated.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/unionTypesInlineFragmentsQuery.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/unionTypesInlineFragmentsQueryObfuscated.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/deepestUniquePathQuery.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/deepestUniqueSinglePathQuery.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/federatedSubGraphQuery.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/fragments.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/inputTypes.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/multipleOperations.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/schemaQuery.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/simpleAnonymousQuery.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/simpleMutation.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/simpleQuery.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/transaction-name-test-data.csv create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/twoTopLevelNames.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/unionTypesAndInlineFragmentQuery.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/unionTypesAndInlineFragmentsQuery.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/validationErrors.gql create mode 100644 instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/variablesInsideFragments.gql diff --git a/instrumentation/graphql-java-17.0/build.gradle b/instrumentation/graphql-java-17.0/build.gradle new file mode 100644 index 0000000000..861f2672f5 --- /dev/null +++ b/instrumentation/graphql-java-17.0/build.gradle @@ -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,17.3]' +} + +site { + title 'GraphQL Java' + type 'Framework' +} + +test { + useJUnitPlatform() +} diff --git a/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLErrorHandler.java b/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLErrorHandler.java new file mode 100644 index 0000000000..24cb7c4352 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLErrorHandler.java @@ -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 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 exceptionResult) { + return exceptionResult != null && exceptionResult.isCompletedExceptionally(); + } + + private static void reportExceptionFromCompletedExceptionally(CompletableFuture 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(); + } +} diff --git a/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLObfuscator.java b/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLObfuscator.java new file mode 100644 index 0000000000..cf87b05286 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLObfuscator.java @@ -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; + } +} + + diff --git a/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLOperationDefinition.java b/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLOperationDefinition.java new file mode 100644 index 0000000000..7e8aa048bc --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLOperationDefinition.java @@ -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 = ""; + 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 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; + } +} diff --git a/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLSpanUtil.java b/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLSpanUtil.java new file mode 100644 index 0000000000..2e56c666ee --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLSpanUtil.java @@ -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 = ""; + + 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)); + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLTransactionName.java b/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLTransactionName.java new file mode 100644 index 0000000000..66bbcbccf6 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/GraphQLTransactionName.java @@ -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 + * NewRelic Node Apollo Server Plugin - Transactions + * + *

+ * 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 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 selections = new ArrayList<>(); + while (selection != null) { + selections.add(selection); + selection = nextNonFederatedSelectionChildFrom(selection); + } + return createPathSuffixFrom(selections); + } + + private static String createPathSuffixFrom(final List 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 selections = selectionSet.getSelections(); + if (isNullOrEmpty(selections)) { + return null; + } + List 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)); + } +} diff --git a/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/Utils.java b/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/Utils.java new file mode 100644 index 0000000000..9a1747e054 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/main/java/com/nr/instrumentation/graphql/Utils.java @@ -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 getValueOrDefault(T value, T defaultValue) { + return value == null ? defaultValue : value; + } + + public static boolean isNullOrEmpty(final Collection c) { + return c == null || c.isEmpty(); + } +} diff --git a/instrumentation/graphql-java-17.0/src/main/java/graphql/ExecutionStrategy_Instrumentation.java b/instrumentation/graphql-java-17.0/src/main/java/graphql/ExecutionStrategy_Instrumentation.java new file mode 100644 index 0000000000..78102128cf --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/main/java/graphql/ExecutionStrategy_Instrumentation.java @@ -0,0 +1,48 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package graphql; + +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionStrategyParameters; +import graphql.execution.FieldValueInfo; +import graphql.schema.DataFetchingEnvironment; + +import java.util.concurrent.CompletableFuture; + +import static com.nr.instrumentation.graphql.GraphQLSpanUtil.*; +import static com.nr.instrumentation.graphql.GraphQLErrorHandler.*; + +@Weave(originalName = "graphql.execution.ExecutionStrategy", type = MatchType.BaseClass) +public class ExecutionStrategy_Instrumentation { + + @Trace + protected CompletableFuture resolveFieldWithInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + + NewRelic.getAgent().getTracedMethod().setMetricName("GraphQL/resolve/" + parameters.getPath().getSegmentName()); + setResolverAttributes(parameters); + return Weaver.callOriginal(); + } + + protected CompletableFuture handleFetchingException(ExecutionContext executionContext, DataFetchingEnvironment environment, Throwable e) { + NewRelic.noticeError(e); + return Weaver.callOriginal(); + } + + protected FieldValueInfo completeValue(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + FieldValueInfo result = Weaver.callOriginal(); + if (result != null) { + reportNonNullableExceptionToNR(result); + } + return result; + } +} diff --git a/instrumentation/graphql-java-17.0/src/main/java/graphql/GraphQL_Instrumentation.java b/instrumentation/graphql-java-17.0/src/main/java/graphql/GraphQL_Instrumentation.java new file mode 100644 index 0000000000..374bd487ec --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/main/java/graphql/GraphQL_Instrumentation.java @@ -0,0 +1,24 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package graphql; + +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import java.util.concurrent.CompletableFuture; + +@Weave(originalName = "graphql.GraphQL", type = MatchType.ExactClass) +public class GraphQL_Instrumentation { + + @Trace + public CompletableFuture executeAsync(ExecutionInput executionInput) { + return Weaver.callOriginal(); + } +} diff --git a/instrumentation/graphql-java-17.0/src/main/java/graphql/ParseAndValidate_Instrumentation.java b/instrumentation/graphql-java-17.0/src/main/java/graphql/ParseAndValidate_Instrumentation.java new file mode 100644 index 0000000000..c616132493 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/main/java/graphql/ParseAndValidate_Instrumentation.java @@ -0,0 +1,51 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package graphql; + +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.nr.instrumentation.graphql.GraphQLTransactionName; +import graphql.language.Document; +import graphql.schema.GraphQLSchema; +import graphql.validation.ValidationError; + +import java.util.List; + +import static com.nr.instrumentation.graphql.GraphQLSpanUtil.*; +import static com.nr.instrumentation.graphql.GraphQLErrorHandler.*; + +@Weave(originalName = "graphql.ParseAndValidate", type = MatchType.ExactClass) +public class ParseAndValidate_Instrumentation { + + public static ParseAndValidateResult parse(ExecutionInput executionInput) { + ParseAndValidateResult result = Weaver.callOriginal(); + if (result != null) { + String transactionName = GraphQLTransactionName.from(result.getDocument()); + NewRelic.getAgent().getTracedMethod().setMetricName("GraphQL/operation" + transactionName); + setOperationAttributes(result.getDocument(), executionInput.getQuery()); + + if (result.isFailure()) { + reportGraphQLException(result.getSyntaxException()); + NewRelic.setTransactionName("GraphQL", "*"); + } else { + NewRelic.setTransactionName("GraphQL", transactionName); + } + } + return result; + } + + public static List validate(GraphQLSchema graphQLSchema, Document parsedDocument) { + List errors = Weaver.callOriginal(); + if (errors != null && !errors.isEmpty()) { + reportGraphQLError(errors.get(0)); + } + return errors; + } +} diff --git a/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/GraphQLObfuscatorTest.java b/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/GraphQLObfuscatorTest.java new file mode 100644 index 0000000000..003baf1050 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/GraphQLObfuscatorTest.java @@ -0,0 +1,37 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.graphql; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvFileSource; + +import static com.nr.instrumentation.graphql.helper.GraphQLTestHelper.readText; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GraphQLObfuscatorTest { + + private final static String OBFUSCATE_DATA_DIR = "obfuscateQueryTestData"; + + @ParameterizedTest + @CsvFileSource(resources = "/obfuscateQueryTestData/obfuscate-query-test-data.csv", delimiter = '|', numLinesToSkip = 2) + public void testObfuscateQuery(String queryToObfuscateFilename, String expectedObfuscatedQueryFilename) { + //setup + queryToObfuscateFilename = queryToObfuscateFilename.trim(); + expectedObfuscatedQueryFilename = expectedObfuscatedQueryFilename.trim(); + String expectedObfuscatedResult = readText(OBFUSCATE_DATA_DIR, expectedObfuscatedQueryFilename); + + //given + String query = readText(OBFUSCATE_DATA_DIR, queryToObfuscateFilename); + + //when + String obfuscatedQuery = GraphQLObfuscator.obfuscate(query); + + //then + assertEquals(expectedObfuscatedResult, obfuscatedQuery); + } +} diff --git a/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/GraphQLSpanUtilTest.java b/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/GraphQLSpanUtilTest.java new file mode 100644 index 0000000000..189ce57a43 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/GraphQLSpanUtilTest.java @@ -0,0 +1,83 @@ +/* + * + * * 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 com.newrelic.agent.bridge.PrivateApi; +import com.nr.instrumentation.graphql.helper.GraphQLTestHelper; +import com.nr.instrumentation.graphql.helper.PrivateApiStub; +import graphql.language.Definition; +import graphql.language.Document; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GraphQLSpanUtilTest { + + private static final List NO_DEFINITIONS = Collections.emptyList(); + + private PrivateApiStub privateApiStub; + private PrivateApi privateApi; + + private static Stream providerForTestEdges() { + return Stream.of( + Arguments.of(null, null, "Unavailable", "", ""), + Arguments.of(null, "{ hello }", "Unavailable", "", "{ hello }"), + Arguments.of(new Document(NO_DEFINITIONS), "", "Unavailable", "", ""), + Arguments.of(new Document(NO_DEFINITIONS), null, "Unavailable", "", "") + ); + } + + @BeforeEach + public void beforeEachTest() { + privateApi = AgentBridge.privateApi; + privateApiStub = new PrivateApiStub(); + AgentBridge.privateApi = privateApiStub; + } + + @AfterEach + public void afterEachTest() { + AgentBridge.privateApi = privateApi; + } + + @ParameterizedTest + @CsvSource(value = { + "query simple { libraries },QUERY,simple", + "query { libraries },QUERY,", + "{ hello },QUERY,", + "mutation { data },MUTATION,", + "mutation bob { data },MUTATION,bob" + }) + public void testSetOperationAttributes(String query, String expectedType, String expectedName) { + Document document = GraphQLTestHelper.parseDocumentFromText(query); + GraphQLSpanUtil.setOperationAttributes(document, query); + + assertEquals(expectedType, privateApiStub.getTracerParameterFor("graphql.operation.type")); + assertEquals(expectedName, privateApiStub.getTracerParameterFor("graphql.operation.name")); + assertEquals(query, privateApiStub.getTracerParameterFor("graphql.operation.query")); + } + + @ParameterizedTest + @MethodSource("providerForTestEdges") + public void testSetOperationAttributesEdgeCases(Document document, String query, String expectedType, String expectedName, String expectedQuery) { + GraphQLSpanUtil.setOperationAttributes(document, query); + + assertEquals(expectedType, privateApiStub.getTracerParameterFor("graphql.operation.type")); + assertEquals(expectedName, privateApiStub.getTracerParameterFor("graphql.operation.name")); + assertEquals(expectedQuery, privateApiStub.getTracerParameterFor("graphql.operation.query")); + } +} diff --git a/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/GraphQLTransactionNameTest.java b/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/GraphQLTransactionNameTest.java new file mode 100644 index 0000000000..ba130cc1a2 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/GraphQLTransactionNameTest.java @@ -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 org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvFileSource; + +import static com.nr.instrumentation.graphql.helper.GraphQLTestHelper.parseDocument; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GraphQLTransactionNameTest { + + private final static String TEST_DATA_DIR = "transactionNameTestData"; + + @ParameterizedTest + @CsvFileSource(resources = "/transactionNameTestData/transaction-name-test-data.csv", delimiter = '|', numLinesToSkip = 2) + public void testQuery(String testFileName, String expectedTransactionName) { + //setup + testFileName = testFileName.trim(); + expectedTransactionName = expectedTransactionName.trim(); + //given + Document document = parseDocument(TEST_DATA_DIR, testFileName); + //when + String transactionName = GraphQLTransactionName.from(document); + //then + assertEquals(expectedTransactionName, transactionName); + } +} diff --git a/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/helper/GraphQLTestHelper.java b/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/helper/GraphQLTestHelper.java new file mode 100644 index 0000000000..9da9fdeed7 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/helper/GraphQLTestHelper.java @@ -0,0 +1,34 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.graphql.helper; + +import graphql.language.Document; +import graphql.parser.Parser; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class GraphQLTestHelper { + public static Document parseDocument(String testDir, String filename) { + return Parser.parse(readText(testDir, filename)); + } + + public static String readText(String testDir, String filename) { + try { + String projectPath = String.format("src/test/resources/%s/%s.gql", testDir, filename); + return new String(Files.readAllBytes(Paths.get(projectPath))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Document parseDocumentFromText(String text) { + return Parser.parse(text); + } +} diff --git a/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/helper/PrivateApiStub.java b/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/helper/PrivateApiStub.java new file mode 100644 index 0000000000..971076d338 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/java/com/nr/instrumentation/graphql/helper/PrivateApiStub.java @@ -0,0 +1,99 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.graphql.helper; + +import com.newrelic.agent.bridge.PrivateApi; + +import javax.management.MBeanServer; +import java.io.Closeable; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class PrivateApiStub implements PrivateApi { + private final Map tracerParameters = new HashMap<>(); + + public String getTracerParameterFor(String key) { + return tracerParameters.get(key); + } + + @Override + public Closeable addSampler(Runnable sampler, int period, TimeUnit timeUnit) { + return null; + } + + @Override + public void setServerInfo(String serverInfo) { + + } + + @Override + public void addCustomAttribute(String key, Number value) { + + } + + @Override + public void addCustomAttribute(String key, Map values) { + + } + + @Override + public void addCustomAttribute(String key, String value) { + + } + + @Override + public void addTracerParameter(String key, Number value) { + + } + + @Override + public void addTracerParameter(String key, String value) { + tracerParameters.put(key, value); + } + + @Override + public void addTracerParameter(String key, Map values) { + + } + + @Override + public void addMBeanServer(MBeanServer server) { + + } + + @Override + public void removeMBeanServer(MBeanServer serverToRemove) { + + } + + @Override + public void reportHTTPError(String message, int statusCode, String uri) { + + } + + @Override + public void reportException(Throwable throwable) { + + } + + @Override + public void setAppServerPort(int port) { + + } + + @Override + public void setServerInfo(String dispatcherName, String version) { + + } + + @Override + public void setInstanceName(String instanceName) { + + } +} diff --git a/instrumentation/graphql-java-17.0/src/test/java/graphql/GraphQL_InstrumentationTest.java b/instrumentation/graphql-java-17.0/src/test/java/graphql/GraphQL_InstrumentationTest.java new file mode 100644 index 0000000000..c98884a8b8 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/java/graphql/GraphQL_InstrumentationTest.java @@ -0,0 +1,245 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package graphql; + +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.SpanEvent; +import com.newrelic.api.agent.Trace; +import graphql.schema.GraphQLSchema; +import graphql.schema.StaticDataFetcher; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.runner.RunWith; + +import java.util.List; +import java.util.stream.Collectors; + +import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring; +import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = {"graphql", "com.nr.instrumentation"}, configName = "distributed_tracing.yml") +public class GraphQL_InstrumentationTest { + private static final long DEFAULT_TIMEOUT_IN_MILLIS = 10_000; + private static final String TEST_ARG = "testArg"; + + private static GraphQL graphQL; + + @BeforeClass + public static void initialize() { + String schema = "type Query{hello(" + TEST_ARG + ": String): String}"; + + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); + + RuntimeWiring runtimeWiring = newRuntimeWiring() + .type("Query", builder -> builder.dataFetcher("hello", + new StaticDataFetcher("world"))) + .build(); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + + graphQL = GraphQL.newGraphQL(graphQLSchema).build(); + } + + @AfterEach + public void cleanUp() { + InstrumentationTestRunner.getIntrospector().clear(); + } + + @Test + public void queryWithNoArg() { + //given + String query = "{hello}"; + //when + trace(createRunnable(query)); + //then + assertRequestNoArg("QUERY//hello", "{hello}"); + } + + @Test + public void queryWithArg() { + //given + String query = "{hello (" + TEST_ARG + ": \"fo)o\")}"; + //when + trace(createRunnable(query)); + //then + assertRequestWithArg("QUERY//hello", "{hello (" + TEST_ARG + ": ***)}"); + } + + @Test + public void parsingException() { + //given + String query = "cause a parse error"; + //when + trace(createRunnable(query)); + //then + String expectedErrorMessage = "Invalid Syntax : offending token 'cause' at line 1 column 1"; + assertErrorOperation("*", "GraphQL/operation", + "graphql.parser.InvalidSyntaxException", expectedErrorMessage, true); + } + + @Test + public void validationException() { + //given + String query = "{noSuchField}"; + //when + trace(createRunnable(query)); + //then + String expectedErrorMessage = "Validation error of type FieldUndefined: Field 'noSuchField' in type 'Query' is undefined @ 'noSuchField'"; + assertErrorOperation("QUERY//noSuchField", + "GraphQL/operation/QUERY//noSuchField", "graphql.GraphqlErrorException", expectedErrorMessage, false); + } + + @Test + public void resolverException() { + //given + String query = "{hello " + + "\n" + + "bye}"; + + //when + trace(createRunnable(query, graphWithResolverException())); + //then + assertExceptionOnSpan("QUERY/", "GraphQL/resolve/hello", "java.lang.RuntimeException", false); + assertExceptionOnSpan("QUERY/", "GraphQL/resolve/bye", "graphql.execution.NonNullableFieldWasNullException", false); + } + + @Trace(dispatcher = true) + private void trace(Runnable runnable) { + runnable.run(); + } + + private Runnable createRunnable(final String query) { + return () -> graphQL.execute(query); + } + + private Runnable createRunnable(final String query, GraphQL graphql) { + return () -> graphql.execute(query); + } + + private GraphQL graphWithResolverException() { + String schema = "type Query{hello(" + TEST_ARG + ": String): String" + + "\n" + + "bye: String!}"; + + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); + + RuntimeWiring runtimeWiring = newRuntimeWiring() + .type(newTypeWiring("Query") + .dataFetcher("hello", environment -> { + throw new RuntimeException("waggle"); + }) + .dataFetcher("bye", environment -> null) + ) + .build(); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + + return GraphQL.newGraphQL(graphQLSchema).build(); + } + + private void txFinishedWithExpectedName(Introspector introspector, String expectedTransactionSuffix, boolean isParseError) { + assertEquals(1, introspector.getFinishedTransactionCount(DEFAULT_TIMEOUT_IN_MILLIS)); + String txName = introspector.getTransactionNames().iterator().next(); + assertEquals("Transaction name is incorrect", + "OtherTransaction/GraphQL/" + expectedTransactionSuffix, txName); + } + + private void attributeValueOnSpan(Introspector introspector, String spanName, String attribute, String value) { + List spanEvents = introspector.getSpanEvents().stream() + .filter(spanEvent -> spanEvent.getName().contains(spanName)) + .collect(Collectors.toList()); + Assert.assertEquals(1, spanEvents.size()); + Assert.assertNotNull(spanEvents.get(0).getAgentAttributes().get(attribute)); + Assert.assertEquals(value, spanEvents.get(0).getAgentAttributes().get(attribute)); + } + + private boolean scopedAndUnscopedMetrics(Introspector introspector, String metricPrefix) { + boolean scoped = introspector.getMetricsForTransaction(introspector.getTransactionNames().iterator().next()) + .keySet().stream().anyMatch(s -> s.contains(metricPrefix)); + boolean unscoped = introspector.getUnscopedMetrics().keySet().stream().anyMatch(s -> s.contains(metricPrefix)); + return scoped && unscoped; + } + + private void expectedMetrics(Introspector introspector) { + assertTrue(scopedAndUnscopedMetrics(introspector, "GraphQL/operation/")); + assertTrue(scopedAndUnscopedMetrics(introspector, "GraphQL/resolve/")); + } + + private void agentAttributeNotOnOtherSpans(Introspector introspector, String spanName, String attributeCategory) { + assertFalse(introspector.getSpanEvents().stream() + .filter(spanEvent -> !spanEvent.getName().contains(spanName)) + .anyMatch(spanEvent -> spanEvent.getAgentAttributes().keySet().stream().anyMatch(key -> key.contains(attributeCategory))) + ); + } + + private void resolverAttributesOnCorrectSpan(Introspector introspector) { + attributeValueOnSpan(introspector, "GraphQL/resolve", "graphql.field.parentType", "Query"); + attributeValueOnSpan(introspector, "GraphQL/resolve", "graphql.field.name", "hello"); + attributeValueOnSpan(introspector, "GraphQL/resolve", "graphql.field.path", "hello"); + agentAttributeNotOnOtherSpans(introspector, "GraphQL/resolve", "graphql.field"); + } + + private void errorAttributesOnCorrectSpan(Introspector introspector, String spanName, String errorClass, String errorMessage) { + attributeValueOnSpan(introspector, spanName, "error.class", errorClass); + attributeValueOnSpan(introspector, spanName, "error.message", errorMessage); + agentAttributeNotOnOtherSpans(introspector, spanName, "error.class"); + agentAttributeNotOnOtherSpans(introspector, spanName, "error.message"); + } + + private void operationAttributesOnCorrectSpan(Introspector introspector, String spanName) { + attributeValueOnSpan(introspector, spanName, "graphql.operation.name", ""); + attributeValueOnSpan(introspector, spanName, "graphql.operation.type", "QUERY"); + agentAttributeNotOnOtherSpans(introspector, "GraphQL/operation", "graphql.operation"); + } + + private void assertRequestNoArg(String expectedTransactionSuffix, String expectedQueryAttribute) { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + txFinishedWithExpectedName(introspector, expectedTransactionSuffix, false); + attributeValueOnSpan(introspector, expectedTransactionSuffix, "graphql.operation.query", expectedQueryAttribute); + operationAttributesOnCorrectSpan(introspector, expectedTransactionSuffix); + resolverAttributesOnCorrectSpan(introspector); + expectedMetrics(introspector); + } + + private void assertRequestWithArg(String expectedTransactionSuffix, String expectedQueryAttribute) { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + txFinishedWithExpectedName(introspector, expectedTransactionSuffix, false); + attributeValueOnSpan(introspector, expectedTransactionSuffix, "graphql.operation.query", expectedQueryAttribute); + operationAttributesOnCorrectSpan(introspector, expectedTransactionSuffix); + resolverAttributesOnCorrectSpan(introspector); + expectedMetrics(introspector); + } + + private void assertErrorOperation(String expectedTransactionSuffix, String spanName, String errorClass, String errorMessage, boolean isParseError) { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + txFinishedWithExpectedName(introspector, expectedTransactionSuffix, isParseError); + errorAttributesOnCorrectSpan(introspector, spanName, errorClass, errorMessage); + } + + private void assertExceptionOnSpan(String expectedTransactionSuffix, String spanName, String errorClass, boolean isParseError) { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + txFinishedWithExpectedName(introspector, expectedTransactionSuffix, isParseError); + attributeValueOnSpan(introspector, spanName, "error.class", errorClass); + } +} diff --git a/instrumentation/graphql-java-17.0/src/test/resources/distributed_tracing.yml b/instrumentation/graphql-java-17.0/src/test/resources/distributed_tracing.yml new file mode 100644 index 0000000000..acdf6d1a3d --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/distributed_tracing.yml @@ -0,0 +1,3 @@ +common: &default_settings + distributed_tracing: + enabled: true \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/federatedSubGraphQuery.gql b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/federatedSubGraphQuery.gql new file mode 100644 index 0000000000..efabfdd597 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/federatedSubGraphQuery.gql @@ -0,0 +1,7 @@ +query { + libraries { + branch + __typename + id + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/federatedSubGraphQueryObfuscated.gql b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/federatedSubGraphQueryObfuscated.gql new file mode 100644 index 0000000000..efabfdd597 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/federatedSubGraphQueryObfuscated.gql @@ -0,0 +1,7 @@ +query { + libraries { + branch + __typename + id + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/obfuscate-query-test-data.csv b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/obfuscate-query-test-data.csv new file mode 100644 index 0000000000..2daa6bf087 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/obfuscate-query-test-data.csv @@ -0,0 +1,8 @@ +GraphQL query filename | Expected obfuscated query file name +--------------------------------------------------------------------------------------------------- +queryWithNameAndArg | queryWithNameAndArgObfuscated +queryMultiLevelAliasArg | queryMultiLevelAliasArgObfuscated +simpleMutation | simpleMutationObfuscated +federatedSubGraphQuery | federatedSubGraphQueryObfuscated +unionTypesInlineFragmentsQuery | unionTypesInlineFragmentsQueryObfuscated + diff --git a/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryMultiLevelAliasArg.gql b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryMultiLevelAliasArg.gql new file mode 100644 index 0000000000..8ef85c82e6 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryMultiLevelAliasArg.gql @@ -0,0 +1,24 @@ +query { + FIRST: libraries (id: 123, name: "me bro") { + branch + booksInStock (password: "hide me") { + title (id: 123), + author + } + bathroomReading: magazinesInStock (password: "hide me") { + magissue, + magtitle + } + } + SECOND: Slibraries (id: 456, name: "no bro") { + Sbranch + profitCenter: SbooksInStock (password: "hide me") { + Sisbn, + Stitle, + } + SmagazinesInStock (password: "hide me") { + Smagissue, + Smagtitle + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryMultiLevelAliasArgObfuscated.gql b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryMultiLevelAliasArgObfuscated.gql new file mode 100644 index 0000000000..362ca346e3 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryMultiLevelAliasArgObfuscated.gql @@ -0,0 +1,24 @@ +query { + FIRST: libraries (id: ***, name: ***) { + branch + booksInStock (password: ***) { + title (id: ***), + author + } + bathroomReading: magazinesInStock (password: ***) { + magissue, + magtitle + } + } + SECOND: Slibraries (id: ***, name: ***) { + Sbranch + profitCenter: SbooksInStock (password: ***) { + Sisbn, + Stitle, + } + SmagazinesInStock (password: ***) { + Smagissue, + Smagtitle + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryWithNameAndArg.gql b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryWithNameAndArg.gql new file mode 100644 index 0000000000..6c8558d443 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryWithNameAndArg.gql @@ -0,0 +1,5 @@ +query fastAndFun { + bookById (id: "book-1") { + title + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryWithNameAndArgObfuscated.gql b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryWithNameAndArgObfuscated.gql new file mode 100644 index 0000000000..cb7c97d674 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/queryWithNameAndArgObfuscated.gql @@ -0,0 +1,5 @@ +query fastAndFun { + bookById (id: ***) { + title + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/simpleMutation.gql b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/simpleMutation.gql new file mode 100644 index 0000000000..aba5da9945 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/simpleMutation.gql @@ -0,0 +1,13 @@ +mutation { + writePost(title: "New Post2", text: "Text", category: null, author: "Author2") { + id + title + category + text + author { + id + name + thumbnail + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/simpleMutationObfuscated.gql b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/simpleMutationObfuscated.gql new file mode 100644 index 0000000000..a6c316a8d9 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/simpleMutationObfuscated.gql @@ -0,0 +1,13 @@ +mutation { + writePost(title: ***, text: ***, category: ***, author: ***) { + id + title + category + text + author { + id + name + thumbnail + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/unionTypesInlineFragmentsQuery.gql b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/unionTypesInlineFragmentsQuery.gql new file mode 100644 index 0000000000..d61192b0be --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/unionTypesInlineFragmentsQuery.gql @@ -0,0 +1,11 @@ +query example { + search(contains: "author") { + __typename + ... on Author { + name + } + ... on Book { + title + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/unionTypesInlineFragmentsQueryObfuscated.gql b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/unionTypesInlineFragmentsQueryObfuscated.gql new file mode 100644 index 0000000000..baf1f4d47d --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/obfuscateQueryTestData/unionTypesInlineFragmentsQueryObfuscated.gql @@ -0,0 +1,11 @@ +query example { + search(contains: ***) { + __typename + ... on Author { + name + } + ... on Book { + title + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/deepestUniquePathQuery.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/deepestUniquePathQuery.gql new file mode 100644 index 0000000000..00b53e9646 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/deepestUniquePathQuery.gql @@ -0,0 +1,14 @@ +query { + libraries { + branch + booksInStock { + isbn, + title, + author + } + magazinesInStock { + issue, + title + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/deepestUniqueSinglePathQuery.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/deepestUniqueSinglePathQuery.gql new file mode 100644 index 0000000000..a00e672829 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/deepestUniqueSinglePathQuery.gql @@ -0,0 +1,7 @@ +query { + libraries { + booksInStock { + title + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/federatedSubGraphQuery.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/federatedSubGraphQuery.gql new file mode 100644 index 0000000000..efabfdd597 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/federatedSubGraphQuery.gql @@ -0,0 +1,7 @@ +query { + libraries { + branch + __typename + id + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/fragments.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/fragments.gql new file mode 100644 index 0000000000..33a22547df --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/fragments.gql @@ -0,0 +1,16 @@ +{ + leftComparison: hero(episode: EMPIRE) { + ...comparisonFields + } + rightComparison: hero(episode: JEDI) { + ...comparisonFields + } +} + +fragment comparisonFields on Character { + name + appearsIn + friends { + name + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/inputTypes.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/inputTypes.gql new file mode 100644 index 0000000000..cf73d7a8ec --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/inputTypes.gql @@ -0,0 +1,6 @@ +mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { + createReview(episode: $ep, review: $review) { + stars + commentary + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/multipleOperations.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/multipleOperations.gql new file mode 100644 index 0000000000..3b49a13cdc --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/multipleOperations.gql @@ -0,0 +1,18 @@ +query getTaskAndUser { + getTask(id: "0x3") { + id + title + completed + } + queryUser(filter: {username: {eq: "dgraphlabs"}}) { + username + name + } +} + +query completedTasks { + queryTask(filter: {completed: true}) { + title + completed + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/schemaQuery.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/schemaQuery.gql new file mode 100644 index 0000000000..312009114d --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/schemaQuery.gql @@ -0,0 +1,7 @@ +{ + __schema { + types { + name + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/simpleAnonymousQuery.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/simpleAnonymousQuery.gql new file mode 100644 index 0000000000..e027f659c0 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/simpleAnonymousQuery.gql @@ -0,0 +1,10 @@ +query { + libraries { + books { + title + author { + name + } + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/simpleMutation.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/simpleMutation.gql new file mode 100644 index 0000000000..aba5da9945 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/simpleMutation.gql @@ -0,0 +1,13 @@ +mutation { + writePost(title: "New Post2", text: "Text", category: null, author: "Author2") { + id + title + category + text + author { + id + name + thumbnail + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/simpleQuery.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/simpleQuery.gql new file mode 100644 index 0000000000..214b292899 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/simpleQuery.gql @@ -0,0 +1,10 @@ +query simple { + libraries { + books { + title + author { + name + } + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/transaction-name-test-data.csv b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/transaction-name-test-data.csv new file mode 100644 index 0000000000..1cdb07e7c7 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/transaction-name-test-data.csv @@ -0,0 +1,17 @@ +GraphQL query filename | Expected transaction name +--------------------------------------------------------------------------------------------------- +simpleQuery | /QUERY/simple/libraries.books +simpleAnonymousQuery | /QUERY//libraries.books +deepestUniquePathQuery | /QUERY//libraries +deepestUniqueSinglePathQuery | /QUERY//libraries.booksInStock.title +federatedSubGraphQuery | /QUERY//libraries.branch +unionTypesAndInlineFragmentsQuery | /QUERY/example/search +validationErrors | /QUERY/GetBooksByLibrary/libraries.books.doesnotexist.name +unionTypesAndInlineFragmentQuery | /QUERY/example/search.name +simpleMutation | /MUTATION//writePost +twoTopLevelNames | /QUERY/ +fragments | /QUERY/ +variablesInsideFragments | /QUERY/HeroComparison +inputTypes | /MUTATION/CreateReviewForEpisode/createReview +schemaQuery | /QUERY//__schema.types.name +multipleOperations | /batch/QUERY/getTaskAndUser/QUERY/completedTasks/queryTask diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/twoTopLevelNames.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/twoTopLevelNames.gql new file mode 100644 index 0000000000..286925129b --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/twoTopLevelNames.gql @@ -0,0 +1,8 @@ +query { + libraries { + branch + } + gyms { + branch + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/unionTypesAndInlineFragmentQuery.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/unionTypesAndInlineFragmentQuery.gql new file mode 100644 index 0000000000..029d987c55 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/unionTypesAndInlineFragmentQuery.gql @@ -0,0 +1,8 @@ +query example { + search(contains: "author") { + __typename + ... on Author { + name + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/unionTypesAndInlineFragmentsQuery.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/unionTypesAndInlineFragmentsQuery.gql new file mode 100644 index 0000000000..d61192b0be --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/unionTypesAndInlineFragmentsQuery.gql @@ -0,0 +1,11 @@ +query example { + search(contains: "author") { + __typename + ... on Author { + name + } + ... on Book { + title + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/validationErrors.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/validationErrors.gql new file mode 100644 index 0000000000..8928d56ddc --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/validationErrors.gql @@ -0,0 +1,9 @@ +query GetBooksByLibrary { + libraries { + books { + doesnotexist { + name + } + } + } +} \ No newline at end of file diff --git a/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/variablesInsideFragments.gql b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/variablesInsideFragments.gql new file mode 100644 index 0000000000..17949313a0 --- /dev/null +++ b/instrumentation/graphql-java-17.0/src/test/resources/transactionNameTestData/variablesInsideFragments.gql @@ -0,0 +1,20 @@ +query HeroComparison($first: Int = 3) { + leftComparison: hero(episode: EMPIRE) { + ...comparisonFields + } + rightComparison: hero(episode: JEDI) { + ...comparisonFields + } +} + +fragment comparisonFields on Character { + name + friendsConnection(first: $first) { + totalCount + edges { + node { + name + } + } + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index e273321659..84b95d43d1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -81,6 +81,7 @@ include 'instrumentation:grails-1.3' include 'instrumentation:grails-2' include 'instrumentation:grails-async-2.3' include 'instrumentation:graphql-java-16.2' +include 'instrumentation:graphql-java-17.0' include 'instrumentation:grpc-1.4.0' include 'instrumentation:grpc-1.22.0' include 'instrumentation:grpc-1.30.0' From db5264f80983d381b19df8d8f1dfcac7057557fc Mon Sep 17 00:00:00 2001 From: xxia Date: Fri, 15 Oct 2021 08:52:47 -0700 Subject: [PATCH 2/2] increase verifier range --- instrumentation/graphql-java-17.0/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/graphql-java-17.0/build.gradle b/instrumentation/graphql-java-17.0/build.gradle index 861f2672f5..666a6a23b7 100644 --- a/instrumentation/graphql-java-17.0/build.gradle +++ b/instrumentation/graphql-java-17.0/build.gradle @@ -18,7 +18,7 @@ jar { } verifyInstrumentation { - passes 'com.graphql-java:graphql-java:[17.0,17.3]' + passes 'com.graphql-java:graphql-java:[17.0,18.0)' } site {