From 47e2eca16b599409845936f206086833b6643f77 Mon Sep 17 00:00:00 2001 From: Yury Gribkov Date: Mon, 29 Jun 2026 18:50:07 -0700 Subject: [PATCH 1/7] test(lettuce5): reproduce the missing hostname on the lettuce5 static master/replica command span --- .../java/Lettuce5StaticMasterReplicaTest.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5StaticMasterReplicaTest.java diff --git a/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5StaticMasterReplicaTest.java b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5StaticMasterReplicaTest.java new file mode 100644 index 00000000000..27cbe972b8e --- /dev/null +++ b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5StaticMasterReplicaTest.java @@ -0,0 +1,98 @@ +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.redis.testcontainers.RedisContainer; +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.core.DDSpan; +import io.lettuce.core.ClientOptions; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.masterslave.MasterSlave; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +class Lettuce5StaticMasterReplicaTest extends AbstractInstrumentationTest { + private static final int DB_INDEX = 0; + private static final ClientOptions CLIENT_OPTIONS = + ClientOptions.builder().autoReconnect(false).build(); + + private RedisContainer redisServer; + private RedisClient redisClient; + private StatefulRedisConnection connection; + private String host; + private int port; + + @BeforeEach + void setUpRedis() throws Exception { + redisServer = + new RedisContainer(DockerImageName.parse("redis:6.2.6")) + .waitingFor(Wait.forListeningPort()); + redisServer.start(); + + host = redisServer.getHost(); + port = redisServer.getFirstMappedPort(); + + RedisURI redisURI = RedisURI.Builder.redis(host, port).withDatabase(DB_INDEX).build(); + redisClient = RedisClient.create(); + redisClient.setOptions(CLIENT_OPTIONS); + // Spring Data RedisStaticMasterReplicaConfiguration uses this static topology path on Lettuce + // 5. + connection = MasterSlave.connect(redisClient, StringCodec.UTF8, singletonList(redisURI)); + connection.sync().ping(); + + writer.waitForTraces(2); + tracer.flush(); + writer.clear(); + } + + @AfterEach + void cleanUpRedis() { + if (connection != null) { + connection.close(); + } + + if (redisClient != null) { + redisClient.shutdown(5, 10, TimeUnit.SECONDS); + } + + if (redisServer != null) { + redisServer.stop(); + } + } + + @Test + void staticMasterReplicaCommandSpanHasPeerHostname() throws Exception { + String result = connection.sync().set("TESTSETKEY", "TESTSETVAL"); + + assertEquals("OK", result); + writer.waitForTraces(1); + + List setSpans = new ArrayList<>(); + for (List trace : writer) { + for (DDSpan span : trace) { + if ("SET".contentEquals(span.getResourceName()) + && "redis-client".equals(String.valueOf(span.getTag(Tags.COMPONENT)))) { + setSpans.add(span); + } + } + } + + assertEquals(1, setSpans.size(), "expected exactly one SET command span"); + DDSpan span = setSpans.get(0); + assertEquals("SET", String.valueOf(span.getResourceName())); + assertEquals("redis-client", String.valueOf(span.getTag(Tags.COMPONENT))); + assertEquals("redis", span.getTag(Tags.DB_TYPE)); + assertNotNull(span.getTag(Tags.PEER_HOSTNAME), "command span should include peer.hostname"); + assertEquals(host, span.getTag(Tags.PEER_HOSTNAME)); + } +} From a2351d0726821a322cab9cc1b144a4e2172162ba Mon Sep 17 00:00:00 2001 From: Yury Gribkov Date: Tue, 30 Jun 2026 14:53:50 -0700 Subject: [PATCH 2/7] fix(lettuce5): decorate Redis command spans with resolved node connection for master-replica connetion providers Lettuce master/replica APIs expose a routing connection, while the concrete node connection is selected only when the command is dispatched. The existing command advice only sees the wrapper connection, so static master/replica routing can leave peer.hostname unset. Instrument the legacy MasterSlaveConnectionProvider#getConnection path and the newer MasterReplicaConnectionProvider#getConnection* paths. When a Redis command span is active, decorate it with the RedisURI from the resolved StatefulRedisConnection. --- .../MasterReplicaConnectionHelper.java | 38 ++++++ ...licaConnectionProviderInstrumentation.java | 118 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionHelper.java create mode 100644 dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionProviderInstrumentation.java diff --git a/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionHelper.java b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionHelper.java new file mode 100644 index 00000000000..39261f49eb1 --- /dev/null +++ b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionHelper.java @@ -0,0 +1,38 @@ +package datadog.trace.instrumentation.lettuce5; + +import static datadog.trace.instrumentation.lettuce5.LettuceClientDecorator.DECORATE; + +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulConnection; +import java.util.function.BiConsumer; + +public final class MasterReplicaConnectionHelper { + + private MasterReplicaConnectionHelper() {} + + public static boolean isRedisClientSpan(final AgentSpan span) { + return span != null && LettuceClientDecorator.REDIS_CLIENT.equals(span.getTag(Tags.COMPONENT)); + } + + public static void onConnection( + final AgentSpan span, + final StatefulConnection connection, + final ContextStore contextStore) { + if (connection == null) { + return; + } + + final RedisURI redisURI = contextStore.get(connection); + if (redisURI != null) { + DECORATE.onConnection(span, redisURI); + } + } + + public static BiConsumer onConnectionComplete( + final AgentSpan span, final ContextStore contextStore) { + return (connection, _throwable) -> onConnection(span, connection, contextStore); + } +} diff --git a/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionProviderInstrumentation.java b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionProviderInstrumentation.java new file mode 100644 index 00000000000..729961fd71e --- /dev/null +++ b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionProviderInstrumentation.java @@ -0,0 +1,118 @@ +package datadog.trace.instrumentation.lettuce5; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +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.agent.tooling.InstrumenterModule; +import datadog.trace.bootstrap.InstrumentationContext; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulConnection; +import io.lettuce.core.api.StatefulRedisConnection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import net.bytebuddy.asm.Advice; + +/** + * Master/replica APIs expose a routing connection ({@code + * StatefulRedisMasterReplicaConnectionImpl}; legacy {@code MasterSlave} wraps it in {@code + * MasterSlaveConnectionWrapper}). The real node connection is selected only after a command span + * has started and the command is dispatched, so this decorates the active span with the RedisURI + * that is available on the real connection, not the wrapper. + */ +@AutoService(InstrumenterModule.class) +public class MasterReplicaConnectionProviderInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForKnownTypes, Instrumenter.HasMethodAdvice { + + public MasterReplicaConnectionProviderInstrumentation() { + super("lettuce", "lettuce-5"); + } + + @Override + public String[] knownMatchingTypes() { + return new String[] { + // Legacy Lettuce [5,7) + "io.lettuce.core.masterslave.MasterSlaveConnectionProvider", + // Newer Lettuce [7,) + "io.lettuce.core.masterreplica.MasterReplicaConnectionProvider" + }; + } + + @Override + public Map contextStore() { + return Collections.singletonMap( + "io.lettuce.core.api.StatefulConnection", "io.lettuce.core.RedisURI"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".LettuceClientDecorator", + packageName + ".MasterReplicaConnectionHelper", + packageName + ".LettuceInstrumentationUtil" + }; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + // Legacy provider, used directly by MasterSlaveChannelWriter. + transformer.applyAdvice( + isMethod() + .and(named("getConnection")) + .and( + takesArgument( + 0, named("io.lettuce.core.masterslave.MasterSlaveConnectionProvider$Intent"))) + .and(returns(named("io.lettuce.core.api.StatefulRedisConnection"))), + MasterReplicaConnectionProviderInstrumentation.class.getName() + "$SyncAdvice"); + // Newer masterreplica provider still exposes a synchronous accessor for blocking callers. + transformer.applyAdvice( + isMethod() + .and(named("getConnection")) + .and(takesArgument(0, named("io.lettuce.core.protocol.ConnectionIntent"))) + .and(returns(named("io.lettuce.core.api.StatefulRedisConnection"))), + MasterReplicaConnectionProviderInstrumentation.class.getName() + "$SyncAdvice"); + // Newer command writers use the async accessor, so update the command span when it resolves. + transformer.applyAdvice( + isMethod() + .and(named("getConnectionAsync")) + .and(takesArgument(0, named("io.lettuce.core.protocol.ConnectionIntent"))) + .and(returns(named("java.util.concurrent.CompletableFuture"))), + MasterReplicaConnectionProviderInstrumentation.class.getName() + "$AsyncAdvice"); + } + + public static class SyncAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Return final StatefulRedisConnection connection) { + final AgentSpan span = activeSpan(); + if (!MasterReplicaConnectionHelper.isRedisClientSpan(span)) { + return; + } + + MasterReplicaConnectionHelper.onConnection( + span, connection, InstrumentationContext.get(StatefulConnection.class, RedisURI.class)); + } + } + + public static class AsyncAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.Return final CompletableFuture connectionFuture) { + final AgentSpan span = activeSpan(); + if (!MasterReplicaConnectionHelper.isRedisClientSpan(span) || connectionFuture == null) { + return; + } + + connectionFuture.whenComplete( + MasterReplicaConnectionHelper.onConnectionComplete( + span, InstrumentationContext.get(StatefulConnection.class, RedisURI.class))); + } + } +} From 90ddf535fa4bf6fa7861736676ecabf7de92bf16 Mon Sep 17 00:00:00 2001 From: Yury Gribkov Date: Tue, 30 Jun 2026 18:47:12 -0700 Subject: [PATCH 3/7] Use reflective Lettuce master-replica API lookup Prefer the newer MasterReplica facade in the Lettuce master/replica test and fall back to the legacy MasterSlave facade when needed. --- ...st.java => Lettuce5MasterReplicaTest.java} | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) rename dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/{Lettuce5StaticMasterReplicaTest.java => Lettuce5MasterReplicaTest.java} (63%) diff --git a/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5StaticMasterReplicaTest.java b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5MasterReplicaTest.java similarity index 63% rename from dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5StaticMasterReplicaTest.java rename to dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5MasterReplicaTest.java index 27cbe972b8e..a68e8d51c82 100644 --- a/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5StaticMasterReplicaTest.java +++ b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5MasterReplicaTest.java @@ -10,8 +10,10 @@ import io.lettuce.core.RedisClient; import io.lettuce.core.RedisURI; import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.codec.StringCodec; -import io.lettuce.core.masterslave.MasterSlave; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -21,7 +23,9 @@ import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; -class Lettuce5StaticMasterReplicaTest extends AbstractInstrumentationTest { +class Lettuce5MasterReplicaTest extends AbstractInstrumentationTest { + private static final String MASTER_REPLICA_CLASS = "io.lettuce.core.masterreplica.MasterReplica"; + private static final String MASTER_SLAVE_CLASS = "io.lettuce.core.masterslave.MasterSlave"; private static final int DB_INDEX = 0; private static final ClientOptions CLIENT_OPTIONS = ClientOptions.builder().autoReconnect(false).build(); @@ -45,9 +49,10 @@ void setUpRedis() throws Exception { RedisURI redisURI = RedisURI.Builder.redis(host, port).withDatabase(DB_INDEX).build(); redisClient = RedisClient.create(); redisClient.setOptions(CLIENT_OPTIONS); - // Spring Data RedisStaticMasterReplicaConfiguration uses this static topology path on Lettuce - // 5. - connection = MasterSlave.connect(redisClient, StringCodec.UTF8, singletonList(redisURI)); + // Prefer the newer MasterReplica facade when this source is compiled for latestDepTest, but + // resolve both APIs reflectively so the same test still compiles with the Lettuce 5.0 baseline + // and can keep compiling if the deprecated MasterSlave facade disappears later. + connection = connectMasterReplica(redisClient, redisURI); connection.sync().ping(); writer.waitForTraces(2); @@ -95,4 +100,33 @@ void staticMasterReplicaCommandSpanHasPeerHostname() throws Exception { assertNotNull(span.getTag(Tags.PEER_HOSTNAME), "command span should include peer.hostname"); assertEquals(host, span.getTag(Tags.PEER_HOSTNAME)); } + + @SuppressWarnings("unchecked") + private static StatefulRedisConnection connectMasterReplica( + RedisClient redisClient, RedisURI redisURI) throws Exception { + Class facade = masterReplicaFacade(); + Method connect = + facade.getMethod("connect", RedisClient.class, RedisCodec.class, Iterable.class); + try { + return (StatefulRedisConnection) + connect.invoke(null, redisClient, StringCodec.UTF8, singletonList(redisURI)); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Exception) { + throw (Exception) cause; + } + if (cause instanceof Error) { + throw (Error) cause; + } + throw e; + } + } + + private static Class masterReplicaFacade() throws ClassNotFoundException { + try { + return Class.forName(MASTER_REPLICA_CLASS); + } catch (ClassNotFoundException ignored) { + return Class.forName(MASTER_SLAVE_CLASS); + } + } } From e0e65be1c110f03675a5f90b6dc655a504e7de02 Mon Sep 17 00:00:00 2001 From: Yury Gribkov Date: Tue, 30 Jun 2026 20:56:12 -0700 Subject: [PATCH 4/7] Add pinned Lettuce 5.1, 6.0, 6.1, and 6.2 test suites to exercise master/replica command span connection tagging across the version range. --- .../instrumentation/lettuce/lettuce-5.0/build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dd-java-agent/instrumentation/lettuce/lettuce-5.0/build.gradle b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/build.gradle index becb0a29ad7..9f5a2579038 100644 --- a/dd-java-agent/instrumentation/lettuce/lettuce-5.0/build.gradle +++ b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/build.gradle @@ -12,6 +12,10 @@ apply from: "$rootDir/gradle/java.gradle" addTestSuiteForDir('latestDepTest', 'test') addTestSuiteExtendingForDir('latestDepForkedTest', 'latestDepTest', 'test') +addTestSuiteForDir('lettuce51Test', 'test') +addTestSuiteForDir('lettuce60Test', 'test') +addTestSuiteForDir('lettuce61Test', 'test') +addTestSuiteForDir('lettuce62Test', 'test') dependencies { compileOnly group: 'io.lettuce', name: 'lettuce-core', version: '5.0.0.RELEASE' @@ -24,6 +28,10 @@ dependencies { latestDepTestImplementation group: 'io.lettuce', name: 'lettuce-core', version: '+' + lettuce51TestImplementation group: 'io.lettuce', name: 'lettuce-core', version: '5.1.0.RELEASE' + lettuce60TestImplementation group: 'io.lettuce', name: 'lettuce-core', version: '6.0.0.RELEASE' + lettuce61TestImplementation group: 'io.lettuce', name: 'lettuce-core', version: '6.1.0.RELEASE' + lettuce62TestImplementation group: 'io.lettuce', name: 'lettuce-core', version: '6.2.0.RELEASE' tasks.withType(Test).configureEach { usesService(testcontainersLimit) From 8add90877f305992f7e9f69c4cf19a217376f8ad Mon Sep 17 00:00:00 2001 From: Yury Gribkov Date: Tue, 30 Jun 2026 21:09:47 -0700 Subject: [PATCH 5/7] Add missing instrumentationfor missed lettuce connection providers --- ...licaConnectionProviderInstrumentation.java | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionProviderInstrumentation.java b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionProviderInstrumentation.java index 729961fd71e..2963ebfecbc 100644 --- a/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionProviderInstrumentation.java +++ b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionProviderInstrumentation.java @@ -37,9 +37,11 @@ public MasterReplicaConnectionProviderInstrumentation() { @Override public String[] knownMatchingTypes() { return new String[] { - // Legacy Lettuce [5,7) + // Legacy Lettuce 5.x "io.lettuce.core.masterslave.MasterSlaveConnectionProvider", - // Newer Lettuce [7,) + // Transitional Lettuce 6.0 provider + "io.lettuce.core.masterreplica.UpstreamReplicaConnectionProvider", + // Lettuce 6.1+ "io.lettuce.core.masterreplica.MasterReplicaConnectionProvider" }; } @@ -77,6 +79,36 @@ public void methodAdvice(MethodTransformer transformer) { .and(takesArgument(0, named("io.lettuce.core.protocol.ConnectionIntent"))) .and(returns(named("io.lettuce.core.api.StatefulRedisConnection"))), MasterReplicaConnectionProviderInstrumentation.class.getName() + "$SyncAdvice"); + // Lettuce 5.1-5.3 command writers use the async accessor with the legacy intent. + transformer.applyAdvice( + isMethod() + .and(named("getConnectionAsync")) + .and( + takesArgument( + 0, named("io.lettuce.core.masterslave.MasterSlaveConnectionProvider$Intent"))) + .and(returns(named("java.util.concurrent.CompletableFuture"))), + MasterReplicaConnectionProviderInstrumentation.class.getName() + "$AsyncAdvice"); + // Lettuce 6.0 command writers use a transitional upstream/replica provider. + transformer.applyAdvice( + isMethod() + .and(named("getConnectionAsync")) + .and( + takesArgument( + 0, + named( + "io.lettuce.core.masterreplica.UpstreamReplicaConnectionProvider$Intent"))) + .and(returns(named("java.util.concurrent.CompletableFuture"))), + MasterReplicaConnectionProviderInstrumentation.class.getName() + "$AsyncAdvice"); + // Lettuce 6.1 uses the masterreplica provider with its own nested intent. + transformer.applyAdvice( + isMethod() + .and(named("getConnectionAsync")) + .and( + takesArgument( + 0, + named("io.lettuce.core.masterreplica.MasterReplicaConnectionProvider$Intent"))) + .and(returns(named("java.util.concurrent.CompletableFuture"))), + MasterReplicaConnectionProviderInstrumentation.class.getName() + "$AsyncAdvice"); // Newer command writers use the async accessor, so update the command span when it resolves. transformer.applyAdvice( isMethod() From c4b629d13370581c5d52f7aa249ff004eba60e09 Mon Sep 17 00:00:00 2001 From: Yury Gribkov Date: Tue, 30 Jun 2026 21:34:15 -0700 Subject: [PATCH 6/7] Fix Lettuce master/replica connection provider matching Collapse version-specific `getConnection*` matchers to public method shape matching and keep coverage for Lettuce 5.0 through latest master/replica command span connection tagging. --- ...licaConnectionProviderInstrumentation.java | 51 +++---------------- 1 file changed, 7 insertions(+), 44 deletions(-) diff --git a/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionProviderInstrumentation.java b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionProviderInstrumentation.java index 2963ebfecbc..e0f93f7bc20 100644 --- a/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionProviderInstrumentation.java +++ b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/main/java/datadog/trace/instrumentation/lettuce5/MasterReplicaConnectionProviderInstrumentation.java @@ -3,8 +3,9 @@ import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.returns; -import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import com.google.auto.service.AutoService; import datadog.trace.agent.tooling.Instrumenter; @@ -63,57 +64,19 @@ public String[] helperClassNames() { @Override public void methodAdvice(MethodTransformer transformer) { - // Legacy provider, used directly by MasterSlaveChannelWriter. + // Intent argument types move across Lettuce versions, but only the returned connection is used. transformer.applyAdvice( isMethod() + .and(isPublic()) .and(named("getConnection")) - .and( - takesArgument( - 0, named("io.lettuce.core.masterslave.MasterSlaveConnectionProvider$Intent"))) + .and(takesArguments(1)) .and(returns(named("io.lettuce.core.api.StatefulRedisConnection"))), MasterReplicaConnectionProviderInstrumentation.class.getName() + "$SyncAdvice"); - // Newer masterreplica provider still exposes a synchronous accessor for blocking callers. - transformer.applyAdvice( - isMethod() - .and(named("getConnection")) - .and(takesArgument(0, named("io.lettuce.core.protocol.ConnectionIntent"))) - .and(returns(named("io.lettuce.core.api.StatefulRedisConnection"))), - MasterReplicaConnectionProviderInstrumentation.class.getName() + "$SyncAdvice"); - // Lettuce 5.1-5.3 command writers use the async accessor with the legacy intent. - transformer.applyAdvice( - isMethod() - .and(named("getConnectionAsync")) - .and( - takesArgument( - 0, named("io.lettuce.core.masterslave.MasterSlaveConnectionProvider$Intent"))) - .and(returns(named("java.util.concurrent.CompletableFuture"))), - MasterReplicaConnectionProviderInstrumentation.class.getName() + "$AsyncAdvice"); - // Lettuce 6.0 command writers use a transitional upstream/replica provider. - transformer.applyAdvice( - isMethod() - .and(named("getConnectionAsync")) - .and( - takesArgument( - 0, - named( - "io.lettuce.core.masterreplica.UpstreamReplicaConnectionProvider$Intent"))) - .and(returns(named("java.util.concurrent.CompletableFuture"))), - MasterReplicaConnectionProviderInstrumentation.class.getName() + "$AsyncAdvice"); - // Lettuce 6.1 uses the masterreplica provider with its own nested intent. - transformer.applyAdvice( - isMethod() - .and(named("getConnectionAsync")) - .and( - takesArgument( - 0, - named("io.lettuce.core.masterreplica.MasterReplicaConnectionProvider$Intent"))) - .and(returns(named("java.util.concurrent.CompletableFuture"))), - MasterReplicaConnectionProviderInstrumentation.class.getName() + "$AsyncAdvice"); - // Newer command writers use the async accessor, so update the command span when it resolves. transformer.applyAdvice( isMethod() + .and(isPublic()) .and(named("getConnectionAsync")) - .and(takesArgument(0, named("io.lettuce.core.protocol.ConnectionIntent"))) + .and(takesArguments(1)) .and(returns(named("java.util.concurrent.CompletableFuture"))), MasterReplicaConnectionProviderInstrumentation.class.getName() + "$AsyncAdvice"); } From c3efcf56b54a48e67cac42818bbf0e200d5bcc4b Mon Sep 17 00:00:00 2001 From: Yury Gribkov Date: Tue, 30 Jun 2026 22:03:52 -0700 Subject: [PATCH 7/7] Clean up Lettuce5MasterReplicaTest.connectMasterReplica --- .../test/java/Lettuce5MasterReplicaTest.java | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5MasterReplicaTest.java b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5MasterReplicaTest.java index a68e8d51c82..40d8564e34c 100644 --- a/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5MasterReplicaTest.java +++ b/dd-java-agent/instrumentation/lettuce/lettuce-5.0/src/test/java/Lettuce5MasterReplicaTest.java @@ -24,12 +24,6 @@ import org.testcontainers.utility.DockerImageName; class Lettuce5MasterReplicaTest extends AbstractInstrumentationTest { - private static final String MASTER_REPLICA_CLASS = "io.lettuce.core.masterreplica.MasterReplica"; - private static final String MASTER_SLAVE_CLASS = "io.lettuce.core.masterslave.MasterSlave"; - private static final int DB_INDEX = 0; - private static final ClientOptions CLIENT_OPTIONS = - ClientOptions.builder().autoReconnect(false).build(); - private RedisContainer redisServer; private RedisClient redisClient; private StatefulRedisConnection connection; @@ -46,12 +40,9 @@ void setUpRedis() throws Exception { host = redisServer.getHost(); port = redisServer.getFirstMappedPort(); - RedisURI redisURI = RedisURI.Builder.redis(host, port).withDatabase(DB_INDEX).build(); + RedisURI redisURI = RedisURI.Builder.redis(host, port).withDatabase(0).build(); redisClient = RedisClient.create(); - redisClient.setOptions(CLIENT_OPTIONS); - // Prefer the newer MasterReplica facade when this source is compiled for latestDepTest, but - // resolve both APIs reflectively so the same test still compiles with the Lettuce 5.0 baseline - // and can keep compiling if the deprecated MasterSlave facade disappears later. + redisClient.setOptions(ClientOptions.builder().autoReconnect(false).build()); connection = connectMasterReplica(redisClient, redisURI); connection.sync().ping(); @@ -104,7 +95,15 @@ void staticMasterReplicaCommandSpanHasPeerHostname() throws Exception { @SuppressWarnings("unchecked") private static StatefulRedisConnection connectMasterReplica( RedisClient redisClient, RedisURI redisURI) throws Exception { - Class facade = masterReplicaFacade(); + // Prefer the newer MasterReplica facade when this source is compiled for latestDepTest, but + // resolve both APIs reflectively so the same test still compiles with the Lettuce 5.0 baseline + // and can keep compiling if the deprecated MasterSlave facade disappears later. + Class facade; + try { + facade = Class.forName("io.lettuce.core.masterreplica.MasterReplica"); + } catch (ClassNotFoundException ignored) { + facade = Class.forName("io.lettuce.core.masterslave.MasterSlave"); + } Method connect = facade.getMethod("connect", RedisClient.class, RedisCodec.class, Iterable.class); try { @@ -121,12 +120,4 @@ private static StatefulRedisConnection connectMasterReplica( throw e; } } - - private static Class masterReplicaFacade() throws ClassNotFoundException { - try { - return Class.forName(MASTER_REPLICA_CLASS); - } catch (ClassNotFoundException ignored) { - return Class.forName(MASTER_SLAVE_CLASS); - } - } }