From ccbd8e3b5d3e1c42bc7037a38dfde2a5d14ec72e Mon Sep 17 00:00:00 2001 From: Gerard Downes Date: Wed, 2 Feb 2022 15:03:18 +0000 Subject: [PATCH] Support for Cassandra dataStax driver 4 (#690) * Support for Cassandra dataStax driver 4 https://github.com/newrelic/newrelic-java-agent/issues/202 * Exclude instrumentation for pre 4.0.0 release versions https://github.com/newrelic/newrelic-java-agent/issues/202 * Exclude unit tests for Java 9+ Cassandra Unit issue - https://github.com/jsevellec/cassandra-unit/issues/249 https://github.com/newrelic/newrelic-java-agent/issues/202 --- .../cassandra-datastax-4.0.0/.gitignore | 1 + .../cassandra-datastax-4.0.0/build.gradle | 29 +++ .../DefaultSession_Instrumentation.java | 51 +++++ .../instrumentation/cassandra/CQLParser.java | 100 ++++++++++ .../cassandra/CassandraUtils.java | 167 ++++++++++++++++ .../cassandra/CassandraInstrumented.java | 184 ++++++++++++++++++ .../cassandra/CassandraNoInstrumentation.java | 62 ++++++ .../cassandra/CassandraTestUtils.java | 68 +++++++ .../src/test/resources/users.cql | 2 + settings.gradle | 1 + 10 files changed, 665 insertions(+) create mode 100644 instrumentation/cassandra-datastax-4.0.0/.gitignore create mode 100644 instrumentation/cassandra-datastax-4.0.0/build.gradle create mode 100644 instrumentation/cassandra-datastax-4.0.0/src/main/java/com/datastax/oss/driver/internal/core/session/DefaultSession_Instrumentation.java create mode 100644 instrumentation/cassandra-datastax-4.0.0/src/main/java/com/nr/agent/instrumentation/cassandra/CQLParser.java create mode 100644 instrumentation/cassandra-datastax-4.0.0/src/main/java/com/nr/agent/instrumentation/cassandra/CassandraUtils.java create mode 100644 instrumentation/cassandra-datastax-4.0.0/src/test/java/com/nr/agent/instrumentation/cassandra/CassandraInstrumented.java create mode 100644 instrumentation/cassandra-datastax-4.0.0/src/test/java/com/nr/agent/instrumentation/cassandra/CassandraNoInstrumentation.java create mode 100644 instrumentation/cassandra-datastax-4.0.0/src/test/java/com/nr/agent/instrumentation/cassandra/CassandraTestUtils.java create mode 100644 instrumentation/cassandra-datastax-4.0.0/src/test/resources/users.cql diff --git a/instrumentation/cassandra-datastax-4.0.0/.gitignore b/instrumentation/cassandra-datastax-4.0.0/.gitignore new file mode 100644 index 0000000000..2f7896d1d1 --- /dev/null +++ b/instrumentation/cassandra-datastax-4.0.0/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/instrumentation/cassandra-datastax-4.0.0/build.gradle b/instrumentation/cassandra-datastax-4.0.0/build.gradle new file mode 100644 index 0000000000..164603d499 --- /dev/null +++ b/instrumentation/cassandra-datastax-4.0.0/build.gradle @@ -0,0 +1,29 @@ +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.cassandra-datastax-4.0.0' } +} + +dependencies { + implementation(project(":agent-bridge")) + implementation(project(":agent-bridge-datastore")) + implementation(project(":newrelic-api")) + implementation(project(":newrelic-weaver-api")) + implementation("com.datastax.oss:java-driver-core:4.3.1") { transitive(true) } + + testImplementation 'org.cassandraunit:cassandra-unit:4.3.1.0' + testImplementation 'org.apache.cassandra:cassandra-all:3.11.5' + testImplementation 'io.netty:netty-all:4.1.35.Final' +} + +verifyInstrumentation { + passesOnly 'com.datastax.oss:java-driver-core:[4.0.0,)' + excludeRegex ".*(rc|beta|alpha).*" +} + +test { + forkEvery(1) +} + +site { + title 'Cassandra' + type 'Datastore' +} diff --git a/instrumentation/cassandra-datastax-4.0.0/src/main/java/com/datastax/oss/driver/internal/core/session/DefaultSession_Instrumentation.java b/instrumentation/cassandra-datastax-4.0.0/src/main/java/com/datastax/oss/driver/internal/core/session/DefaultSession_Instrumentation.java new file mode 100644 index 0000000000..dcafd14dc4 --- /dev/null +++ b/instrumentation/cassandra-datastax-4.0.0/src/main/java/com/datastax/oss/driver/internal/core/session/DefaultSession_Instrumentation.java @@ -0,0 +1,51 @@ +package com.datastax.oss.driver.internal.core.session; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Segment; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.nr.agent.instrumentation.cassandra.CassandraUtils; + +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +@Weave(type = MatchType.ExactClass, originalName = "com.datastax.oss.driver.internal.core.session.DefaultSession") +public class DefaultSession_Instrumentation { + public ResultT execute(RequestT request, GenericType resultType) { + Segment segment = null; + + if (request instanceof Statement && (resultType.equals(Statement.SYNC) || resultType.equals(Statement.ASYNC)) ) { + segment = NewRelic.getAgent().getTransaction().startSegment("execute"); + } + + try { + Object result = Weaver.callOriginal(); + if (request instanceof Statement && (resultType.equals(Statement.SYNC))) { + return (ResultT) CassandraUtils.wrapSyncRequest((Statement) request, (ResultSet) result, getKeyspace().orElse(null), segment); + } else if (request instanceof Statement && (resultType.equals(Statement.ASYNC))) { + return (ResultT) CassandraUtils.wrapAsyncRequest((Statement) request, (CompletionStage) result, getKeyspace().orElse(null), segment); + } else { + return (ResultT) result; + } + } catch (Exception e) { + AgentBridge.privateApi.reportException(e); + throw e; + } finally { + if(request instanceof Statement && (resultType.equals(Statement.SYNC) && segment != null)) { + segment.end(); + } + } + } + + public Optional getKeyspace() { + return Weaver.callOriginal(); + } +} diff --git a/instrumentation/cassandra-datastax-4.0.0/src/main/java/com/nr/agent/instrumentation/cassandra/CQLParser.java b/instrumentation/cassandra-datastax-4.0.0/src/main/java/com/nr/agent/instrumentation/cassandra/CQLParser.java new file mode 100644 index 0000000000..2368a38152 --- /dev/null +++ b/instrumentation/cassandra-datastax-4.0.0/src/main/java/com/nr/agent/instrumentation/cassandra/CQLParser.java @@ -0,0 +1,100 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.cassandra; + +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A very simplistic CQL "Parser" that attempts to extract the information we care about for Datastore requests: + * + * - Operation (SELECT, INSERT, UPDATE, etc) + * - Table Name (Column Family) + */ +public class CQLParser { + + private static final String IDENTIFIER_REGEX = "[a-zA-Z][a-zA-Z0-9_\\.]*"; + private static final int FLAGS = Pattern.DOTALL | Pattern.CASE_INSENSITIVE; + + private static final Pattern SELECT_PATTERN = Pattern.compile("^(SELECT(?:\\s+JSON)?)\\s+.+?FROM\\s+(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final Pattern INSERT_PATTERN = Pattern.compile("^(INSERT)\\s+INTO\\s+(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final Pattern UPDATE_PATTERN = Pattern.compile("^(UPDATE)\\s+(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final Pattern DELETE_PATTERN = Pattern.compile("^(DELETE).+?FROM\\s+(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final Pattern BATCH_PATTERN = Pattern.compile("^BEGIN\\s+(?:(?:UNLOGGED|COUNTER)\\s+)?(BATCH)", FLAGS); + private static final Pattern USE_PATTERN = Pattern.compile("^(USE)\\s+(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final Pattern KEYSPACE_PATTERN = Pattern.compile("^([A-Za-z]+\\s+KEYSPACE)\\s+(?:IF\\s+(?:NOT\\s+)?EXISTS\\s+)?(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final Pattern TABLE_PATTERN = Pattern.compile("^([A-Za-z]+\\s+(?:TABLE|COLUMNFAMILY))\\s+(?:IF\\s+(?:NOT\\s+)?EXISTS\\s+)?(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final Pattern TRUNCATE_PATTERN = Pattern.compile("^(TRUNCATE)\\s+(?:(?:TABLE|COLUMNFAMILY)\\s+)?(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final Pattern CREATE_INDEX_PATTERN = Pattern.compile("^(CREATE\\s+(?:CUSTOM\\s+)?INDEX).+?ON\\s+(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final Pattern DROP_INDEX_PATTERN = Pattern.compile("^(DROP\\s+INDEX)\\s+(?:IF EXISTS\\s+)?(?:'|\")?(.*)", FLAGS); + private static final Pattern TYPE_PATTERN = Pattern.compile("^([A-Za-z]+\\s+TYPE)\\s+(?:IF\\s+(?:NOT\\s+)?EXISTS\\s+)?(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final Pattern TRIGGER_PATTERN = Pattern.compile("^([A-Za-z]+\\s+TRIGGER)\\s+.*?ON\\s+(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final Pattern CREATE_FUNCTION_PATTERN = Pattern.compile("^(CREATE\\s+(?:OR\\s+REPLACE\\s+)?FUNCTION)\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(.+?)\\s", FLAGS); + private static final Pattern DROP_FUNCTION_PATTERN = Pattern.compile("^(DROP\\s+FUNCTION)\\s+(?:IF\\s+EXISTS\\s+)?(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final Pattern CREATE_AGGREGATE_PATTERN = Pattern.compile("^(CREATE\\s+(?:OR\\s+REPLACE\\s+)?AGGREGATE)\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final Pattern DROP_AGGREGATE_PATTERN = Pattern.compile("^(DROP\\s+AGGREGATE)\\s+(?:IF\\s+EXISTS\\s+)?(?:'|\")?(" + IDENTIFIER_REGEX + ")", FLAGS); + private static final String COMMENT_PATTERN = "/\\*(?:.|[\\r\\n])*?\\*/"; + + private static final List PATTERNS = new LinkedList<>(); + + static { + // The order here is a performance optimization to favor more common queries first + PATTERNS.add(SELECT_PATTERN); + PATTERNS.add(UPDATE_PATTERN); + PATTERNS.add(INSERT_PATTERN); + PATTERNS.add(DELETE_PATTERN); + PATTERNS.add(BATCH_PATTERN); + PATTERNS.add(TRUNCATE_PATTERN); // This needs to be before TABLE_PATTERN + PATTERNS.add(TABLE_PATTERN); + PATTERNS.add(KEYSPACE_PATTERN); + PATTERNS.add(USE_PATTERN); + PATTERNS.add(TYPE_PATTERN); + PATTERNS.add(CREATE_INDEX_PATTERN); + PATTERNS.add(DROP_INDEX_PATTERN); + PATTERNS.add(CREATE_FUNCTION_PATTERN); + PATTERNS.add(DROP_FUNCTION_PATTERN); + PATTERNS.add(CREATE_AGGREGATE_PATTERN); + PATTERNS.add(DROP_AGGREGATE_PATTERN); + PATTERNS.add(TRIGGER_PATTERN); + } + + public OperationAndTableName getOperationAndTableName(String rawQuery) { + rawQuery = rawQuery.replaceAll(COMMENT_PATTERN, "").trim(); + + String operation = null; + String tableName = null; + for (Pattern pattern : PATTERNS) { + Matcher matcher = pattern.matcher(rawQuery); + if (matcher.find()) { + if (matcher.groupCount() >= 1) { + operation = matcher.group(1); + } + if (matcher.groupCount() == 2) { + tableName = matcher.group(2); + } + return new OperationAndTableName(operation, tableName); + } + } + return null; + } + + public class OperationAndTableName { + public final String operation; + public final String tableName; + + public OperationAndTableName(String operation, String tableName) { + this.operation = operation.toUpperCase().replaceAll("\\s", "_"); + if (tableName != null) { + tableName = tableName.replaceAll(";|'|\"", ""); + } + this.tableName = tableName; + } + } +} diff --git a/instrumentation/cassandra-datastax-4.0.0/src/main/java/com/nr/agent/instrumentation/cassandra/CassandraUtils.java b/instrumentation/cassandra-datastax-4.0.0/src/main/java/com/nr/agent/instrumentation/cassandra/CassandraUtils.java new file mode 100644 index 0000000000..d5dcd1bdae --- /dev/null +++ b/instrumentation/cassandra-datastax-4.0.0/src/main/java/com/nr/agent/instrumentation/cassandra/CassandraUtils.java @@ -0,0 +1,167 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.cassandra; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.BatchStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.Request; +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.agent.bridge.datastore.DatastoreVendor; +import com.newrelic.api.agent.DatastoreParameters; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.QueryConverter; +import com.newrelic.api.agent.Segment; + +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.logging.Level; +import java.util.regex.Pattern; + +public class CassandraUtils { + + private static final String SINGLE_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 CASSANDRA_DIALECT_PATTERN; + private static final Pattern CASSANDRA_UNMATCHED_PATTERN; + private static final CQLParser CASSANDRA_QUERY_PARSER = new CQLParser(); + + static { + String cassandraDialectPattern = String.join("|", Arrays.asList(SINGLE_QUOTE, COMMENT, MULTILINE_COMMENT, UUID, HEX, BOOLEAN, NUMBER)); + + CASSANDRA_DIALECT_PATTERN = Pattern.compile(cassandraDialectPattern, Pattern.DOTALL | Pattern.CASE_INSENSITIVE); + CASSANDRA_UNMATCHED_PATTERN = Pattern.compile("'|/\\*|\\*/", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); + } + + public static void metrics(String queryString, String host, Integer port, String keyspace, Transaction tx, + Segment segment) { + try { + CQLParser.OperationAndTableName result = CASSANDRA_QUERY_PARSER.getOperationAndTableName(queryString); + if (result == null) { + NewRelic.getAgent().getLogger().log(Level.FINE, "Unable to parse cql statement"); + return; + } + + CassandraUtils.metrics(queryString, result.tableName, result.operation, host, port, keyspace, tx, segment); + } catch (Exception e) { + NewRelic.getAgent().getLogger().log(Level.FINEST, "ERROR: Problem parsing cql statement. {0}", e); + } + } + + public static void metrics(String queryString, String collection, String operation, String host, Integer port, + String keyspace, Transaction tx, Segment segment) { + + segment.reportAsExternal(DatastoreParameters + .product(DatastoreVendor.Cassandra.name()) + .collection(collection) + .operation(operation) + .instance(host, port) + .databaseName(keyspace) // may be null, indicating no keyspace for the command + .slowQuery(queryString, CASSANDRA_QUERY_CONVERTER) + .build()); + } + + public static QueryConverter CASSANDRA_QUERY_CONVERTER = new QueryConverter() { + + @Override + public String toRawQueryString(String statement) { + return statement; + } + + @Override + public String toObfuscatedQueryString(String statement) { + return obfuscateQuery(statement); + } + + private String obfuscateQuery(String rawQuery) { + String obfuscatedSql = CASSANDRA_DIALECT_PATTERN.matcher(rawQuery).replaceAll("?"); + return checkForUnmatchedPairs(CASSANDRA_UNMATCHED_PATTERN, obfuscatedSql); + } + + /** + * This method will check to see if there are any open single quotes or comment open/closes still left in the + * obfuscated string. If so, it means something didn't obfuscate properly so we will return "?" instead to + * prevent any data from leaking. + */ + private String checkForUnmatchedPairs(Pattern pattern, String obfuscatedSql) { + return pattern.matcher(obfuscatedSql).find() ? "?" : obfuscatedSql; + } + }; + + public static ResultSet wrapSyncRequest(Statement request, ResultSet result, CqlIdentifier keyspace, Segment segment) { + if(result != null) { + reportMetric(request, keyspace, result.getExecutionInfo().getCoordinator(), segment); + } + return result; + } + + public static CompletionStage wrapAsyncRequest(Statement request, CompletionStage completionStage, CqlIdentifier keyspace, Segment segment) { + return Objects.requireNonNull(completionStage).whenComplete( + (result, throwable) -> { + if (throwable instanceof CompletionException) { + throwable = throwable.getCause(); + } + if (throwable != null) { + System.out.println(throwable); + AgentBridge.privateApi.reportException(throwable); + } + if(result != null) { + reportMetric(request, keyspace, result.getExecutionInfo().getCoordinator(), segment); + } + segment.end(); + }); + } + + private static void reportMetric(Statement request, CqlIdentifier keyspace, Node coordinator, Segment segment) { + if(request instanceof BatchStatement) { + CassandraUtils.metrics(null, null, "BATCH", + Optional.ofNullable(coordinator).flatMap(x -> x.getBroadcastAddress().map(InetSocketAddress::getHostName)).orElse(null), + Optional.ofNullable(coordinator).flatMap(x -> x.getBroadcastAddress().map(InetSocketAddress::getPort)).orElse(null), + Optional.ofNullable(keyspace).map(CqlIdentifier::asInternal).orElse(null), + AgentBridge.getAgent().getTransaction(), + segment); + } else { + CassandraUtils.metrics( + getQuery(request), + Optional.ofNullable(coordinator).flatMap(x -> x.getBroadcastAddress().map(InetSocketAddress::getHostName)).orElse(null), + Optional.ofNullable(coordinator).flatMap(x -> x.getBroadcastAddress().map(InetSocketAddress::getPort)).orElse(null), + Optional.ofNullable(keyspace).map(CqlIdentifier::asInternal).orElse(null), + AgentBridge.getAgent().getTransaction(), + segment + ); + } + } + + public static String getQuery(final RequestT statement) { + String query = null; + if (statement instanceof BoundStatement) { + query = ((BoundStatement) statement).getPreparedStatement().getQuery(); + } else if (statement instanceof SimpleStatement) { + query = ((SimpleStatement) statement).getQuery(); + } + + return query == null ? "" : query; + } + +} diff --git a/instrumentation/cassandra-datastax-4.0.0/src/test/java/com/nr/agent/instrumentation/cassandra/CassandraInstrumented.java b/instrumentation/cassandra-datastax-4.0.0/src/test/java/com/nr/agent/instrumentation/cassandra/CassandraInstrumented.java new file mode 100644 index 0000000000..d338f80829 --- /dev/null +++ b/instrumentation/cassandra-datastax-4.0.0/src/test/java/com/nr/agent/instrumentation/cassandra/CassandraInstrumented.java @@ -0,0 +1,184 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.cassandra; + +import com.newrelic.agent.bridge.datastore.DatastoreVendor; +import com.newrelic.agent.introspec.DatastoreHelper; +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.TraceSegment; +import com.newrelic.agent.introspec.TransactionTrace; +import com.newrelic.test.marker.Java10IncompatibleTest; +import com.newrelic.test.marker.Java11IncompatibleTest; +import com.newrelic.test.marker.Java12IncompatibleTest; +import com.newrelic.test.marker.Java13IncompatibleTest; +import com.newrelic.test.marker.Java14IncompatibleTest; +import com.newrelic.test.marker.Java15IncompatibleTest; +import com.newrelic.test.marker.Java16IncompatibleTest; +import com.newrelic.test.marker.Java17IncompatibleTest; +import com.newrelic.test.marker.Java18IncompatibleTest; +import com.newrelic.test.marker.Java9IncompatibleTest; +import org.cassandraunit.CassandraCQLUnit; +import org.cassandraunit.dataset.cql.ClassPathCQLDataSet; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +// Issue when running cassandra unit on Java 9+ - https://github.com/jsevellec/cassandra-unit/issues/249 +@Category({Java9IncompatibleTest.class, Java10IncompatibleTest.class, Java11IncompatibleTest.class, Java12IncompatibleTest.class, Java13IncompatibleTest.class, Java14IncompatibleTest.class, + Java15IncompatibleTest.class, Java16IncompatibleTest.class, Java17IncompatibleTest.class, Java18IncompatibleTest.class}) +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "com.datastax.oss.driver" }) +public class CassandraInstrumented { + + @Rule + public CassandraCQLUnit cassandra = new CassandraCQLUnit(new ClassPathCQLDataSet("users.cql", "users")); + + @Test + public void testSyncBasicRequests() { + //Given + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + DatastoreHelper helper = new DatastoreHelper(DatastoreVendor.Cassandra.toString()); + + //When + CassandraTestUtils.syncBasicRequests(cassandra.session); + + //Then + assertEquals(1, introspector.getFinishedTransactionCount(1000)); + assertEquals(1, introspector.getTransactionNames().size()); + String transactionName = introspector.getTransactionNames().stream().findFirst().orElse(""); + helper.assertScopedStatementMetricCount(transactionName, "INSERT", "users", 1); + helper.assertScopedStatementMetricCount(transactionName, "SELECT", "users", 3); + helper.assertScopedStatementMetricCount(transactionName, "UPDATE", "users", 1); + helper.assertScopedStatementMetricCount(transactionName, "DELETE", "users", 1); + helper.assertAggregateMetrics(); + helper.assertUnscopedOperationMetricCount("INSERT", 1); + helper.assertUnscopedOperationMetricCount("SELECT", 3); + helper.assertUnscopedOperationMetricCount("UPDATE", 1); + helper.assertUnscopedOperationMetricCount("DELETE", 1); + helper.assertUnscopedStatementMetricCount("INSERT", "users", 1); + helper.assertUnscopedStatementMetricCount("SELECT", "users", 3); + helper.assertUnscopedStatementMetricCount("UPDATE", "users", 1); + helper.assertUnscopedStatementMetricCount("DELETE", "users", 1); + } + + @Test + public void testSyncStatementRequests() { + //Given + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + DatastoreHelper helper = new DatastoreHelper(DatastoreVendor.Cassandra.toString()); + + //When + CassandraTestUtils.statementRequests(cassandra.session); + + //Then + assertEquals(1, introspector.getFinishedTransactionCount(1000)); + assertEquals(1, introspector.getTransactionNames().size()); + String transactionName = introspector.getTransactionNames().stream().findFirst().orElse(""); + helper.assertScopedStatementMetricCount(transactionName, "INSERT", "users", 1); + helper.assertScopedStatementMetricCount(transactionName, "SELECT", "users", 3); + helper.assertScopedStatementMetricCount(transactionName, "UPDATE", "users", 1); + helper.assertScopedStatementMetricCount(transactionName, "DELETE", "users", 1); + helper.assertAggregateMetrics(); + helper.assertUnscopedOperationMetricCount("INSERT", 1); + helper.assertUnscopedOperationMetricCount("SELECT", 3); + helper.assertUnscopedOperationMetricCount("UPDATE", 1); + helper.assertUnscopedOperationMetricCount("DELETE", 1); + helper.assertUnscopedStatementMetricCount("INSERT", "users", 1); + helper.assertUnscopedStatementMetricCount("SELECT", "users", 3); + helper.assertUnscopedStatementMetricCount("UPDATE", "users", 1); + helper.assertUnscopedStatementMetricCount("DELETE", "users", 1); + } + + @Test + public void testSyncBatchStatementRequests() { + //Given + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + DatastoreHelper helper = new DatastoreHelper(DatastoreVendor.Cassandra.toString()); + + //When + CassandraTestUtils.batchStatementRequests(cassandra.session); + + //Then + assertEquals(1, introspector.getFinishedTransactionCount(1000)); + assertEquals(1, introspector.getTransactionNames().size()); + String transactionName = introspector.getTransactionNames().stream().findFirst().orElse(""); + Collection traces = introspector.getTransactionTracesForTransaction(transactionName); + List traceSegments = traces.stream() + .findFirst() + .map(TransactionTrace::getInitialTraceSegment) + .map(TraceSegment::getChildren) + .orElse(Collections.emptyList()); + helper.assertScopedOperationMetricCount(transactionName, "BATCH", 1); + helper.assertAggregateMetrics(); + helper.assertUnscopedOperationMetricCount("BATCH", 1); + assertEquals(1, traces.size()); + assertEquals(1, traceSegments.size()); + traceSegments.stream().map(TraceSegment::getTracerAttributes).forEach(x -> { + assertNotNull(x.get("host")); + assertNotNull(x.get("port_path_or_id")); + assertNotNull(x.get("db.instance")); + }); + } + + @Test + public void testSyncRequestError() { + //Given + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + + //When + CassandraTestUtils.syncRequestError(cassandra.session); + + //Then + assertEquals(1, introspector.getFinishedTransactionCount(1000)); + assertEquals(1, introspector.getErrors().size()); + assertEquals(1, introspector.getErrorEvents().size()); + } + + @Test + public void testAsyncBasicRequests() { + //Given + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + DatastoreHelper helper = new DatastoreHelper(DatastoreVendor.Cassandra.toString()); + + //When + CassandraTestUtils.asyncBasicRequests(cassandra.session); + + //Then + assertEquals(1, introspector.getFinishedTransactionCount(1000)); + assertEquals(1, introspector.getTransactionNames().size()); + String transactionName = introspector.getTransactionNames().stream().findFirst().orElse(""); + helper.assertScopedStatementMetricCount(transactionName, "SELECT", "users", 2); + helper.assertAggregateMetrics(); + helper.assertUnscopedOperationMetricCount("SELECT", 2); + helper.assertUnscopedStatementMetricCount("SELECT", "users", 2); + } + + @Test + public void testAsyncRequestError() { + //Given + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + + //When + CassandraTestUtils.asyncRequestError(cassandra.session); + + //Then + assertEquals(1, introspector.getFinishedTransactionCount(1000)); + assertEquals(1, introspector.getErrors().size()); + assertEquals(1, introspector.getErrorEvents().size()); + } +} diff --git a/instrumentation/cassandra-datastax-4.0.0/src/test/java/com/nr/agent/instrumentation/cassandra/CassandraNoInstrumentation.java b/instrumentation/cassandra-datastax-4.0.0/src/test/java/com/nr/agent/instrumentation/cassandra/CassandraNoInstrumentation.java new file mode 100644 index 0000000000..1e9e5e9835 --- /dev/null +++ b/instrumentation/cassandra-datastax-4.0.0/src/test/java/com/nr/agent/instrumentation/cassandra/CassandraNoInstrumentation.java @@ -0,0 +1,62 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.cassandra; + +import com.newrelic.agent.bridge.datastore.DatastoreVendor; +import com.newrelic.agent.introspec.DatastoreHelper; +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.test.marker.Java10IncompatibleTest; +import com.newrelic.test.marker.Java11IncompatibleTest; +import com.newrelic.test.marker.Java12IncompatibleTest; +import com.newrelic.test.marker.Java13IncompatibleTest; +import com.newrelic.test.marker.Java14IncompatibleTest; +import com.newrelic.test.marker.Java15IncompatibleTest; +import com.newrelic.test.marker.Java16IncompatibleTest; +import com.newrelic.test.marker.Java17IncompatibleTest; +import com.newrelic.test.marker.Java18IncompatibleTest; +import com.newrelic.test.marker.Java9IncompatibleTest; +import org.cassandraunit.CassandraCQLUnit; +import org.cassandraunit.dataset.cql.ClassPathCQLDataSet; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; + +// Issue when running cassandra unit on Java 9+ - https://github.com/jsevellec/cassandra-unit/issues/249 +@Category({Java9IncompatibleTest.class, Java10IncompatibleTest.class, Java11IncompatibleTest.class, Java12IncompatibleTest.class, Java13IncompatibleTest.class, Java14IncompatibleTest.class, + Java15IncompatibleTest.class, Java16IncompatibleTest.class, Java17IncompatibleTest.class, Java18IncompatibleTest.class}) +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "none" }) +public class CassandraNoInstrumentation { + + @Rule + public CassandraCQLUnit cassandra = new CassandraCQLUnit(new ClassPathCQLDataSet("users.cql", "users")); + + @Test + public void testSyncBasicRequests() { + //Given + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + DatastoreHelper helper = new DatastoreHelper(DatastoreVendor.Cassandra.toString()); + + //When + CassandraTestUtils.syncBasicRequests(cassandra.session); + + //Then + assertEquals(1, introspector.getFinishedTransactionCount(1000)); + assertEquals(1, introspector.getTransactionNames().size()); + String transactionName = introspector.getTransactionNames().stream().findFirst().orElse(""); + helper.assertScopedStatementMetricCount(transactionName, "INSERT", "users", 0); + helper.assertScopedStatementMetricCount(transactionName, "SELECT", "users", 0); + helper.assertScopedStatementMetricCount(transactionName, "UPDATE", "users", 0); + helper.assertScopedStatementMetricCount(transactionName, "DELETE", "users", 0); + } +} diff --git a/instrumentation/cassandra-datastax-4.0.0/src/test/java/com/nr/agent/instrumentation/cassandra/CassandraTestUtils.java b/instrumentation/cassandra-datastax-4.0.0/src/test/java/com/nr/agent/instrumentation/cassandra/CassandraTestUtils.java new file mode 100644 index 0000000000..af10df8c78 --- /dev/null +++ b/instrumentation/cassandra-datastax-4.0.0/src/test/java/com/nr/agent/instrumentation/cassandra/CassandraTestUtils.java @@ -0,0 +1,68 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.BatchStatement; +import com.datastax.oss.driver.api.core.cql.BatchType; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.newrelic.api.agent.Trace; + +public class CassandraTestUtils { + @Trace(dispatcher = true) + public static void syncBasicRequests(CqlSession session) { + session.execute("INSERT INTO users (lastname, age, city, email, firstname) VALUES ('Jones', 35, 'Austin', 'bob@example.com', 'Bob')"); + session.execute("SELECT * FROM users WHERE lastname='Jones'"); + session.execute("UPDATE users SET age = 36 WHERE lastname = 'Jones'"); + session.execute("SELECT * FROM users WHERE lastname='Jones'"); + session.execute("DELETE FROM users WHERE lastname = 'Jones'"); + session.execute("SELECT * FROM users"); + } + + @Trace(dispatcher = true) + public static void asyncBasicRequests(CqlSession session) { + session.executeAsync("SELECT * FROM users WHERE lastname='Jones'"); + session.executeAsync("SELECT * FROM users WHERE lastname='Jones'"); + } + + @Trace(dispatcher = true) + public static void statementRequests(CqlSession session) { + session.execute(SimpleStatement.builder("INSERT INTO users (lastname, age, city, email, firstname) VALUES ('Jones', 35, 'Austin', 'bob@example.com', 'Bob')").build()); + session.execute(SimpleStatement.builder("SELECT * FROM users WHERE lastname='Jones'").build()); + session.execute(SimpleStatement.builder("UPDATE users SET age = 36 WHERE lastname = 'Jones'").build()); + session.execute(SimpleStatement.builder("SELECT * FROM users WHERE lastname='Jones'").build()); + session.execute(SimpleStatement.builder("DELETE FROM users WHERE lastname = 'Jones'").build()); + session.execute(SimpleStatement.builder("SELECT * FROM users").build()); + } + + @Trace(dispatcher = true) + public static void batchStatementRequests(CqlSession session) { + session.execute(BatchStatement.builder(BatchType.LOGGED) + .addStatement(SimpleStatement.builder("INSERT INTO users (lastname, age, city, email, firstname) VALUES ('Jones', 35, 'Austin', 'bob@example.com', 'Bob')").build()) + .addStatement(SimpleStatement.builder("INSERT INTO users2 (lastname, age, city, email, firstname) VALUES ('Jones', 35, 'Austin', 'bob@example.com', 'Bob')").build()) + .addStatement(SimpleStatement.builder("DELETE FROM users WHERE lastname = 'Jones'").build()) + .addStatement(SimpleStatement.builder("DELETE FROM users2 WHERE lastname = 'Jones'").build()) + .build()); + } + + @Trace(dispatcher = true) + public static void syncRequestError(CqlSession session) { + try { + session.execute("SELECT * FORM users WHERE lastname='Jones'"); + } catch (Exception ignored) { + } + } + + @Trace(dispatcher = true) + public static void asyncRequestError(CqlSession session) { + try { + session.executeAsync("SELECT * FORM users WHERE lastname='Jones'"); + } catch (Exception ignored) { + } + } +} diff --git a/instrumentation/cassandra-datastax-4.0.0/src/test/resources/users.cql b/instrumentation/cassandra-datastax-4.0.0/src/test/resources/users.cql new file mode 100644 index 0000000000..98ae33ac4d --- /dev/null +++ b/instrumentation/cassandra-datastax-4.0.0/src/test/resources/users.cql @@ -0,0 +1,2 @@ +CREATE TABLE users (firstname text, lastname text, age int, email text, city text, PRIMARY KEY (lastname)); +CREATE TABLE users2 (firstname text, lastname text, age int, email text, city text, PRIMARY KEY (lastname)); \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index a9cf9772a2..70a772e327 100644 --- a/settings.gradle +++ b/settings.gradle @@ -123,6 +123,7 @@ include 'instrumentation:async-http-client-2.1.0' include 'instrumentation:cassandra-datastax-2.1.2' include 'instrumentation:cassandra-datastax-3.0.0' include 'instrumentation:cassandra-datastax-3.8.0' +include 'instrumentation:cassandra-datastax-4.0.0' include 'instrumentation:cxf-2.7' include 'instrumentation:ejb-3.0' include 'instrumentation:glassfish-3'