Repository: redis/go-redis

Issue:
redisotel leaks passwords in traces
Distributed traces for `hello` commands leak passwords. If there are other redis commands that include password they likely leak there too. 

I realize there is an option to disable the statement but that makes the traces less useful and there should be a finite amount of commands that can accept passwords. 

## Expected Behavior

Passwords are obfuscated or not included.

## Current Behavior

Passwords are leaked in the db.statement attribute.

```
{
  "value": "hello 3 auth default <redacted>",
  "key": "db.statement"
}
```

## Possible Solution

1. Introspect command and remove password
2. Allow folks to configure filters for commands

## Steps to Reproduce


Hints from issue comments:
@ndyakov: Hello @Sovietaced, previously the hooks were not including the Hello command, but now they do. I would like the approach for `redisotel` to provide a way for users to configure a filter. @vmihailenco may take a look at this. At the meantime, feel free to open a PR for filtering the commands in `redisotel`. We are not actively working on the `extra` packages, the community is welcomed to contribute to them and we will review the PRs@Sovietaced: > Hello @Sovietaced, previously the hooks were not including the Hello command, but now they do. I would like the approach for `redisotel` to provide a way for users to configure a filter. @vmihailenco may take a look at this. At the meantime, feel free to open a PR for filtering the commands in `redisotel`. We are not actively working on the `extra` packages, the community is welcomed to contribute to them and we will review the PRs

Sounds good. I'll file a pull request soon.

Solution patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 0a62809..8185352 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -39,7 +39,7 @@ jobs:
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v3
+      uses: github/codeql-action/init@v4
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,7 +50,7 @@ jobs:
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
-      uses: github/codeql-action/autobuild@v3
+      uses: github/codeql-action/autobuild@v4
 
     # ℹ️ Command-line programs to run using the OS shell.
     # 📚 https://git.io/JvXDl
@@ -64,4 +64,4 @@ jobs:
     #   make release
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v3
+      uses: github/codeql-action/analyze@v4
diff --git a/extra/redisotel/config.go b/extra/redisotel/config.go
index 6d90abf..62b3c9b 100644
--- a/extra/redisotel/config.go
+++ b/extra/redisotel/config.go
@@ -1,6 +1,9 @@
 package redisotel
 
 import (
+	"strings"
+
+	"github.com/redis/go-redis/v9"
 	"go.opentelemetry.io/otel"
 	"go.opentelemetry.io/otel/attribute"
 	"go.opentelemetry.io/otel/metric"
@@ -21,6 +24,7 @@ type config struct {
 
 	dbStmtEnabled bool
 	callerEnabled bool
+	filter        func(cmd redis.Cmder) bool
 
 	// Metrics options.
 
@@ -124,6 +128,37 @@ func WithCallerEnabled(on bool) TracingOption {
 	})
 }
 
+// WithCommandFilter allows filtering of commands when tracing to omit commands that may have sensitive details like
+// passwords.
+func WithCommandFilter(filter func(cmd redis.Cmder) bool) TracingOption {
+	return tracingOption(func(conf *config) {
+		conf.filter = filter
+	})
+}
+
+func BasicCommandFilter(cmd redis.Cmder) bool {
+	if strings.ToLower(cmd.Name()) == "auth" {
+		return true
+	}
+
+	if strings.ToLower(cmd.Name()) == "hello" {
+		if len(cmd.Args()) < 3 {
+			return false
+		}
+
+		arg, exists := cmd.Args()[2].(string)
+		if !exists {
+			return false
+		}
+
+		if strings.ToLower(arg) == "auth" {
+			return true
+		}
+	}
+
+	return false
+}
+
 //------------------------------------------------------------------------------
 
 type MetricsOption interface {
diff --git a/extra/redisotel/tracing.go b/extra/redisotel/tracing.go
index 40df5a2..5c91710 100644
--- a/extra/redisotel/tracing.go
+++ b/extra/redisotel/tracing.go
@@ -102,6 +102,12 @@ func (th *tracingHook) DialHook(hook redis.DialHook) redis.DialHook {
 func (th *tracingHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook {
 	return func(ctx context.Context, cmd redis.Cmder) error {
 
+		// Check if the command should be filtered out
+		if th.conf.filter != nil && th.conf.filter(cmd) {
+			// If so, just call the next hook
+			return hook(ctx, cmd)
+		}
+
 		attrs := make([]attribute.KeyValue, 0, 8)
 		if th.conf.callerEnabled {
 			fn, file, line := funcFileLine("github.com/redis/go-redis")



Test patch:
diff --git a/extra/redisotel/tracing_test.go b/extra/redisotel/tracing_test.go
index a3e3ccc..0ae70c2 100644
--- a/extra/redisotel/tracing_test.go
+++ b/extra/redisotel/tracing_test.go
@@ -95,6 +95,138 @@ func TestWithoutCaller(t *testing.T) {
 	}
 }
 
+func TestWithCommandFilter(t *testing.T) {
+
+	t.Run("filter out ping command", func(t *testing.T) {
+		provider := sdktrace.NewTracerProvider()
+		hook := newTracingHook(
+			"",
+			WithTracerProvider(provider),
+			WithCommandFilter(func(cmd redis.Cmder) bool {
+				return cmd.Name() == "ping"
+			}),
+		)
+		ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test")
+		cmd := redis.NewCmd(ctx, "ping")
+		defer span.End()
+
+		processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {
+			innerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)
+			if innerSpan.Name() != "redis-test" || innerSpan.Name() == "ping" {
+				t.Fatalf("ping command should not be traced")
+			}
+
+			return nil
+		})
+		err := processHook(ctx, cmd)
+		if err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	t.Run("do not filter ping command", func(t *testing.T) {
+		provider := sdktrace.NewTracerProvider()
+		hook := newTracingHook(
+			"",
+			WithTracerProvider(provider),
+			WithCommandFilter(func(cmd redis.Cmder) bool {
+				return false // never filter
+			}),
+		)
+		ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test")
+		cmd := redis.NewCmd(ctx, "ping")
+		defer span.End()
+
+		processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {
+			innerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)
+			if innerSpan.Name() != "ping" {
+				t.Fatalf("ping command should be traced")
+			}
+
+			return nil
+		})
+		err := processHook(ctx, cmd)
+		if err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	t.Run("auth command filtered with basic command filter", func(t *testing.T) {
+		provider := sdktrace.NewTracerProvider()
+		hook := newTracingHook(
+			"",
+			WithTracerProvider(provider),
+			WithCommandFilter(BasicCommandFilter),
+		)
+		ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test")
+		cmd := redis.NewCmd(ctx, "auth", "test-password")
+		defer span.End()
+
+		processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {
+			innerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)
+			if innerSpan.Name() != "redis-test" || innerSpan.Name() == "auth" {
+				t.Fatalf("auth command should not be traced by default")
+			}
+
+			return nil
+		})
+		err := processHook(ctx, cmd)
+		if err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	t.Run("hello command filtered with basic command filter when sensitive", func(t *testing.T) {
+		provider := sdktrace.NewTracerProvider()
+		hook := newTracingHook(
+			"",
+			WithTracerProvider(provider),
+			WithCommandFilter(BasicCommandFilter),
+		)
+		ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test")
+		cmd := redis.NewCmd(ctx, "hello", 3, "AUTH", "test-user", "test-password")
+		defer span.End()
+
+		processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {
+			innerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)
+			if innerSpan.Name() != "redis-test" || innerSpan.Name() == "hello" {
+				t.Fatalf("auth command should not be traced by default")
+			}
+
+			return nil
+		})
+		err := processHook(ctx, cmd)
+		if err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	t.Run("hello command not filtered with basic command filter when not sensitive", func(t *testing.T) {
+		provider := sdktrace.NewTracerProvider()
+		hook := newTracingHook(
+			"",
+			WithTracerProvider(provider),
+			WithCommandFilter(BasicCommandFilter),
+		)
+		ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test")
+		cmd := redis.NewCmd(ctx, "hello", 3)
+		defer span.End()
+
+		processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {
+			innerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)
+			if innerSpan.Name() != "hello" {
+				t.Fatalf("hello command should be traced")
+			}
+
+			return nil
+		})
+		err := processHook(ctx, cmd)
+		if err != nil {
+			t.Fatal(err)
+		}
+	})
+}
+
 func TestTracingHook_DialHook(t *testing.T) {
 	imsb := tracetest.NewInMemoryExporter()
 	provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb))