config: Add UnderscoreEscapingWithoutSuffixes translation strategy (#16849)

The last permutation of the translation options does underscore translation but does not add suffixes.
This translation option already exists in Mimir as otel_metric_suffixes_enabled, indicating external demand for this strategy.
There is an accompanying update to prometheus-docs to explain the use of this mode: https://github.com/prometheus/docs/pull/2688

Signed-off-by: Owen Williams <owen.williams@grafana.com>
This commit is contained in:
Owen Williams 2025-07-10 11:27:23 -04:00 committed by GitHub
parent b7f984d6d2
commit d2f1f4fb27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 88 additions and 20 deletions

View File

@ -104,7 +104,7 @@ func Load(s string, logger *slog.Logger) (*Config, error) {
}
switch cfg.OTLPConfig.TranslationStrategy {
case UnderscoreEscapingWithSuffixes:
case UnderscoreEscapingWithSuffixes, UnderscoreEscapingWithoutSuffixes:
case "":
case NoTranslation, NoUTF8EscapingWithSuffixes:
if cfg.GlobalConfig.MetricNameValidationScheme == model.LegacyValidation {
@ -1534,31 +1534,68 @@ func getGoGC() int {
type translationStrategyOption string
var (
// NoUTF8EscapingWithSuffixes will accept metric/label names as they are.
// Unit and type suffixes may be added to metric names, according to certain rules.
// NoUTF8EscapingWithSuffixes will accept metric/label names as they are. Unit
// and type suffixes may be added to metric names, according to certain rules.
NoUTF8EscapingWithSuffixes translationStrategyOption = "NoUTF8EscapingWithSuffixes"
// UnderscoreEscapingWithSuffixes is the default option for translating OTLP to Prometheus.
// This option will translate metric name characters that are not alphanumerics/underscores/colons to underscores,
// and label name characters that are not alphanumerics/underscores to underscores.
// Unit and type suffixes may be appended to metric names, according to certain rules.
// UnderscoreEscapingWithSuffixes is the default option for translating OTLP
// to Prometheus. This option will translate metric name characters that are
// not alphanumerics/underscores/colons to underscores, and label name
// characters that are not alphanumerics/underscores to underscores. Unit and
// type suffixes may be appended to metric names, according to certain rules.
UnderscoreEscapingWithSuffixes translationStrategyOption = "UnderscoreEscapingWithSuffixes"
// UnderscoreEscapingWithoutSuffixes translates metric name characters that
// are not alphanumerics/underscores/colons to underscores, and label name
// characters that are not alphanumerics/underscores to underscores, but
// unlike UnderscoreEscapingWithSuffixes it does not append any suffixes to
// the names.
UnderscoreEscapingWithoutSuffixes translationStrategyOption = "UnderscoreEscapingWithoutSuffixes"
// NoTranslation (EXPERIMENTAL): disables all translation of incoming metric
// and label names. This offers a way for the OTLP users to use native metric names, reducing confusion.
// and label names. This offers a way for the OTLP users to use native metric
// names, reducing confusion.
//
// WARNING: This setting has significant known risks and limitations (see
// https://prometheus.io/docs/practices/naming/ for details):
// * Impaired UX when using PromQL in plain YAML (e.g. alerts, rules, dashboard, autoscaling configuration).
// * Series collisions which in the best case may result in OOO errors, in the worst case a silently malformed
// time series. For instance, you may end up in situation of ingesting `foo.bar` series with unit
// `seconds` and a separate series `foo.bar` with unit `milliseconds`.
// https://prometheus.io/docs/practices/naming/ for details): * Impaired UX
// when using PromQL in plain YAML (e.g. alerts, rules, dashboard, autoscaling
// configuration). * Series collisions which in the best case may result in
// OOO errors, in the worst case a silently malformed time series. For
// instance, you may end up in situation of ingesting `foo.bar` series with
// unit `seconds` and a separate series `foo.bar` with unit `milliseconds`.
//
// As a result, this setting is experimental and currently, should not be used in
// production systems.
// As a result, this setting is experimental and currently, should not be used
// in production systems.
//
// TODO(ArthurSens): Mention `type-and-unit-labels` feature (https://github.com/prometheus/proposals/pull/39) once released, as potential mitigation of the above risks.
// TODO(ArthurSens): Mention `type-and-unit-labels` feature
// (https://github.com/prometheus/proposals/pull/39) once released, as
// potential mitigation of the above risks.
NoTranslation translationStrategyOption = "NoTranslation"
)
// ShouldEscape returns true if the translation strategy requires that metric
// names be escaped.
func (o translationStrategyOption) ShouldEscape() bool {
switch o {
case UnderscoreEscapingWithSuffixes, UnderscoreEscapingWithoutSuffixes:
return true
case NoTranslation, NoUTF8EscapingWithSuffixes:
return false
default:
return false
}
}
// ShouldAddSuffixes returns a bool deciding whether the given translation
// strategy should have suffixes added.
func (o translationStrategyOption) ShouldAddSuffixes() bool {
switch o {
case UnderscoreEscapingWithSuffixes, NoUTF8EscapingWithSuffixes:
return true
case UnderscoreEscapingWithoutSuffixes, NoTranslation:
return false
default:
return false
}
}
// OTLPConfig is the configuration for writing to the OTLP endpoint.
type OTLPConfig struct {
PromoteAllResourceAttributes bool `yaml:"promote_all_resource_attributes,omitempty"`

View File

@ -197,6 +197,11 @@ otlp:
# - "NoUTF8EscapingWithSuffixes" is a mode that relies on UTF-8 support in Prometheus.
# It preserves all special characters like dots, but still adds required metric name suffixes
# for units and _total, as UnderscoreEscapingWithSuffixes does.
# - "UnderscoreEscapingWithoutSuffixes" translates metric name characters that
# are not alphanumerics/underscores/colons to underscores, and label name
# characters that are not alphanumerics/underscores to underscores, but
# unlike UnderscoreEscapingWithSuffixes it does not append any suffixes to
# the names.
# - (EXPERIMENTAL) "NoTranslation" is a mode that relies on UTF-8 support in Prometheus.
# It preserves all special character like dots and won't append special suffixes for metric
# unit and type.
@ -2286,7 +2291,7 @@ The following meta labels are available on targets during [relabeling](#relabel_
See below for the configuration options for STACKIT discovery:
```yaml
# The STACKIT project
# The STACKIT project
project: <string>
# STACKIT region to use. No automatic discovery of the region is done.

View File

@ -594,9 +594,10 @@ func (rw *rwExporter) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) er
otlpCfg := rw.config().OTLPConfig
converter := otlptranslator.NewPrometheusConverter()
annots, err := converter.FromMetrics(ctx, md, otlptranslator.Settings{
AddMetricSuffixes: otlpCfg.TranslationStrategy != config.NoTranslation,
AllowUTF8: otlpCfg.TranslationStrategy != config.UnderscoreEscapingWithSuffixes,
AddMetricSuffixes: otlpCfg.TranslationStrategy.ShouldAddSuffixes(),
AllowUTF8: !otlpCfg.TranslationStrategy.ShouldEscape(),
PromoteResourceAttributes: otlptranslator.NewPromoteResourceAttributes(otlpCfg),
KeepIdentifyingResourceAttributes: otlpCfg.KeepIdentifyingResourceAttributes,
ConvertHistogramsToNHCB: otlpCfg.ConvertHistogramsToNHCB,

View File

@ -440,7 +440,32 @@ func TestOTLPWriteHandler(t *testing.T) {
},
},
},
{
name: "UnderscoreEscapingWithoutSuffixes",
otlpCfg: config.OTLPConfig{
TranslationStrategy: config.UnderscoreEscapingWithoutSuffixes,
},
expectedSamples: []mockSample{
{
l: labels.New(labels.Label{Name: "__name__", Value: "test_counter"},
labels.Label{Name: "foo_bar", Value: "baz"},
labels.Label{Name: "instance", Value: "test-instance"},
labels.Label{Name: "job", Value: "test-service"}),
t: timestamp.UnixMilli(),
v: 10.0,
},
{
l: labels.New(
labels.Label{Name: "__name__", Value: "target_info"},
labels.Label{Name: "host_name", Value: "test-host"},
labels.Label{Name: "instance", Value: "test-instance"},
labels.Label{Name: "job", Value: "test-service"},
),
t: timestamp.UnixMilli(),
v: 1,
},
},
},
{
name: "NoUTF8EscapingWithSuffixes",
otlpCfg: config.OTLPConfig{