1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-01-12 02:28:07 +02:00

Sanitize metric names in the prometheus exporter (#3212)

* sanitize metric names in the prometheus exporter

* add changelog entry

* fix now invalid comment

* replace sanitizeName algorithm with one based on strings.Map

* check that digits as first characters are replaced

* Update exporters/prometheus/exporter.go

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>

* add missing utf8 import

* add explicit tests for sanitizeName

* fix testdata with digit prepend

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
Co-authored-by: Chester Cheung <cheung.zhy.csu@gmail.com>
This commit is contained in:
Damien Mathieu 2022-09-22 16:23:16 +02:00 committed by GitHub
parent 2ae5922848
commit bfaff3a097
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 118 additions and 9 deletions

View File

@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- The metric SDK in `go.opentelemetry.io/otel/sdk/metric` is completely refactored to comply with the OpenTelemetry specification. - The metric SDK in `go.opentelemetry.io/otel/sdk/metric` is completely refactored to comply with the OpenTelemetry specification.
Please see the package documentation for how the new SDK is initialized and configured. (#3175) Please see the package documentation for how the new SDK is initialized and configured. (#3175)
- Update the minimum supported go version to go1.18. Removes support for go1.17 (#3179) - Update the minimum supported go version to go1.18. Removes support for go1.17 (#3179)
- Instead of dropping metric, the Prometheus exporter will replace any invalid character in metric names with `_`. (#3212)
### Removed ### Removed

View File

@ -19,6 +19,7 @@ import (
"sort" "sort"
"strings" "strings"
"unicode" "unicode"
"unicode/utf8"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -142,7 +143,7 @@ func getHistogramMetricData(histogram metricdata.Histogram, m metricdata.Metrics
dataPoints := make([]*metricData, 0, len(histogram.DataPoints)) dataPoints := make([]*metricData, 0, len(histogram.DataPoints))
for _, dp := range histogram.DataPoints { for _, dp := range histogram.DataPoints {
keys, values := getAttrs(dp.Attributes) keys, values := getAttrs(dp.Attributes)
desc := prometheus.NewDesc(m.Name, m.Description, keys, nil) desc := prometheus.NewDesc(sanitizeName(m.Name), m.Description, keys, nil)
buckets := make(map[float64]uint64, len(dp.Bounds)) buckets := make(map[float64]uint64, len(dp.Bounds))
for i, bound := range dp.Bounds { for i, bound := range dp.Bounds {
buckets[bound] = dp.BucketCounts[i] buckets[bound] = dp.BucketCounts[i]
@ -165,7 +166,7 @@ func getSumMetricData[N int64 | float64](sum metricdata.Sum[N], m metricdata.Met
dataPoints := make([]*metricData, 0, len(sum.DataPoints)) dataPoints := make([]*metricData, 0, len(sum.DataPoints))
for _, dp := range sum.DataPoints { for _, dp := range sum.DataPoints {
keys, values := getAttrs(dp.Attributes) keys, values := getAttrs(dp.Attributes)
desc := prometheus.NewDesc(m.Name, m.Description, keys, nil) desc := prometheus.NewDesc(sanitizeName(m.Name), m.Description, keys, nil)
md := &metricData{ md := &metricData{
name: m.Name, name: m.Name,
description: desc, description: desc,
@ -182,7 +183,7 @@ func getGaugeMetricData[N int64 | float64](gauge metricdata.Gauge[N], m metricda
dataPoints := make([]*metricData, 0, len(gauge.DataPoints)) dataPoints := make([]*metricData, 0, len(gauge.DataPoints))
for _, dp := range gauge.DataPoints { for _, dp := range gauge.DataPoints {
keys, values := getAttrs(dp.Attributes) keys, values := getAttrs(dp.Attributes)
desc := prometheus.NewDesc(m.Name, m.Description, keys, nil) desc := prometheus.NewDesc(sanitizeName(m.Name), m.Description, keys, nil)
md := &metricData{ md := &metricData{
name: m.Name, name: m.Name,
description: desc, description: desc,
@ -230,3 +231,56 @@ func sanitizeRune(r rune) rune {
} }
return '_' return '_'
} }
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()
}

View File

@ -103,8 +103,8 @@ func TestPrometheusExporter(t *testing.T) {
}, },
}, },
{ {
name: "invalid instruments are dropped", name: "invalid instruments are renamed",
expectedFile: "testdata/gauge.txt", expectedFile: "testdata/sanitized_names.txt",
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) { recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
attrs := []attribute.KeyValue{ attrs := []attribute.KeyValue{
attribute.Key("A").String("B"), attribute.Key("A").String("B"),
@ -116,16 +116,16 @@ func TestPrometheusExporter(t *testing.T) {
gauge.Add(ctx, 100, attrs...) gauge.Add(ctx, 100, attrs...)
gauge.Add(ctx, -25, attrs...) gauge.Add(ctx, -25, attrs...)
// Invalid, should be dropped. // Invalid, will be renamed.
gauge, err = meter.SyncFloat64().UpDownCounter("invalid.gauge.name") gauge, err = meter.SyncFloat64().UpDownCounter("invalid.gauge.name", instrument.WithDescription("a gauge with an invalid name"))
require.NoError(t, err) require.NoError(t, err)
gauge.Add(ctx, 100, attrs...) gauge.Add(ctx, 100, attrs...)
counter, err := meter.SyncFloat64().Counter("invalid.counter.name") counter, err := meter.SyncFloat64().Counter("0invalid.counter.name", instrument.WithDescription("a counter with an invalid name"))
require.NoError(t, err) require.NoError(t, err)
counter.Add(ctx, 100, attrs...) counter.Add(ctx, 100, attrs...)
histogram, err := meter.SyncFloat64().Histogram("invalid.hist.name") histogram, err := meter.SyncFloat64().Histogram("invalid.hist.name", instrument.WithDescription("a histogram with an invalid name"))
require.NoError(t, err) require.NoError(t, err)
histogram.Record(ctx, 23, attrs...) histogram.Record(ctx, 23, attrs...)
}, },
@ -155,3 +155,33 @@ func TestPrometheusExporter(t *testing.T) {
}) })
} }
} }
func TestSantitizeName(t *testing.T) {
tests := []struct {
input string
want string
}{
{"nam€_with_3_width_rune", "nam__with_3_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)
}
}

View File

@ -0,0 +1,24 @@
# HELP bar a fun little gauge
# TYPE bar counter
bar{A="B",C="D"} 75
# HELP _0invalid_counter_name a counter with an invalid name
# TYPE _0invalid_counter_name counter
_0invalid_counter_name{A="B",C="D"} 100
# HELP invalid_gauge_name a gauge with an invalid name
# TYPE invalid_gauge_name counter
invalid_gauge_name{A="B",C="D"} 100
# HELP invalid_hist_name a histogram with an invalid name
# TYPE invalid_hist_name histogram
invalid_hist_name_bucket{A="B",C="D",le="0"} 0
invalid_hist_name_bucket{A="B",C="D",le="5"} 0
invalid_hist_name_bucket{A="B",C="D",le="10"} 0
invalid_hist_name_bucket{A="B",C="D",le="25"} 1
invalid_hist_name_bucket{A="B",C="D",le="50"} 0
invalid_hist_name_bucket{A="B",C="D",le="75"} 0
invalid_hist_name_bucket{A="B",C="D",le="100"} 0
invalid_hist_name_bucket{A="B",C="D",le="250"} 0
invalid_hist_name_bucket{A="B",C="D",le="500"} 0
invalid_hist_name_bucket{A="B",C="D",le="1000"} 0
invalid_hist_name_bucket{A="B",C="D",le="+Inf"} 1
invalid_hist_name_sum{A="B",C="D"} 23
invalid_hist_name_count{A="B",C="D"} 1