From c6164f89c1fd12395976d46c0b321453acb27621 Mon Sep 17 00:00:00 2001 From: Arthur Silva Sens Date: Fri, 27 Sep 2024 12:35:58 -0300 Subject: [PATCH] wip: allow utf8 Signed-off-by: Arthur Silva Sens --- exporter/prometheusexporter/README.md | 5 + exporter/prometheusexporter/collector.go | 9 +- exporter/prometheusexporter/prometheus.go | 1 + pkg/translator/prometheus/feature_gates.go | 39 +++++++ pkg/translator/prometheus/go.mod | 8 +- pkg/translator/prometheus/go.sum | 16 ++- pkg/translator/prometheus/normalize_label.go | 53 --------- .../prometheus/normalize_label_test.go | 35 ------ pkg/translator/prometheus/normalize_name.go | 8 -- pkg/translator/prometheus/translator.go | 104 ++++++++++++++++++ pkg/translator/prometheus/translator_test.go | 73 ++++++++++++ 11 files changed, 243 insertions(+), 108 deletions(-) create mode 100644 pkg/translator/prometheus/feature_gates.go delete mode 100644 pkg/translator/prometheus/normalize_label.go delete mode 100644 pkg/translator/prometheus/normalize_label_test.go create mode 100644 pkg/translator/prometheus/translator.go create mode 100644 pkg/translator/prometheus/translator_test.go diff --git a/exporter/prometheusexporter/README.md b/exporter/prometheusexporter/README.md index e61a99217bad4..ca39f9a735d9d 100644 --- a/exporter/prometheusexporter/README.md +++ b/exporter/prometheusexporter/README.md @@ -60,6 +60,11 @@ Given the example, metrics will be available at `https://1.2.3.4:1234/metrics`. OpenTelemetry metric names and attributes are normalized to be compliant with Prometheus naming rules. [Details on this normalization process are described in the Prometheus translator module](../../pkg/translator/prometheus/). +Prometheus 2.55.0 introduced support for UTF-8 characters behind the feature-flag `utf8-names`. Prometheus 3.0.0 and later accept UTF-8 by default. This means that name and attribute normalization is not required if you're using those versions. +To allow UTF-8 characters to be exposed without normalization, start the collector with the feature gate: `--feature-gates=exporter.prometheus.allow_utf8`. + +The scraper must include `scaping=allow-utf-8` in the `Accept` header for UTF-8 characters to be exposed. + ## Setting resource attributes as metric labels By default, resource attributes are added to a special metric called `target_info`. To select and group by metrics by resource attributes, you [need to do join on `target_info`](https://prometheus.io/docs/prometheus/latest/querying/operators/#many-to-one-and-one-to-many-vector-matches). For example, to select metrics with `k8s_namespace_name` attribute equal to `my-namespace`: diff --git a/exporter/prometheusexporter/collector.go b/exporter/prometheusexporter/collector.go index f6065307eb348..f124f8cd47e7c 100644 --- a/exporter/prometheusexporter/collector.go +++ b/exporter/prometheusexporter/collector.go @@ -30,6 +30,8 @@ type collector struct { addMetricSuffixes bool namespace string constLabels prometheus.Labels + + translator prometheustranslator.Translator } func newCollector(config *Config, logger *zap.Logger) *collector { @@ -40,6 +42,7 @@ func newCollector(config *Config, logger *zap.Logger) *collector { sendTimestamps: config.SendTimestamps, constLabels: config.ConstLabels, addMetricSuffixes: config.AddMetricSuffixes, + translator: prometheustranslator.NewTranslator(), } } @@ -110,7 +113,7 @@ func (c *collector) getMetricMetadata(metric pmetric.Metric, attributes pcommon. values := make([]string, 0, attributes.Len()+2) attributes.Range(func(k string, v pcommon.Value) bool { - keys = append(keys, prometheustranslator.NormalizeLabel(k)) + keys = append(keys, c.translator.TranslateAttribute(k)) values = append(values, v.AsString()) return true }) @@ -125,7 +128,7 @@ func (c *collector) getMetricMetadata(metric pmetric.Metric, attributes pcommon. } return prometheus.NewDesc( - prometheustranslator.BuildCompliantName(metric, c.namespace, c.addMetricSuffixes), + c.translator.TranslateMetric(metric, c.namespace, c.addMetricSuffixes), metric.Description(), keys, c.constLabels, @@ -327,7 +330,7 @@ func (c *collector) createTargetInfoMetrics(resourceAttrs []pcommon.Map) ([]prom }) attributes.Range(func(k string, v pcommon.Value) bool { - finalKey := prometheustranslator.NormalizeLabel(k) + finalKey := c.translator.TranslateAttribute(k) if existingVal, ok := labels[finalKey]; ok { labels[finalKey] = existingVal + ";" + v.AsString() } else { diff --git a/exporter/prometheusexporter/prometheus.go b/exporter/prometheusexporter/prometheus.go index 893653d45326c..8cebc17d87eb4 100644 --- a/exporter/prometheusexporter/prometheus.go +++ b/exporter/prometheusexporter/prometheus.go @@ -38,6 +38,7 @@ func newPrometheusExporter(config *Config, set exporter.Settings) (*prometheusEx collector := newCollector(config, set.Logger) registry := prometheus.NewRegistry() _ = registry.Register(collector) + return &prometheusExporter{ config: *config, name: set.ID.String(), diff --git a/pkg/translator/prometheus/feature_gates.go b/pkg/translator/prometheus/feature_gates.go new file mode 100644 index 0000000000000..4ff77b96e1b69 --- /dev/null +++ b/pkg/translator/prometheus/feature_gates.go @@ -0,0 +1,39 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package prometheus // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus" + +import ( + "github.com/prometheus/common/model" + "go.opentelemetry.io/collector/featuregate" +) + +var ( + dropSanitizationGate = featuregate.GlobalRegistry().MustRegister( + "pkg.translator.prometheus.PermissiveLabelSanitization", + featuregate.StageAlpha, + featuregate.WithRegisterDescription("Controls whether to change labels starting with '_' to 'key_'."), + featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/8950"), + ) + + normalizeNameGate = featuregate.GlobalRegistry().MustRegister( + "pkg.translator.prometheus.NormalizeName", + featuregate.StageBeta, + featuregate.WithRegisterDescription("Controls whether metrics names are automatically normalized to follow Prometheus naming convention"), + featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/8950"), + ) + + allowUTF8FeatureGate = featuregate.GlobalRegistry().MustRegister( + "pkg.translator.prometheus.allowUTF8", + featuregate.StageAlpha, + featuregate.WithRegisterDescription("When enabled, metric names and labels will not be normalized to the traditional Prometheus naming rules, fundamentally allowing UTF-8 characters."), + featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/35459"), + featuregate.WithRegisterFromVersion("v0.110.0"), + ) +) + +func init() { + if allowUTF8FeatureGate.IsEnabled() { + model.NameValidationScheme = model.UTF8Validation + } +} diff --git a/pkg/translator/prometheus/go.mod b/pkg/translator/prometheus/go.mod index 9bc8231a36538..5b06bca3801d4 100644 --- a/pkg/translator/prometheus/go.mod +++ b/pkg/translator/prometheus/go.mod @@ -4,6 +4,7 @@ go 1.22.0 require ( github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.110.0 + github.com/prometheus/common v0.59.1 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/collector/featuregate v1.16.0 go.opentelemetry.io/collector/pdata v1.16.0 @@ -19,10 +20,11 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect google.golang.org/grpc v1.66.2 // indirect google.golang.org/protobuf v1.34.2 // indirect diff --git a/pkg/translator/prometheus/go.sum b/pkg/translator/prometheus/go.sum index aa91eb5167428..0b499d9c96e15 100644 --- a/pkg/translator/prometheus/go.sum +++ b/pkg/translator/prometheus/go.sum @@ -24,6 +24,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= +github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -49,20 +53,20 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/pkg/translator/prometheus/normalize_label.go b/pkg/translator/prometheus/normalize_label.go deleted file mode 100644 index af0960e862373..0000000000000 --- a/pkg/translator/prometheus/normalize_label.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package prometheus // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus" - -import ( - "strings" - "unicode" - - "go.opentelemetry.io/collector/featuregate" -) - -var dropSanitizationGate = featuregate.GlobalRegistry().MustRegister( - "pkg.translator.prometheus.PermissiveLabelSanitization", - featuregate.StageAlpha, - featuregate.WithRegisterDescription("Controls whether to change labels starting with '_' to 'key_'."), - featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/8950"), -) - -// Normalizes the specified label to follow Prometheus label names standard -// -// See rules at https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels -// -// Labels that start with non-letter rune will be prefixed with "key_" -// -// Exception is made for double-underscores which are allowed -func NormalizeLabel(label string) string { - - // Trivial case - if len(label) == 0 { - return label - } - - // Replace all non-alphanumeric runes with underscores - label = strings.Map(sanitizeRune, label) - - // If label starts with a number, prepend with "key_" - if unicode.IsDigit(rune(label[0])) { - label = "key_" + label - } else if strings.HasPrefix(label, "_") && !strings.HasPrefix(label, "__") && !dropSanitizationGate.IsEnabled() { - label = "key" + label - } - - return label -} - -// Return '_' for anything non-alphanumeric -func sanitizeRune(r rune) rune { - if unicode.IsLetter(r) || unicode.IsDigit(r) { - return r - } - return '_' -} diff --git a/pkg/translator/prometheus/normalize_label_test.go b/pkg/translator/prometheus/normalize_label_test.go deleted file mode 100644 index f00183909735f..0000000000000 --- a/pkg/translator/prometheus/normalize_label_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package prometheus // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus" - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/open-telemetry/opentelemetry-collector-contrib/internal/common/testutil" -) - -func TestSanitize(t *testing.T) { - - defer testutil.SetFeatureGateForTest(t, dropSanitizationGate, false)() - - require.Equal(t, "", NormalizeLabel(""), "") - require.Equal(t, "key_test", NormalizeLabel("_test")) - require.Equal(t, "key_0test", NormalizeLabel("0test")) - require.Equal(t, "test", NormalizeLabel("test")) - require.Equal(t, "test__", NormalizeLabel("test_/")) - require.Equal(t, "__test", NormalizeLabel("__test")) -} - -func TestSanitizeDropSanitization(t *testing.T) { - - defer testutil.SetFeatureGateForTest(t, dropSanitizationGate, true)() - - require.Equal(t, "", NormalizeLabel("")) - require.Equal(t, "_test", NormalizeLabel("_test")) - require.Equal(t, "key_0test", NormalizeLabel("0test")) - require.Equal(t, "test", NormalizeLabel("test")) - require.Equal(t, "__test", NormalizeLabel("__test")) -} diff --git a/pkg/translator/prometheus/normalize_name.go b/pkg/translator/prometheus/normalize_name.go index 72fc04cea220c..0a91d7abe1dcc 100644 --- a/pkg/translator/prometheus/normalize_name.go +++ b/pkg/translator/prometheus/normalize_name.go @@ -7,7 +7,6 @@ import ( "strings" "unicode" - "go.opentelemetry.io/collector/featuregate" "go.opentelemetry.io/collector/pdata/pmetric" ) @@ -65,13 +64,6 @@ var perUnitMap = map[string]string{ "y": "year", } -var normalizeNameGate = featuregate.GlobalRegistry().MustRegister( - "pkg.translator.prometheus.NormalizeName", - featuregate.StageBeta, - featuregate.WithRegisterDescription("Controls whether metrics names are automatically normalized to follow Prometheus naming convention"), - featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/8950"), -) - // BuildCompliantName builds a Prometheus-compliant metric name for the specified metric // // Metric name is prefixed with specified namespace and underscore (if any). diff --git a/pkg/translator/prometheus/translator.go b/pkg/translator/prometheus/translator.go new file mode 100644 index 0000000000000..e2cbf0acef28f --- /dev/null +++ b/pkg/translator/prometheus/translator.go @@ -0,0 +1,104 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package prometheus // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus" + +import ( + "strings" + "unicode" + + "go.opentelemetry.io/collector/pdata/pmetric" +) + +type Translator interface { + TranslateAttribute(string) string + TranslateMetric(metric pmetric.Metric, namespace string, addMetricSuffixes bool) string +} + +type UTF8AllowedTranslator struct{} + +func (UTF8AllowedTranslator) TranslateAttribute(attribute string) string { + return addKeyPrefixIfNeeded(attribute) +} + +func (UTF8AllowedTranslator) TranslateMetric(metric pmetric.Metric, namespace string, addMetricSuffixes bool) string { + tokens := make([]string, 0) + if namespace != "" { + tokens = append(tokens, namespace) + } + tokens = append(tokens, metric.Name()) + + if addMetricSuffixes && normalizeNameGate.IsEnabled() { + // Split unit at the '/' if any + unitTokens := strings.SplitN(metric.Unit(), "/", 2) + + if len(unitTokens) > 0 { + mainUnitOtel := strings.TrimSpace(unitTokens[0]) + if mainUnitOtel != "" && !strings.ContainsAny(mainUnitOtel, "{}") { + tokens = append(tokens, unitMapGetOrDefault(mainUnitOtel)) + } + + // Per unit + // Append if not blank and doesn't contain '{}' + if len(unitTokens) > 1 && unitTokens[1] != "" { + perUnitOtel := strings.TrimSpace(unitTokens[1]) + if perUnitOtel != "" && !strings.ContainsAny(perUnitOtel, "{}") { + tokens = append(tokens, perUnitMapGetOrDefault(perUnitOtel)) + } + } + + } + } + + metricName := strings.Join(tokens, ".") + + // Append _total for Counters + if addMetricSuffixes && normalizeNameGate.IsEnabled() && metric.Type() == pmetric.MetricTypeSum && metric.Sum().IsMonotonic() { + metricName += "_total" + } + return metricName +} + +type ClassicTranslator struct{} + +func (ClassicTranslator) TranslateAttribute(attribute string) string { + // Trivial case + if len(attribute) == 0 { + return attribute + } + + // Replace all non-alphanumeric runes with underscores + attribute = strings.Map(sanitizeRune, attribute) + attribute = addKeyPrefixIfNeeded(attribute) + + return attribute +} + +// Return '_' for anything non-alphanumeric +func sanitizeRune(r rune) rune { + if unicode.IsLetter(r) || unicode.IsDigit(r) { + return r + } + return '_' +} + +func addKeyPrefixIfNeeded(attribute string) string { + // If label starts with a number, prepend with "key_" + if unicode.IsDigit(rune(attribute[0])) { + attribute = "key_" + attribute + } else if strings.HasPrefix(attribute, "_") && !strings.HasPrefix(attribute, "__") && !dropSanitizationGate.IsEnabled() { + attribute = "key" + attribute + } + return attribute +} + +func (ClassicTranslator) TranslateMetric(metric pmetric.Metric, namespace string, addMetricSuffixes bool) string { + return BuildCompliantName(metric, namespace, addMetricSuffixes) +} + +func NewTranslator() Translator { + if allowUTF8FeatureGate.IsEnabled() { + return UTF8AllowedTranslator{} + } + return ClassicTranslator{} +} diff --git a/pkg/translator/prometheus/translator_test.go b/pkg/translator/prometheus/translator_test.go new file mode 100644 index 0000000000000..f56e9fc1fad56 --- /dev/null +++ b/pkg/translator/prometheus/translator_test.go @@ -0,0 +1,73 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package prometheus // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus" + +import ( + "testing" + + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/common/testutil" + "github.com/stretchr/testify/require" +) + +func TestTranslateAttribute(t *testing.T) { + testCases := []struct { + attribute string + utfAllowed bool + dropSantization bool + expected string + }{ + { + attribute: "", + expected: "", + }, + { + attribute: "test", + expected: "test", + }, + { + attribute: "__test", + expected: "__test", + }, + { + attribute: "_test", + expected: "key_test", + }, + { + attribute: "_test", + dropSantization: true, + expected: "_test", + }, + { + attribute: "0test", + expected: "key_0test", + }, + { + attribute: "test_/", + expected: "test__", + }, + { + attribute: "test_/", + utfAllowed: true, + expected: "test_/", + }, + { + attribute: "test.test", + expected: "test_test", + }, + { + attribute: "test.test", + utfAllowed: true, + expected: "test.test", + }, + } + + for _, tc := range testCases { + t.Run(tc.attribute, func(t *testing.T) { + testutil.SetFeatureGateForTest(t, allowUTF8FeatureGate, tc.utfAllowed) + testutil.SetFeatureGateForTest(t, dropSanitizationGate, tc.dropSantization) + translator := NewTranslator() + require.Equal(t, tc.expected, translator.TranslateAttribute(tc.attribute)) + }) + } +}