diff --git a/dd-java-agent/instrumentation/rediscala-1.8.0/rediscala-1.8.0.gradle b/dd-java-agent/instrumentation/rediscala-1.8.0/rediscala-1.8.0.gradle new file mode 100644 index 00000000000..fc136888437 --- /dev/null +++ b/dd-java-agent/instrumentation/rediscala-1.8.0/rediscala-1.8.0.gradle @@ -0,0 +1,66 @@ +ext { + minJavaVersionForTests = JavaVersion.VERSION_1_8 +} + +muzzle { + pass { + group = "com.github.etaty" + module = "rediscala_2.11" + versions = "[1.5.0,)" + assertInverse = true + } + + pass { + group = "com.github.etaty" + module = "rediscala_2.12" + versions = "[1.8.0,)" + assertInverse = true + } + + pass { + group = "com.github.etaty" + module = "rediscala_2.13" + versions = "[1.9.0,)" + assertInverse = true + } + + pass { + group = "com.github.Ma27" + module = "rediscala_2.11" + versions = "[1.8.1,)" + assertInverse = true + } + + pass { + group = "com.github.Ma27" + module = "rediscala_2.12" + versions = "[1.8.1,)" + assertInverse = true + } + + pass { + group = "com.github.Ma27" + module = "rediscala_2.13" + versions = "[1.9.0,)" + assertInverse = true + } +} + +apply from: "${rootDir}/gradle/java.gradle" + +apply plugin: 'org.unbroken-dome.test-sets' + +testSets { + latestDepTest { + dirName = 'test' + } +} + +dependencies { + compileOnly group: 'com.github.etaty', name: 'rediscala_2.11', version: '1.8.0' + + testCompile group: 'com.github.etaty', name: 'rediscala_2.11', version: '1.8.0' + testCompile group: 'com.github.kstyrc', name: 'embedded-redis', version: '0.6' + + latestDepTestCompile group: 'com.github.etaty', name: 'rediscala_2.11', version: '+' +} diff --git a/dd-java-agent/instrumentation/rediscala-1.8.0/src/main/java/datadog/trace/instrumentation/rediscala/RediscalaClientDecorator.java b/dd-java-agent/instrumentation/rediscala-1.8.0/src/main/java/datadog/trace/instrumentation/rediscala/RediscalaClientDecorator.java new file mode 100644 index 00000000000..dbbdd9fd2fd --- /dev/null +++ b/dd-java-agent/instrumentation/rediscala-1.8.0/src/main/java/datadog/trace/instrumentation/rediscala/RediscalaClientDecorator.java @@ -0,0 +1,50 @@ +package datadog.trace.instrumentation.rediscala; + +import datadog.trace.api.DDSpanTypes; +import datadog.trace.bootstrap.instrumentation.decorator.DatabaseClientDecorator; +import redis.RedisCommand; +import redis.protocol.RedisReply; + +public class RediscalaClientDecorator + extends DatabaseClientDecorator> { + + private static final String SERVICE_NAME = "redis"; + private static final String COMPONENT_NAME = SERVICE_NAME + "-command"; + + public static final RediscalaClientDecorator DECORATE = new RediscalaClientDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"rediscala", "redis"}; + } + + @Override + protected String service() { + return SERVICE_NAME; + } + + @Override + protected String component() { + return COMPONENT_NAME; + } + + @Override + protected String spanType() { + return DDSpanTypes.REDIS; + } + + @Override + protected String dbType() { + return "redis"; + } + + @Override + protected String dbUser(final RedisCommand session) { + return null; + } + + @Override + protected String dbInstance(final RedisCommand session) { + return null; + } +} diff --git a/dd-java-agent/instrumentation/rediscala-1.8.0/src/main/java/datadog/trace/instrumentation/rediscala/RediscalaInstrumentation.java b/dd-java-agent/instrumentation/rediscala-1.8.0/src/main/java/datadog/trace/instrumentation/rediscala/RediscalaInstrumentation.java new file mode 100644 index 00000000000..ba0f40f56ec --- /dev/null +++ b/dd-java-agent/instrumentation/rediscala-1.8.0/src/main/java/datadog/trace/instrumentation/rediscala/RediscalaInstrumentation.java @@ -0,0 +1,122 @@ +package datadog.trace.instrumentation.rediscala; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.DDElementMatchers.safeHasSuperType; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeScope; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.rediscala.RediscalaClientDecorator.DECORATE; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.context.TraceScope; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import redis.RedisCommand; +import scala.concurrent.ExecutionContext; +import scala.concurrent.Future; +import scala.runtime.AbstractFunction1; +import scala.util.Try; + +@AutoService(Instrumenter.class) +public final class RediscalaInstrumentation extends Instrumenter.Default { + + public RediscalaInstrumentation() { + super("rediscala", "redis"); + } + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType(named("redis.ActorRequest")) + .or(safeHasSuperType(named("redis.Request"))) + .or(safeHasSuperType(named("redis.BufferedRequest"))) + .or(safeHasSuperType(named("redis.RoundRobinPoolRequest"))); + } + + @Override + public String[] helperClassNames() { + return new String[] { + RediscalaInstrumentation.class.getName() + "$OnCompleteHandler", + "datadog.trace.bootstrap.instrumentation.decorator.BaseDecorator", + "datadog.trace.bootstrap.instrumentation.decorator.ClientDecorator", + "datadog.trace.bootstrap.instrumentation.decorator.DatabaseClientDecorator", + packageName + ".RediscalaClientDecorator", + }; + } + + @Override + public Map, String> transformers() { + return singletonMap( + isMethod() + .and(isPublic()) + .and(named("send")) + .and(takesArgument(0, named("redis.RedisCommand"))) + .and(returns(named("scala.concurrent.Future"))), + RediscalaInstrumentation.class.getName() + "$RediscalaAdvice"); + } + + public static class RediscalaAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope onEnter(@Advice.Argument(0) final RedisCommand cmd) { + final AgentSpan span = startSpan("redis.command"); + DECORATE.afterStart(span); + DECORATE.onStatement(span, cmd.getClass().getName()); + return activateSpan(span, true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, + @Advice.Thrown final Throwable throwable, + @Advice.FieldValue("executionContext") final ExecutionContext ctx, + @Advice.Return(readOnly = false) final Future responseFuture) { + + final AgentSpan span = scope.span(); + + if (throwable == null) { + responseFuture.onComplete(new OnCompleteHandler(span), ctx); + } else { + DECORATE.onError(span, throwable); + DECORATE.beforeFinish(span); + span.finish(); + } + scope.close(); + } + } + + public static class OnCompleteHandler extends AbstractFunction1, Void> { + private final AgentSpan span; + + public OnCompleteHandler(final AgentSpan span) { + this.span = span; + } + + @Override + public Void apply(final Try result) { + try { + if (result.isFailure()) { + DECORATE.onError(span, result.failed().get()); + } + DECORATE.beforeFinish(span); + final TraceScope scope = activeScope(); + if (scope != null) { + scope.setAsyncPropagation(false); + } + } finally { + span.finish(); + } + return null; + } + } +} diff --git a/dd-java-agent/instrumentation/rediscala-1.8.0/src/test/groovy/RediscalaClientTest.groovy b/dd-java-agent/instrumentation/rediscala-1.8.0/src/test/groovy/RediscalaClientTest.groovy new file mode 100644 index 00000000000..14e35138e61 --- /dev/null +++ b/dd-java-agent/instrumentation/rediscala-1.8.0/src/test/groovy/RediscalaClientTest.groovy @@ -0,0 +1,140 @@ +import akka.actor.ActorSystem +import datadog.trace.agent.test.AgentTestRunner +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 redis.ByteStringSerializerLowPriority +import redis.ByteStringDeserializerDefault +import redis.RedisClient +import redis.RedisDispatcher +import redis.embedded.RedisServer +import scala.Option +import scala.concurrent.Await +import scala.concurrent.duration.Duration +import spock.lang.Shared + +class RediscalaClientTest extends AgentTestRunner { + + @Shared + int port = PortUtils.randomOpenPort() + + @Shared + RedisServer redisServer = RedisServer.builder() + // bind to localhost to avoid firewall popup + .setting("bind 127.0.0.1") + // set max memory to avoid problems in CI + .setting("maxmemory 128M") + .port(port).build() + + @Shared + ActorSystem system + + @Shared + RedisClient redisClient + + def setupSpec() { + system = ActorSystem.create() + redisClient = new RedisClient("localhost", + port, + Option.apply(null), + Option.apply(null), + "RedisClient", + Option.apply(null), + system, + new RedisDispatcher("rediscala.rediscala-client-worker-dispatcher")) + + println "Using redis: $redisServer.args" + redisServer.start() + + // This setting should have no effect since decorator returns null for the instance. + System.setProperty(Config.PREFIX + Config.DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "true") + } + + def cleanupSpec() { + redisServer.stop() + system?.terminate() + System.clearProperty(Config.PREFIX + Config.DB_CLIENT_HOST_SPLIT_BY_INSTANCE) + } + + def setup() { + TEST_WRITER.start() + } + + def "set command"() { + when: + def value = redisClient.set("foo", + "bar", + Option.apply(null), + Option.apply(null), + false, + false, + new ByteStringSerializerLowPriority.String$()) + + + then: + Await.result(value, Duration.apply("3 second")) == true + assertTraces(1) { + trace(0, 1) { + span(0) { + serviceName "redis" + operationName "redis.query" + resourceName "redis.api.strings.Set" + spanType DDSpanTypes.REDIS + tags { + "$Tags.COMPONENT" "redis-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "redis" + defaultTags() + } + } + } + } + } + + def "get command"() { + when: + def write = redisClient.set("bar", + "baz", + Option.apply(null), + Option.apply(null), + false, + false, + new ByteStringSerializerLowPriority.String$()) + def value = redisClient.get("bar", new ByteStringDeserializerDefault.String$()) + + then: + Await.result(write, Duration.apply("3 second")) == true + Await.result(value, Duration.apply("3 second")) == Option.apply("baz") + assertTraces(2) { + trace(0, 1) { + span(0) { + serviceName "redis" + operationName "redis.query" + resourceName "redis.api.strings.Set" + spanType DDSpanTypes.REDIS + tags { + "$Tags.COMPONENT" "redis-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "redis" + defaultTags() + } + } + } + trace(1, 1) { + span(0) { + serviceName "redis" + operationName "redis.query" + resourceName "redis.api.strings.Get" + spanType DDSpanTypes.REDIS + tags { + "$Tags.COMPONENT" "redis-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "redis" + defaultTags() + } + } + } + } + } +} diff --git a/settings.gradle b/settings.gradle index 6e7e67050a2..70e04f45663 100644 --- a/settings.gradle +++ b/settings.gradle @@ -126,6 +126,7 @@ include ':dd-java-agent:instrumentation:play-ws-2' include ':dd-java-agent:instrumentation:play-ws-2.1' include ':dd-java-agent:instrumentation:rabbitmq-amqp-2.7' include ':dd-java-agent:instrumentation:ratpack-1.4' +include ':dd-java-agent:instrumentation:rediscala-1.8.0' include ':dd-java-agent:instrumentation:reactor-core-3.1' include ':dd-java-agent:instrumentation:rmi' include ':dd-java-agent:instrumentation:rxjava-1'