diff --git a/dd-java-agent/instrumentation/valkey-java/build.gradle b/dd-java-agent/instrumentation/valkey-java/build.gradle new file mode 100644 index 00000000000..a3f013b59d3 --- /dev/null +++ b/dd-java-agent/instrumentation/valkey-java/build.gradle @@ -0,0 +1,20 @@ + +muzzle { + pass { + group = "io.valkey" + module = "valkey-java" + versions = "[5.3.0,)" + } +} + +apply from: "$rootDir/gradle/java.gradle" + +addTestSuiteForDir('latestDepTest', 'test') + +dependencies { + compileOnly group: 'io.valkey', name: 'valkey-java', version: '5.3.0' + + testImplementation group: 'com.github.codemonstur', name: 'embedded-redis', version: '1.4.3' + testImplementation group: 'io.valkey', name: 'valkey-java', version: '5.3.0' + latestDepTestImplementation group: 'io.valkey', name: 'valkey-java', version: '5.+' +} diff --git a/dd-java-agent/instrumentation/valkey-java/src/main/java/datadog/trace/instrumentation/valkey/ValkeyInstrumentation.java b/dd-java-agent/instrumentation/valkey-java/src/main/java/datadog/trace/instrumentation/valkey/ValkeyInstrumentation.java new file mode 100644 index 00000000000..c07ccf2d304 --- /dev/null +++ b/dd-java-agent/instrumentation/valkey-java/src/main/java/datadog/trace/instrumentation/valkey/ValkeyInstrumentation.java @@ -0,0 +1,82 @@ +package datadog.trace.instrumentation.valkey; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static io.valkey.ValkeyClientDecorator.DECORATE; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import io.valkey.CommandObject; +import io.valkey.Connection; +import io.valkey.Protocol; +import io.valkey.ValkeyClientDecorator; +import io.valkey.commands.ProtocolCommand; +import net.bytebuddy.asm.Advice; + +@AutoService(InstrumenterModule.class) +public final class ValkeyInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + + public ValkeyInstrumentation() { + super("valkey"); + } + + @Override + public String instrumentedType() { + return "io.valkey.Connection"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + "io.valkey.ValkeyClientDecorator", + }; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(isPublic()) + .and(named("executeCommand")) + .and(takesArgument(0, named("io.valkey.CommandObject"))), + ValkeyInstrumentation.class.getName() + "$ValkeyAdvice"); + } + + public static class ValkeyAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope onEnter( + @Advice.Argument(0) final CommandObject commandObject, + @Advice.This final Connection thiz) { + final AgentSpan span = startSpan("valkey", ValkeyClientDecorator.OPERATION_NAME); + DECORATE.afterStart(span); + DECORATE.onConnection(span, thiz); + + final ProtocolCommand command = commandObject.getArguments().getCommand(); + + if (command instanceof Protocol.Command) { + DECORATE.onStatement(span, ((Protocol.Command) command).name()); + } else { + DECORATE.onStatement(span, new String(command.getRaw())); + } + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + DECORATE.onError(scope.span(), throwable); + DECORATE.beforeFinish(scope.span()); + scope.close(); + scope.span().finish(); + } + } +} diff --git a/dd-java-agent/instrumentation/valkey-java/src/main/java/io/valkey/ValkeyClientDecorator.java b/dd-java-agent/instrumentation/valkey-java/src/main/java/io/valkey/ValkeyClientDecorator.java new file mode 100644 index 00000000000..0e0e6cf43bf --- /dev/null +++ b/dd-java-agent/instrumentation/valkey-java/src/main/java/io/valkey/ValkeyClientDecorator.java @@ -0,0 +1,57 @@ +package io.valkey; + +import datadog.trace.api.naming.SpanNaming; +import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.decorator.DBTypeProcessingDatabaseClientDecorator; + +public class ValkeyClientDecorator extends DBTypeProcessingDatabaseClientDecorator { + public static final ValkeyClientDecorator DECORATE = new ValkeyClientDecorator(); + + private static final String VALKEY = "valkey"; + public static final CharSequence OPERATION_NAME = + UTF8BytesString.create(SpanNaming.instance().namingSchema().cache().operation(VALKEY)); + private static final String SERVICE_NAME = + SpanNaming.instance().namingSchema().cache().service(VALKEY); + private static final CharSequence COMPONENT_NAME = UTF8BytesString.create("valkey-command"); + + @Override + protected String[] instrumentationNames() { + return new String[] {"valkey", VALKEY}; + } + + @Override + protected String service() { + return SERVICE_NAME; + } + + @Override + protected CharSequence component() { + return COMPONENT_NAME; + } + + @Override + protected CharSequence spanType() { + return InternalSpanTypes.VALKEY; + } + + @Override + protected String dbType() { + return VALKEY; + } + + @Override + protected String dbUser(final Connection connection) { + return null; + } + + @Override + protected String dbInstance(final Connection connection) { + return null; + } + + @Override + protected String dbHostname(Connection connection) { + return connection.getHostAndPort().getHost(); + } +} diff --git a/dd-java-agent/instrumentation/valkey-java/src/test/groovy/ValkeyClientTest.groovy b/dd-java-agent/instrumentation/valkey-java/src/test/groovy/ValkeyClientTest.groovy new file mode 100644 index 00000000000..7d620699fb3 --- /dev/null +++ b/dd-java-agent/instrumentation/valkey-java/src/test/groovy/ValkeyClientTest.groovy @@ -0,0 +1,358 @@ +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace +import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_INSTANCE +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan + +import datadog.trace.agent.test.naming.VersionedNamingTestBase +import datadog.trace.agent.test.utils.PortUtils +import datadog.trace.api.Config +import datadog.trace.api.DDSpanTypes +import datadog.trace.bootstrap.instrumentation.api.Tags +import io.valkey.Jedis +import redis.embedded.RedisServer + +import spock.lang.Shared + +abstract class ValkeyClientTest extends VersionedNamingTestBase { + + @Shared + int port = PortUtils.randomOpenPort() + + @Shared + RedisServer redisServer = RedisServer.newRedisServer() + .port(port) + .setting("bind 127.0.0.1") // good for local development on Windows to prevent security popups + .setting("maxmemory 128M") + .build() + + @Shared + Jedis jedis = new Jedis("localhost", port) + + @Override + void configurePreAgent() { + super.configurePreAgent() + + injectSysConfig(DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "true") + } + + def setupSpec() { + redisServer.start() + } + + def cleanupSpec() { + redisServer.stop() + jedis.close() + } + + def setup() { + def cleanupSpan = runUnderTrace("cleanup") { + jedis.flushAll() + activeSpan() + } + TEST_WRITER.waitUntilReported(cleanupSpan) + TEST_WRITER.start() + } + + def "set command"() { + when: + jedis.set("foo", "bar") + + then: + assertTraces(1) { + trace(1) { + span { + serviceName service() + operationName operation() + resourceName "SET" + spanType DDSpanTypes.VALKEY + measured true + tags { + "$Tags.COMPONENT" "valkey-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "valkey" + "$Tags.PEER_HOSTNAME" "localhost" + peerServiceFrom(Tags.PEER_HOSTNAME) + defaultTags() + } + } + } + } + } + + def "get command"() { + when: + jedis.set("foo", "bar") + def value = jedis.get("foo") + + then: + value == "bar" + + assertTraces(2) { + trace(1) { + span { + serviceName service() + operationName operation() + resourceName "SET" + spanType DDSpanTypes.VALKEY + tags { + "$Tags.COMPONENT" "valkey-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "valkey" + "$Tags.PEER_HOSTNAME" "localhost" + peerServiceFrom(Tags.PEER_HOSTNAME) + defaultTags() + } + } + } + trace(1) { + span { + serviceName service() + operationName operation() + resourceName "GET" + spanType DDSpanTypes.VALKEY + measured true + tags { + "$Tags.COMPONENT" "valkey-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "valkey" + "$Tags.PEER_HOSTNAME" "localhost" + peerServiceFrom(Tags.PEER_HOSTNAME) + defaultTags() + } + } + } + } + } + + def "command with no arguments"() { + when: + jedis.set("foo", "bar") + def value = jedis.randomKey() + + then: + value == "foo" + + assertTraces(2) { + trace(1) { + span { + serviceName service() + operationName operation() + resourceName "SET" + spanType DDSpanTypes.VALKEY + measured true + tags { + "$Tags.COMPONENT" "valkey-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "valkey" + "$Tags.PEER_HOSTNAME" "localhost" + peerServiceFrom(Tags.PEER_HOSTNAME) + defaultTags() + } + } + } + trace(1) { + span { + serviceName service() + operationName operation() + resourceName "RANDOMKEY" + spanType DDSpanTypes.VALKEY + measured true + tags { + "$Tags.COMPONENT" "valkey-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "valkey" + "$Tags.PEER_HOSTNAME" "localhost" + peerServiceFrom(Tags.PEER_HOSTNAME) + defaultTags() + } + } + } + } + } + + def "hmset and hgetAll commands"() { + when: + + Map h = new HashMap<>() + h.put("key1", "value1") + h.put("key2", "value2") + jedis.hmset("map", h) + + Map result = jedis.hgetAll("map") + + then: + result != null + result == h + + assertTraces(2) { + trace(1) { + span { + serviceName service() + operationName operation() + resourceName "HMSET" + spanType DDSpanTypes.VALKEY + tags { + "$Tags.COMPONENT" "valkey-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "valkey" + "$Tags.PEER_HOSTNAME" "localhost" + peerServiceFrom(Tags.PEER_HOSTNAME) + defaultTags() + } + } + } + trace(1) { + span { + serviceName service() + operationName operation() + resourceName "HGETALL" + spanType DDSpanTypes.VALKEY + tags { + "$Tags.COMPONENT" "valkey-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "valkey" + "$Tags.PEER_HOSTNAME" "localhost" + peerServiceFrom(Tags.PEER_HOSTNAME) + defaultTags() + } + } + } + } + } + + def "zadd and zrangeByScore commands"() { + when: + jedis.zadd("foo", 1d, "a") + jedis.zadd("foo", 10d, "b") + jedis.zadd("foo", 0.1d, "c") + jedis.zadd("foo", 2d, "d") + + Set verify = new HashSet() + verify.add("a") + verify.add("c") + verify.add("d") + Set val = jedis.zrangeByScore("foo", 0d, 2d) + + then: + val != null + val == verify + + assertTraces(5) { + trace(1) { + span { + serviceName service() + operationName operation() + resourceName "ZADD" + spanType DDSpanTypes.VALKEY + tags { + "$Tags.COMPONENT" "valkey-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "valkey" + "$Tags.PEER_HOSTNAME" "localhost" + peerServiceFrom(Tags.PEER_HOSTNAME) + defaultTags() + } + } + } + trace(1) { + span { + serviceName service() + operationName operation() + resourceName "ZADD" + spanType DDSpanTypes.VALKEY + tags { + "$Tags.COMPONENT" "valkey-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "valkey" + "$Tags.PEER_HOSTNAME" "localhost" + peerServiceFrom(Tags.PEER_HOSTNAME) + defaultTags() + } + } + } + trace(1) { + span { + serviceName service() + operationName operation() + resourceName "ZADD" + spanType DDSpanTypes.VALKEY + tags { + "$Tags.COMPONENT" "valkey-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "valkey" + "$Tags.PEER_HOSTNAME" "localhost" + peerServiceFrom(Tags.PEER_HOSTNAME) + defaultTags() + } + } + } + trace(1) { + span { + serviceName service() + operationName operation() + resourceName "ZADD" + spanType DDSpanTypes.VALKEY + tags { + "$Tags.COMPONENT" "valkey-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "valkey" + "$Tags.PEER_HOSTNAME" "localhost" + peerServiceFrom(Tags.PEER_HOSTNAME) + defaultTags() + } + } + } + trace(1) { + span { + serviceName service() + operationName operation() + resourceName "ZRANGEBYSCORE" + spanType DDSpanTypes.VALKEY + tags { + "$Tags.COMPONENT" "valkey-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "valkey" + "$Tags.PEER_HOSTNAME" "localhost" + peerServiceFrom(Tags.PEER_HOSTNAME) + defaultTags() + } + } + } + } + } +} + +class ValkeyClientV0ForkedTest extends ValkeyClientTest { + + @Override + int version() { + return 0 + } + + @Override + String service() { + return "valkey" + } + + @Override + String operation() { + return "valkey.query" + } +} + +class ValkeyClientV1ForkedTest extends ValkeyClientTest { + + @Override + int version() { + return 1 + } + + @Override + String service() { + return Config.get().getServiceName() + } + + @Override + String operation() { + return "valkey.command" + } +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/DDSpanTypes.java b/dd-trace-api/src/main/java/datadog/trace/api/DDSpanTypes.java index 908421d8538..a0118b62899 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/DDSpanTypes.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/DDSpanTypes.java @@ -36,4 +36,5 @@ public class DDSpanTypes { public static final String PROTOBUF = "protobuf"; public static final String MULE = "mule"; + public static final String VALKEY = "valkey"; } diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InternalSpanTypes.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InternalSpanTypes.java index 38944151d45..6fc420b9e9a 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InternalSpanTypes.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InternalSpanTypes.java @@ -48,4 +48,5 @@ public class InternalSpanTypes { public static final UTF8BytesString TIBCO_BW = UTF8BytesString.create("tibco_bw"); public static final UTF8BytesString MULE = UTF8BytesString.create(DDSpanTypes.MULE); + public static final CharSequence VALKEY = UTF8BytesString.create(DDSpanTypes.VALKEY); } diff --git a/settings.gradle b/settings.gradle index a6d1c0a2090..182d671a46c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -492,6 +492,7 @@ include ':dd-java-agent:instrumentation:unbescape' include ':dd-java-agent:instrumentation:undertow' include ':dd-java-agent:instrumentation:undertow:undertow-2.0' include ':dd-java-agent:instrumentation:undertow:undertow-2.2' +include ':dd-java-agent:instrumentation:valkey-java' include ':dd-java-agent:instrumentation:velocity' include ':dd-java-agent:instrumentation:vertx-mysql-client-3.9' include ':dd-java-agent:instrumentation:vertx-mysql-client-4.0'