Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ The next release will require at least [Go 1.24].
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)
- Add experimental self-observability span and batch span processor metrics in `go.opentelemetry.io/otel/sdk/trace`.
Check the `go.opentelemetry.io/otel/sdk/trace/internal/x` package documentation for more information. (#7027, #6393, #7209)
- 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)
- Add native histogram exemplar support in `go.opentelemetry.io/otel/exporters/prometheus`. (#6772)
- Add experimental self-observability log metrics in `go.opentelemetry.io/otel/sdk/log`.
Check the `go.opentelemetry.io/otel/sdk/log/internal/x` package documentation for more information. (#7121)
Expand All @@ -71,10 +72,11 @@ The next release will require at least [Go 1.24].
### Deprecated

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

### Fixed

- Fix `go.opentelemetry.io/otel/exporters/prometheus` to deduplicate suffixes if already present in metric name when UTF8 is enabled. (#7088)
- Fix `go.opentelemetry.io/otel/exporters/prometheus` to not append a suffix if it's already present in metric name. (#7088)
- `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)
- Fix the `go.opentelemetry.io/otel/exporters/stdout/stdouttrace` self-observability component type and name. (#7195)
- Fix partial export count metric in `go.opentelemetry.io/otel/exporters/stdout/stdouttrace`. (#7199)
Expand Down
78 changes: 72 additions & 6 deletions exporters/prometheus/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"sync"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/prometheus/otlptranslator"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/internal/global"
Expand All @@ -17,6 +19,7 @@ import (
type config struct {
registerer prometheus.Registerer
disableTargetInfo bool
translationStrategy otlptranslator.TranslationStrategyOption
withoutUnits bool
withoutCounterSuffixes bool
readerOpts []metric.ManualReaderOption
Expand All @@ -25,9 +28,9 @@ type config struct {
resourceAttributesFilter attribute.Filter
}

var logDeprecatedLegacyScheme = sync.OnceFunc(func() {
var logTemporaryDefault = sync.OnceFunc(func() {
global.Warn(
"prometheus exporter legacy scheme deprecated: support for the legacy NameValidationScheme will be removed in a future release",
"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.",
)
})

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

if cfg.translationStrategy == "" {
// If no translation strategy was specified, deduce one based on the global
// NameValidationScheme. NOTE: this logic will change in the future, always
// defaulting to UnderscoreEscapingWithSuffixes

//nolint:staticcheck // NameValidationScheme is deprecated but we still need it for now.
if model.NameValidationScheme == model.UTF8Validation {
logTemporaryDefault()
cfg.translationStrategy = otlptranslator.NoUTF8EscapingWithSuffixes
} else {
cfg.translationStrategy = otlptranslator.UnderscoreEscapingWithSuffixes
}
} else {
// Note, if the translation strategy implies that suffixes should be added,
// the user can still use WithoutUnits and WithoutCounterSuffixes to
// explicitly disable specific suffixes. We do not override their preference
// in this case. However if the chosen strategy disables suffixes, we should
// forcibly disable all of them.
if !cfg.translationStrategy.ShouldAddSuffixes() {
cfg.withoutCounterSuffixes = true
cfg.withoutUnits = true
}
}

if cfg.registerer == nil {
cfg.registerer = prometheus.DefaultRegisterer
}
Expand Down Expand Up @@ -95,6 +122,30 @@ func WithoutTargetInfo() Option {
})
}

// WithTranslationStrategy provides a standardized way to define how metric and
// label names should be handled during translation to Prometheus format. See:
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.48.0/specification/metrics/sdk_exporters/prometheus.md#configuration.
// The recommended approach is to use either
// [otlptranslator.UnderscoreEscapingWithSuffixes] for full Prometheus-style
// compatibility or [otlptranslator.NoTranslation] for OpenTelemetry-style names.
//
// By default, if the NameValidationScheme variable in
// [github.com/prometheus/common/model] is "legacy", the default strategy is
// [otlptranslator.UnderscoreEscapingWithSuffixes]. If the validation scheme is
// "utf8", then currently the default Strategy is
// [otlptranslator.NoUTF8EscapingWithSuffixes].
//
// Notice: It is planned that a future release of this SDK will change the
// default to always be [otlptranslator.UnderscoreEscapingWithSuffixes] in all
// circumstances. Users wanting a different translation strategy should specify
// it explicitly.
func WithTranslationStrategy(strategy otlptranslator.TranslationStrategyOption) Option {
Comment thread
pellared marked this conversation as resolved.
return optionFunc(func(cfg config) config {
cfg.translationStrategy = strategy
return cfg
})
}

// WithoutUnits disables exporter's addition of unit suffixes to metric names,
Comment thread
ywwg marked this conversation as resolved.
// and will also prevent unit comments from being added in OpenMetrics once
// unit comments are supported.
Expand All @@ -103,19 +154,32 @@ func WithoutTargetInfo() Option {
// conventions. For example, the counter metric request.duration, with unit
// milliseconds would become request_duration_milliseconds_total.
// With this option set, the name would instead be request_duration_total.
//
// Can be used in conjunction with [WithTranslationStrategy] to disable unit
// suffixes in strategies that would otherwise add suffixes, but this behavior
// is not recommended and may be removed in a future release.
//
// Deprecated: Use [WithTranslationStrategy] instead.
func WithoutUnits() Option {
return optionFunc(func(cfg config) config {
cfg.withoutUnits = true
return cfg
})
}

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

// WithNamespace configures the Exporter to prefix metric with the given namespace.
// Metadata metrics such as target_info are not prefixed since these
// have special behavior based on their name.
// WithNamespace configures the Exporter to prefix metric with the given
// namespace. Metadata metrics such as target_info are not prefixed since these
// have special behavior based on their name. Namespaces will be prepended even
// if [otlptranslator.NoTranslation] is set as a translation strategy. If the provided namespace
// is empty, nothing will be prepended to metric names.
func WithNamespace(ns string) Option {
return optionFunc(func(cfg config) config {
cfg.namespace = ns
Expand Down
132 changes: 111 additions & 21 deletions exporters/prometheus/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"testing"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/prometheus/otlptranslator"
"github.com/stretchr/testify/assert"

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

testCases := []struct {
name string
options []Option
wantConfig config
name string
options []Option
wantConfig config
legacyValidation bool
}{
{
name: "Default",
options: nil,
wantConfig: config{
registerer: prometheus.DefaultRegisterer,
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
},
},
{
Expand All @@ -38,7 +42,8 @@ func TestNewConfig(t *testing.T) {
WithRegisterer(registry),
},
wantConfig: config{
registerer: registry,
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
registerer: registry,
},
},
{
Expand All @@ -47,8 +52,9 @@ func TestNewConfig(t *testing.T) {
WithAggregationSelector(aggregationSelector),
},
wantConfig: config{
registerer: prometheus.DefaultRegisterer,
readerOpts: []metric.ManualReaderOption{metric.WithAggregationSelector(aggregationSelector)},
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
readerOpts: []metric.ManualReaderOption{metric.WithAggregationSelector(aggregationSelector)},
},
},
{
Expand All @@ -57,8 +63,9 @@ func TestNewConfig(t *testing.T) {
WithProducer(producer),
},
wantConfig: config{
registerer: prometheus.DefaultRegisterer,
readerOpts: []metric.ManualReaderOption{metric.WithProducer(producer)},
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
readerOpts: []metric.ManualReaderOption{metric.WithProducer(producer)},
},
},
{
Expand All @@ -70,7 +77,8 @@ func TestNewConfig(t *testing.T) {
},

wantConfig: config{
registerer: registry,
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
registerer: registry,
readerOpts: []metric.ManualReaderOption{
metric.WithAggregationSelector(aggregationSelector),
metric.WithProducer(producer),
Expand All @@ -83,7 +91,8 @@ func TestNewConfig(t *testing.T) {
WithRegisterer(nil),
},
wantConfig: config{
registerer: prometheus.DefaultRegisterer,
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
},
},
{
Expand All @@ -92,8 +101,42 @@ func TestNewConfig(t *testing.T) {
WithoutTargetInfo(),
},
wantConfig: config{
registerer: prometheus.DefaultRegisterer,
disableTargetInfo: true,
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
disableTargetInfo: true,
},
},
{
name: "legacy validation mode default",
options: []Option{},
legacyValidation: true,
wantConfig: config{
translationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
},
},
{
name: "legacy validation mode, unit suffixes disabled",
options: []Option{
WithoutUnits(),
},
legacyValidation: true,
wantConfig: config{
translationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
withoutUnits: true,
},
},
{
name: "legacy validation mode, counter suffixes disabled",
options: []Option{
WithoutCounterSuffixes(),
},
legacyValidation: true,
wantConfig: config{
translationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
withoutCounterSuffixes: true,
},
},
{
Expand All @@ -102,8 +145,45 @@ func TestNewConfig(t *testing.T) {
WithoutUnits(),
},
wantConfig: config{
registerer: prometheus.DefaultRegisterer,
withoutUnits: true,
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
withoutUnits: true,
},
},
{
name: "NoTranslation implies no suffixes",
options: []Option{
WithTranslationStrategy(otlptranslator.NoTranslation),
},
wantConfig: config{
translationStrategy: otlptranslator.NoTranslation,
withoutUnits: true,
withoutCounterSuffixes: true,
registerer: prometheus.DefaultRegisterer,
},
},
{
name: "translation strategy does not override unit suffixes disabled",
options: []Option{
WithTranslationStrategy(otlptranslator.UnderscoreEscapingWithSuffixes),
WithoutUnits(),
},
wantConfig: config{
translationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
withoutUnits: true,
},
},
{
name: "translation strategy does not override counter suffixes disabled",
options: []Option{
WithTranslationStrategy(otlptranslator.UnderscoreEscapingWithSuffixes),
WithoutCounterSuffixes(),
},
wantConfig: config{
translationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
withoutCounterSuffixes: true,
},
},
{
Expand All @@ -112,8 +192,9 @@ func TestNewConfig(t *testing.T) {
WithNamespace("test"),
},
wantConfig: config{
registerer: prometheus.DefaultRegisterer,
namespace: "test",
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
namespace: "test",
},
},
{
Expand All @@ -122,8 +203,9 @@ func TestNewConfig(t *testing.T) {
WithNamespace("test"),
},
wantConfig: config{
registerer: prometheus.DefaultRegisterer,
namespace: "test",
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
namespace: "test",
},
},
{
Expand All @@ -132,13 +214,21 @@ func TestNewConfig(t *testing.T) {
WithNamespace("test/"),
},
wantConfig: config{
registerer: prometheus.DefaultRegisterer,
namespace: "test/",
translationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
registerer: prometheus.DefaultRegisterer,
namespace: "test/",
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
if tt.legacyValidation {
//nolint:staticcheck
model.NameValidationScheme = model.LegacyValidation
} else {
//nolint:staticcheck
model.NameValidationScheme = model.UTF8Validation
}
cfg := newConfig(tt.options...)
// only check the length of readerOpts, since they are not comparable
assert.Len(t, cfg.readerOpts, len(tt.wantConfig.readerOpts))
Expand Down
Loading
Loading