Skip to content

Commit d0cab86

Browse files
ywwgpellared
andauthored
prometheus: Add support for setting Translation Strategy config option (#7111)
As per the specification: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk_exporters/prometheus.md#configuration This is part of a broader effort to unify the behavior of all the touch points between open telemetry metrics and prometheus: prometheus/prometheus#16542 Fixes #6668 --------- Signed-off-by: Owen Williams <[email protected]> Co-authored-by: Robert Pająk <[email protected]>
1 parent 3342341 commit d0cab86

29 files changed

+541
-136
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ The next release will require at least [Go 1.24].
5151
See the [migration documentation](./semconv/v1.36.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.34.0.`(#7032)
5252
- Add experimental self-observability span and batch span processor metrics in `go.opentelemetry.io/otel/sdk/trace`.
5353
Check the `go.opentelemetry.io/otel/sdk/trace/internal/x` package documentation for more information. (#7027, #6393, #7209)
54+
- Add support for configuring Prometheus name translation using `WithTranslationStrategy` option in `go.opentelemetry.io/otel/exporters/prometheus`. The current default translation strategy when UTF-8 mode is enabled is `NoUTF8EscapingWithSuffixes`, but a future release will change the default strategy to `UnderscoreEscapingWithSuffixes` for compliance with the specification. (#7111)
5455
- Add native histogram exemplar support in `go.opentelemetry.io/otel/exporters/prometheus`. (#6772)
5556
- Add experimental self-observability log metrics in `go.opentelemetry.io/otel/sdk/log`.
5657
Check the `go.opentelemetry.io/otel/sdk/log/internal/x` package documentation for more information. (#7121)
@@ -71,10 +72,11 @@ The next release will require at least [Go 1.24].
7172
### Deprecated
7273

7374
- Deprecate support for `OTEL_GO_X_CARDINALITY_LIMIT` environment variable in `go.opentelemetry.io/otel/sdk/metric`. Use `WithCardinalityLimit` option instead. (#7166)
75+
- Deprecate `WithoutUnits` and `WithoutCounterSuffixes` options, preferring `WithTranslationStrategy` instead. (#7111)
7476

7577
### Fixed
7678

77-
- Fix `go.opentelemetry.io/otel/exporters/prometheus` to deduplicate suffixes if already present in metric name when UTF8 is enabled. (#7088)
79+
- Fix `go.opentelemetry.io/otel/exporters/prometheus` to not append a suffix if it's already present in metric name. (#7088)
7880
- `SetBody` method of `Record` in `go.opentelemetry.io/otel/sdk/log` now deduplicates key-value collections (`log.Value` of `log.KindMap` from `go.opentelemetry.io/otel/log`). (#7002)
7981
- Fix the `go.opentelemetry.io/otel/exporters/stdout/stdouttrace` self-observability component type and name. (#7195)
8082
- Fix partial export count metric in `go.opentelemetry.io/otel/exporters/stdout/stdouttrace`. (#7199)

exporters/prometheus/config.go

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"sync"
88

99
"github.com/prometheus/client_golang/prometheus"
10+
"github.com/prometheus/common/model"
11+
"github.com/prometheus/otlptranslator"
1012

1113
"go.opentelemetry.io/otel/attribute"
1214
"go.opentelemetry.io/otel/internal/global"
@@ -17,6 +19,7 @@ import (
1719
type config struct {
1820
registerer prometheus.Registerer
1921
disableTargetInfo bool
22+
translationStrategy otlptranslator.TranslationStrategyOption
2023
withoutUnits bool
2124
withoutCounterSuffixes bool
2225
readerOpts []metric.ManualReaderOption
@@ -25,9 +28,9 @@ type config struct {
2528
resourceAttributesFilter attribute.Filter
2629
}
2730

28-
var logDeprecatedLegacyScheme = sync.OnceFunc(func() {
31+
var logTemporaryDefault = sync.OnceFunc(func() {
2932
global.Warn(
30-
"prometheus exporter legacy scheme deprecated: support for the legacy NameValidationScheme will be removed in a future release",
33+
"The default Prometheus naming translation strategy is planned to be changed from otlptranslator.NoUTF8EscapingWithSuffixes to otlptranslator.UnderscoreEscapingWithSuffixes in a future release. Add prometheus.WithTranslationStrategy(otlptranslator.NoUTF8EscapingWithSuffixes) to preserve the existing behavior, or prometheus.WithTranslationStrategy(otlptranslator.UnderscoreEscapingWithSuffixes) to opt into the future default behavior.",
3134
)
3235
})
3336

@@ -38,6 +41,30 @@ func newConfig(opts ...Option) config {
3841
cfg = opt.apply(cfg)
3942
}
4043

44+
if cfg.translationStrategy == "" {
45+
// If no translation strategy was specified, deduce one based on the global
46+
// NameValidationScheme. NOTE: this logic will change in the future, always
47+
// defaulting to UnderscoreEscapingWithSuffixes
48+
49+
//nolint:staticcheck // NameValidationScheme is deprecated but we still need it for now.
50+
if model.NameValidationScheme == model.UTF8Validation {
51+
logTemporaryDefault()
52+
cfg.translationStrategy = otlptranslator.NoUTF8EscapingWithSuffixes
53+
} else {
54+
cfg.translationStrategy = otlptranslator.UnderscoreEscapingWithSuffixes
55+
}
56+
} else {
57+
// Note, if the translation strategy implies that suffixes should be added,
58+
// the user can still use WithoutUnits and WithoutCounterSuffixes to
59+
// explicitly disable specific suffixes. We do not override their preference
60+
// in this case. However if the chosen strategy disables suffixes, we should
61+
// forcibly disable all of them.
62+
if !cfg.translationStrategy.ShouldAddSuffixes() {
63+
cfg.withoutCounterSuffixes = true
64+
cfg.withoutUnits = true
65+
}
66+
}
67+
4168
if cfg.registerer == nil {
4269
cfg.registerer = prometheus.DefaultRegisterer
4370
}
@@ -95,6 +122,30 @@ func WithoutTargetInfo() Option {
95122
})
96123
}
97124

125+
// WithTranslationStrategy provides a standardized way to define how metric and
126+
// label names should be handled during translation to Prometheus format. See:
127+
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.48.0/specification/metrics/sdk_exporters/prometheus.md#configuration.
128+
// The recommended approach is to use either
129+
// [otlptranslator.UnderscoreEscapingWithSuffixes] for full Prometheus-style
130+
// compatibility or [otlptranslator.NoTranslation] for OpenTelemetry-style names.
131+
//
132+
// By default, if the NameValidationScheme variable in
133+
// [github.com/prometheus/common/model] is "legacy", the default strategy is
134+
// [otlptranslator.UnderscoreEscapingWithSuffixes]. If the validation scheme is
135+
// "utf8", then currently the default Strategy is
136+
// [otlptranslator.NoUTF8EscapingWithSuffixes].
137+
//
138+
// Notice: It is planned that a future release of this SDK will change the
139+
// default to always be [otlptranslator.UnderscoreEscapingWithSuffixes] in all
140+
// circumstances. Users wanting a different translation strategy should specify
141+
// it explicitly.
142+
func WithTranslationStrategy(strategy otlptranslator.TranslationStrategyOption) Option {
143+
return optionFunc(func(cfg config) config {
144+
cfg.translationStrategy = strategy
145+
return cfg
146+
})
147+
}
148+
98149
// WithoutUnits disables exporter's addition of unit suffixes to metric names,
99150
// and will also prevent unit comments from being added in OpenMetrics once
100151
// unit comments are supported.
@@ -103,19 +154,32 @@ func WithoutTargetInfo() Option {
103154
// conventions. For example, the counter metric request.duration, with unit
104155
// milliseconds would become request_duration_milliseconds_total.
105156
// With this option set, the name would instead be request_duration_total.
157+
//
158+
// Can be used in conjunction with [WithTranslationStrategy] to disable unit
159+
// suffixes in strategies that would otherwise add suffixes, but this behavior
160+
// is not recommended and may be removed in a future release.
161+
//
162+
// Deprecated: Use [WithTranslationStrategy] instead.
106163
func WithoutUnits() Option {
107164
return optionFunc(func(cfg config) config {
108165
cfg.withoutUnits = true
109166
return cfg
110167
})
111168
}
112169

113-
// WithoutCounterSuffixes disables exporter's addition _total suffixes on counters.
170+
// WithoutCounterSuffixes disables exporter's addition _total suffixes on
171+
// counters.
114172
//
115173
// By default, metric names include a _total suffix to follow Prometheus naming
116174
// conventions. For example, the counter metric happy.people would become
117175
// happy_people_total. With this option set, the name would instead be
118176
// happy_people.
177+
//
178+
// Can be used in conjunction with [WithTranslationStrategy] to disable counter
179+
// suffixes in strategies that would otherwise add suffixes, but this behavior
180+
// is not recommended and may be removed in a future release.
181+
//
182+
// Deprecated: Use [WithTranslationStrategy] instead.
119183
func WithoutCounterSuffixes() Option {
120184
return optionFunc(func(cfg config) config {
121185
cfg.withoutCounterSuffixes = true
@@ -132,9 +196,11 @@ func WithoutScopeInfo() Option {
132196
})
133197
}
134198

135-
// WithNamespace configures the Exporter to prefix metric with the given namespace.
136-
// Metadata metrics such as target_info are not prefixed since these
137-
// have special behavior based on their name.
199+
// WithNamespace configures the Exporter to prefix metric with the given
200+
// namespace. Metadata metrics such as target_info are not prefixed since these
201+
// have special behavior based on their name. Namespaces will be prepended even
202+
// if [otlptranslator.NoTranslation] is set as a translation strategy. If the provided namespace
203+
// is empty, nothing will be prepended to metric names.
138204
func WithNamespace(ns string) Option {
139205
return optionFunc(func(cfg config) config {
140206
cfg.namespace = ns

exporters/prometheus/config_test.go

Lines changed: 111 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"testing"
99

1010
"github.com/prometheus/client_golang/prometheus"
11+
"github.com/prometheus/common/model"
12+
"github.com/prometheus/otlptranslator"
1113
"github.com/stretchr/testify/assert"
1214

1315
"go.opentelemetry.io/otel/sdk/metric"
@@ -21,15 +23,17 @@ func TestNewConfig(t *testing.T) {
2123
producer := &noopProducer{}
2224

2325
testCases := []struct {
24-
name string
25-
options []Option
26-
wantConfig config
26+
name string
27+
options []Option
28+
wantConfig config
29+
legacyValidation bool
2730
}{
2831
{
2932
name: "Default",
3033
options: nil,
3134
wantConfig: config{
32-
registerer: prometheus.DefaultRegisterer,
35+
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
36+
registerer: prometheus.DefaultRegisterer,
3337
},
3438
},
3539
{
@@ -38,7 +42,8 @@ func TestNewConfig(t *testing.T) {
3842
WithRegisterer(registry),
3943
},
4044
wantConfig: config{
41-
registerer: registry,
45+
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
46+
registerer: registry,
4247
},
4348
},
4449
{
@@ -47,8 +52,9 @@ func TestNewConfig(t *testing.T) {
4752
WithAggregationSelector(aggregationSelector),
4853
},
4954
wantConfig: config{
50-
registerer: prometheus.DefaultRegisterer,
51-
readerOpts: []metric.ManualReaderOption{metric.WithAggregationSelector(aggregationSelector)},
55+
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
56+
registerer: prometheus.DefaultRegisterer,
57+
readerOpts: []metric.ManualReaderOption{metric.WithAggregationSelector(aggregationSelector)},
5258
},
5359
},
5460
{
@@ -57,8 +63,9 @@ func TestNewConfig(t *testing.T) {
5763
WithProducer(producer),
5864
},
5965
wantConfig: config{
60-
registerer: prometheus.DefaultRegisterer,
61-
readerOpts: []metric.ManualReaderOption{metric.WithProducer(producer)},
66+
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
67+
registerer: prometheus.DefaultRegisterer,
68+
readerOpts: []metric.ManualReaderOption{metric.WithProducer(producer)},
6269
},
6370
},
6471
{
@@ -70,7 +77,8 @@ func TestNewConfig(t *testing.T) {
7077
},
7178

7279
wantConfig: config{
73-
registerer: registry,
80+
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
81+
registerer: registry,
7482
readerOpts: []metric.ManualReaderOption{
7583
metric.WithAggregationSelector(aggregationSelector),
7684
metric.WithProducer(producer),
@@ -83,7 +91,8 @@ func TestNewConfig(t *testing.T) {
8391
WithRegisterer(nil),
8492
},
8593
wantConfig: config{
86-
registerer: prometheus.DefaultRegisterer,
94+
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
95+
registerer: prometheus.DefaultRegisterer,
8796
},
8897
},
8998
{
@@ -92,8 +101,42 @@ func TestNewConfig(t *testing.T) {
92101
WithoutTargetInfo(),
93102
},
94103
wantConfig: config{
95-
registerer: prometheus.DefaultRegisterer,
96-
disableTargetInfo: true,
104+
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
105+
registerer: prometheus.DefaultRegisterer,
106+
disableTargetInfo: true,
107+
},
108+
},
109+
{
110+
name: "legacy validation mode default",
111+
options: []Option{},
112+
legacyValidation: true,
113+
wantConfig: config{
114+
translationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
115+
registerer: prometheus.DefaultRegisterer,
116+
},
117+
},
118+
{
119+
name: "legacy validation mode, unit suffixes disabled",
120+
options: []Option{
121+
WithoutUnits(),
122+
},
123+
legacyValidation: true,
124+
wantConfig: config{
125+
translationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
126+
registerer: prometheus.DefaultRegisterer,
127+
withoutUnits: true,
128+
},
129+
},
130+
{
131+
name: "legacy validation mode, counter suffixes disabled",
132+
options: []Option{
133+
WithoutCounterSuffixes(),
134+
},
135+
legacyValidation: true,
136+
wantConfig: config{
137+
translationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
138+
registerer: prometheus.DefaultRegisterer,
139+
withoutCounterSuffixes: true,
97140
},
98141
},
99142
{
@@ -102,8 +145,45 @@ func TestNewConfig(t *testing.T) {
102145
WithoutUnits(),
103146
},
104147
wantConfig: config{
105-
registerer: prometheus.DefaultRegisterer,
106-
withoutUnits: true,
148+
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
149+
registerer: prometheus.DefaultRegisterer,
150+
withoutUnits: true,
151+
},
152+
},
153+
{
154+
name: "NoTranslation implies no suffixes",
155+
options: []Option{
156+
WithTranslationStrategy(otlptranslator.NoTranslation),
157+
},
158+
wantConfig: config{
159+
translationStrategy: otlptranslator.NoTranslation,
160+
withoutUnits: true,
161+
withoutCounterSuffixes: true,
162+
registerer: prometheus.DefaultRegisterer,
163+
},
164+
},
165+
{
166+
name: "translation strategy does not override unit suffixes disabled",
167+
options: []Option{
168+
WithTranslationStrategy(otlptranslator.UnderscoreEscapingWithSuffixes),
169+
WithoutUnits(),
170+
},
171+
wantConfig: config{
172+
translationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
173+
registerer: prometheus.DefaultRegisterer,
174+
withoutUnits: true,
175+
},
176+
},
177+
{
178+
name: "translation strategy does not override counter suffixes disabled",
179+
options: []Option{
180+
WithTranslationStrategy(otlptranslator.UnderscoreEscapingWithSuffixes),
181+
WithoutCounterSuffixes(),
182+
},
183+
wantConfig: config{
184+
translationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
185+
registerer: prometheus.DefaultRegisterer,
186+
withoutCounterSuffixes: true,
107187
},
108188
},
109189
{
@@ -112,8 +192,9 @@ func TestNewConfig(t *testing.T) {
112192
WithNamespace("test"),
113193
},
114194
wantConfig: config{
115-
registerer: prometheus.DefaultRegisterer,
116-
namespace: "test",
195+
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
196+
registerer: prometheus.DefaultRegisterer,
197+
namespace: "test",
117198
},
118199
},
119200
{
@@ -122,8 +203,9 @@ func TestNewConfig(t *testing.T) {
122203
WithNamespace("test"),
123204
},
124205
wantConfig: config{
125-
registerer: prometheus.DefaultRegisterer,
126-
namespace: "test",
206+
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
207+
registerer: prometheus.DefaultRegisterer,
208+
namespace: "test",
127209
},
128210
},
129211
{
@@ -132,13 +214,21 @@ func TestNewConfig(t *testing.T) {
132214
WithNamespace("test/"),
133215
},
134216
wantConfig: config{
135-
registerer: prometheus.DefaultRegisterer,
136-
namespace: "test/",
217+
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
218+
registerer: prometheus.DefaultRegisterer,
219+
namespace: "test/",
137220
},
138221
},
139222
}
140223
for _, tt := range testCases {
141224
t.Run(tt.name, func(t *testing.T) {
225+
if tt.legacyValidation {
226+
//nolint:staticcheck
227+
model.NameValidationScheme = model.LegacyValidation
228+
} else {
229+
//nolint:staticcheck
230+
model.NameValidationScheme = model.UTF8Validation
231+
}
142232
cfg := newConfig(tt.options...)
143233
// only check the length of readerOpts, since they are not comparable
144234
assert.Len(t, cfg.readerOpts, len(tt.wantConfig.readerOpts))

0 commit comments

Comments
 (0)