diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java index 092fa4d391..463f00652c 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.connection.jedis; +import redis.clients.jedis.ClientSetInfoConfig; import redis.clients.jedis.Connection; import redis.clients.jedis.DefaultJedisClientConfig; import redis.clients.jedis.HostAndPort; @@ -59,10 +60,12 @@ import org.springframework.data.redis.connection.RedisConfiguration.WithDatabaseIndex; import org.springframework.data.redis.connection.RedisConfiguration.WithPassword; import org.springframework.data.redis.connection.jedis.JedisClusterConnection.JedisClusterTopologyProvider; +import org.springframework.data.redis.util.RedisClientLibraryInfo; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * Connection factory creating Jedis based connections. @@ -130,6 +133,13 @@ public class JedisConnectionFactory private RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration("localhost", Protocol.DEFAULT_PORT); + /** + * Upstream framework library suffixes (without the Spring Data Redis entry). + * Values are collected from calls to {@link #addUpstreamLibNameSuffix(String)} and combined + * into the final CLIENT SETINFO LIB-NAME when configuring the client. + */ + private @Nullable String upstreamLibNameSuffix; + /** * Lifecycle state of this factory. */ @@ -654,6 +664,32 @@ public void setConvertPipelineAndTxResults(boolean convertPipelineAndTxResults) this.convertPipelineAndTxResults = convertPipelineAndTxResults; } + /** + * Add a library name suffix used for CLIENT SETINFO. + * This method is primarily intended for upstream framework integrations (for example, + * Spring Session Data Redis or Spring Security) to contribute their identifiers to the + * CLIENT SETINFO library name chain. + *
+ * The given value should contain framework identifiers without the core driver name,
+ * for example {@code "spring-session-data-redis_v3.0.0"}. Multiple calls will
+ * accumulate values; the final CLIENT SETINFO suffix is assembled by appending the
+ * Spring Data Redis entry via {@link RedisClientLibraryInfo#getLibNameSuffix(String)}.
+ *
+ * @param libNameSuffix the additional library name suffix to add; can be {@code null}.
+ * @since 4.0
+ */
+ public void addUpstreamLibNameSuffix(@Nullable String libNameSuffix) {
+ if (!StringUtils.hasText(libNameSuffix)) {
+ return;
+ }
+ if (!StringUtils.hasText(this.upstreamLibNameSuffix)) {
+ this.upstreamLibNameSuffix = libNameSuffix;
+ }
+ else if (!this.upstreamLibNameSuffix.contains(libNameSuffix)) {
+ this.upstreamLibNameSuffix = this.upstreamLibNameSuffix + ";" + libNameSuffix;
+ }
+ }
+
/**
* @return true when {@link RedisSentinelConfiguration} is present.
* @since 1.4
@@ -688,6 +724,9 @@ private JedisClientConfig createClientConfig(int database, @Nullable String user
builder.connectionTimeoutMillis(getConnectTimeout());
builder.socketTimeoutMillis(getReadTimeout());
+ String suffix = RedisClientLibraryInfo.getLibNameSuffix(this.upstreamLibNameSuffix);
+ builder.clientSetInfoConfig(new ClientSetInfoConfig(suffix));
+
builder.database(database);
if (!ObjectUtils.isEmpty(username)) {
diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java
index f7a8d12a6f..1e0a58e131 100644
--- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java
+++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java
@@ -65,6 +65,7 @@
import org.springframework.data.redis.connection.RedisConfiguration.ClusterConfiguration;
import org.springframework.data.redis.connection.RedisConfiguration.WithDatabaseIndex;
import org.springframework.data.redis.connection.RedisConfiguration.WithPassword;
+import org.springframework.data.redis.util.RedisClientLibraryInfo;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
@@ -154,6 +155,13 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv
private RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration("localhost", 6379);
+ /**
+ * Upstream framework library suffixes (without the Spring Data Redis entry).
+ * Values are collected from calls to {@link #addUpstreamLibNameSuffix(String)} and combined
+ * into the final CLIENT SETINFO LIB-NAME when configuring the client.
+ */
+ private @Nullable String upstreamLibNameSuffix;
+
private @Nullable SharedConnection
+ * The given value should contain framework identifiers without the core driver name,
+ * for example {@code "spring-session-data-redis_v3.0.0"}. Multiple calls will
+ * accumulate values; the final CLIENT SETINFO suffix is assembled by appending the
+ * Spring Data Redis entry via {@link RedisClientLibraryInfo#getLibNameSuffix(String)}.
+ *
+ * @param libNameSuffix the additional library name suffix to add; can be {@code null}.
+ * @since 4.0
+ */
+ public void addUpstreamLibNameSuffix(@Nullable String libNameSuffix) {
+ if (!StringUtils.hasText(libNameSuffix)) {
+ return;
+ }
+ if (!StringUtils.hasText(this.upstreamLibNameSuffix)) {
+ this.upstreamLibNameSuffix = libNameSuffix;
+ }
+ else if (!this.upstreamLibNameSuffix.contains(libNameSuffix)) {
+ this.upstreamLibNameSuffix = this.upstreamLibNameSuffix + ";" + libNameSuffix;
+ }
+ }
+
/**
* Returns the native {@link AbstractRedisClient} used by this instance. The client is initialized as part of
* {@link #afterPropertiesSet() the bean initialization lifecycle} and only available when this connection factory is
@@ -1482,6 +1516,10 @@ private RedisURI createRedisURIAndApplySettings(String host, int port) {
builder.withStartTls(clientConfiguration.isStartTls());
builder.withTimeout(clientConfiguration.getCommandTimeout());
+ String libName = RedisClientLibraryInfo.getLibName(RedisClientLibraryInfo.DRIVER_LETTUCE,
+ this.upstreamLibNameSuffix);
+ builder.withLibraryName(libName);
+
return builder.build();
}
diff --git a/src/main/java/org/springframework/data/redis/util/RedisClientLibraryInfo.java b/src/main/java/org/springframework/data/redis/util/RedisClientLibraryInfo.java
new file mode 100644
index 0000000000..a6f91b15ea
--- /dev/null
+++ b/src/main/java/org/springframework/data/redis/util/RedisClientLibraryInfo.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.redis.util;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * Utility class for building Spring Data Redis client library identification
+ * strings for Redis CLIENT SETINFO.
+ *
+ * Supports the Redis CLIENT SETINFO custom suffix pattern:
+ * {@code (?
+ * Note: The underscore before 'v' follows the Redis CLIENT SETINFO pattern recommendation.
+ *
+ * @return the library name suffix
+ */
+ public static String getLibNameSuffix() {
+ return FRAMEWORK_NAME + VERSION_SEPARATOR + getVersion();
+ }
+
+ /**
+ * Build a library name suffix with additional framework suffix(es) for CLIENT SETINFO.
+ * This allows multiple higher-level frameworks to identify themselves in a chain.
+ *
+ * The {@code additionalSuffix} parameter should already be formatted according to the pattern
+ * and can contain multiple frameworks separated by semicolons.
+ *
+ * Format: {@code
+ * Example with multiple frameworks:
+ *
+ * Format: {@code
+ * Example:
+ *
+ * String suffix = RedisClientInfo.getLibNameSuffix(
+ * "spring-security_v6.0.0;spring-session-data-redis_v3.0.0"
+ * );
+ * // Returns: "spring-security_v6.0.0;spring-session-data-redis_v3.0.0;spring-data-redis_v4.0.0"
+ *
+ *
+ * @param additionalSuffix pre-formatted suffix string containing one or more framework identifiers,
+ * already in the format "name_version" and separated by semicolons if multiple
+ * @return the combined library name suffix with all frameworks and Spring Data Redis info
+ */
+ public static String getLibNameSuffix(@Nullable String additionalSuffix) {
+ if (!StringUtils.hasText(additionalSuffix)) {
+ return getLibNameSuffix();
+ }
+ return additionalSuffix + SUFFIX_DELIMITER + getLibNameSuffix();
+ }
+
+ /**
+ * Build a complete library name for CLIENT SETINFO by wrapping the suffix with the core driver name.
+ * This allows multiple higher-level frameworks to identify themselves in a chain.
+ *
+ * String libName = RedisClientInfo.getLibName("lettuce",
+ * "spring-security_v6.0.0;spring-session-data-redis_v3.0.0");
+ * // Returns: "lettuce(spring-security_v6.0.0;spring-session-data-redis_v3.0.0;spring-data-redis_v4.0.0)"
+ *
+ *
+ * @param driverName the core Redis driver name (e.g., "lettuce", "jedis")
+ * @param additionalSuffix pre-formatted suffix string containing one or more framework identifiers,
+ * already in the format "name_version" and separated by semicolons if multiple
+ * @return the complete library name in the format "driverName(additionalSuffix;spring-data-redis_version)"
+ */
+ public static String getLibName(String driverName, @Nullable String additionalSuffix) {
+ return driverName + "(" + getLibNameSuffix(additionalSuffix) + ")";
+ }
+}
+
diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java
index 0dbffd22ad..f86dc8ad6e 100644
--- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java
+++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java
@@ -18,6 +18,9 @@
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
+import java.util.List;
+import org.springframework.data.redis.core.types.RedisClientInfo;
+
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@@ -74,6 +77,64 @@ void connectionAppliesClientName() {
assertThat(connection.getClientName()).isEqualTo("clientName");
}
+ @Test // CLIENT SETINFO
+ void clientListReportsJedisLibNameWithSpringDataSuffix() {
+
+ factory = new JedisConnectionFactory(
+ new RedisStandaloneConfiguration(SettingsUtils.getHost(), SettingsUtils.getPort()),
+ JedisClientConfiguration.builder().clientName("clientName").build());
+ factory.afterPropertiesSet();
+ factory.start();
+
+ try (RedisConnection connection = factory.getConnection()) {
+
+ List