Files
VictoriaMetrics/lib/protoparser/opentelemetry/stream/sanitize.go
andriibeee 85e0253569 lib/protoparser: add flag to allow OpenTelemetry underscore labels to pass through without being prefixed (#10475)
Add `-opentelemetry.labelNameUnderscoreSanitization` command-line flag to control whether to enable prepending of `key` to labels starting with `_` when `-opentelemetry.usePrometheusNaming` is enabled. The labels starting with `__` are not modified.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9663

Signed-off-by: andriibeee <154226341+andriibeee@users.noreply.github.com>
Co-authored-by: Max Kotliar <mkotlyar@victoriametrics.com>
2026-05-12 15:17:36 +03:00

205 lines
6.4 KiB
Go

package stream
import (
"flag"
"slices"
"strings"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/pb"
)
var (
usePrometheusNaming = flag.Bool("opentelemetry.usePrometheusNaming", false, "Whether to convert metric names and labels into Prometheus-compatible format for the metrics ingested "+
"via OpenTelemetry protocol; see https://docs.victoriametrics.com/victoriametrics/integrations/opentelemetry/")
convertMetricNamesToPrometheus = flag.Bool("opentelemetry.convertMetricNamesToPrometheus", false, "Whether to convert only metric names into Prometheus-compatible format for the metrics ingested "+
"via OpenTelemetry protocol; see https://docs.victoriametrics.com/victoriametrics/integrations/opentelemetry/")
labelNameUnderscoreSanitization = flag.Bool("opentelemetry.labelNameUnderscoreSanitization", true, "Whether to enable prepending of 'key' to labels starting with '_' "+
"when -opentelemetry.usePrometheusNaming is enabled. Reserved labels starting with '__' are not modified. "+
"See https://docs.victoriametrics.com/victoriametrics/integrations/opentelemetry/")
)
// unitMap is obtained from https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/b8655058501bed61a06bb660869051491f46840b/pkg/translator/prometheus/normalize_name.go#L19
var unitMap = map[string]string{
// Time
"d": "days",
"h": "hours",
"min": "minutes",
"s": "seconds",
"ms": "milliseconds",
"us": "microseconds",
"ns": "nanoseconds",
// Bytes
"By": "bytes",
"KiBy": "kibibytes",
"MiBy": "mebibytes",
"GiBy": "gibibytes",
"TiBy": "tibibytes",
"KBy": "kilobytes",
"MBy": "megabytes",
"GBy": "gigabytes",
"TBy": "terabytes",
// SI
"m": "meters",
"V": "volts",
"A": "amperes",
"J": "joules",
"W": "watts",
"g": "grams",
// Misc
"Cel": "celsius",
"Hz": "hertz",
"1": "",
"%": "percent",
}
// perUnitMap is copied from https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/b8655058501bed61a06bb660869051491f46840b/pkg/translator/prometheus/normalize_name.go#L58
var perUnitMap = map[string]string{
"s": "second",
"m": "minute",
"h": "hour",
"d": "day",
"w": "week",
"mo": "month",
"y": "year",
}
type sanitizerContext struct {
metricNameTokens []string
metricNameBuf []byte
labelBuf []byte
}
func (sctx *sanitizerContext) reset() {
clear(sctx.metricNameTokens)
sctx.metricNameTokens = sctx.metricNameTokens[:0]
sctx.metricNameBuf = sctx.metricNameBuf[:0]
sctx.labelBuf = sctx.labelBuf[:0]
}
// The returned string is valid until the next call to sanitizeLabelName.
func (sctx *sanitizerContext) sanitizeLabelName(labelName string) string {
if !*usePrometheusNaming {
return labelName
}
return sctx.sanitizePrometheusLabelName(labelName)
}
// sanitizePrometheusLabelName performs conversion and normalization of OpenTelemetry attributes to Prometheus labels.
// It follows the Prometheus guidelines: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/translator/prometheus#labels
func (sctx *sanitizerContext) sanitizePrometheusLabelName(labelName string) string {
if len(labelName) == 0 {
return ""
}
labelName = promrelabel.SanitizeLabelName(labelName)
if labelName[0] >= '0' && labelName[0] <= '9' {
return sctx.concatLabel("key_", labelName)
} else if *labelNameUnderscoreSanitization && strings.HasPrefix(labelName, "_") && !strings.HasPrefix(labelName, "__") {
return sctx.concatLabel("key", labelName)
}
return labelName
}
func (sctx *sanitizerContext) concatLabel(a, b string) string {
sctx.labelBuf = append(sctx.labelBuf[:0], a...)
sctx.labelBuf = append(sctx.labelBuf, b...)
return bytesutil.ToUnsafeString(sctx.labelBuf)
}
// See https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/b8655058501bed61a06bb660869051491f46840b/pkg/translator/prometheus/normalize_name.go#L83
//
// The returned string is valid until the next call to sanitizeMetricName.
func (sctx *sanitizerContext) sanitizeMetricName(mm *pb.MetricMetadata) string {
if !*usePrometheusNaming && !*convertMetricNamesToPrometheus {
return mm.Name
}
return sctx.sanitizePrometheusMetricName(mm)
}
func (sctx *sanitizerContext) sanitizePrometheusMetricName(mm *pb.MetricMetadata) string {
sctx.splitMetricNameToTokens(mm.Name)
n := strings.IndexByte(mm.Unit, '/')
mainUnit := mm.Unit
perUnit := ""
if n >= 0 {
mainUnit = mm.Unit[:n]
perUnit = mm.Unit[n+1:]
}
mainUnit = strings.TrimSpace(mainUnit)
perUnit = strings.TrimSpace(perUnit)
if mainUnit != "" && !strings.Contains(mainUnit, "{") {
if u, ok := unitMap[mainUnit]; ok {
mainUnit = u
}
if mainUnit != "" && !slices.Contains(sctx.metricNameTokens, mainUnit) {
sctx.metricNameTokens = append(sctx.metricNameTokens, mainUnit)
}
}
if perUnit != "" && !strings.Contains(perUnit, "{") {
if u, ok := perUnitMap[perUnit]; ok {
perUnit = u
}
if perUnit != "" && !slices.Contains(sctx.metricNameTokens, perUnit) {
sctx.metricNameTokens = append(sctx.metricNameTokens, "per", perUnit)
}
}
if mm.Type == prompb.MetricTypeCounter {
sctx.metricNameTokens = moveOrAppend(sctx.metricNameTokens, "total")
} else if mm.Unit == "1" && mm.Type == prompb.MetricTypeGauge {
sctx.metricNameTokens = moveOrAppend(sctx.metricNameTokens, "ratio")
}
sctx.metricNameBuf = joinMetricNameTokens(sctx.metricNameBuf[:0], sctx.metricNameTokens)
return bytesutil.ToUnsafeString(sctx.metricNameBuf)
}
func (sctx *sanitizerContext) splitMetricNameToTokens(metricName string) {
clear(sctx.metricNameTokens)
sctx.metricNameTokens = sctx.metricNameTokens[:0]
s := metricName
for len(s) > 0 {
n := strings.IndexAny(s, "/_.-: ")
if n < 0 {
sctx.metricNameTokens = append(sctx.metricNameTokens, s)
return
}
if n > 0 {
sctx.metricNameTokens = append(sctx.metricNameTokens, s[:n])
}
s = s[n+1:]
}
}
func joinMetricNameTokens(dst []byte, metricNameTokens []string) []byte {
if len(metricNameTokens) == 0 {
return dst
}
for _, token := range metricNameTokens {
dst = append(dst, token...)
dst = append(dst, '_')
}
return dst[:len(dst)-1]
}
func moveOrAppend(tokens []string, value string) []string {
for i := range tokens {
if tokens[i] == value {
tokens = append(tokens[:i], tokens[i+1:]...)
break
}
}
return append(tokens, value)
}