Jedis 4 instrumentation
MODIFY
instrumentation/jedis/jedis-1.4/javaagent/build.gradle.kts
instrumentation/jedis/jedis-1.4/javaagent/build.gradle.kts
@@ -17,6 +17,9 @@ dependencies {
compileOnly("com.google.auto.value:auto-value-annotations")
annotationProcessor("com.google.auto.value:auto-value")
+ testInstrumentation(project(":instrumentation:jedis:jedis-3.0:javaagent"))
+ testInstrumentation(project(":instrumentation:jedis:jedis-4.0:javaagent"))
+
// Jedis 3.0 has API changes that prevent instrumentation from applying
latestDepTestLibrary("redis.clients:jedis:2.+")
}

MODIFY
instrumentation/jedis/jedis-3.0/javaagent/build.gradle.kts
instrumentation/jedis/jedis-3.0/javaagent/build.gradle.kts
@@ -3,18 +3,12 @@ plugins {
}
muzzle {
- fail {
- group.set("redis.clients")
- module.set("jedis")
- versions.set("[,3.0.0)")
- // TODO: remove this once jedis people release the new version correctly
- skip("jedis-3.6.2")
- }
-
pass {
group.set("redis.clients")
module.set("jedis")
- versions.set("[3.0.0,)")
+ versions.set("[3.0.0,4)")
+ skip("jedis-3.6.2")
+ assertInverse.set(true)
}
}
@@ -28,7 +22,7 @@ dependencies {
// the tests in the event it does. The tests will end up with double spans
testInstrumentation(project(":instrumentation:jedis:jedis-1.4:javaagent"))
- testLibrary("redis.clients:jedis:3.+")
+ latestDepTestLibrary("redis.clients:jedis:3.+")
}
tasks {

MODIFY
instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisInstrumentationModule.java
instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisInstrumentationModule.java
package io.opentelemetry.javaagent.instrumentation.jedis.v3_0;
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static java.util.Collections.singletonList;
+import static net.bytebuddy.matcher.ElementMatchers.not;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;
+import net.bytebuddy.matcher.ElementMatcher;
@AutoService(InstrumentationModule.class)
public class JedisInstrumentationModule extends InstrumentationModule {
@@ -19,6 +22,11 @@ public class JedisInstrumentationModule extends InstrumentationModule {
super("jedis", "jedis-3.0");
}
+ @Override
+ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
+ return not(hasClassesNamed("redis.clients.jedis.CommandArguments"));
+ }
+
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new JedisConnectionInstrumentation());

ADD
instrumentation/jedis/jedis-4.0/javaagent/build.gradle.kts
+plugins {
+ id("otel.javaagent-instrumentation")
+}
+
+muzzle {
+ pass {
+ group.set("redis.clients")
+ module.set("jedis")
+ versions.set("[4.0.0-beta1,)")
+ skip("jedis-3.6.2")
+ assertInverse.set(true)
+ }
+}
+
+dependencies {
+ library("redis.clients:jedis:4.0.0-beta1")
+
+ compileOnly("com.google.auto.value:auto-value-annotations")
+ annotationProcessor("com.google.auto.value:auto-value")
+
+ testInstrumentation(project(":instrumentation:jedis:jedis-1.4:javaagent"))
+ testInstrumentation(project(":instrumentation:jedis:jedis-3.0:javaagent"))
+}
+
+tasks {
+ test {
+ usesService(gradle.sharedServices.registrations["testcontainersBuildService"].getService())
+ }
+}

ADD
instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisConnectionInstrumentation.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jedis.v4_0;
+
+import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext;
+import static io.opentelemetry.javaagent.instrumentation.jedis.v4_0.JedisSingletons.instrumenter;
+import static java.util.Arrays.asList;
+import static net.bytebuddy.matcher.ElementMatchers.is;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import java.net.Socket;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import redis.clients.jedis.CommandArguments;
+import redis.clients.jedis.commands.ProtocolCommand;
+
+public class JedisConnectionInstrumentation implements TypeInstrumentation {
+ @Override
+ public ElementMatcher<TypeDescription> typeMatcher() {
+ return named("redis.clients.jedis.Connection");
+ }
+
+ @Override
+ public void transform(TypeTransformer transformer) {
+ transformer.applyAdviceToMethod(
+ isMethod()
+ .and(named("sendCommand"))
+ .and(takesArguments(1))
+ .and(takesArgument(0, named("redis.clients.jedis.CommandArguments"))),
+ this.getClass().getName() + "$SendCommand2Advice");
+
+ transformer.applyAdviceToMethod(
+ isMethod()
+ .and(named("sendCommand"))
+ .and(takesArguments(2))
+ .and(takesArgument(0, named("redis.clients.jedis.commands.ProtocolCommand")))
+ .and(takesArgument(1, is(byte[][].class))),
+ this.getClass().getName() + "$SendCommandAdvice");
+ // FIXME: This instrumentation only incorporates sending the command, not processing the result.
+ }
+
+ @SuppressWarnings("unused")
+ public static class SendCommandAdvice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void onEnter(
+ @Advice.Argument(0) ProtocolCommand command,
+ @Advice.Argument(1) byte[][] args,
+ @Advice.Local("otelJedisRequest") JedisRequest request,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ Context parentContext = currentContext();
+ request = JedisRequest.create(command, asList(args));
+ if (!instrumenter().shouldStart(parentContext, request)) {
+ return;
+ }
+
+ context = instrumenter().start(parentContext, request);
+ scope = context.makeCurrent();
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void stopSpan(
+ @Advice.FieldValue("socket") Socket socket,
+ @Advice.Thrown Throwable throwable,
+ @Advice.Local("otelJedisRequest") JedisRequest request,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ if (scope == null) {
+ return;
+ }
+
+ request.setSocket(socket);
+
+ scope.close();
+ instrumenter().end(context, request, null, throwable);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static class SendCommand2Advice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void onEnter(
+ @Advice.Argument(0) CommandArguments command,
+ @Advice.Local("otelJedisRequest") JedisRequest request,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ Context parentContext = currentContext();
+ request = JedisRequest.create(command);
+ if (!instrumenter().shouldStart(parentContext, request)) {
+ return;
+ }
+
+ context = instrumenter().start(parentContext, request);
+ scope = context.makeCurrent();
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void stopSpan(
+ @Advice.FieldValue("socket") Socket socket,
+ @Advice.Thrown Throwable throwable,
+ @Advice.Local("otelJedisRequest") JedisRequest request,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ if (scope == null) {
+ return;
+ }
+
+ request.setSocket(socket);
+
+ scope.close();
+ instrumenter().end(context, request, null, throwable);
+ }
+ }
+}

ADD
instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisDbAttributesExtractor.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jedis.v4_0;
+
+import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+import javax.annotation.Nullable;
+
+final class JedisDbAttributesExtractor extends DbAttributesExtractor<JedisRequest, Void> {
+ @Override
+ protected String system(JedisRequest request) {
+ return SemanticAttributes.DbSystemValues.REDIS;
+ }
+
+ @Override
+ @Nullable
+ protected String user(JedisRequest request) {
+ return null;
+ }
+
+ @Override
+ protected String name(JedisRequest request) {
+ return null;
+ }
+
+ @Override
+ protected String connectionString(JedisRequest request) {
+ return null;
+ }
+
+ @Override
+ protected String statement(JedisRequest request) {
+ return request.getStatement();
+ }
+
+ @Override
+ protected String operation(JedisRequest request) {
+ return request.getOperation();
+ }
+}

ADD
instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisInstrumentationModule.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jedis.v4_0;
+
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
+import static java.util.Collections.singletonList;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import java.util.List;
+import net.bytebuddy.matcher.ElementMatcher;
+
+@AutoService(InstrumentationModule.class)
+public class JedisInstrumentationModule extends InstrumentationModule {
+
+ public JedisInstrumentationModule() {
+ super("jedis", "jedis-4.0");
+ }
+
+ @Override
+ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
+ return hasClassesNamed("redis.clients.jedis.CommandArguments");
+ }
+
+ @Override
+ public List<TypeInstrumentation> typeInstrumentations() {
+ return singletonList(new JedisConnectionInstrumentation());
+ }
+}

ADD
instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisNetAttributesExtractor.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jedis.v4_0;
+
+import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetClientAttributesExtractor;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import javax.annotation.Nullable;
+
+final class JedisNetAttributesExtractor
+ extends InetSocketAddressNetClientAttributesExtractor<JedisRequest, Void> {
+
+ @Override
+ @Nullable
+ public InetSocketAddress getAddress(JedisRequest jedisRequest, @Nullable Void unused) {
+ SocketAddress socketAddress = jedisRequest.getRemoteSocketAddress();
+ if (socketAddress != null && socketAddress instanceof InetSocketAddress) {
+ return (InetSocketAddress) socketAddress;
+ }
+ return null;
+ }
+
+ @Override
+ public String transport(JedisRequest jedisRequest, @Nullable Void unused) {
+ return SemanticAttributes.NetTransportValues.IP_TCP;
+ }
+}

ADD
instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisRequest.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jedis.v4_0;
+
+import com.google.auto.value.AutoValue;
+import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import redis.clients.jedis.CommandArguments;
+import redis.clients.jedis.Protocol;
+import redis.clients.jedis.args.Rawable;
+import redis.clients.jedis.commands.ProtocolCommand;
+
+@AutoValue
+public abstract class JedisRequest {
+
+ public static JedisRequest create(ProtocolCommand command, List<byte[]> args) {
+ return new AutoValue_JedisRequest(command, args);
+ }
+
+ public static JedisRequest create(CommandArguments commandArguments) {
+ ProtocolCommand command = commandArguments.getCommand();
+ List<byte[]> arguments = new ArrayList<>();
+ boolean first = true;
+ for (Rawable rawable : commandArguments) {
+ if (first) {
+ first = false;
+ continue;
+ }
+ arguments.add(rawable.getRaw());
+ }
+ return create(command, arguments);
+ }
+
+ public abstract ProtocolCommand getCommand();
+
+ public abstract List<byte[]> getArgs();
+
+ public String getOperation() {
+ ProtocolCommand command = getCommand();
+ if (command instanceof Protocol.Command) {
+ return ((Protocol.Command) command).name();
+ } else {
+ // Protocol.Command is the only implementation in the Jedis lib as of 3.1 but this will save
+ // us if that changes
+ return new String(command.getRaw(), StandardCharsets.UTF_8);
+ }
+ }
+
+ public String getStatement() {
+ return RedisCommandSanitizer.sanitize(getOperation(), getArgs());
+ }
+
+ private SocketAddress remoteSocketAddress;
+
+ public void setSocket(Socket socket) {
+ if (socket != null) {
+ remoteSocketAddress = socket.getRemoteSocketAddress();
+ }
+ }
+
+ public SocketAddress getRemoteSocketAddress() {
+ return remoteSocketAddress;
+ }
+}

ADD
instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisSingletons.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jedis.v4_0;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.instrumentation.api.instrumenter.PeerServiceAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.db.DbSpanNameExtractor;
+
+public final class JedisSingletons {
+ private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jedis-3.0";
+
+ private static final Instrumenter<JedisRequest, Void> INSTRUMENTER;
+
+ static {
+ DbAttributesExtractor<JedisRequest, Void> attributesExtractor =
+ new JedisDbAttributesExtractor();
+ SpanNameExtractor<JedisRequest> spanName = DbSpanNameExtractor.create(attributesExtractor);
+ JedisNetAttributesExtractor netAttributesExtractor = new JedisNetAttributesExtractor();
+
+ INSTRUMENTER =
+ Instrumenter.<JedisRequest, Void>builder(
+ GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName)
+ .addAttributesExtractor(attributesExtractor)
+ .addAttributesExtractor(netAttributesExtractor)
+ .addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor))
+ .newInstrumenter(SpanKindExtractor.alwaysClient());
+ }
+
+ public static Instrumenter<JedisRequest, Void> instrumenter() {
+ return INSTRUMENTER;
+ }
+
+ private JedisSingletons() {}
+}

ADD
instrumentation/jedis/jedis-4.0/javaagent/src/test/groovy/Jedis40ClientTest.groovy
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
+import org.testcontainers.containers.GenericContainer
+import redis.clients.jedis.Jedis
+import spock.lang.Shared
+
+import static io.opentelemetry.api.trace.SpanKind.CLIENT
+
+class Jedis40ClientTest extends AgentInstrumentationSpecification {
+
+ private static GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379)
+
+ @Shared
+ int port
+
+ @Shared
+ Jedis jedis
+
+ def setupSpec() {
+ redisServer.start()
+ port = redisServer.getMappedPort(6379)
+ jedis = new Jedis("localhost", port)
+ }
+
+ def cleanupSpec() {
+ redisServer.stop()
+ jedis.close()
+ }
+
+ def setup() {
+ jedis.flushAll()
+ clearExportedData()
+ }
+
+ def "set command"() {
+ when:
+ jedis.set("foo", "bar")
+
+ then:
+ assertTraces(1) {
+ trace(0, 1) {
+ span(0) {
+ name "SET"
+ kind CLIENT
+ attributes {
+ "$SemanticAttributes.DB_SYSTEM.key" "redis"
+ "$SemanticAttributes.DB_STATEMENT.key" "SET foo ?"
+ "$SemanticAttributes.DB_OPERATION.key" "SET"
+ "$SemanticAttributes.NET_PEER_NAME.key" "localhost"
+ "$SemanticAttributes.NET_PEER_PORT.key" port
+ "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
+ "${SemanticAttributes.NET_TRANSPORT}" SemanticAttributes.NetTransportValues.IP_TCP
+ }
+ }
+ }
+ }
+ }
+
+ def "get command"() {
+ when:
+ jedis.set("foo", "bar")
+ def value = jedis.get("foo")
+
+ then:
+ value == "bar"
+
+ assertTraces(2) {
+ trace(0, 1) {
+ span(0) {
+ name "SET"
+ kind CLIENT
+ attributes {
+ "$SemanticAttributes.DB_SYSTEM.key" "redis"
+ "$SemanticAttributes.DB_STATEMENT.key" "SET foo ?"
+ "$SemanticAttributes.DB_OPERATION.key" "SET"
+ "$SemanticAttributes.NET_PEER_NAME.key" "localhost"
+ "$SemanticAttributes.NET_PEER_PORT.key" port
+ "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
+ "${SemanticAttributes.NET_TRANSPORT}" SemanticAttributes.NetTransportValues.IP_TCP
+ }
+ }
+ }
+ trace(1, 1) {
+ span(0) {
+ name "GET"
+ kind CLIENT
+ attributes {
+ "$SemanticAttributes.DB_SYSTEM.key" "redis"
+ "$SemanticAttributes.DB_STATEMENT.key" "GET foo"
+ "$SemanticAttributes.DB_OPERATION.key" "GET"
+ "$SemanticAttributes.NET_PEER_NAME.key" "localhost"
+ "$SemanticAttributes.NET_PEER_PORT.key" port
+ "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
+ "${SemanticAttributes.NET_TRANSPORT}" SemanticAttributes.NetTransportValues.IP_TCP
+ }
+ }
+ }
+ }
+ }
+
+ def "command with no arguments"() {
+ when:
+ jedis.set("foo", "bar")
+ def value = jedis.randomKey()
+
+ then:
+ value == "foo"
+
+ assertTraces(2) {
+ trace(0, 1) {
+ span(0) {
+ name "SET"
+ kind CLIENT
+ attributes {
+ "$SemanticAttributes.DB_SYSTEM.key" "redis"
+ "$SemanticAttributes.DB_STATEMENT.key" "SET foo ?"
+ "$SemanticAttributes.DB_OPERATION.key" "SET"
+ "$SemanticAttributes.NET_PEER_NAME.key" "localhost"
+ "$SemanticAttributes.NET_PEER_PORT.key" port
+ "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
+ "${SemanticAttributes.NET_TRANSPORT}" SemanticAttributes.NetTransportValues.IP_TCP
+ }
+ }
+ }
+ trace(1, 1) {
+ span(0) {
+ name "RANDOMKEY"
+ kind CLIENT
+ attributes {
+ "$SemanticAttributes.DB_SYSTEM.key" "redis"
+ "$SemanticAttributes.DB_STATEMENT.key" "RANDOMKEY"
+ "$SemanticAttributes.DB_OPERATION.key" "RANDOMKEY"
+ "$SemanticAttributes.NET_PEER_NAME.key" "localhost"
+ "$SemanticAttributes.NET_PEER_PORT.key" port
+ "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
+ "${SemanticAttributes.NET_TRANSPORT}" SemanticAttributes.NetTransportValues.IP_TCP
+ }
+ }
+ }
+ }
+ }
+}

MODIFY
settings.gradle.kts
settings.gradle.kts
@@ -205,6 +205,7 @@ include(":instrumentation:jdbc:library")
include(":instrumentation:jdbc:testing")
include(":instrumentation:jedis:jedis-1.4:javaagent")
include(":instrumentation:jedis:jedis-3.0:javaagent")
+include(":instrumentation:jedis:jedis-4.0:javaagent")
include(":instrumentation:jetty:jetty-8.0:javaagent")
include(":instrumentation:jetty:jetty-11.0:javaagent")
include(":instrumentation:jetty:jetty-common:javaagent")

Jedis 4 instrumentation (#4555)