feat(gotenberg): add telemetry attribute helpers for redaction and capping

This commit is contained in:
Julien Neuhart
2026-06-02 19:11:40 +02:00
parent c63dd5ce1e
commit e1c28d8450
2 changed files with 116 additions and 0 deletions

54
pkg/gotenberg/attrs.go Normal file
View File

@@ -0,0 +1,54 @@
package gotenberg
import (
"net/url"
)
// maxAttrRunes bounds the length of a string span attribute to keep payload
// size and backend cardinality in check.
const maxAttrRunes = 256
// CapAttr truncates s to at most [maxAttrRunes] runes, appending an ellipsis
// when it shortens the value. It is multibyte-safe.
func CapAttr(s string) string {
runes := []rune(s)
if len(runes) <= maxAttrRunes {
return s
}
return string(runes[:maxAttrRunes-1]) + "…"
}
// RedactURL parses raw and returns a redacted, length-capped form safe to use
// as a span attribute or event value. Userinfo, query, and fragment are
// dropped because they may carry credentials or other sensitive data. It
// returns an empty string when raw is empty or cannot be parsed.
func RedactURL(raw string) string {
if raw == "" {
return ""
}
parsed, err := url.Parse(raw)
if err != nil {
return ""
}
parsed.User = nil
parsed.RawQuery = ""
parsed.ForceQuery = false
parsed.Fragment = ""
parsed.RawFragment = ""
return CapAttr(parsed.String())
}
// MapEnum returns value when it belongs to allowed, otherwise "other". It keeps
// a span attribute or metric dimension bounded even when an upstream tool
// introduces a new enum value.
func MapEnum(value string, allowed ...string) string {
for _, candidate := range allowed {
if value == candidate {
return value
}
}
return "other"
}

View File

@@ -0,0 +1,62 @@
package gotenberg
import (
"strings"
"testing"
)
func TestRedactURL(t *testing.T) {
for _, tc := range []struct {
name string
raw string
want string
}{
{"empty", "", ""},
{"strips userinfo query fragment", "https://user:pass@example.com/path?token=secret#frag", "https://example.com/path"},
{"keeps host and path", "http://example.com/a/b", "http://example.com/a/b"},
{"parse error", "http://example.com/%zz", ""},
} {
t.Run(tc.name, func(t *testing.T) {
if got := RedactURL(tc.raw); got != tc.want {
t.Errorf("RedactURL(%q) = %q, want %q", tc.raw, got, tc.want)
}
})
}
}
func TestRedactURL_Caps(t *testing.T) {
raw := "https://example.com/" + strings.Repeat("a", 400)
got := RedactURL(raw)
if n := len([]rune(got)); n != maxAttrRunes {
t.Errorf("expected capped length %d, got %d", maxAttrRunes, n)
}
}
func TestCapAttr(t *testing.T) {
t.Run("short unchanged", func(t *testing.T) {
if got := CapAttr("short"); got != "short" {
t.Errorf("expected unchanged, got %q", got)
}
})
t.Run("multibyte truncated to rune cap", func(t *testing.T) {
got := CapAttr(strings.Repeat("é", 400))
if n := len([]rune(got)); n != maxAttrRunes {
t.Errorf("expected %d runes, got %d", maxAttrRunes, n)
}
if !strings.HasSuffix(got, "…") {
t.Error("expected an ellipsis suffix on a truncated value")
}
})
}
func TestMapEnum(t *testing.T) {
allowed := []string{"document", "stylesheet", "script"}
if got := MapEnum("script", allowed...); got != "script" {
t.Errorf("expected member passthrough, got %q", got)
}
if got := MapEnum("websocket", allowed...); got != "other" {
t.Errorf("expected non-member to map to other, got %q", got)
}
}