Repository navigation
feat(redisotel): truncate statement span attributes - #4114
ronitgupta138 wants to merge 11 commits into
Conversation
Truncate db.statement span attributes by default to avoid pushing large command payloads into telemetry. Arguments exceeding the limit are cut to a prefix with a summary marker '…(+N bytes)', and pipeline statements are capped. Full data remains opt-in via WithDBStatementMaxLen(0). Credential arguments remain redacted. Closes redis#4092
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 004c76283c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| b = append(b, redactedArg...) | ||
| continue | ||
| } | ||
| b = appendArgTruncated(b, arg, maxLen) |
There was a problem hiding this comment.
Enforce the remaining statement budget per argument
When a command has a long but sub-limit key followed by a max-sized value (for example, SET with a 500-byte key and 512-byte value under the default), the pre-append check still passes and this call grants the value another full maxLen; the resulting db.statement is about 1 KiB (and binary data can be larger after hex encoding), so WithDBStatementMaxLen does not provide its documented overall statement cap. Pass only the remaining budget, accounting for encoded output and markers, when appending.
Useful? React with 👍 / 👎.
| _, cmdsString := rediscmd.CmdsString(cmds) | ||
| return cmdsString | ||
| } | ||
| b := make([]byte, 0, 64*len(cmds)) |
There was a problem hiding this comment.
Bound pipeline buffer allocation by the limit
When tracing a large pipeline with truncation enabled, this allocation reserves 64 * len(cmds) bytes before the loop stops near the configured cap; a 100,000-command pipeline therefore creates a 6.4 MB temporary buffer to retain only roughly 512 bytes. This makes the instrumentation's memory and GC cost scale with the entire pipeline despite the new cap, so bound the initial capacity by the configured maximum.
Useful? React with 👍 / 👎.
…e buffer allocation (redis#4092) - Subtract written statement bytes from maxLen before appending arguments to strictly enforce overall statement cap - Account for 2x hex expansion on non-printable binary arguments when computing truncation thresholds - Cap pipeline buffer allocation to maxLen to avoid allocating multi-megabyte buffers on large pipelines
|
Addressed the review findings in commit
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b77fac2edb
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if isHex && limit > 0 { | ||
| byteLimit = limit / 2 | ||
| } | ||
| if byteLimit > 0 && len(v) > byteLimit { | ||
| b = appendUTF8String(b, v[:byteLimit]) |
There was a problem hiding this comment.
Treat a zero binary budget as exhausted
With truncation enabled, the new remaining-budget path can pass limit == 1 here; integer division then sets byteLimit to zero, and the byteLimit > 0 guard skips truncation so the fallthrough hex-encodes the entire argument. For example, at the default 512-byte limit, SET plus a 506-byte key leaves one byte for a 1 MiB non-printable value and produces roughly 2 MiB of db.statement, so this still defeats the cap and recreates the large telemetry allocation; treat zero as an exhausted encoded budget rather than as unlimited.
Useful? React with 👍 / 👎.
| bytesVal := []byte(v) | ||
| isHex := !isSimple(bytesVal) |
There was a problem hiding this comment.
Bound classification work to the retained prefix
When truncation is enabled for a large printable string, such as a multi-megabyte JSON value, isSimple(bytesVal) walks the entire argument before appendArgTruncated retains only the first remaining-budget bytes. Because this hook runs synchronously before the Redis operation, default tracing still adds O(payload size) work despite the 512-byte limit, so inspect only the retained prefix (or otherwise bound classification work) rather than the discarded suffix.
Useful? React with 👍 / 👎.
…ard zero binary budget (redis#4092) - Restrict isSimple check to at most limit bytes to avoid O(payload) work on large strings/bytes - Avoid full []byte(v) allocations by slicing string arguments directly before encoding - Guard byteLimit <= 0 when remaining budget is 1 byte, summarizing without hex-dumping entire payload
|
Addressed the review findings in commit
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6e59a1b106
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| case string: | ||
| return v | ||
| case []byte: | ||
| return string(v) |
There was a problem hiding this comment.
Avoid copying byte-slice rules before truncation
When ACL SETUSER receives a large rule as a []byte (for example, a multi-megabyte >password), secretArgs calls argString for every rule and this conversion copies the entire slice before the rule is redacted; the later truncation therefore keeps at most maxLen but the tracing hook still incurs a payload-sized allocation. Inspect the byte slice directly or compare it without converting the whole value to a string.
Useful? React with 👍 / 👎.
| if maxLen > 0 && bufCap > maxLen+64 { | ||
| bufCap = maxLen + 64 |
There was a problem hiding this comment.
Guard the pipeline capacity addition from overflow
When a caller supplies WithDBStatementMaxLen a value above math.MaxInt-64, maxLen+64 wraps negative, this branch assigns a negative bufCap, and the subsequent make panics before the wrapped Redis hook is called. Compute this cap with overflow-safe or saturating arithmetic so a valid positive configuration cannot crash pipeline processing.
AGENTS.md reference: AGENTS.md:L117-L121
Useful? React with 👍 / 👎.
…rsion on byte-slice ACL rules (redis#4092) - Guard pipeline buffer capacity calculation against integer overflow for extreme maxLen values - Inspect ACL rules directly on []byte slices without allocating temporary strings
|
Addressed the review findings in commit
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b4dd5f9a4f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| case time.Time: | ||
| return v.AppendFormat(b, time.RFC3339Nano) | ||
| default: | ||
| s := fmt.Sprint(v) |
There was a problem hiding this comment.
Avoid materializing custom arguments before truncation
When a valid command argument is a large custom value (for example, a named []byte implementing encoding.BinaryMarshaler, which internal/proto.Writer.WriteArg accepts), fmt.Sprint(v) constructs a representation of the entire payload before limit is applied. A 1 MiB binary-marshaler slice can therefore allocate a multi-megabyte decimal string synchronously in the tracing hook despite the default 512-byte setting, defeating the new allocation bound; handle supported custom values without formatting the discarded suffix.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 59d9d7d: added explicit support for encoding.BinaryMarshaler, net.IP, and pointer arguments in appendArgTruncated, directly bounding the binary representation to the retained prefix without materializing full formatted strings via fmt.Sprint.
redis#4092) - Handle encoding.BinaryMarshaler, net.IP, and pointer arguments in appendArgTruncated without falling through to fmt.Sprint - Bound custom binary arguments to byteLimit prefix without materializing full formatted slice - Add unit test coverage for large custom BinaryMarshaler payloads and pointer arguments Signed-off-by: Ronit Gupta <[email protected]>
|
Addressed the review findings in commit
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 87dc67b9e4
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| case encoding.BinaryMarshaler: | ||
| data, err := v.MarshalBinary() |
There was a problem hiding this comment.
Match time values before BinaryMarshaler
When tracing an HSET struct with a nil *time.Time field (a value supported by the command writer), the pointer satisfies encoding.BinaryMarshaler and reaches this call before the nil-aware *time.Time branch, so Time.MarshalBinary panics even though internal/proto/writer.go:189-210 would serialize it as the zero time; non-nil time values are likewise logged as binary hex instead of the RFC3339 value sent to Redis. Move the time cases before the interface dispatch so instrumentation does not crash or misreport otherwise valid commands.
AGENTS.md reference: AGENTS.md:L117-L123
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit a2dd0ee: moved time.Time, *time.Time, time.Duration, and *time.Duration before any custom type interface dispatch. Non-nil timestamps format as RFC3339Nano matching proto.Writer, and nil *time.Time pointers safely format as <nil> without dereferencing or panicking.
| } | ||
| return appendBytesTruncated(b, *v, limit) | ||
| case encoding.BinaryMarshaler: | ||
| data, err := v.MarshalBinary() |
There was a problem hiding this comment.
Avoid fully marshaling truncated custom values
When tracing a large encoding.BinaryMarshaler whose method builds a fresh result, this call constructs the entire payload before appendBytesTruncated can apply the 512-byte budget, and internal/proto/writer.go:205-210 invokes it again to send the request. The fresh evidence after the earlier custom-argument fix is that MarshalBinary accepts no limit and returns the complete byte slice, so a multi-megabyte value still adds a payload-sized tracing allocation; use a bounded representation that does not fully marshal it in the hook.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit a2dd0ee: custom types now inspect underlying []byte / string slices directly via reflection to avoid payload-sized allocations in tracing hooks.
…ll marshaling (redis#4092) - Move time.Time, *time.Time, and time.Duration cases before custom type reflection to ensure timestamps serialize as RFC3339Nano and nil pointers format as <nil> without panicking - Use reflect-based underlying byte slice extraction for custom types to truncate without full payload marshaling allocations - Add unit tests verifying nil *time.Time safety and non-nil time.Time RFC3339Nano formatting Signed-off-by: Ronit Gupta <[email protected]>
|
Addressed the review findings in commit
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a2dd0ee7ef
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if val.Kind() == reflect.String { | ||
| return appendStringTruncated(b, val.String(), limit) |
There was a problem hiding this comment.
Preserve redaction supplied by named Stringer arguments
When a named string argument implements both fmt.Stringer (returning a redacted value) and encoding.BinaryMarshaler (so internal/proto.Writer.WriteArg accepts it), this reflection branch reads the underlying string directly instead of using the safe String result. The parent path through rediscmd.CmdString used fmt.Sprint for such named types, so the default max-length path now exports secrets that were previously masked in db.statement; keep a bounded representation without exposing the underlying value.
Useful? React with 👍 / 👎.
| } | ||
| } | ||
|
|
||
| s := fmt.Sprint(v) |
There was a problem hiding this comment.
Bound formatting of struct-backed custom arguments
When a valid argument is a struct-backed encoding.BinaryMarshaler containing a large byte slice, internal/proto.Writer.WriteArg accepts it but this fallback renders the entire struct and slice before appendStringTruncated applies the 512-byte budget. The fresh evidence after the earlier custom-argument fixes is that the new reflection branches only bound underlying string/slice kinds; a 1 MiB marshaler struct still creates a multi-megabyte decimal string synchronously in tracing, so use a bounded placeholder or representation instead of formatting the whole value.
Useful? React with 👍 / 👎.
|
|
||
| if err := cmd.Err(); err != nil { | ||
| b = append(b, ": "...) | ||
| b = append(b, err.Error()...) |
There was a problem hiding this comment.
Truncate command errors within the statement budget
When a command enters Process with Err() already set to a long message (callers can do this through the public Cmder.SetErr API), this append bypasses all argument budgeting. A 1 MiB pre-set error therefore still creates and copies a 1 MiB db.statement under the default 512-byte limit, and pipelines likewise grow beyond their cap; append only the error prefix that fits the remaining budget.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 33fa0b1b99
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
| return append(b, v.String()...) | ||
| case int: | ||
| return strconv.AppendInt(b, int64(v), 10) |
There was a problem hiding this comment.
Truncate scalar arguments to the remaining budget
When WithDBStatementMaxLen is smaller than a scalar's textual form, the numeric, time, and bool branches append the entire value and ignore limit; for example, INCRBY key math.MaxInt64 with maxLen == 16 has five bytes left here but emits all 19 digits, producing a 30-byte statement with no truncation marker. The caller now passes the correct remaining budget, but these branches still bypass it, so scalar formatting should also use a bounded append path.
Useful? React with 👍 / 👎.
|
Thanks for picking this up. We hit #4092 in our service: cached values of about 1 MB, hex-encoded into 1. Limit each argument, not the whole statement. With a per-argument limit, every command keeps its name, keys and options: 2. Make the total cap optional and off by default. 3. Expose formatting as options, so users can pick a strategy. // Formats one argument. The default shows at most maxLen characters, then "...(+N bytes)".
WithDBStatementArgFormatter(func(b []byte, arg any) []byte)
DefaultDBStatementArgFormatter(maxLen int)
// Or whole statements, for full control.
WithDBStatementFormatter(func(cmd redis.Cmder) string)
WithPipelineDBStatementFormatter(func(cmds []redis.Cmder) string)An argument formatter that is handed the bytes would keep credential redaction in redisotel and leave only the argument rendering to the user. Strategies could then include the size only ( 4. Don't build the statement just for the span name. Our current workaround, in case it helps others. go-redis sends an type value struct {
b []byte
format func([]byte) string
}
func (v value) MarshalBinary() ([]byte, error) { return v.b, nil }
func (v value) String() string { return v.format(v.b) }
pipe.Set(ctx, key, value{b, format}, ttl)Redis gets the exact bytes, and the statement shows |
ndyakov
left a comment
There was a problem hiding this comment.
Thanks @ronitgupta138, and apologies that this took a while: we've had a big wave of contributions lately and the review queue backed up. The truncation does what #4092 asks.
Two things before merge:
- Credential leak on the default path (inline 2): a named string type with a redacting
String()is written raw, soSET k Secret("hunter2")becomesSET k hunter2, while master andWithDBStatementMaxLen(0)giveSET k ***. Named[]bytetypes have the same problem. - Please add the limit to
extra/rediscmdand call it from here instead of copying its redaction table (inline 1), so redaction stays in one place.
Smaller points are inline 3 and 4. Also please drop (#4092) from the title; squash merge adds the PR number.
|
|
||
| // secretArgs marks the positions in args that hold a credential. It returns nil | ||
| // when the command carries none, which is the common case. | ||
| func secretArgs(args []interface{}) []bool { |
There was a problem hiding this comment.
secretArgs, secretConfigParams, equalFoldArg, argString, appendUTF8String, isSimple and the number cases of appendArgTruncated copy extra/rediscmd/rediscmd.go. Please move the limit into rediscmd and call it from here, so redaction lives in one place. hasSecretRulePrefix and the pointer cases can move with it. argString can then use the existing zero-copy String(v) instead of string(v), which also covers the open Codex thread on line 164.
| if val.Kind() == reflect.String { | ||
| return appendStringTruncated(b, val.String(), limit) |
There was a problem hiding this comment.
reflect.Value.String() on a Kind() == String value returns the underlying string and skips the type's own String() method. With type Secret string that has String() string { return "***" } (plus MarshalBinary, so WriteArg accepts it), the default path writes the secret. The []byte-kind branch on line 409 has the same problem. When truncation is on, a fmt.Stringer should go through appendStringTruncated(b, v.String(), limit). Only a string/[]byte kind with no methods should use the kind fast path. Please add a test with a redacting Stringer.
| } | ||
| } | ||
|
|
||
| s := fmt.Sprint(v) |
There was a problem hiding this comment.
This is still unbounded for struct-backed BinaryMarshaler values: fmt.Sprint formats the whole value and the result is cut afterwards. Master does the same, but the PR description says large payloads are not copied. A bounded option is a type placeholder such as <main.Blob> when truncation is on, with fmt.Sprint kept only for maxLen <= 0.
| // WithDBStatementMaxLen specifies the maximum length for statements and arguments in the tracing hook. | ||
| // Arguments exceeding this limit are truncated with a summary marker `…(+N bytes)`. | ||
| // If maxLen <= 0, statements and arguments are not truncated (full data opt-in). |
There was a problem hiding this comment.
The limit is one budget for the whole statement (one command, or all commands of a pipeline), and the marker and the command error text can go past it. Suggested wording:
| // WithDBStatementMaxLen specifies the maximum length for statements and arguments in the tracing hook. | |
| // Arguments exceeding this limit are truncated with a summary marker `…(+N bytes)`. | |
| // If maxLen <= 0, statements and arguments are not truncated (full data opt-in). | |
| // WithDBStatementMaxLen sets the byte budget for the db.statement attribute | |
| // (default DefaultDBStatementMaxLen). The budget covers the whole statement: | |
| // one command, or all commands of a pipeline. Arguments that do not fit are cut | |
| // and get a `…(+N bytes)` marker; the marker and the command error text can go | |
| // past the budget. If maxLen <= 0, statements are not truncated. |
…iscmd - Move statement truncation logic and length limits into extra/rediscmd/rediscmd.go via CmdStringLimit, CmdsStringLimit, and AppendCmdLimit. - Prioritize fmt.Stringer to ensure custom String() redaction masks (e.g. Secret types) are preserved and not leaked via raw reflection. - Bound struct-backed BinaryMarshaler representations to type placeholders when truncated. - Use zero-copy rediscmd.String(v) in argString. - Update WithDBStatementMaxLen docstring per maintainer review. - Add tests for Stringer redaction and truncation in both rediscmd and redisotel. Signed-off-by: Ronit Gupta <[email protected]>
|
Thanks @ndyakov for the thorough review! Addressed all points in
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3a328b93de
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if v == nil || (reflect.ValueOf(v).Kind() == reflect.Pointer && reflect.ValueOf(v).IsNil()) { | ||
| return append(b, "<nil>"...) | ||
| } | ||
| return appendStringTruncated(b, s.String(), limit) |
There was a problem hiding this comment.
Contain Stringer panics inside statement rendering
When an otherwise valid encoding.BinaryMarshaler also implements fmt.Stringer and its String method panics for a particular value, this direct call lets the panic escape the built-in tracing hook before next is called; the Redis writer would only call MarshalBinary, and the previous fmt.Sprint path recovered formatter panics. Use a panic-safe rendering path so telemetry cannot abort an otherwise serializable Redis operation.
AGENTS.md reference: AGENTS.md:L117-L122
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| func pipelineSummary(cmds []redis.Cmder) string { | ||
| summary, _ := rediscmd.CmdsStringLimit(cmds, 0) |
There was a problem hiding this comment.
Build pipeline summaries without rendering arguments
When ProcessPipelineHook sees a pipeline containing a large value, this call passes 0, so CmdsStringLimit renders the entire pipeline into the discarded second return value before buildPipelineStatement performs its bounded render. The fresh evidence after the earlier pipeline-capacity fix is this new unbounded summary path: a 1 MiB SET is still copied, and large pipelines are fully formatted despite the default cap; derive the summary directly from command names instead.
Useful? React with 👍 / 👎.
| return appendStringTruncated(b, placeholder, limit) | ||
| } | ||
|
|
||
| s := fmt.Sprint(v) |
There was a problem hiding this comment.
Bound non-struct custom argument formatting
When a valid command argument implements encoding.BinaryMarshaler on a large map, array, or pointer to an array, it reaches this fallback because the bounded branches only cover strings, byte slices, and struct-backed values. The fresh evidence after the struct-backed custom-argument fix is that a type Blob [1<<20]byte with MarshalBinary still makes fmt.Sprint allocate a multi-megabyte decimal representation before the 512-byte truncation is applied; use a bounded placeholder for the remaining custom backing kinds.
Useful? React with 👍 / 👎.
ndyakov
left a comment
There was a problem hiding this comment.
Thanks @ronitgupta138, and apologies for the wait on this round. The redacting-Stringer leak is closed (SET k Secret("hunter2") gives SET k ***), the limit lives in rediscmd now, and the title is fixed. Two things block:
- A
String()that panics now escapes the tracing hook. Master formats the panic insidefmt, and the statement becomes%!v(PANIC=String method: ...). This PR panics beforenextruns, which breaks the hook contract in AGENTS.md. Recover around theString()call, and fall back tofmt.Sprint, which recovers by itself (inline). pipelineSummarystill renders every argument with a limit of 0, and the string is then thrown away. This happens even whendb.statementis off. A pipeline with one 1 MiBSETcosts about 0.56 ms and 1 MB of allocation per pipeline here, against about 2 KB with the limit. The summary only needs the command names, so build it fromName()(inline).
Not blocking, but it is the same bug class: a BinaryMarshaler on a non-struct type, such as a [1<<20]byte array, still goes through fmt.Sprint. At a 512-byte limit that costs about 21 ms and 12.7 MB per command. The placeholder you added for structs can cover every kind that is not string or []byte. Either here or as a follow-up.
| if v == nil || (reflect.ValueOf(v).Kind() == reflect.Pointer && reflect.ValueOf(v).IsNil()) { | ||
| return append(b, "<nil>"...) | ||
| } | ||
| return appendStringTruncated(b, s.String(), limit) |
There was a problem hiding this comment.
String() runs here with no recover. A value whose String() panics now crashes the tracing hook, and the hook contract (AGENTS.md) says hooks must not panic. Master's fmt.Sprint recovered from it. Call through a helper:
func safeString(s fmt.Stringer) (out string) {
defer func() {
if recover() != nil {
out = fmt.Sprint(s) // fmt recovers the panic itself
}
}()
return s.String()
}Then use appendStringTruncated(b, safeString(s), limit).
| } | ||
|
|
||
| func pipelineSummary(cmds []redis.Cmder) string { | ||
| summary, _ := rediscmd.CmdsStringLimit(cmds, 0) |
There was a problem hiding this comment.
This renders every argument, including 1 MiB values, and discards the statement. It runs on every pipeline, even with db.statement off. The span name only needs the unique command names (up to 10). Add a rediscmd helper that returns those names and use it here, so the summary never renders arguments.
… from command names - Recover around String() call in rediscmd using safeString helper and fall back to fmt.Sprint to contain Stringer panics within statement rendering. - Add rediscmd.CmdsSummary to extract up to 10 unique command names directly without formatting or allocating command arguments for pipeline spans. - Use type placeholder for non-string/non-[]byte custom BinaryMarshaler values (arrays, maps, structs) when truncation is enabled to avoid large memory allocations. - Add unit tests verifying panicking Stringer recovery, CmdsSummary zero-arg formatting, and non-struct custom type truncation. Signed-off-by: Ronit Gupta <[email protected]>
|
Thanks @ndyakov for the thorough review. Addressed in commit
Added unit tests in both |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 29a2863f85
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if val.Kind() == reflect.String { | ||
| return appendStringTruncated(b, val.String(), limit) |
There was a problem hiding this comment.
Preserve error-backed redaction before reflecting strings
When a valid argument is a named string that implements both error and encoding.BinaryMarshaler, the old fmt.Sprint path used its Error() representation, but this reflection branch now reads the backing string directly. For a secret wrapper whose Error() returns *** while MarshalBinary() returns hunter2, both default redisotel tracing and existing rediscmd.CmdString callers now export hunter2; honor panic-safe error formatting before exposing the underlying value.
Useful? React with 👍 / 👎.
| func safeString(s fmt.Stringer) (out string) { | ||
| defer func() { | ||
| if recover() != nil { | ||
| out = fmt.Sprint(s) // fmt recovers the panic itself |
There was a problem hiding this comment.
Guard the recovery fallback against nested formatter panics
When a valid encoding.BinaryMarshaler's String() panics with a value whose own String or Error formatter also panics, this recovery handler calls fmt.Sprint(s), which re-enters String() and Go's nested-panic guard re-panics while formatting the panic value; the panic therefore still escapes ProcessHook before next runs. Fresh evidence after the simple Stringer recovery is this nested formatter path; recover the fallback too or emit a fixed placeholder.
AGENTS.md reference: AGENTS.md:L117-L122
Useful? React with 👍 / 👎.
| if v == nil || (reflect.ValueOf(v).Kind() == reflect.Pointer && reflect.ValueOf(v).IsNil()) { | ||
| return append(b, "<nil>"...) | ||
| } | ||
| return appendStringTruncated(b, safeString(s), limit) |
There was a problem hiding this comment.
Avoid materializing full Stringer output before truncation
When truncation is enabled and a valid encoding.BinaryMarshaler also implements fmt.Stringer by converting a large []byte payload to a string, safeString(s) materializes the entire result before the 512-byte budget is applied. Fresh evidence after the bounded reflection fix is that this prioritized Stringer branch bypasses the bounded byte-slice path, so a 1 MiB argument still adds a payload-sized synchronous tracing allocation; use a bounded placeholder or path when String output cannot be produced incrementally.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Reviewed by Cursor Bugbot for commit 29a2863. Configure here.
| b = append(b, '\n') | ||
| } | ||
| b = AppendCmd(b, cmd) | ||
| b = AppendCmdLimit(b, cmd, maxLen) |
There was a problem hiding this comment.
Pipeline summary drops later commands
Low Severity
CmdsStringLimit builds its command-name summary in the same loop that truncates the statement, and breaks once the byte budget is exhausted. Later unique names never enter the summary even though they are still in the pipeline. CmdsSummary already walks the full command list for this, which is why tracing uses it for span names, but the new limited API still returns the partial list.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 29a2863. Configure here.


Summary
Fixes #4092.
Truncates
db.statementspan attributes by default inextra/redisotelto prevent large command payloads from being copied and pushed into telemetry.Details
DefaultDBStatementMaxLen = 512) with a summary suffix…(+N bytes).WithDBStatementMaxLen(maxLen int) TracingOptionallowing callers to customize the truncation threshold, or opt in to full statements by passingmaxLen <= 0.AUTH,HELLO ... AUTH,CONFIG SET requirepass,ACL SETUSER,MIGRATE ... AUTH,SENTINEL SET auth-pass) across both truncated and untruncated statements.pipelineSummarywithout copying or allocating argument strings.Test Plan
extra/redisotel/statement_test.goverifies:WithDBStatementMaxLen.WithDBStatementMaxLen(0).go test -race -v ./extra/redisotel/...passes cleanly.Note
Medium Risk
Default telemetry behavior changes (truncated
db.statement) and touches sensitive-command formatting, though redaction and limits are heavily tested.Overview
Adds length-bounded Redis command rendering in
extra/rediscmd(CmdStringLimit,CmdsStringLimit,CmdsSummary) so large arguments are truncated with…(+N bytes)without copying megabyte payloads, while keeping existing credential redaction and extending it (ACL rule pointers, customStringertypes, panickingString()recovery).extra/redisotelnow defaultsdb.statementto a 512-byte budget viaDefaultDBStatementMaxLenandWithDBStatementMaxLen, wiring tracing hooks throughbuildCmdStatement/buildPipelineStatementand deriving pipeline span names fromCmdsSummaryinstead of formatting every argument. Callers can passmaxLen <= 0to retain full statements (existing long-statement tests updated accordingly).Reviewed by Cursor Bugbot for commit 29a2863. Bugbot is set up for automated code reviews on this repo. Configure here.