Feature Request: Filter out specific commands for OTEL Tracing
Hello everyone, it would be nice if there was an option to exclude specific commands from being covered by traces. The idea is that you can specify a slice of commands which you don't want traces to be created for, and can pass it as options when configuring the tracer.
If that's a feasible idea, I could make my hands dirty and start right away!
Hello @cruzer200, Thanks for raising this issue. Currently, we are in the process of rethinking and realigning the metrics and tracing across all of our client libraries. We will take your feedback into consideration!
Hello @htemelski-redis thank you for considering my proposal. I just opened a PR just in case with a first proposal.
#3519 solved filter for only ProcessHook what about expanding support to ProcessPipelineHook and ability to disable dial hook, do you guys think will it be useful?
@Udhayarajan definitely. Would you like to work on that ? If so, feel free to add the filters to all of the hooks.
Technical Proposal: Unified and Optimized Trace Filtering API
After reviewing the evolution from #3481 → #3519 → #3550, I'd like to propose a more comprehensive approach to the trace filtering API:
Current State Analysis
PR #3481: Initial approach with commandExclusions []string using slices.Contains - O(n) lookup per command
PR #3519: Evolved to flexible filter function WithCommandFilter(func(cmd redis.Cmder) bool) - only for ProcessHook
PR #3550: Extended filtering to ProcessPipelineHook and DialHook - maintains same pattern
Identified Issues
-
Performance: Using slice lookups (
slices.Contains) for command exclusions is O(n). For production systems excluding multiple commands (PING, INFO, SELECT, etc.), this creates unnecessary overhead on every traced operation. -
API Inconsistency: The filter is applied inconsistently:
ProcessHook: filters individual commandsProcessPipelineHook: filters pipelines (but doesn't filter individual commands within the pipeline)DialHook: binary on/off via filter
-
Limited Flexibility: The current function signature
func(cmd redis.Cmder) boolworks forProcessHookbut doesn't generalize well to other hook types.
Proposed Improvements
1. Use Set-based Exclusion for Performance
For the common use case of excluding specific commands, provide a dedicated optimized option:
type TracingOptions struct {
// ExcludedCommands provides O(1) lookup for command exclusions
ExcludedCommands map[string]struct{}
// ... existing options
}
// Helper constructor
func WithExcludedCommands(commands ...string) TracingOption {
set := make(map[string]struct{}, len(commands))
for _, cmd := range commands {
set[strings.ToUpper(cmd)] = struct{}{}
}
return func(opts *TracingOptions) {
opts.ExcludedCommands = set
}
}
Usage:
redisotel.WithExcludedCommands("PING", "INFO", "SELECT")
2. Unified Hook Filtering API
Provide hook-specific filters with clear semantics:
type TracingOptions struct {
// Filters for each hook type
ProcessFilter func(cmd redis.Cmder) bool
ProcessPipelineFilter func(cmds []redis.Cmder) bool
DialFilter func(network, addr string) bool
// ... existing options
}
This allows:
- Fine-grained control per hook type
- Type-safe filtering with appropriate context
- Clear documentation of what each filter receives
3. Combine Both Approaches
The ExcludedCommands set should be checked first (fast path), then custom filters if provided:
func (t *TracingHook) shouldTrace(cmd redis.Cmder) bool {
// Fast path: check exclusion set
if _, excluded := t.opts.ExcludedCommands[strings.ToUpper(cmd.Name())]; excluded {
return false
}
// Custom filter if provided
if t.opts.ProcessFilter != nil {
return t.opts.ProcessFilter(cmd)
}
return true
}
Benefits
- Performance: O(1) command exclusion lookups instead of O(n)
- Memory efficient:
map[string]struct{}uses minimal memory - Backward compatible: Can deprecate slice-based approach gradually
- Extensible: Clear pattern for adding filters to future hooks
- User-friendly: Simple API for common case, powerful API for advanced cases
Would this approach align with the library's goals? Happy to contribute an implementation if this direction seems reasonable.
@zadil redisotel is maintained mainly by the community, so feel free to open a pr and we can discuss it. Overall your suggestions sound reasonable to me, I would like @Udhayarajan and @lucawol to comment as well.
Hi @zadil!, really appreciate your thoughtful points and here I like to share some perspectives.
0. Unified filter field:
Current code structure offers great flexibility. Not everyone wants to filter out all the HGET, it might be based on key & fields too,
e.g., filter only HGET user_session refresh_token. (may be that is why @htemelski-redis favored this approach.)
1. Performance
If someone’s priority is performance, they can optimize with something like:
type CustomFilterOptions struct {
ExcludedCommands map[string]struct{}
}
func (f CustomFilterOptions) FilterPipeline(cmds []redis.Cmder) bool {
for _, cmd := range cmds {
if _, excluded := f.ExcludedCommands[cmd.Name()]; excluded {
return true
}
}
return false
}
func (f CustomFilterOptions) FilterCommand(cmd redis.Cmder) bool {
_, excluded := f.ExcludedCommands[cmd.Name()]
return excluded
}
Example usage:
func init() {
to := CustomFilterOptions{
ExcludedCommands: map[string]struct{}{
"PING": {},
"INFO": {},
},
}
instrumentTracing := InstrumentTracing(rdb,
WithCommandsFilter(to.FilterPipeline),
WithCommandFilter(to.FilterCommand),
)
}
2. Filters pipelines - filter individual commands within the pipeline:
A pipeline sends multiple commands in a single round-trip to Redis. Traces are spanned for each round trip. One pipeline call multiple commands and one trace.
However, as you noted, we might want tracing to remain active, but omit or redact certain commands within the db.statement.
PS: If there is scope, we can also optimize the DefaultCommandFilter,
EDIT: I've modified the code bit, my opinion is even with what method(s) library currently offers can be simply used to achieve O(1)
Hi @Udhayarajan, Thank you for your detailed feedback and examples!
On flexibility: I completely agree that some users need fine-grained filtering (e.g., filtering a command only for specific keys or fields). With the unified API, both simple (command-based) exclusions and advanced scenarios (per-key, per-field logic via custom filter functions) can co-exist, and users can choose what best fits their needs.
Performance: Absolutely, a set-based O(1) exclusion is optimal for frequent cases like excluding PING, INFO, SELECT globally. For custom business logic, the hook-based filters offer the needed extensibility without sacrificing perf for the common case.
Pipelines: Good point about tracing round-trips versus filtering individual commands within a pipeline. The unified API allows users to decide whether to filter at the pipeline level or inspect/filter sub-commands for trace granularity and security/redaction scenarios.
I’ve opened PR #3567 with these capabilities so the community can review and iterate on top. Let me know if you have suggestions about the structure (e.g., preferred filter signatures or further optimizations on the default filter).
Thanks again for your insights! Looking forward to the team’s feedback.