Skip to content
Open
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module github.com/prometheus/otlptranslator

go 1.23.0
go 1.24.0

toolchain go1.24.1
4 changes: 4 additions & 0 deletions label_namer.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ func (ln *LabelNamer) Build(label string) (string, error) {
return label, nil
}

if canFastPathLabel(label, ln.PreserveMultipleUnderscores, ln.UnderscoreLabelSanitization) {
return label, nil
}

normalizedName := sanitizeLabelName(label, ln.PreserveMultipleUnderscores)

// If label starts with a number, prepend with "key_".
Expand Down
11 changes: 10 additions & 1 deletion label_namer_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ var labelBenchmarkInputs = []struct {
name: "empty label",
label: "",
},
{
name: "already-valid label",
label: "http_method_total",
},
{
name: "label with colons",
label: "label:with:colons",
Expand Down Expand Up @@ -42,13 +46,18 @@ var labelBenchmarkInputs = []struct {
name: "label starting with 2 underscores",
label: "__label_starting_with_2underscores",
},
{
name: "reserved label",
label: "__reserved__label__name__",
},
}

func BenchmarkNormalizeLabel(b *testing.B) {
labelNamer := LabelNamer{UTF8Allowed: false}
for _, input := range labelBenchmarkInputs {
b.Run(input.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
b.ReportAllocs()
for b.Loop() {
//nolint:errcheck
labelNamer.Build(input.label)
}
Expand Down
72 changes: 72 additions & 0 deletions label_namer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ var labelTestCases = []struct {
sanitizedMultipleUnderscores: "label_with_foreign_characters___",
},
{label: "label.with.dots", sanitized: "label_with_dots"},
{
label: ".foo",
sanitized: "_foo",
sanitizedUnderscoreLabelSanitization: "key_foo",
},
{label: "123label", sanitized: "key_123label"},
{
label: "_label_starting_with_underscore",
Expand Down Expand Up @@ -215,3 +220,70 @@ func TestBuildLabel_UTF8Allowed(t *testing.T) {
})
}
}

// TestCanFastPathLabel verifies that the fast-path predicate agrees with
// LabelNamer.Build across every entry in labelTestCases × the four configs.
// Whenever canFastPathLabel returns true, Build must succeed and return the
// input unchanged; whenever Build returns the input unchanged for a valid
// input, canFastPathLabel must return true.
func TestCanFastPathLabel(t *testing.T) {
configs := []struct {
name string
preserveMultipleUnderscores bool
underscoreLabelSanitization bool
}{
{"default", false, false},
{"preserve multiple underscores", true, false},
{"underscore label sanitization", false, true},
{"both", true, true},
}
for _, cfg := range configs {
t.Run(cfg.name, func(t *testing.T) {
for _, tt := range labelTestCases {
t.Run(tt.label, func(t *testing.T) {
if tt.label == "" {
return
}

namer := LabelNamer{
PreserveMultipleUnderscores: cfg.preserveMultipleUnderscores,
UnderscoreLabelSanitization: cfg.underscoreLabelSanitization,
}
got, err := namer.Build(tt.label)
fast := canFastPathLabel(tt.label, cfg.preserveMultipleUnderscores, cfg.underscoreLabelSanitization)

if fast {
if err != nil {
t.Fatalf("canFastPathLabel=true but Build returned error: %s", err)
}
if got != tt.label {
t.Fatalf("canFastPathLabel=true but Build returned %q, want input %q", got, tt.label)
}
return
}

// fast == false: nothing to assert about Build, except that if
// it succeeded with output equal to input, the predicate missed a
// no-op case (a false negative). False negatives are correct but
// suboptimal; we flag them so the predicate stays tight.
if err == nil && got == tt.label {
t.Fatalf("canFastPathLabel=false but Build returned input unchanged (false negative)")
}
})
}
})
}
}

// TestLabelNamerBuildZeroAlloc asserts that Build is allocation-free on the
// fast path. The chosen input is already Prometheus-compliant; it must be
// returned unchanged with no heap allocations.
func TestLabelNamerBuildZeroAlloc(t *testing.T) {
namer := LabelNamer{}
got := testing.AllocsPerRun(100, func() {
_, _ = namer.Build("http_method_total")
})
if got > 0 {
t.Fatalf("Build allocated %f times per run on the fast path, want 0", got)
}
}
55 changes: 54 additions & 1 deletion strconv.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package otlptranslator

import (
"strings"
"unicode"
)

// sanitizeLabelName replaces any characters not valid according to the
Expand Down Expand Up @@ -52,6 +53,9 @@ func sanitizeLabelName(name string, preserveMultipleUnderscores bool) string {
// Collapse multiple underscores while replacing invalid characters.
var b strings.Builder
b.Grow(nameLength)
if isReserved {
b.WriteString("__")
}
prevWasUnderscore := false

for _, r := range name {
Expand All @@ -65,7 +69,7 @@ func sanitizeLabelName(name string, preserveMultipleUnderscores bool) string {
}
}
if isReserved {
return "__" + b.String() + "__"
b.WriteString("__")
}
return b.String()
}
Expand All @@ -75,6 +79,55 @@ func isValidCompliantLabelChar(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')
}

// canFastPathLabel reports whether LabelNamer.Build would return label unchanged when UTF8Allowed is false.
// When it returns true, the label can be returned directly. The predicate must remain
// consistent with sanitizeLabelName and the post-sanitize prefix logic in LabelNamer.Build.
func canFastPathLabel(label string, preserveMultipleUnderscores, underscoreLabelSanitization bool) bool {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be delegating any of this logic to the prometheus/common validators? E.g. https://github.com/prometheus/common/blob/0f3c348807322ea84d92fc7688b1b37a08e17d1f/model/metric.go#L175

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a look, thanks.

n := len(label)
if n == 0 {
return false
}

// Leading digit triggers a "key_" prepend.
if unicode.IsDigit(rune(label[0])) {
return false
}
// Single leading '_' under sanitization triggers a "key" prepend.
if underscoreLabelSanitization && strings.HasPrefix(label, "_") && !strings.HasPrefix(label, "__") {
return false
}

// Reserved labels (__...__) under !preserveMultipleUnderscores get stripped,
// sanitized, then re-wrapped. The output equals the input iff the inner range
// already sanitizes to itself.
start, end := 0, n
if !preserveMultipleUnderscores && n >= 4 && strings.HasPrefix(label, "__") && strings.HasSuffix(label, "__") {
start, end = 2, n-2
}

prevWasUnderscore := false
sawNonUnderscore := false
for i := start; i < end; i++ {
c := label[i]
if !isValidCompliantLabelChar(rune(c)) && c != '_' {
// Non-ASCII bytes (lead/continuation of multi-byte runes) fall here.
return false
}
if c == '_' {
if !preserveMultipleUnderscores && prevWasUnderscore {
return false
}
prevWasUnderscore = true
} else {
prevWasUnderscore = false
sawNonUnderscore = true
}
}
// An all-underscore (or empty inner) result would hit Build's hasUnderscoresOnly
// error path; let the slow path produce the error.
return sawNonUnderscore
}

// isReservedLabel checks if a label is a reserved label.
// Reserved labels are labels that start and end with exactly __.
// The returned label name is the label name without the __ prefix and suffix.
Expand Down
Loading