Skip to content

Commit

Permalink
switch to prometheus name escaping function
Browse files Browse the repository at this point in the history
  • Loading branch information
dashpole committed Sep 9, 2024
1 parent ff554f3 commit ff43372
Show file tree
Hide file tree
Showing 4 changed files with 10 additions and 100 deletions.
6 changes: 5 additions & 1 deletion exporters/prometheus/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/metric"
Expand Down Expand Up @@ -131,7 +132,10 @@ func WithoutScopeInfo() Option {
// have special behavior based on their name.
func WithNamespace(ns string) Option {
return optionFunc(func(cfg config) config {
ns = sanitizeName(ns)
if model.NameValidationScheme != model.UTF8Validation {
// Only sanitize if prometheus does not support UTF-8.
ns = model.EscapeName(ns, model.NameEscapingScheme)
}
if !strings.HasSuffix(ns, "_") {
// namespace and metric names should be separated with an underscore,
// adds a trailing underscore if there is not one already.
Expand Down
66 changes: 2 additions & 64 deletions exporters/prometheus/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import (
"slices"
"strings"
"sync"
"unicode"
"unicode/utf8"

"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
Expand Down Expand Up @@ -318,7 +316,7 @@ func getAttrs(attrs attribute.Set, ks, vs [2]string, resourceKV keyVals) ([]stri
keysMap := make(map[string][]string)
for itr.Next() {
kv := itr.Attribute()
key := strings.Map(sanitizeRune, string(kv.Key))
key := model.EscapeName(string(kv.Key), model.NameEscapingScheme)
if _, ok := keysMap[key]; !ok {
keysMap[key] = []string{kv.Value.Emit()}
} else {
Expand Down Expand Up @@ -358,13 +356,6 @@ func createScopeInfoMetric(scope instrumentation.Scope) (prometheus.Metric, erro
return prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(1), scope.Name, scope.Version)
}

func sanitizeRune(r rune) rune {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == ':' || r == '_' {
return r
}
return '_'
}

var unitSuffixes = map[string]string{
// Time
"d": "_days",
Expand Down Expand Up @@ -406,7 +397,7 @@ func (c *collector) getName(m metricdata.Metrics, typ *dto.MetricType) string {
name := m.Name
if model.NameValidationScheme != model.UTF8Validation {
// Only sanitize if prometheus does not support UTF-8.
name = sanitizeName(m.Name)
name = model.EscapeName(name, model.NameEscapingScheme)
}
addCounterSuffix := !c.withoutCounterSuffixes && *typ == dto.MetricType_COUNTER
if addCounterSuffix {
Expand All @@ -426,59 +417,6 @@ func (c *collector) getName(m metricdata.Metrics, typ *dto.MetricType) string {
return name
}

func sanitizeName(n string) string {
// This algorithm is based on strings.Map from Go 1.19.
const replacement = '_'

valid := func(i int, r rune) bool {
// Taken from
// https://github.com/prometheus/common/blob/dfbc25bd00225c70aca0d94c3c4bb7744f28ace0/model/metric.go#L92-L102
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_' || r == ':' || (r >= '0' && r <= '9' && i > 0) {
return true
}
return false
}

// This output buffer b is initialized on demand, the first time a
// character needs to be replaced.
var b strings.Builder
for i, c := range n {
if valid(i, c) {
continue
}

if i == 0 && c >= '0' && c <= '9' {
// Prefix leading number with replacement character.
b.Grow(len(n) + 1)
_ = b.WriteByte(byte(replacement))
break
}
b.Grow(len(n))
_, _ = b.WriteString(n[:i])
_ = b.WriteByte(byte(replacement))
width := utf8.RuneLen(c)
n = n[i+width:]
break
}

// Fast path for unchanged input.
if b.Cap() == 0 { // b.Grow was not called above.
return n
}

for _, c := range n {
// Due to inlining, it is more performant to invoke WriteByte rather then
// WriteRune.
if valid(1, c) { // We are guaranteed to not be at the start.
_ = b.WriteByte(byte(c))
} else {
_ = b.WriteByte(byte(replacement))
}
}

return b.String()
}

func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType {
switch v := m.Data.(type) {
case metricdata.Histogram[int64], metricdata.Histogram[float64]:
Expand Down
32 changes: 0 additions & 32 deletions exporters/prometheus/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,11 +437,9 @@ func TestPrometheusExporter(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
if tc.enableUTF8 {
model.NameValidationScheme = model.UTF8Validation
model.NameEscapingScheme = model.NoEscaping
defer func() {
// Reset to defaults
model.NameValidationScheme = model.LegacyValidation
model.NameEscapingScheme = model.ValueEncodingEscaping
}()
}
ctx := context.Background()
Expand Down Expand Up @@ -493,36 +491,6 @@ func TestPrometheusExporter(t *testing.T) {
}
}

func TestSantitizeName(t *testing.T) {
tests := []struct {
input string
want string
}{
{"name€_with_4_width_rune", "name__with_4_width_rune"},
{"`", "_"},
{
`! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWKYZ[]\^_abcdefghijklmnopqrstuvwkyz{|}~`,
`________________0123456789:______ABCDEFGHIJKLMNOPQRSTUVWKYZ_____abcdefghijklmnopqrstuvwkyz____`,
},

// Test cases taken from
// https://github.com/prometheus/common/blob/dfbc25bd00225c70aca0d94c3c4bb7744f28ace0/model/metric_test.go#L85-L136
{"Avalid_23name", "Avalid_23name"},
{"_Avalid_23name", "_Avalid_23name"},
{"1valid_23name", "_1valid_23name"},
{"avalid_23name", "avalid_23name"},
{"Ava:lid_23name", "Ava:lid_23name"},
{"a lid_23name", "a_lid_23name"},
{":leading_colon", ":leading_colon"},
{"colon:in:the:middle", "colon:in:the:middle"},
{"", ""},
}

for _, test := range tests {
require.Equalf(t, test.want, sanitizeName(test.input), "input: %q", test.input)
}
}

func TestMultiScopes(t *testing.T) {
ctx := context.Background()
registry := prometheus.NewRegistry()
Expand Down
6 changes: 3 additions & 3 deletions exporters/prometheus/testdata/sanitized_names.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# HELP bar a fun little gauge
# TYPE bar gauge
bar{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 75
# HELP _0invalid_counter_name_total a counter with an invalid name
# TYPE _0invalid_counter_name_total counter
_0invalid_counter_name_total{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 100
# HELP _invalid_counter_name_total a counter with an invalid name
# TYPE _invalid_counter_name_total counter
_invalid_counter_name_total{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 100
# HELP invalid_gauge_name a gauge with an invalid name
# TYPE invalid_gauge_name gauge
invalid_gauge_name{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 100
Expand Down

0 comments on commit ff43372

Please sign in to comment.