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
34 changes: 34 additions & 0 deletions .chloggen/prometheusexporter-translation-strategies.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: exporter/prometheus

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add `translation_strategy` configuration option to control how OTLP metric names are translated to Prometheus format.

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [35459]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext: |
The new `translation_strategy` option provides four different translation modes:
- `UnderscoreEscapingWithSuffixes`: Escapes special characters to underscores and appends type/unit suffixes
- `UnderscoreEscapingWithoutSuffixes`: Escapes special characters but omits suffixes
- `NoUTF8EscapingWithSuffixes`: Preserves UTF-8 characters while adding suffixes
- `NoTranslation`: Passes metric names through unaltered
When `translation_strategy` is set, it always takes precedence over the deprecated `add_metric_suffixes` option.
The `exporter.prometheusexporter.DisableAddMetricSuffixes` feature gate can be used to completely ignore the deprecated `add_metric_suffixes` setting.

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [user]
13 changes: 11 additions & 2 deletions exporter/prometheusexporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ The following settings can be optionally configured:
- `resource_to_telemetry_conversion`
- `enabled` (default = false): If `enabled` is `true`, all the resource attributes will be converted to metric labels by default.
- `enable_open_metrics`: (default = `false`): If true, metrics will be exported using the OpenMetrics format. Exemplars are only exported in the OpenMetrics format, and only for histogram and monotonic sum (i.e. counter) metrics.
- `add_metric_suffixes`: (default = `true`): If false, addition of type and unit suffixes is disabled.
- `add_metric_suffixes`: (default = `true`): If false, addition of type and unit suffixes is disabled. **Deprecated**: Use `translation_strategy` instead. This setting is ignored when `translation_strategy` is explicitly set.
- `translation_strategy`: Controls how OTLP metric and attribute names are translated into Prometheus metric and label names. When set, this takes precedence over `add_metric_suffixes`. Available options:
- `UnderscoreEscapingWithSuffixes`: Fully escapes metric names for classic Prometheus metric name compatibility, and includes appending type and unit suffixes.
- `UnderscoreEscapingWithoutSuffixes`: Metric names will continue to escape special characters to `_`, but suffixes won't be attached.
- `NoUTF8EscapingWithSuffixes`: Disables changing special characters to `_`. Special suffixes like units and `_total` for counters will be attached.
- `NoTranslation`: Bypasses all metric and label name translation, passing them through unaltered.

Example:

Expand All @@ -50,7 +55,9 @@ exporters:
send_timestamps: true
metric_expiration: 180m
enable_open_metrics: true
# Legacy configuration - deprecated, ignored when translation_strategy is set
add_metric_suffixes: false
translation_strategy: "UnderscoreEscapingWithoutSuffixes"
resource_to_telemetry_conversion:
enabled: true
```
Expand All @@ -59,7 +66,9 @@ Given the example, metrics will be available at `https://1.2.3.4:1234/metrics`.

## Metric names and labels normalization

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/).
By Default, OpenTelemetry metric names and attributes are normalized to be compliant with [Prometheus naming rules](https://prometheus.io/docs/practices/naming/).

Optionally, users can set different `translation_strategy` options to control how metrics are exposed. Please be aware that Prometheus itself uses content negotiation to decide how to ingest metrics, and underscore escaping might be applied even though this exporter is configured to keep UTF-8 characters. For more details, read [Prometheus' Content Negotiation documentation](https://prometheus.io/docs/instrumenting/content_negotiation/).

## Setting resource attributes as metric labels

Expand Down
78 changes: 62 additions & 16 deletions exporter/prometheusexporter/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ type collector struct {
accumulator accumulator
logger *zap.Logger

sendTimestamps bool
addMetricSuffixes bool
namespace string
constLabels prometheus.Labels
metricFamilies sync.Map
metricExpiration time.Duration
sendTimestamps bool
namespace string
constLabels prometheus.Labels
metricFamilies sync.Map
metricExpiration time.Duration

metricNamer otlptranslator.MetricNamer
labelNamer otlptranslator.LabelNamer
Expand All @@ -48,20 +47,67 @@ type metricFamily struct {
}

func newCollector(config *Config, logger *zap.Logger) *collector {
labelNamer := otlptranslator.LabelNamer{}
labelNamer := configureLabelNamer(config)
return &collector{
accumulator: newAccumulator(logger, config.MetricExpiration),
logger: logger,
namespace: labelNamer.Build(config.Namespace),
sendTimestamps: config.SendTimestamps,
constLabels: config.ConstLabels,
addMetricSuffixes: config.AddMetricSuffixes,
metricExpiration: config.MetricExpiration,
metricNamer: otlptranslator.MetricNamer{WithMetricSuffixes: config.AddMetricSuffixes, Namespace: config.Namespace},
labelNamer: labelNamer,
accumulator: newAccumulator(logger, config.MetricExpiration),
logger: logger,
namespace: labelNamer.Build(config.Namespace),
sendTimestamps: config.SendTimestamps,
constLabels: config.ConstLabels,
metricExpiration: config.MetricExpiration,
metricNamer: configureMetricNamer(config),
labelNamer: labelNamer,
}
}

// configureMetricNamer configures the MetricNamer based on the translation strategy or legacy configuration
func configureMetricNamer(config *Config) otlptranslator.MetricNamer {
withSuffixes, utf8Allowed := getTranslationConfiguration(config)
return otlptranslator.MetricNamer{
WithMetricSuffixes: withSuffixes,
Namespace: config.Namespace,
UTF8Allowed: utf8Allowed,
}
}

// configureLabelNamer configures the LabelNamer based on the translation strategy or legacy configuration
func configureLabelNamer(config *Config) otlptranslator.LabelNamer {
_, utf8Allowed := getTranslationConfiguration(config)
return otlptranslator.LabelNamer{
UTF8Allowed: utf8Allowed,
}
}

// getTranslationConfiguration returns the translation configuration based on the strategy or legacy settings
// Returns (withSuffixes, allowUTF8)
func getTranslationConfiguration(config *Config) (bool, bool) {
// If TranslationStrategy is explicitly set, use it (takes precedence)
if config.TranslationStrategy != "" {
switch config.TranslationStrategy {
case underscoreEscapingWithSuffixes:
return true, false
case underscoreEscapingWithoutSuffixes:
return false, false
case noUTF8EscapingWithSuffixes:
return true, true
case noTranslation:
return false, true
default:
// Fallback to default behavior, suffixes enabled, UTF-8 escaped to underscores.
return true, false
}
}

// If feature gate is enabled, ignore AddMetricSuffixes (for deprecation)
if disableAddMetricSuffixesFeatureGate.IsEnabled() {
// Default to UnderscoreEscapingWithSuffixes behavior when AddMetricSuffixes is deprecated
return true, false
}

// Fall back to legacy AddMetricSuffixes behavior, UTF-8 escaped to underscores.
return config.AddMetricSuffixes, false
}

func convertExemplars(exemplars pmetric.ExemplarSlice) []prometheus.Exemplar {
length := exemplars.Len()
result := make([]prometheus.Exemplar, length)
Expand Down
41 changes: 40 additions & 1 deletion exporter/prometheusexporter/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
package prometheusexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusexporter"

import (
"fmt"
"time"

"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/confighttp"
"go.opentelemetry.io/collector/featuregate"

"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/resourcetotelemetry"
)
Expand Down Expand Up @@ -36,12 +38,49 @@ type Config struct {
EnableOpenMetrics bool `mapstructure:"enable_open_metrics"`

// AddMetricSuffixes controls whether suffixes are added to metric names. Defaults to true.
// Deprecated: Use TranslationStrategy instead. This setting is ignored when TranslationStrategy is explicitly set.
AddMetricSuffixes bool `mapstructure:"add_metric_suffixes"`

// TranslationStrategy controls how OTLP metric and attribute names are translated into Prometheus metric and label names.
// When set, this takes precedence over AddMetricSuffixes.
TranslationStrategy translationStrategy `mapstructure:"translation_strategy"`
}

var _ component.Config = (*Config)(nil)

// Validate checks if the exporter configuration is valid
func (*Config) Validate() error {
func (cfg *Config) Validate() error {
// Validate translation strategy if set
if cfg.TranslationStrategy != "" {
switch cfg.TranslationStrategy {
case underscoreEscapingWithSuffixes, underscoreEscapingWithoutSuffixes, noUTF8EscapingWithSuffixes, noTranslation:
default:
return fmt.Errorf("invalid translation_strategy: %s", cfg.TranslationStrategy)
}
}
return nil
}

type translationStrategy string

const (
// underscoreEscapingWithSuffixes fully escapes metric names for classic Prometheus metric name compatibility,
// and includes appending type and unit suffixes
underscoreEscapingWithSuffixes translationStrategy = "UnderscoreEscapingWithSuffixes"

// underscoreEscapingWithoutSuffixes escapes special characters to '_', but suffixes won't be attached
underscoreEscapingWithoutSuffixes translationStrategy = "UnderscoreEscapingWithoutSuffixes"

// noUTF8EscapingWithSuffixes disables changing special characters to '_'. Special suffixes like units and '_total' for counters will be attached
noUTF8EscapingWithSuffixes translationStrategy = "NoUTF8EscapingWithSuffixes"

// noTranslation bypasses all metric and label name translation, passing them through unaltered
noTranslation translationStrategy = "NoTranslation"
)

var disableAddMetricSuffixesFeatureGate = featuregate.GlobalRegistry().MustRegister(
"exporter.prometheusexporter.DisableAddMetricSuffixes",
featuregate.StageAlpha,
featuregate.WithRegisterDescription("When enabled, the deprecated add_metric_suffixes configuration option is ignored and translation_strategy is always used"),
featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-specification/pull/4533"),
)
2 changes: 1 addition & 1 deletion exporter/prometheusexporter/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ require (
go.opentelemetry.io/collector/consumer v1.37.0
go.opentelemetry.io/collector/exporter v0.131.0
go.opentelemetry.io/collector/exporter/exportertest v0.131.0
go.opentelemetry.io/collector/featuregate v1.37.0
go.opentelemetry.io/collector/pdata v1.37.0
go.opentelemetry.io/collector/receiver/receivertest v0.131.0
go.opentelemetry.io/otel v1.37.0
Expand Down Expand Up @@ -206,7 +207,6 @@ require (
go.opentelemetry.io/collector/extension/extensionauth v1.37.0 // indirect
go.opentelemetry.io/collector/extension/extensionmiddleware v0.131.0 // indirect
go.opentelemetry.io/collector/extension/xextension v0.131.0 // indirect
go.opentelemetry.io/collector/featuregate v1.37.0 // indirect
go.opentelemetry.io/collector/internal/telemetry v0.131.0 // indirect
go.opentelemetry.io/collector/pdata/pprofile v0.131.0 // indirect
go.opentelemetry.io/collector/pdata/xpdata v0.131.0 // indirect
Expand Down
Loading
Loading