Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
muzzle {
pass {
group = "com.datastax.oss"
module = "java-driver-core"
versions = "[4.0.0,)"
}
}

apply from: "$rootDir/gradle/java.gradle"

addTestSuiteForDir('latestDepTest', 'test')

dependencies {
compileOnly group: 'com.datastax.oss', name: 'java-driver-core', version: '4.0.0'

testImplementation group: 'com.datastax.oss', name: 'java-driver-core', version: '4.0.0'
testImplementation group: 'com.github.jbellis', name: 'jamm', version: '0.3.3'
testImplementation (group: 'org.testcontainers', name: 'cassandra', version: libs.versions.testcontainers.get()) {
exclude group: 'com.datastax.cassandra', module: 'cassandra-driver-core'
}

latestDepTestImplementation group: 'com.datastax.oss', name: 'java-driver-core', version: '4.+'
}

tasks.withType(Test).configureEach {
jvmArgs '-Dtestcontainers.ryuk.disabled=true'
environment 'TESTCONTAINERS_RYUK_DISABLED', 'true'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package datadog.trace.instrumentation.cassandra4;

import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.cql.ExecutionInfo;
import com.datastax.oss.driver.api.core.cql.ResultSet;
import com.datastax.oss.driver.api.core.metadata.EndPoint;
import com.datastax.oss.driver.api.core.metadata.Node;
import datadog.trace.api.naming.SpanNaming;
import datadog.trace.api.normalize.SQLNormalizer;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.bootstrap.instrumentation.decorator.DBTypeProcessingDatabaseClientDecorator;
import datadog.trace.bootstrap.instrumentation.jdbc.DBQueryInfo;
import java.net.InetSocketAddress;
import java.net.SocketAddress;

public class CassandraClientDecorator extends DBTypeProcessingDatabaseClientDecorator<CqlSession> {
private static final String DB_TYPE = "cassandra";
private static final String SERVICE_NAME =
SpanNaming.instance().namingSchema().database().service(DB_TYPE);
public static final String OPERATION_NAME =
SpanNaming.instance().namingSchema().database().operation(DB_TYPE);
public static final String JAVA_CASSANDRA = "java-cassandra";

public static final CassandraClientDecorator DECORATE = new CassandraClientDecorator();

@Override
protected String[] instrumentationNames() {
return new String[] {"cassandra"};
}

@Override
protected String service() {
return SERVICE_NAME;
}

@Override
protected CharSequence component() {
return JAVA_CASSANDRA;
}

@Override
protected CharSequence spanType() {
return InternalSpanTypes.CASSANDRA;
}

@Override
protected String dbType() {
return DB_TYPE;
}

@Override
protected String dbUser(final CqlSession session) {
return null;
}

@Override
protected String dbInstance(final CqlSession session) {
return session.getKeyspace().map(k -> k.asCql(false)).orElse(null);
Comment on lines +59 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use unquoted keyspace names for db.instance

For sessions built with a keyspace, asCql(false) always renders the identifier in double-quoted CQL form, so a normal keyspace like peer_test is tagged as "peer_test". That changes db.instance and any peer-service or split-by-instance naming derived from it compared with the existing Cassandra instrumentation, so use the internal/unquoted keyspace representation here.

Useful? React with πŸ‘Β / πŸ‘Ž.

}

@Override
protected String dbHostname(final CqlSession session) {
return ContactPointsUtil.getFirstHost(session);
}

public void onStatement(final AgentSpan span, final CharSequence statement) {
span.setResourceName(SQLNormalizer.normalize(statement.toString()).toString());
final CharSequence operation = DBQueryInfo.extractOperation(statement);
if (operation != null) {
span.setTag(Tags.DB_OPERATION, operation);
}
}

public void onResponse(final AgentSpan span, final ResultSet result) {
if (result != null) {
final ExecutionInfo executionInfo = result.getExecutionInfo();
Comment on lines +76 to +78

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve statement keyspaces in db.instance

When cassandra-keyspace-statement-extraction is enabled, the existing Cassandra 4 instrumentation updates db.instance from result metadata so SELECT * FROM test_keyspace.users is attributed to test_keyspace even if the session has no keyspace or a different one. This response handling only records the coordinator, so those spans keep a null or stale session keyspace and split-by-instance/peer-service naming is wrong for fully-qualified statements.

Useful? React with πŸ‘Β / πŸ‘Ž.

if (executionInfo != null) {
final Node coordinator = executionInfo.getCoordinator();
if (coordinator != null) {
final EndPoint endPoint = coordinator.getEndPoint();
if (endPoint != null) {
final SocketAddress socketAddress = endPoint.resolve();
if (socketAddress instanceof InetSocketAddress) {
onPeerConnection(span, (InetSocketAddress) socketAddress);
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package datadog.trace.instrumentation.cassandra4;

import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.agent.tooling.InstrumenterModule;
import java.util.Collections;
import java.util.List;

@AutoService(InstrumenterModule.class)
public class CassandraClientModule extends InstrumenterModule.Tracing {

public CassandraClientModule() {
super("cassandra");
}

@Override
public String[] helperClassNames() {
return new String[] {
packageName + ".CassandraClientDecorator",
packageName + ".CassandraDBMUtil",
packageName + ".ContactPointsUtil",
packageName + ".SpanFinishingCallback",
};
}

@Override
public List<Instrumenter> typeInstrumentations() {
return Collections.singletonList(new CqlSessionExecuteInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package datadog.trace.instrumentation.cassandra4;

import static datadog.trace.api.Config.DBM_PROPAGATION_MODE_FULL;
import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DBM_TRACE_INJECTED;

import datadog.trace.api.Config;
import datadog.trace.api.propagation.W3CTraceParent;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.dbm.SharedDBCommenter;

/**
* Utility class for Cassandra Database Monitoring (DBM) comment injection. When DBM propagation is
* enabled, this injects trace context as a CQL comment prepended to the query string so that the
* Datadog database agent can correlate queries back to traces.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid redundant comments?
The purpose of the class is clear from the code and requires no further explanation

public final class CassandraDBMUtil {

private CassandraDBMUtil() {}

/**
* Injects a DBM trace comment into the CQL query string if DBM propagation is enabled. Sets the
* {@code _dd.dbm_trace_injected} tag on the span when injection occurs.
*
* @param span the current agent span
* @param query the original CQL query string
* @param hostname the database host (may be null)
* @param dbName the database/keyspace name (may be null)
* @return the query string with DBM comment prepended, or the original query if DBM is disabled
*/
public static String injectComment(AgentSpan span, String query, String hostname, String dbName) {
if (!Config.get().isDbmCommentInjectionEnabled()) {
return query;
}

if (query == null || query.isEmpty()) {
return query;
}

if (span.forceSamplingDecision() == null) {
return query;
}

String dbService = span.getServiceName();
String traceParent =
Config.get().getDbmPropagationMode().equals(DBM_PROPAGATION_MODE_FULL)
? W3CTraceParent.from(span)
: null;

String commentContent =
SharedDBCommenter.buildComment(dbService, "cassandra", hostname, dbName, traceParent);
if (commentContent == null || commentContent.isEmpty()) {
return query;
}

// Check for duplicate injection
if (SharedDBCommenter.containsTraceComment(query)) {
return query;
}

span.setTag(DBM_TRACE_INJECTED, true);

// Prepend the DBM comment as a CQL comment
return "/* " + commentContent + " */ " + query;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package datadog.trace.instrumentation.cassandra4;

import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.cql.BoundStatement;
import com.datastax.oss.driver.api.core.cql.Statement;
import com.datastax.oss.driver.api.core.metadata.EndPoint;
import com.datastax.oss.driver.api.core.metadata.Node;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collection;
import java.util.Optional;

public final class ContactPointsUtil {

private ContactPointsUtil() {}

public static String getContactPoints(final CqlSession session) {
try {
Collection<Node> nodes = session.getMetadata().getNodes().values();
if (nodes.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
for (Node node : nodes) {
if (sb.length() > 0) {
sb.append(",");
}
EndPoint endPoint = node.getEndPoint();
if (endPoint != null) {
SocketAddress socketAddress = endPoint.resolve();
if (socketAddress instanceof InetSocketAddress) {
InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
sb.append(inetSocketAddress.getHostString())
.append(":")
.append(inetSocketAddress.getPort());
}
}
}
return sb.length() > 0 ? sb.toString() : null;
} catch (Throwable ignored) {
return null;
}
}

public static String getFirstHost(final CqlSession session) {
try {
Collection<Node> nodes = session.getMetadata().getNodes().values();
for (Node node : nodes) {
EndPoint endPoint = node.getEndPoint();
if (endPoint != null) {
SocketAddress socketAddress = endPoint.resolve();
if (socketAddress instanceof InetSocketAddress) {
return ((InetSocketAddress) socketAddress).getHostString();
}
}
}
} catch (Throwable ignored) {
// Connection metadata may not be available
}
return null;
}

public static String getKeyspace(final CqlSession session) {
try {
Optional<CqlIdentifier> keyspace = session.getKeyspace();
if (keyspace.isPresent()) {
return keyspace.get().asCql(false);
}
} catch (Throwable ignored) {
// Keyspace may not be available
}
return null;
}

public static String getQuery(final Statement<?> statement) {
String query = null;
if (statement instanceof com.datastax.oss.driver.api.core.cql.SimpleStatement) {
query = ((com.datastax.oss.driver.api.core.cql.SimpleStatement) statement).getQuery();
} else if (statement instanceof BoundStatement) {
query = ((BoundStatement) statement).getPreparedStatement().getQuery();
}
return query == null ? "" : query;
}
}
Loading