Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions tracelog/tracelog.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,41 @@ func logQueryArgs(args []any) []any {
for _, a := range args {
switch v := a.(type) {
case []byte:
if len(v) < 64 {
if len(v) <= 64 {
a = hex.EncodeToString(v)
} else {
a = fmt.Sprintf("%x (truncated %d bytes)", v[:64], len(v)-64)
// Show first 60 bytes as hex (120 chars) + ellipsis.
// This ensures the result is always shorter than the
// full hex encoding: 120 + 3 < 130+ for any 65+ byte input.
a = hex.EncodeToString(v[:60]) + "…"
}
case string:
if len(v) > 64 {
// Find the rune boundary nearest to 64 bytes,
// then back off so the ellipsis (3 bytes) still
// makes the result shorter than the original.
l := 0
for w := 0; l < 64; l += w {
_, w = utf8.DecodeRuneInString(v[l:])
}
if len(v) > l {
a = fmt.Sprintf("%s (truncated %d bytes)", v[:l], len(v)-l)
// Ensure truncation + ellipsis is strictly shorter than original.
// Ellipsis is 3 bytes, so cut+3 must be < len(v), i.e. cut <= len(v)-4.
maxCut := len(v) - 4
if maxCut < 1 {
maxCut = 1
}
cut := 0
var prevW int
for w := 0; cut < maxCut; cut += w {
prevW = w
_, w = utf8.DecodeRuneInString(v[cut:])
}
// If cut overshot, back up to the previous rune boundary.
if cut > maxCut && prevW > 0 {
cut -= prevW
}
a = v[:cut] + "…"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion tracelog/tracelog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ func TestLogQueryArgsHandlesUTF8(t *testing.T) {
logs = logger.FilterByMsg("Query")
require.Len(t, logs, 1)
require.Equal(t, tracelog.LogLevelInfo, logs[0].lvl)
require.Equal(t, s+" (truncated 3 bytes)", logs[0].data["args"].([]any)[0])
require.Equal(t, s+"", logs[0].data["args"].([]any)[0])
})
}

Expand Down
60 changes: 60 additions & 0 deletions tracelog/truncation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package tracelog

import (
"encoding/hex"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestLogQueryArgsTruncation(t *testing.T) {
t.Parallel()

t.Run("short string not truncated", func(t *testing.T) {
args := logQueryArgs([]any{"hello"})
assert.Equal(t, "hello", args[0])
})

t.Run("long string truncated with ellipsis", func(t *testing.T) {
s := strings.Repeat("a", 65)
args := logQueryArgs([]any{s})
result := args[0].(string)
// maxCut = len(v)-4 = 61, so output = 61 a's + … (3 bytes) = 64 bytes
assert.Equal(t, strings.Repeat("a", 61)+"…", result)
// Result must be shorter than original
assert.Less(t, len(result), len(s))
})

t.Run("short bytes not truncated", func(t *testing.T) {
b := []byte{0x01, 0x02, 0x03}
args := logQueryArgs([]any{b})
assert.Equal(t, "010203", args[0])
})

t.Run("long bytes truncated with ellipsis", func(t *testing.T) {
b := make([]byte, 65)
for i := range b {
b[i] = byte(i)
}
args := logQueryArgs([]any{b})
result := args[0].(string)
// Should be 120 hex chars (60 bytes) + "…", always shorter than full hex
assert.True(t, strings.HasSuffix(result, "…"), "truncated bytes should end with …")
assert.Less(t, len(result), len(hex.EncodeToString(b)), "truncated result should be shorter than full hex")
})

t.Run("UTF-8 string truncated at rune boundary", func(t *testing.T) {
// 63 ASCII chars + 1 multi-byte rune (4 bytes) = 67 bytes, should not truncate
s := strings.Repeat("x", 63) + "😊"
args := logQueryArgs([]any{s})
assert.Equal(t, s, args[0])

// 67 bytes + more = should truncate at rune boundary, result shorter than original
s2 := s + "aaa" // 70 bytes
args2 := logQueryArgs([]any{s2})
result := args2[0].(string)
assert.True(t, strings.HasSuffix(result, "…"), "truncated string should end with …")
assert.Less(t, len(result), len(s2), "truncated result should be shorter than original")
})
}