diff --git a/instrumentation/graphql-java-21.0/build.gradle b/instrumentation/graphql-java-21.0/build.gradle new file mode 100644 index 0000000000..87e4fff5cf --- /dev/null +++ b/instrumentation/graphql-java-21.0/build.gradle @@ -0,0 +1,43 @@ +dependencies { + implementation(project(":agent-bridge")) + + implementation 'com.graphql-java:graphql-java:21.0' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.2' + testImplementation 'org.mockito:mockito-core:4.6.1' + testImplementation 'org.mockito:mockito-junit-jupiter:4.6.1' + + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2' + testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.7.2'} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.graphql-java-21.0' } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } +} + +verifyInstrumentation { + passesOnly 'com.graphql-java:graphql-java:[21.0,)' + excludeRegex 'com.graphql-java:graphql-java:(0.0.0|201|202).*' + excludeRegex 'com.graphql-java:graphql-java:.*(vTEST|-beta|-alpha1|-nf-execution|-rc|-TEST).*' +} + +site { + title 'GraphQL Java' + type 'Framework' +} + +test { + useJUnitPlatform() + // These instrumentation tests only run on Java 11+ regardless of the -PtestN gradle property that is set. + onlyIf { + !project.hasProperty('test8') + } + +} + diff --git a/instrumentation/graphql-java-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLErrorHandler.java b/instrumentation/graphql-java-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLErrorHandler.java new file mode 100644 index 0000000000..d742edafbd --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLErrorHandler.java @@ -0,0 +1,56 @@ +/* + * + * * Copyright 2023 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-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLObfuscator.java b/instrumentation/graphql-java-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLObfuscator.java new file mode 100644 index 0000000000..2427c00c51 --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLObfuscator.java @@ -0,0 +1,48 @@ +/* + * + * * Copyright 2023 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-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLOperationDefinition.java b/instrumentation/graphql-java-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLOperationDefinition.java new file mode 100644 index 0000000000..7eac4fec72 --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLOperationDefinition.java @@ -0,0 +1,34 @@ +/* + * + * * Copyright 2023 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-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLSpanUtil.java b/instrumentation/graphql-java-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLSpanUtil.java new file mode 100644 index 0000000000..bbd91d9884 --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLSpanUtil.java @@ -0,0 +1,67 @@ +/* + * + * * Copyright 2023 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.GraphQLNamedSchemaElement; +import graphql.schema.GraphQLOutputType; + +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()); + AgentBridge.privateApi.addTracerParameter("graphql.field.name", parameters.getField().getName()); + // ExecutionStepInfo is not nullable according to documentation + GraphQLOutputType graphQLOutputType = parameters.getExecutionStepInfo().getType(); + setGraphQLFieldParentTypeIfPossible(graphQLOutputType); + } + + private static void setGraphQLFieldParentTypeIfPossible(GraphQLOutputType graphQLOutputType) { + // graphql.field.parentType is NOT required according to the Agent Spec + if (graphQLOutputType instanceof GraphQLNamedSchemaElement) { + GraphQLNamedSchemaElement named = (GraphQLNamedSchemaElement) graphQLOutputType; + AgentBridge.privateApi.addTracerParameter("graphql.field.parentType", named.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-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLTransactionName.java b/instrumentation/graphql-java-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLTransactionName.java new file mode 100644 index 0000000000..c5152db5e5 --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/main/java/com/nr/instrumentation/graphql/GraphQLTransactionName.java @@ -0,0 +1,165 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.graphql; + +import graphql.language.Document; +import graphql.language.Field; +import graphql.language.InlineFragment; +import graphql.language.OperationDefinition; +import graphql.language.Selection; +import graphql.language.SelectionSet; +import graphql.language.SelectionSetContainer; +import graphql.language.TypeName; + +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-21.0/src/main/java/com/nr/instrumentation/graphql/Utils.java b/instrumentation/graphql-java-21.0/src/main/java/com/nr/instrumentation/graphql/Utils.java new file mode 100644 index 0000000000..53baafd95f --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/main/java/com/nr/instrumentation/graphql/Utils.java @@ -0,0 +1,21 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +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-21.0/src/main/java/graphql/ExecutionStrategy_Instrumentation.java b/instrumentation/graphql-java-21.0/src/main/java/graphql/ExecutionStrategy_Instrumentation.java new file mode 100644 index 0000000000..c032033ae4 --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/main/java/graphql/ExecutionStrategy_Instrumentation.java @@ -0,0 +1,48 @@ +/* + * + * * Copyright 2023 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.GraphQLErrorHandler.reportNonNullableExceptionToNR; +import static com.nr.instrumentation.graphql.GraphQLSpanUtil.setResolverAttributes; + +@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(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-21.0/src/main/java/graphql/GraphQL_Instrumentation.java b/instrumentation/graphql-java-21.0/src/main/java/graphql/GraphQL_Instrumentation.java new file mode 100644 index 0000000000..8d0122b5bd --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/main/java/graphql/GraphQL_Instrumentation.java @@ -0,0 +1,24 @@ +/* + * + * * Copyright 2023 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-21.0/src/main/java/graphql/ParseAndValidate_Instrumentation.java b/instrumentation/graphql-java-21.0/src/main/java/graphql/ParseAndValidate_Instrumentation.java new file mode 100644 index 0000000000..a19bc1cd93 --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/main/java/graphql/ParseAndValidate_Instrumentation.java @@ -0,0 +1,62 @@ +/* + * + * * Copyright 2023 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 java.util.Locale; +import java.util.function.Predicate; + +import static com.nr.instrumentation.graphql.GraphQLErrorHandler.reportGraphQLError; +import static com.nr.instrumentation.graphql.GraphQLErrorHandler.reportGraphQLException; +import static com.nr.instrumentation.graphql.GraphQLSpanUtil.setOperationAttributes; + +@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, Predicate> rulePredicate, Locale locale) { + List errors = Weaver.callOriginal(); + if (errors != null && !errors.isEmpty()) { + reportGraphQLError(errors.get(0)); + } + return errors; + } + + public static List validate(GraphQLSchema graphQLSchema, Document parsedDocument, Predicate> rulePredicate) { + List errors = Weaver.callOriginal(); + if (errors != null && !errors.isEmpty()) { + reportGraphQLError(errors.get(0)); + } + return errors; + } +} diff --git a/instrumentation/graphql-java-21.0/src/test/java/com/nr/instrumentation/graphql/GraphQLObfuscatorTest.java b/instrumentation/graphql-java-21.0/src/test/java/com/nr/instrumentation/graphql/GraphQLObfuscatorTest.java new file mode 100644 index 0000000000..da5a060476 --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/test/java/com/nr/instrumentation/graphql/GraphQLObfuscatorTest.java @@ -0,0 +1,40 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.graphql; + +import com.newrelic.test.marker.Java8IncompatibleTest; +import org.junit.experimental.categories.Category; +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; + +@Category({ Java8IncompatibleTest.class }) +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-21.0/src/test/java/com/nr/instrumentation/graphql/GraphQLSpanUtilTest.java b/instrumentation/graphql-java-21.0/src/test/java/com/nr/instrumentation/graphql/GraphQLSpanUtilTest.java new file mode 100644 index 0000000000..56cd588770 --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/test/java/com/nr/instrumentation/graphql/GraphQLSpanUtilTest.java @@ -0,0 +1,134 @@ +/* + * + * * Copyright 2023 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.newrelic.test.marker.Java8IncompatibleTest; +import com.nr.instrumentation.graphql.helper.GraphQLTestHelper; +import com.nr.instrumentation.graphql.helper.PrivateApiStub; +import graphql.execution.ExecutionStepInfo; +import graphql.execution.ExecutionStrategyParameters; +import graphql.execution.MergedField; +import graphql.execution.ResultPath; +import graphql.language.Definition; +import graphql.language.Document; +import graphql.schema.GraphQLNonNull; +import org.junit.experimental.categories.Category; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Category({ Java8IncompatibleTest.class }) +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")); + } + + // This test verifies https://issues.newrelic.com/browse/NEWRELIC-4936 no longer exists + @Test + public void testInvalidClassExceptionFix() { + // given execution parameters with execution step info type that is NOT GraphQLNamedSchemaElement + String expectedFieldPath = "fieldPath"; + String expectedFieldName = "fieldName"; + ExecutionStrategyParameters parameters = + mockExecutionStrategyParametersForInvalidClassCastExceptionTest(expectedFieldPath, expectedFieldName); + + // when (this had ClassCastException) + GraphQLSpanUtil.setResolverAttributes(parameters); + + // then tracer parameters set correctly without exception + assertEquals(expectedFieldPath, privateApiStub.getTracerParameterFor("graphql.field.path")); + assertEquals(expectedFieldName, privateApiStub.getTracerParameterFor("graphql.field.name")); + + // and 'graphql.field.parentType' is not set + assertNull(privateApiStub.getTracerParameterFor("graphql.field.parentType"), + "'graphql.field.parentType' is not required and should be null here"); + } + + private static ExecutionStrategyParameters mockExecutionStrategyParametersForInvalidClassCastExceptionTest(String graphqlFieldPath, + String graphqlFieldName) { + ExecutionStrategyParameters parameters = mock(ExecutionStrategyParameters.class); + ResultPath resultPath = mock(ResultPath.class); + MergedField mergedField = mock(MergedField.class); + ExecutionStepInfo executionStepInfo = mock(ExecutionStepInfo.class); + GraphQLNonNull notGraphQLNamedSchemaElement = mock(GraphQLNonNull.class); + + when(resultPath.getSegmentName()).thenReturn(graphqlFieldPath); + when(mergedField.getName()).thenReturn(graphqlFieldName); + when(parameters.getPath()).thenReturn(resultPath); + when(parameters.getField()).thenReturn(mergedField); + when(parameters.getExecutionStepInfo()).thenReturn(executionStepInfo); + when(executionStepInfo.getType()).thenReturn(notGraphQLNamedSchemaElement); + + return parameters; + } +} diff --git a/instrumentation/graphql-java-21.0/src/test/java/com/nr/instrumentation/graphql/GraphQLTransactionNameTest.java b/instrumentation/graphql-java-21.0/src/test/java/com/nr/instrumentation/graphql/GraphQLTransactionNameTest.java new file mode 100644 index 0000000000..2080fe3671 --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/test/java/com/nr/instrumentation/graphql/GraphQLTransactionNameTest.java @@ -0,0 +1,37 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.graphql; + +import com.newrelic.test.marker.Java8IncompatibleTest; +import graphql.language.Document; +import org.junit.experimental.categories.Category; +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; + +@Category({ Java8IncompatibleTest.class }) +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-21.0/src/test/java/com/nr/instrumentation/graphql/helper/GraphQLTestHelper.java b/instrumentation/graphql-java-21.0/src/test/java/com/nr/instrumentation/graphql/helper/GraphQLTestHelper.java new file mode 100644 index 0000000000..78a17f663e --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/test/java/com/nr/instrumentation/graphql/helper/GraphQLTestHelper.java @@ -0,0 +1,37 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.graphql.helper; + +import com.newrelic.test.marker.Java8IncompatibleTest; +import graphql.language.Document; +import graphql.parser.Parser; +import org.junit.experimental.categories.Category; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +@Category({ Java8IncompatibleTest.class }) +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-21.0/src/test/java/com/nr/instrumentation/graphql/helper/PrivateApiStub.java b/instrumentation/graphql-java-21.0/src/test/java/com/nr/instrumentation/graphql/helper/PrivateApiStub.java new file mode 100644 index 0000000000..f5ed2da690 --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/test/java/com/nr/instrumentation/graphql/helper/PrivateApiStub.java @@ -0,0 +1,102 @@ +/* + * + * * Copyright 2023 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 com.newrelic.test.marker.Java8IncompatibleTest; +import org.junit.experimental.categories.Category; + +import javax.management.MBeanServer; +import java.io.Closeable; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Category({ Java8IncompatibleTest.class }) +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-21.0/src/test/java/graphql/GraphQL_InstrumentationTest.java b/instrumentation/graphql-java-21.0/src/test/java/graphql/GraphQL_InstrumentationTest.java new file mode 100644 index 0000000000..bca3c401bd --- /dev/null +++ b/instrumentation/graphql-java-21.0/src/test/java/graphql/GraphQL_InstrumentationTest.java @@ -0,0 +1,248 @@ +/* + * + * * Copyright 2023 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 com.newrelic.test.marker.Java8IncompatibleTest; +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.experimental.categories.Category; +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") +@Category({ Java8IncompatibleTest.class }) +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 with 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 (FieldUndefined@[noSuchField]) : Field 'noSuchField' in type 'Query' is undefined"; + 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-21.0/src/test/resources/distributed_tracing.yml b/instrumentation/graphql-java-21.0/src/test/resources/distributed_tracing.yml new file mode 100644 index 0000000000..acdf6d1a3d --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/obfuscateQueryTestData/federatedSubGraphQuery.gql b/instrumentation/graphql-java-21.0/src/test/resources/obfuscateQueryTestData/federatedSubGraphQuery.gql new file mode 100644 index 0000000000..efabfdd597 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/obfuscateQueryTestData/federatedSubGraphQueryObfuscated.gql b/instrumentation/graphql-java-21.0/src/test/resources/obfuscateQueryTestData/federatedSubGraphQueryObfuscated.gql new file mode 100644 index 0000000000..efabfdd597 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/obfuscateQueryTestData/obfuscate-query-test-data.csv b/instrumentation/graphql-java-21.0/src/test/resources/obfuscateQueryTestData/obfuscate-query-test-data.csv new file mode 100644 index 0000000000..2daa6bf087 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/obfuscateQueryTestData/queryMultiLevelAliasArg.gql b/instrumentation/graphql-java-21.0/src/test/resources/obfuscateQueryTestData/queryMultiLevelAliasArg.gql new file mode 100644 index 0000000000..8ef85c82e6 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/obfuscateQueryTestData/queryMultiLevelAliasArgObfuscated.gql b/instrumentation/graphql-java-21.0/src/test/resources/obfuscateQueryTestData/queryMultiLevelAliasArgObfuscated.gql new file mode 100644 index 0000000000..362ca346e3 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/obfuscateQueryTestData/queryWithNameAndArg.gql b/instrumentation/graphql-java-21.0/src/test/resources/obfuscateQueryTestData/queryWithNameAndArg.gql new file mode 100644 index 0000000000..6c8558d443 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/obfuscateQueryTestData/queryWithNameAndArgObfuscated.gql b/instrumentation/graphql-java-21.0/src/test/resources/obfuscateQueryTestData/queryWithNameAndArgObfuscated.gql new file mode 100644 index 0000000000..cb7c97d674 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/obfuscateQueryTestData/simpleMutation.gql b/instrumentation/graphql-java-21.0/src/test/resources/obfuscateQueryTestData/simpleMutation.gql new file mode 100644 index 0000000000..aba5da9945 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/obfuscateQueryTestData/simpleMutationObfuscated.gql b/instrumentation/graphql-java-21.0/src/test/resources/obfuscateQueryTestData/simpleMutationObfuscated.gql new file mode 100644 index 0000000000..a6c316a8d9 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/obfuscateQueryTestData/unionTypesInlineFragmentsQuery.gql b/instrumentation/graphql-java-21.0/src/test/resources/obfuscateQueryTestData/unionTypesInlineFragmentsQuery.gql new file mode 100644 index 0000000000..d61192b0be --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/obfuscateQueryTestData/unionTypesInlineFragmentsQueryObfuscated.gql b/instrumentation/graphql-java-21.0/src/test/resources/obfuscateQueryTestData/unionTypesInlineFragmentsQueryObfuscated.gql new file mode 100644 index 0000000000..baf1f4d47d --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/deepestUniquePathQuery.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/deepestUniquePathQuery.gql new file mode 100644 index 0000000000..00b53e9646 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/deepestUniqueSinglePathQuery.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/deepestUniqueSinglePathQuery.gql new file mode 100644 index 0000000000..a00e672829 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/federatedSubGraphQuery.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/federatedSubGraphQuery.gql new file mode 100644 index 0000000000..efabfdd597 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/fragments.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/fragments.gql new file mode 100644 index 0000000000..33a22547df --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/inputTypes.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/inputTypes.gql new file mode 100644 index 0000000000..cf73d7a8ec --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/multipleOperations.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/multipleOperations.gql new file mode 100644 index 0000000000..3b49a13cdc --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/schemaQuery.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/schemaQuery.gql new file mode 100644 index 0000000000..312009114d --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/simpleAnonymousQuery.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/simpleAnonymousQuery.gql new file mode 100644 index 0000000000..e027f659c0 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/simpleMutation.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/simpleMutation.gql new file mode 100644 index 0000000000..aba5da9945 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/simpleQuery.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/simpleQuery.gql new file mode 100644 index 0000000000..214b292899 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/transaction-name-test-data.csv b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/transaction-name-test-data.csv new file mode 100644 index 0000000000..1cdb07e7c7 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/twoTopLevelNames.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/twoTopLevelNames.gql new file mode 100644 index 0000000000..286925129b --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/unionTypesAndInlineFragmentQuery.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/unionTypesAndInlineFragmentQuery.gql new file mode 100644 index 0000000000..029d987c55 --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/unionTypesAndInlineFragmentsQuery.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/unionTypesAndInlineFragmentsQuery.gql new file mode 100644 index 0000000000..d61192b0be --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/validationErrors.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/validationErrors.gql new file mode 100644 index 0000000000..8928d56ddc --- /dev/null +++ b/instrumentation/graphql-java-21.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-21.0/src/test/resources/transactionNameTestData/variablesInsideFragments.gql b/instrumentation/graphql-java-21.0/src/test/resources/transactionNameTestData/variablesInsideFragments.gql new file mode 100644 index 0000000000..17949313a0 --- /dev/null +++ b/instrumentation/graphql-java-21.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 fd11d9eb3c..6cc9301bd7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -104,6 +104,7 @@ include 'instrumentation:grails-2' include 'instrumentation:grails-async-2.3' include 'instrumentation:graphql-java-16.2' include 'instrumentation:graphql-java-17.0' +include 'instrumentation:graphql-java-21.0' include 'instrumentation:grpc-1.4.0' include 'instrumentation:grpc-1.22.0' include 'instrumentation:grpc-1.30.0'