Skip to content

Commit d59fd68

Browse files
Identify spring-data-redis using underlying drivers (jedis or lettuce)
Expose methods for identifying upstream libraries using spring-data-redis. Signed-off-by: viktoriya.kutsarova <viktoriya.kutsarova@redis.com>
1 parent 19573e2 commit d59fd68

File tree

7 files changed

+413
-0
lines changed

7 files changed

+413
-0
lines changed

src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.redis.connection.jedis;
1717

18+
import redis.clients.jedis.ClientSetInfoConfig;
1819
import redis.clients.jedis.Connection;
1920
import redis.clients.jedis.DefaultJedisClientConfig;
2021
import redis.clients.jedis.HostAndPort;
@@ -59,10 +60,12 @@
5960
import org.springframework.data.redis.connection.RedisConfiguration.WithDatabaseIndex;
6061
import org.springframework.data.redis.connection.RedisConfiguration.WithPassword;
6162
import org.springframework.data.redis.connection.jedis.JedisClusterConnection.JedisClusterTopologyProvider;
63+
import org.springframework.data.redis.util.RedisClientLibraryInfo;
6264
import org.springframework.util.Assert;
6365
import org.springframework.util.ClassUtils;
6466
import org.springframework.util.CollectionUtils;
6567
import org.springframework.util.ObjectUtils;
68+
import org.springframework.util.StringUtils;
6669

6770
/**
6871
* Connection factory creating <a href="https://github.com/redis/jedis">Jedis</a> based connections.
@@ -130,6 +133,13 @@ public class JedisConnectionFactory
130133
private RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration("localhost",
131134
Protocol.DEFAULT_PORT);
132135

136+
/**
137+
* Upstream framework library suffixes (without the Spring Data Redis entry).
138+
* Values are collected from calls to {@link #addUpstreamLibNameSuffix(String)} and combined
139+
* into the final CLIENT SETINFO LIB-NAME when configuring the client.
140+
*/
141+
private @Nullable String upstreamLibNameSuffix;
142+
133143
/**
134144
* Lifecycle state of this factory.
135145
*/
@@ -654,6 +664,32 @@ public void setConvertPipelineAndTxResults(boolean convertPipelineAndTxResults)
654664
this.convertPipelineAndTxResults = convertPipelineAndTxResults;
655665
}
656666

667+
/**
668+
* Add a library name suffix used for CLIENT SETINFO.
669+
* This method is primarily intended for upstream framework integrations (for example,
670+
* Spring Session Data Redis or Spring Security) to contribute their identifiers to the
671+
* CLIENT SETINFO library name chain.
672+
* <p>
673+
* The given value should contain framework identifiers without the core driver name,
674+
* for example {@code "spring-session-data-redis_v3.0.0"}. Multiple calls will
675+
* accumulate values; the final CLIENT SETINFO suffix is assembled by appending the
676+
* Spring Data Redis entry via {@link RedisClientLibraryInfo#getLibNameSuffix(String)}.
677+
*
678+
* @param libNameSuffix the additional library name suffix to add; can be {@code null}.
679+
* @since 4.0
680+
*/
681+
public void addUpstreamLibNameSuffix(@Nullable String libNameSuffix) {
682+
if (!StringUtils.hasText(libNameSuffix)) {
683+
return;
684+
}
685+
if (!StringUtils.hasText(this.upstreamLibNameSuffix)) {
686+
this.upstreamLibNameSuffix = libNameSuffix;
687+
}
688+
else if (!this.upstreamLibNameSuffix.contains(libNameSuffix)) {
689+
this.upstreamLibNameSuffix = this.upstreamLibNameSuffix + ";" + libNameSuffix;
690+
}
691+
}
692+
657693
/**
658694
* @return true when {@link RedisSentinelConfiguration} is present.
659695
* @since 1.4
@@ -688,6 +724,9 @@ private JedisClientConfig createClientConfig(int database, @Nullable String user
688724
builder.connectionTimeoutMillis(getConnectTimeout());
689725
builder.socketTimeoutMillis(getReadTimeout());
690726

727+
String suffix = RedisClientLibraryInfo.getLibNameSuffix(this.upstreamLibNameSuffix);
728+
builder.clientSetInfoConfig(new ClientSetInfoConfig(suffix));
729+
691730
builder.database(database);
692731

693732
if (!ObjectUtils.isEmpty(username)) {

src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import org.springframework.data.redis.connection.RedisConfiguration.ClusterConfiguration;
6666
import org.springframework.data.redis.connection.RedisConfiguration.WithDatabaseIndex;
6767
import org.springframework.data.redis.connection.RedisConfiguration.WithPassword;
68+
import org.springframework.data.redis.util.RedisClientLibraryInfo;
6869
import org.springframework.util.Assert;
6970
import org.springframework.util.ClassUtils;
7071
import org.springframework.util.ObjectUtils;
@@ -154,6 +155,13 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv
154155

155156
private RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration("localhost", 6379);
156157

158+
/**
159+
* Upstream framework library suffixes (without the Spring Data Redis entry).
160+
* Values are collected from calls to {@link #addUpstreamLibNameSuffix(String)} and combined
161+
* into the final CLIENT SETINFO LIB-NAME when configuring the client.
162+
*/
163+
private @Nullable String upstreamLibNameSuffix;
164+
157165
private @Nullable SharedConnection<byte[]> connection;
158166
private @Nullable SharedConnection<ByteBuffer> reactiveConnection;
159167

@@ -637,6 +645,32 @@ public void setClientName(@Nullable String clientName) {
637645
this.getMutableConfiguration().setClientName(clientName);
638646
}
639647

648+
/**
649+
* Add a library name suffix used for CLIENT SETINFO.
650+
* This method is primarily intended for upstream framework integrations (for example,
651+
* Spring Session Data Redis) to contribute their identifiers to the
652+
* CLIENT SETINFO library name chain.
653+
* <p>
654+
* The given value should contain framework identifiers without the core driver name,
655+
* for example {@code "spring-session-data-redis_v3.0.0"}. Multiple calls will
656+
* accumulate values; the final CLIENT SETINFO suffix is assembled by appending the
657+
* Spring Data Redis entry via {@link RedisClientLibraryInfo#getLibNameSuffix(String)}.
658+
*
659+
* @param libNameSuffix the additional library name suffix to add; can be {@code null}.
660+
* @since 4.0
661+
*/
662+
public void addUpstreamLibNameSuffix(@Nullable String libNameSuffix) {
663+
if (!StringUtils.hasText(libNameSuffix)) {
664+
return;
665+
}
666+
if (!StringUtils.hasText(this.upstreamLibNameSuffix)) {
667+
this.upstreamLibNameSuffix = libNameSuffix;
668+
}
669+
else if (!this.upstreamLibNameSuffix.contains(libNameSuffix)) {
670+
this.upstreamLibNameSuffix = this.upstreamLibNameSuffix + ";" + libNameSuffix;
671+
}
672+
}
673+
640674
/**
641675
* Returns the native {@link AbstractRedisClient} used by this instance. The client is initialized as part of
642676
* {@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) {
14821516
builder.withStartTls(clientConfiguration.isStartTls());
14831517
builder.withTimeout(clientConfiguration.getCommandTimeout());
14841518

1519+
String libName = RedisClientLibraryInfo.getLibName(RedisClientLibraryInfo.DRIVER_LETTUCE,
1520+
this.upstreamLibNameSuffix);
1521+
builder.withLibraryName(libName);
1522+
14851523
return builder.build();
14861524
}
14871525

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.util;
17+
18+
import org.jspecify.annotations.Nullable;
19+
20+
import org.springframework.util.StringUtils;
21+
22+
/**
23+
* Utility class for building Spring Data Redis client library identification
24+
* strings for Redis CLIENT SETINFO.
25+
* <p>
26+
* Supports the Redis CLIENT SETINFO custom suffix pattern:
27+
* {@code (?<custom-name>[ -~]+) v(?<custom-version>[\d\.]+)}.
28+
* Multiple suffixes can be delimited with semicolons. The recommended format
29+
* for individual suffixes is {@code <custom-name>_v<custom-version>}.
30+
*
31+
* @author Viktoriya Kutsarova
32+
* @since 4.0
33+
*/
34+
public final class RedisClientLibraryInfo {
35+
36+
/**
37+
* Lettuce driver name constant for CLIENT SETINFO.
38+
*/
39+
public static final String DRIVER_LETTUCE = "lettuce";
40+
41+
/**
42+
* Spring Data Redis framework name constant for CLIENT SETINFO.
43+
*/
44+
public static final String FRAMEWORK_NAME = "spring-data-redis";
45+
46+
private static final String SUFFIX_DELIMITER = ";";
47+
48+
private static final String VERSION_SEPARATOR = "_v";
49+
50+
private static final String UNKNOWN_VERSION = "unknown";
51+
52+
/**
53+
* Get the Spring Data Redis version from the package manifest.
54+
* Returns "unknown" if the version cannot be determined (for example when
55+
* running from an IDE or tests without a populated Implementation-Version).
56+
*
57+
* @return the Spring Data Redis version, or "unknown" if not available
58+
*/
59+
public static String getVersion() {
60+
Package pkg = RedisClientLibraryInfo.class.getPackage();
61+
String version = (pkg != null ? pkg.getImplementationVersion() : null);
62+
return (version != null ? version : UNKNOWN_VERSION);
63+
}
64+
65+
private RedisClientLibraryInfo() {
66+
}
67+
68+
/**
69+
* Build a library name suffix for CLIENT SETINFO in the format:
70+
* {@code spring-data-redis_v<version>}
71+
* <p>
72+
* Note: The underscore before 'v' follows the Redis CLIENT SETINFO pattern recommendation.
73+
*
74+
* @return the library name suffix
75+
*/
76+
public static String getLibNameSuffix() {
77+
return FRAMEWORK_NAME + VERSION_SEPARATOR + getVersion();
78+
}
79+
80+
/**
81+
* Build a library name suffix with additional framework suffix(es) for CLIENT SETINFO.
82+
* This allows multiple higher-level frameworks to identify themselves in a chain.
83+
* <p>
84+
* The {@code additionalSuffix} parameter should already be formatted according to the pattern
85+
* and can contain multiple frameworks separated by semicolons.
86+
* <p>
87+
* Format: {@code <additionalSuffix>;spring-data-redis_v<version>}
88+
* <p>
89+
* Example with multiple frameworks:
90+
* <pre>
91+
* String suffix = RedisClientInfo.getLibNameSuffix(
92+
* "spring-security_v6.0.0;spring-session-data-redis_v3.0.0"
93+
* );
94+
* // Returns: "spring-security_v6.0.0;spring-session-data-redis_v3.0.0;spring-data-redis_v4.0.0"
95+
* </pre>
96+
*
97+
* @param additionalSuffix pre-formatted suffix string containing one or more framework identifiers,
98+
* already in the format "name_version" and separated by semicolons if multiple
99+
* @return the combined library name suffix with all frameworks and Spring Data Redis info
100+
*/
101+
public static String getLibNameSuffix(@Nullable String additionalSuffix) {
102+
if (!StringUtils.hasText(additionalSuffix)) {
103+
return getLibNameSuffix();
104+
}
105+
return additionalSuffix + SUFFIX_DELIMITER + getLibNameSuffix();
106+
}
107+
108+
/**
109+
* Build a complete library name for CLIENT SETINFO by wrapping the suffix with the core driver name.
110+
* This allows multiple higher-level frameworks to identify themselves in a chain.
111+
* <p>
112+
* Format: {@code <driverName>(<additionalSuffix>;spring-data-redis_v<version>)}
113+
* <p>
114+
* Example:
115+
* <pre>
116+
* String libName = RedisClientInfo.getLibName("lettuce",
117+
* "spring-security_v6.0.0;spring-session-data-redis_v3.0.0");
118+
* // Returns: "lettuce(spring-security_v6.0.0;spring-session-data-redis_v3.0.0;spring-data-redis_v4.0.0)"
119+
* </pre>
120+
*
121+
* @param driverName the core Redis driver name (e.g., "lettuce", "jedis")
122+
* @param additionalSuffix pre-formatted suffix string containing one or more framework identifiers,
123+
* already in the format "name_version" and separated by semicolons if multiple
124+
* @return the complete library name in the format "driverName(additionalSuffix;spring-data-redis_version)"
125+
*/
126+
public static String getLibName(String driverName, @Nullable String additionalSuffix) {
127+
return driverName + "(" + getLibNameSuffix(additionalSuffix) + ")";
128+
}
129+
}
130+

src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
import static org.assertj.core.api.Assertions.*;
1919
import static org.mockito.Mockito.*;
2020

21+
import java.util.List;
22+
import org.springframework.data.redis.core.types.RedisClientInfo;
23+
2124
import org.jspecify.annotations.Nullable;
2225
import org.junit.jupiter.api.AfterEach;
2326
import org.junit.jupiter.api.Test;
@@ -74,6 +77,64 @@ void connectionAppliesClientName() {
7477
assertThat(connection.getClientName()).isEqualTo("clientName");
7578
}
7679

80+
@Test // CLIENT SETINFO
81+
void clientListReportsJedisLibNameWithSpringDataSuffix() {
82+
83+
factory = new JedisConnectionFactory(
84+
new RedisStandaloneConfiguration(SettingsUtils.getHost(), SettingsUtils.getPort()),
85+
JedisClientConfiguration.builder().clientName("clientName").build());
86+
factory.afterPropertiesSet();
87+
factory.start();
88+
89+
try (RedisConnection connection = factory.getConnection()) {
90+
91+
List<RedisClientInfo> clients = connection.serverCommands().getClientList();
92+
93+
RedisClientInfo self = clients.stream()
94+
.filter(info -> "clientName".equals(info.getName()))
95+
.findFirst()
96+
.orElseThrow();
97+
98+
String libName = self.get("lib-name");
99+
100+
assertThat(libName).isNotNull();
101+
assertThat(libName).contains("jedis(");
102+
assertThat(libName).contains("spring-data-redis_v");
103+
}
104+
}
105+
106+
@Test // CLIENT SETINFO
107+
void clientListReportsJedisLibNameWithUpstreamSuffix() {
108+
109+
String upstreamLibNameSuffix = "spring-session-data-redis_v3.0.0";
110+
111+
factory = new JedisConnectionFactory(
112+
new RedisStandaloneConfiguration(SettingsUtils.getHost(), SettingsUtils.getPort()),
113+
JedisClientConfiguration.builder().clientName("clientName").build());
114+
factory.addUpstreamLibNameSuffix(upstreamLibNameSuffix);
115+
factory.afterPropertiesSet();
116+
factory.start();
117+
118+
try (RedisConnection connection = factory.getConnection()) {
119+
120+
List<RedisClientInfo> clients = connection.serverCommands().getClientList();
121+
122+
RedisClientInfo self = clients.stream()
123+
.filter(info -> "clientName".equals(info.getName()))
124+
.findFirst()
125+
.orElseThrow();
126+
127+
String libName = self.get("lib-name");
128+
129+
assertThat(libName).isNotNull();
130+
assertThat(libName).contains("jedis(");
131+
assertThat(libName).contains("spring-data-redis_v");
132+
assertThat(libName).contains(upstreamLibNameSuffix);
133+
}
134+
}
135+
136+
137+
77138
@Test // GH-2503
78139
void startStopStartConnectionFactory() {
79140

src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,47 @@ void earlyStartupDoesNotStartConnectionFactory() {
403403
assertThat(ReflectionTestUtils.getField(connectionFactory, "pool")).isNull();
404404
}
405405

406+
@Test // CLIENT SETINFO
407+
void addUpstreamLibNameSuffixShouldIgnoreNullAndBlankValues() {
408+
409+
JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
410+
411+
connectionFactory.addUpstreamLibNameSuffix(null);
412+
connectionFactory.addUpstreamLibNameSuffix("");
413+
connectionFactory.addUpstreamLibNameSuffix(" ");
414+
415+
Object upstreamLibNameSuffix = ReflectionTestUtils.getField(connectionFactory, "upstreamLibNameSuffix");
416+
417+
assertThat(upstreamLibNameSuffix).isNull();
418+
}
419+
420+
@Test // CLIENT SETINFO
421+
void addUpstreamLibNameSuffixShouldAccumulateValuesInOrder() {
422+
423+
JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
424+
425+
connectionFactory.addUpstreamLibNameSuffix("spring-session-data-redis_v3.0.0");
426+
connectionFactory.addUpstreamLibNameSuffix("spring-security_v6.0.0");
427+
428+
Object upstreamLibNameSuffix = ReflectionTestUtils.getField(connectionFactory, "upstreamLibNameSuffix");
429+
430+
assertThat(upstreamLibNameSuffix).isEqualTo("spring-session-data-redis_v3.0.0;spring-security_v6.0.0");
431+
}
432+
433+
@Test // CLIENT SETINFO
434+
void addUpstreamLibNameSuffixShouldNotAddDuplicates() {
435+
436+
JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
437+
438+
connectionFactory.addUpstreamLibNameSuffix("spring-session-data-redis_v3.0.0");
439+
connectionFactory.addUpstreamLibNameSuffix("spring-session-data-redis_v3.0.0");
440+
441+
Object upstreamLibNameSuffix = ReflectionTestUtils.getField(connectionFactory, "upstreamLibNameSuffix");
442+
443+
assertThat(upstreamLibNameSuffix).isEqualTo("spring-session-data-redis_v3.0.0");
444+
}
445+
446+
406447
private JedisConnectionFactory initSpyedConnectionFactory(RedisSentinelConfiguration sentinelConfiguration,
407448
@Nullable JedisPoolConfig poolConfig) {
408449

0 commit comments

Comments
 (0)