Repository: go-redis/redis
Pull request #2333 (2022-12-28 14:10:30)

feat: add ClientName option
See #2312


Patch:
diff --git a/cluster.go b/cluster.go
index a70ff12..d36b5c3 100644
--- a/cluster.go
+++ b/cluster.go
@@ -29,6 +29,9 @@ type ClusterOptions struct {
 	// A seed list of host:port addresses of cluster nodes.
 	Addrs []string
 
+	// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
+	ClientName string
+
 	// NewClient creates a cluster node client with provided name and options.
 	NewClient func(opt *Options) *Client
 
@@ -208,6 +211,7 @@ func setupClusterConn(u *url.URL, host string, o *ClusterOptions) (*ClusterOptio
 func setupClusterQueryParams(u *url.URL, o *ClusterOptions) (*ClusterOptions, error) {
 	q := queryOptions{q: u.Query()}
 
+	o.ClientName = q.string("client_name")
 	o.MaxRedirects = q.int("max_redirects")
 	o.ReadOnly = q.bool("read_only")
 	o.RouteByLatency = q.bool("route_by_latency")
@@ -250,8 +254,9 @@ func setupClusterQueryParams(u *url.URL, o *ClusterOptions) (*ClusterOptions, er
 
 func (opt *ClusterOptions) clientOptions() *Options {
 	return &Options{
-		Dialer:    opt.Dialer,
-		OnConnect: opt.OnConnect,
+		ClientName: opt.ClientName,
+		Dialer:     opt.Dialer,
+		OnConnect:  opt.OnConnect,
 
 		Username: opt.Username,
 		Password: opt.Password,
@@ -871,7 +876,7 @@ func (c *ClusterClient) Close() error {
 	return c.nodes.Close()
 }
 
-// Do creates a Cmd from the args and processes the cmd.
+// Do create a Cmd from the args and processes the cmd.
 func (c *ClusterClient) Do(ctx context.Context, args ...interface{}) *Cmd {
 	cmd := NewCmd(ctx, args...)
 	_ = c.Process(ctx, cmd)
diff --git a/extra/redisotel/metrics.go b/extra/redisotel/metrics.go
index b41417b..76f545b 100644
--- a/extra/redisotel/metrics.go
+++ b/extra/redisotel/metrics.go
@@ -193,11 +193,7 @@ func (mh *metricsHook) DialHook(hook redis.DialHook) redis.DialHook {
 
 		conn, err := hook(ctx, network, addr)
 
-		attrs := make([]attribute.KeyValue, 0, len(mh.attrs)+1)
-		attrs = append(attrs, mh.attrs...)
-		attrs = append(attrs, statusAttr(err))
-
-		mh.createTime.Record(ctx, milliseconds(time.Since(start)), attrs...)
+		mh.createTime.Record(ctx, milliseconds(time.Since(start)), mh.attrs...)
 		return conn, err
 	}
 }
diff --git a/internal/hscan/hscan.go b/internal/hscan/hscan.go
index 203ec4a..852c8bd 100644
--- a/internal/hscan/hscan.go
+++ b/internal/hscan/hscan.go
@@ -10,12 +10,6 @@ import (
 // decoderFunc represents decoding functions for default built-in types.
 type decoderFunc func(reflect.Value, string) error
 
-// Scanner is the interface implemented by themselves,
-// which will override the decoding behavior of decoderFunc.
-type Scanner interface {
-	ScanRedis(s string) error
-}
-
 var (
 	// List of built-in decoders indexed by their numeric constant values (eg: reflect.Bool = 1).
 	decoders = []decoderFunc{
diff --git a/internal/hscan/structmap.go b/internal/hscan/structmap.go
index 9befd98..6839412 100644
--- a/internal/hscan/structmap.go
+++ b/internal/hscan/structmap.go
@@ -84,29 +84,7 @@ func (s StructValue) Scan(key string, value string) error {
 	if !ok {
 		return nil
 	}
-
-	v := s.value.Field(field.index)
-	isPtr := v.Kind() == reflect.Pointer
-
-	if isPtr && v.IsNil() {
-		v.Set(reflect.New(v.Type().Elem()))
-	}
-	if !isPtr && v.Type().Name() != "" && v.CanAddr() {
-		v = v.Addr()
-		isPtr = true
-	}
-
-	if isPtr && v.Type().NumMethod() > 0 && v.CanInterface() {
-		if scan, ok := v.Interface().(Scanner); ok {
-			return scan.ScanRedis(value)
-		}
-	}
-
-	if isPtr {
-		v = v.Elem()
-	}
-
-	if err := field.fn(v, value); err != nil {
+	if err := field.fn(s.value.Field(field.index), value); err != nil {
 		t := s.value.Type()
 		return fmt.Errorf("cannot scan redis.result %s into struct field %s.%s of type %s, error-%s",
 			value, t.Name(), t.Field(field.index).Name, t.Field(field.index).Type, err.Error())
diff --git a/options.go b/options.go
index 651d0d4..49abe21 100644
--- a/options.go
+++ b/options.go
@@ -27,7 +27,7 @@ type Limiter interface {
 	ReportResult(result error)
 }
 
-// Options keeps the settings to setup redis connection.
+// Options keeps the settings to set up redis connection.
 type Options struct {
 	// The network type, either tcp or unix.
 	// Default is tcp.
@@ -35,6 +35,9 @@ type Options struct {
 	// host:port address.
 	Addr string
 
+	// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
+	ClientName string
+
 	// Dialer creates new network connection and has priority over
 	// Network and Addr options.
 	Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
@@ -426,6 +429,7 @@ func setupConnParams(u *url.URL, o *Options) (*Options, error) {
 		o.DB = db
 	}
 
+	o.ClientName = q.string("client_name")
 	o.MaxRetries = q.int("max_retries")
 	o.MinRetryBackoff = q.duration("min_retry_backoff")
 	o.MaxRetryBackoff = q.duration("max_retry_backoff")
diff --git a/redis.go b/redis.go
index 83e9d40..d587c1f 100644
--- a/redis.go
+++ b/redis.go
@@ -10,14 +10,10 @@ import (
 	"time"
 
 	"github.com/go-redis/redis/v9/internal"
-	"github.com/go-redis/redis/v9/internal/hscan"
 	"github.com/go-redis/redis/v9/internal/pool"
 	"github.com/go-redis/redis/v9/internal/proto"
 )
 
-// Scanner internal/hscan.Scanner exposed interface.
-type Scanner = hscan.Scanner
-
 // Nil reply returned by Redis when key does not exist.
 const Nil = proto.Nil
 
@@ -260,6 +256,10 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
 			pipe.ReadOnly(ctx)
 		}
 
+		if c.opt.ClientName != "" {
+			pipe.ClientSetName(ctx, c.opt.ClientName)
+		}
+
 		return nil
 	})
 	if err != nil {
diff --git a/ring.go b/ring.go
index 0a1069d..bc299da 100644
--- a/ring.go
+++ b/ring.go
@@ -51,6 +51,9 @@ type RingOptions struct {
 	// NewClient creates a shard client with provided options.
 	NewClient func(opt *Options) *Client
 
+	// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
+	ClientName string
+
 	// Frequency of PING commands sent to check shards availability.
 	// Shard is considered down after 3 subsequent failed checks.
 	HeartbeatFrequency time.Duration
@@ -129,8 +132,9 @@ func (opt *RingOptions) init() {
 
 func (opt *RingOptions) clientOptions() *Options {
 	return &Options{
-		Dialer:    opt.Dialer,
-		OnConnect: opt.OnConnect,
+		ClientName: opt.ClientName,
+		Dialer:     opt.Dialer,
+		OnConnect:  opt.OnConnect,
 
 		Username: opt.Username,
 		Password: opt.Password,
@@ -522,7 +526,7 @@ func (c *Ring) SetAddrs(addrs map[string]string) {
 	c.sharding.SetAddrs(addrs)
 }
 
-// Do creates a Cmd from the args and processes the cmd.
+// Do create a Cmd from the args and processes the cmd.
 func (c *Ring) Do(ctx context.Context, args ...interface{}) *Cmd {
 	cmd := NewCmd(ctx, args...)
 	_ = c.Process(ctx, cmd)
diff --git a/sentinel.go b/sentinel.go
index 44b073f..1feeb03 100644
--- a/sentinel.go
+++ b/sentinel.go
@@ -24,6 +24,9 @@ type FailoverOptions struct {
 	// A seed list of host:port addresses of sentinel nodes.
 	SentinelAddrs []string
 
+	// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
+	ClientName string
+
 	// If specified with SentinelPassword, enables ACL-based authentication (via
 	// AUTH <user> <pass>).
 	SentinelUsername string
@@ -78,7 +81,8 @@ type FailoverOptions struct {
 
 func (opt *FailoverOptions) clientOptions() *Options {
 	return &Options{
-		Addr: "FailoverClient",
+		Addr:       "FailoverClient",
+		ClientName: opt.ClientName,
 
 		Dialer:    opt.Dialer,
 		OnConnect: opt.OnConnect,
@@ -110,7 +114,8 @@ func (opt *FailoverOptions) clientOptions() *Options {
 
 func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
 	return &Options{
-		Addr: addr,
+		Addr:       addr,
+		ClientName: opt.ClientName,
 
 		Dialer:    opt.Dialer,
 		OnConnect: opt.OnConnect,
@@ -141,6 +146,8 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
 
 func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
 	return &ClusterOptions{
+		ClientName: opt.ClientName,
+
 		Dialer:    opt.Dialer,
 		OnConnect: opt.OnConnect,
 
diff --git a/universal.go b/universal.go
index 73bbf17..9d1a852 100644
--- a/universal.go
+++ b/universal.go
@@ -14,6 +14,9 @@ type UniversalOptions struct {
 	// of cluster/sentinel nodes.
 	Addrs []string
 
+	// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
+	ClientName string
+
 	// Database to be selected after connecting to the server.
 	// Only single-node and failover clients.
 	DB int
@@ -69,9 +72,10 @@ func (o *UniversalOptions) Cluster() *ClusterOptions {
 	}
 
 	return &ClusterOptions{
-		Addrs:     o.Addrs,
-		Dialer:    o.Dialer,
-		OnConnect: o.OnConnect,
+		Addrs:      o.Addrs,
+		ClientName: o.ClientName,
+		Dialer:     o.Dialer,
+		OnConnect:  o.OnConnect,
 
 		Username: o.Username,
 		Password: o.Password,
@@ -112,6 +116,7 @@ func (o *UniversalOptions) Failover() *FailoverOptions {
 	return &FailoverOptions{
 		SentinelAddrs: o.Addrs,
 		MasterName:    o.MasterName,
+		ClientName:    o.ClientName,
 
 		Dialer:    o.Dialer,
 		OnConnect: o.OnConnect,
@@ -151,9 +156,10 @@ func (o *UniversalOptions) Simple() *Options {
 	}
 
 	return &Options{
-		Addr:      addr,
-		Dialer:    o.Dialer,
-		OnConnect: o.OnConnect,
+		Addr:       addr,
+		ClientName: o.ClientName,
+		Dialer:     o.Dialer,
+		OnConnect:  o.OnConnect,
 
 		DB:       o.DB,
 		Username: o.Username,


Test patch:
diff --git a/cluster_test.go b/cluster_test.go
index f9dc679..2827d3f 100644
--- a/cluster_test.go
+++ b/cluster_test.go
@@ -589,6 +589,7 @@ var _ = Describe("ClusterClient", func() {
 	Describe("ClusterClient", func() {
 		BeforeEach(func() {
 			opt = redisClusterOptions()
+			opt.ClientName = "cluster_hi"
 			client = cluster.newClusterClient(ctx, opt)
 
 			err := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {
@@ -679,6 +680,20 @@ var _ = Describe("ClusterClient", func() {
 			Expect(assertSlotsEqual(res, wanted)).NotTo(HaveOccurred())
 		})
 
+		It("should cluster client setname", func() {
+			err := client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {
+				return c.Ping(ctx).Err()
+			})
+			Expect(err).NotTo(HaveOccurred())
+
+			_ = client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {
+				val, err := c.ClientList(ctx).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(val).Should(ContainSubstring("name=cluster_hi"))
+				return nil
+			})
+		})
+
 		It("should CLUSTER NODES", func() {
 			res, err := client.ClusterNodes(ctx).Result()
 			Expect(err).NotTo(HaveOccurred())
@@ -1408,6 +1423,10 @@ func TestParseClusterURL(t *testing.T) {
 			test: "UseDefault",
 			url:  "redis://localhost:123?conn_max_idle_time=",
 			o:    &redis.ClusterOptions{Addrs: []string{"localhost:123"}, ConnMaxIdleTime: 0},
+		}, {
+			test: "ClientName",
+			url:  "redis://localhost:123?client_name=cluster_hi",
+			o:    &redis.ClusterOptions{Addrs: []string{"localhost:123"}, ClientName: "cluster_hi"},
 		}, {
 			test: "UseDefaultMissing=",
 			url:  "redis://localhost:123?conn_max_idle_time",
diff --git a/commands_test.go b/commands_test.go
index d0c43c7..476644d 100644
--- a/commands_test.go
+++ b/commands_test.go
@@ -14,15 +14,6 @@ import (
 	"github.com/go-redis/redis/v9/internal/proto"
 )
 
-type TimeValue struct {
-	time.Time
-}
-
-func (t *TimeValue) ScanRedis(s string) (err error) {
-	t.Time, err = time.Parse(time.RFC3339Nano, s)
-	return
-}
-
 var _ = Describe("Commands", func() {
 	ctx := context.TODO()
 	var client *redis.Client
@@ -1201,28 +1192,19 @@ var _ = Describe("Commands", func() {
 		})
 
 		It("should scan Mget", func() {
-			now := time.Now()
-
-			err := client.MSet(ctx, "key1", "hello1", "key2", 123, "time", now.Format(time.RFC3339Nano)).Err()
+			err := client.MSet(ctx, "key1", "hello1", "key2", 123).Err()
 			Expect(err).NotTo(HaveOccurred())
 
-			res := client.MGet(ctx, "key1", "key2", "_", "time")
+			res := client.MGet(ctx, "key1", "key2", "_")
 			Expect(res.Err()).NotTo(HaveOccurred())
 
 			type data struct {
-				Key1 string    `redis:"key1"`
-				Key2 int       `redis:"key2"`
-				Time TimeValue `redis:"time"`
+				Key1 string `redis:"key1"`
+				Key2 int    `redis:"key2"`
 			}
 			var d data
 			Expect(res.Scan(&d)).NotTo(HaveOccurred())
-			Expect(d.Time.UnixNano()).To(Equal(now.UnixNano()))
-			d.Time.Time = time.Time{}
-			Expect(d).To(Equal(data{
-				Key1: "hello1",
-				Key2: 123,
-				Time: TimeValue{Time: time.Time{}},
-			}))
+			Expect(d).To(Equal(data{Key1: "hello1", Key2: 123}))
 		})
 
 		It("should MSetNX", func() {
@@ -1750,28 +1732,19 @@ var _ = Describe("Commands", func() {
 		})
 
 		It("should scan", func() {
-			now := time.Now()
-
-			err := client.HMSet(ctx, "hash", "key1", "hello1", "key2", 123, "time", now.Format(time.RFC3339Nano)).Err()
+			err := client.HMSet(ctx, "hash", "key1", "hello1", "key2", 123).Err()
 			Expect(err).NotTo(HaveOccurred())
 
 			res := client.HGetAll(ctx, "hash")
 			Expect(res.Err()).NotTo(HaveOccurred())
 
 			type data struct {
-				Key1 string    `redis:"key1"`
-				Key2 int       `redis:"key2"`
-				Time TimeValue `redis:"time"`
+				Key1 string `redis:"key1"`
+				Key2 int    `redis:"key2"`
 			}
 			var d data
 			Expect(res.Scan(&d)).NotTo(HaveOccurred())
-			Expect(d.Time.UnixNano()).To(Equal(now.UnixNano()))
-			d.Time.Time = time.Time{}
-			Expect(d).To(Equal(data{
-				Key1: "hello1",
-				Key2: 123,
-				Time: TimeValue{Time: time.Time{}},
-			}))
+			Expect(d).To(Equal(data{Key1: "hello1", Key2: 123}))
 		})
 
 		It("should HIncrBy", func() {
diff --git a/internal/hscan/hscan_test.go b/internal/hscan/hscan_test.go
index 72979fb..ab4c0e1 100644
--- a/internal/hscan/hscan_test.go
+++ b/internal/hscan/hscan_test.go
@@ -4,7 +4,6 @@ import (
 	"math"
 	"strconv"
 	"testing"
-	"time"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
@@ -31,20 +30,6 @@ type data struct {
 	Bool    bool    `redis:"bool"`
 }
 
-type TimeRFC3339Nano struct {
-	time.Time
-}
-
-func (t *TimeRFC3339Nano) ScanRedis(s string) (err error) {
-	t.Time, err = time.Parse(time.RFC3339Nano, s)
-	return
-}
-
-type TimeData struct {
-	Name string           `redis:"name"`
-	Time *TimeRFC3339Nano `redis:"login"`
-}
-
 type i []interface{}
 
 func TestGinkgoSuite(t *testing.T) {
@@ -190,14 +175,4 @@ var _ = Describe("Scan", func() {
 		Expect(Scan(&d, i{"bool"}, i{""})).To(HaveOccurred())
 		Expect(Scan(&d, i{"bool"}, i{"123"})).To(HaveOccurred())
 	})
-
-	It("Implements Scanner", func() {
-		var td TimeData
-
-		now := time.Now()
-		Expect(Scan(&td, i{"name", "login"}, i{"hello", now.Format(time.RFC3339Nano)})).NotTo(HaveOccurred())
-		Expect(td.Name).To(Equal("hello"))
-		Expect(td.Time.UnixNano()).To(Equal(now.UnixNano()))
-		Expect(td.Time.Format(time.RFC3339Nano)).To(Equal(now.Format(time.RFC3339Nano)))
-	})
 })
diff --git a/options_test.go b/options_test.go
index 47fa9cb..4ad9175 100644
--- a/options_test.go
+++ b/options_test.go
@@ -59,6 +59,9 @@ func TestParseURL(t *testing.T) {
 		}, {
 			url: "redis://localhost:123/?db=2&conn_max_idle_time", // missing "=" at the end
 			o:   &Options{Addr: "localhost:123", DB: 2, ConnMaxIdleTime: 0},
+		}, {
+			url: "redis://localhost:123/?db=2&client_name=hi", // client name
+			o:   &Options{Addr: "localhost:123", DB: 2, ClientName: "hi"},
 		}, {
 			url: "unix:///tmp/redis.sock",
 			o:   &Options{Addr: "/tmp/redis.sock"},
diff --git a/redis_test.go b/redis_test.go
index 8df1dd8..4cbc389 100644
--- a/redis_test.go
+++ b/redis_test.go
@@ -169,6 +169,21 @@ var _ = Describe("Client", func() {
 		Expect(db2.Close()).NotTo(HaveOccurred())
 	})
 
+	It("should client setname", func() {
+		opt := redisOptions()
+		opt.ClientName = "hi"
+		db := redis.NewClient(opt)
+
+		defer func() {
+			Expect(db.Close()).NotTo(HaveOccurred())
+		}()
+
+		Expect(db.Ping(ctx).Err()).NotTo(HaveOccurred())
+		val, err := db.ClientList(ctx).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).Should(ContainSubstring("name=hi"))
+	})
+
 	It("processes custom commands", func() {
 		cmd := redis.NewCmd(ctx, "PING")
 		_ = client.Process(ctx, cmd)
diff --git a/ring_test.go b/ring_test.go
index 7caa868..df33599 100644
--- a/ring_test.go
+++ b/ring_test.go
@@ -29,6 +29,7 @@ var _ = Describe("Redis Ring", func() {
 
 	BeforeEach(func() {
 		opt := redisRingOptions()
+		opt.ClientName = "ring_hi"
 		opt.HeartbeatFrequency = heartbeat
 		ring = redis.NewRing(opt)
 
@@ -50,6 +51,20 @@ var _ = Describe("Redis Ring", func() {
 		Expect(err).To(MatchError("context canceled"))
 	})
 
+	It("should ring client setname", func() {
+		err := ring.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {
+			return c.Ping(ctx).Err()
+		})
+		Expect(err).NotTo(HaveOccurred())
+
+		_ = ring.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {
+			val, err := c.ClientList(ctx).Result()
+			Expect(err).NotTo(HaveOccurred())
+			Expect(val).Should(ContainSubstring("name=ring_hi"))
+			return nil
+		})
+	})
+
 	It("distributes keys", func() {
 		setRingKeys()
 
diff --git a/sentinel_test.go b/sentinel_test.go
index ecde161..cfa8b01 100644
--- a/sentinel_test.go
+++ b/sentinel_test.go
@@ -1,6 +1,7 @@
 package redis_test
 
 import (
+	"context"
 	"net"
 
 	. "github.com/onsi/ginkgo"
@@ -17,6 +18,7 @@ var _ = Describe("Sentinel", func() {
 
 	BeforeEach(func() {
 		client = redis.NewFailoverClient(&redis.FailoverOptions{
+			ClientName:    "sentinel_hi",
 			MasterName:    sentinelName,
 			SentinelAddrs: sentinelAddrs,
 			MaxRetries:    -1,
@@ -125,6 +127,13 @@ var _ = Describe("Sentinel", func() {
 		err := client.Ping(ctx).Err()
 		Expect(err).NotTo(HaveOccurred())
 	})
+
+	It("should sentinel client setname", func() {
+		Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred())
+		val, err := client.ClientList(ctx).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).Should(ContainSubstring("name=sentinel_hi"))
+	})
 })
 
 var _ = Describe("NewFailoverClusterClient", func() {
@@ -134,6 +143,7 @@ var _ = Describe("NewFailoverClusterClient", func() {
 
 	BeforeEach(func() {
 		client = redis.NewFailoverClusterClient(&redis.FailoverOptions{
+			ClientName:    "sentinel_cluster_hi",
 			MasterName:    sentinelName,
 			SentinelAddrs: sentinelAddrs,
 
@@ -213,6 +223,20 @@ var _ = Describe("NewFailoverClusterClient", func() {
 		_, err = startRedis(masterPort)
 		Expect(err).NotTo(HaveOccurred())
 	})
+
+	It("should sentinel cluster client setname", func() {
+		err := client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {
+			return c.Ping(ctx).Err()
+		})
+		Expect(err).NotTo(HaveOccurred())
+
+		_ = client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {
+			val, err := c.ClientList(ctx).Result()
+			Expect(err).NotTo(HaveOccurred())
+			Expect(val).Should(ContainSubstring("name=sentinel_cluster_hi"))
+			return nil
+		})
+	})
 })
 
 var _ = Describe("SentinelAclAuth", func() {