diff options
Diffstat (limited to 'vendor/go.opentelemetry.io/otel/exporters/prometheus/exporter.go')
-rw-r--r-- | vendor/go.opentelemetry.io/otel/exporters/prometheus/exporter.go | 117 |
1 files changed, 82 insertions, 35 deletions
diff --git a/vendor/go.opentelemetry.io/otel/exporters/prometheus/exporter.go b/vendor/go.opentelemetry.io/otel/exporters/prometheus/exporter.go index b0f5f3730..d2e387e60 100644 --- a/vendor/go.opentelemetry.io/otel/exporters/prometheus/exporter.go +++ b/vendor/go.opentelemetry.io/otel/exporters/prometheus/exporter.go @@ -11,10 +11,11 @@ import ( "slices" "strings" "sync" + "unicode" + "unicode/utf8" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" - "github.com/prometheus/common/model" "google.golang.org/protobuf/proto" "go.opentelemetry.io/otel" @@ -297,40 +298,30 @@ func addGaugeMetric[N int64 | float64](ch chan<- prometheus.Metric, gauge metric } // getAttrs parses the attribute.Set to two lists of matching Prometheus-style -// keys and values. +// keys and values. It sanitizes invalid characters and handles duplicate keys +// (due to sanitization) by sorting and concatenating the values following the spec. func getAttrs(attrs attribute.Set, ks, vs [2]string, resourceKV keyVals) ([]string, []string) { - keys := make([]string, 0, attrs.Len()) - values := make([]string, 0, attrs.Len()) + keysMap := make(map[string][]string) itr := attrs.Iter() - - if model.NameValidationScheme == model.UTF8Validation { - // Do not perform sanitization if prometheus supports UTF-8. - for itr.Next() { - kv := itr.Attribute() - keys = append(keys, string(kv.Key)) - values = append(values, kv.Value.Emit()) - } - } else { - // It sanitizes invalid characters and handles duplicate keys - // (due to sanitization) by sorting and concatenating the values following the spec. - keysMap := make(map[string][]string) - for itr.Next() { - kv := itr.Attribute() - key := model.EscapeName(string(kv.Key), model.NameEscapingScheme) - if _, ok := keysMap[key]; !ok { - keysMap[key] = []string{kv.Value.Emit()} - } else { - // if the sanitized key is a duplicate, append to the list of keys - keysMap[key] = append(keysMap[key], kv.Value.Emit()) - } - } - for key, vals := range keysMap { - keys = append(keys, key) - slices.Sort(vals) - values = append(values, strings.Join(vals, ";")) + for itr.Next() { + kv := itr.Attribute() + key := strings.Map(sanitizeRune, string(kv.Key)) + if _, ok := keysMap[key]; !ok { + keysMap[key] = []string{kv.Value.Emit()} + } else { + // if the sanitized key is a duplicate, append to the list of keys + keysMap[key] = append(keysMap[key], kv.Value.Emit()) } } + keys := make([]string, 0, attrs.Len()) + values := make([]string, 0, attrs.Len()) + for key, vals := range keysMap { + keys = append(keys, key) + slices.Sort(vals) + values = append(values, strings.Join(vals, ";")) + } + if ks[0] != "" { keys = append(keys, ks[:]...) values = append(values, vs[:]...) @@ -356,6 +347,13 @@ 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", @@ -394,11 +392,7 @@ var unitSuffixes = map[string]string{ // getName returns the sanitized name, prefixed with the namespace and suffixed with unit. 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 = model.EscapeName(name, model.NameEscapingScheme) - } + name := sanitizeName(m.Name) addCounterSuffix := !c.withoutCounterSuffixes && *typ == dto.MetricType_COUNTER if addCounterSuffix { // Remove the _total suffix here, as we will re-add the total suffix @@ -417,6 +411,59 @@ 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]: |