1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-08-10 22:31:50 +02:00

Migrate prometheus exporter to otlptranslator (#7044)

Fixes https://github.com/open-telemetry/opentelemetry-go/issues/7039
Fixes https://github.com/open-telemetry/opentelemetry-go/issues/6704

This uses the common prometheus/otlptranslator library to handle name
conversion. It was a little tricky to work around the fact that the
library only lets us configure whether all suffixes are added or not.
But we want to keep supporting WithoutUnit and WithoutCounterSuffixes
for a while longer. Those will eventually be deprecated and replaced
after
https://github.com/open-telemetry/opentelemetry-specification/pull/4533
is released.

We decided to go ahead with the changes despite it being a small
behavioral change when UTF8 is enabled. See:
https://github.com/prometheus/otlptranslator/issues/44 for the
rationale.

This adds a unit test to verify that it properly handles bracketed
units. The test fails on main with:

```
--- FAIL: TestPrometheusExporter (0.01s)
    --- FAIL: TestPrometheusExporter/counter_with_bracketed_unit (0.00s)
        exporter_test.go:646:
            	Error Trace:	/usr/local/google/home/dashpole/go/src/go.opentelemetry.io/opentelemetry-go/exporters/prometheus/exporter_test.go:646
            	Error:      	Received unexpected error:
            	            	-# HELP "foo_{spans}_total" a simple counter
            	            	-# TYPE "foo_{spans}_total" counter
            	            	-{"foo_{spans}_total",A="B",C="D",E="true",F="42",otel_scope_fizz="buzz",otel_scope_name="testmeter",otel_scope_schema_url="",otel_scope_version="v0.1.0"} 24.3
            	            	-{"foo_{spans}_total",A="D",C="B",E="true",F="42",otel_scope_fizz="buzz",otel_scope_name="testmeter",otel_scope_schema_url="",otel_scope_version="v0.1.0"} 5
            	            	+# HELP foo_total a simple counter
            	            	+# TYPE foo_total counter
            	            	+foo_total{A="B",C="D",E="true",F="42",otel_scope_fizz="buzz",otel_scope_name="testmeter",otel_scope_schema_url="",otel_scope_version="v0.1.0"} 24.3
            	            	+foo_total{A="D",C="B",E="true",F="42",otel_scope_fizz="buzz",otel_scope_name="testmeter",otel_scope_schema_url="",otel_scope_version="v0.1.0"} 5
            	            	 # HELP target_info Target metadata
            	            	 # TYPE target_info gauge
            	            	 target_info{"service.name"="prometheus_test","telemetry.sdk.language"="go","telemetry.sdk.name"="opentelemetry","telemetry.sdk.version"="latest"} 1

            	Test:       	TestPrometheusExporter/counter_with_bracketed_unit
2025/07/18 15:07:47 internal_logging.go:50: "msg"="Using existing type definition." "error"="instrument type conflict" "instrument"="foo_total" "existing"="COUNTER" "dropped"="GAUGE"
2025/07/18 15:07:47 internal_logging.go:50: "msg"="Using existing type definition." "error"="instrument type conflict" "instrument"="foo_bytes" "existing"="GAUGE" "dropped"="HISTOGRAM"
FAIL
FAIL	go.opentelemetry.io/otel/exporters/prometheus	0.054s
FAIL
```

cc @TylerHelmuth @ywwg @ArthurSens

---------

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
Co-authored-by: Robert Pająk <pellared@hotmail.com>
This commit is contained in:
David Ashpole
2025-07-21 11:58:28 -04:00
committed by GitHub
parent f6b5fb98a5
commit 8e6e28f962
10 changed files with 220 additions and 157 deletions

View File

@@ -51,6 +51,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Change `AssertEqual` in `go.opentelemetry.io/otel/log/logtest` to accept `TestingT` in order to support benchmarks and fuzz tests. (#6908) - Change `AssertEqual` in `go.opentelemetry.io/otel/log/logtest` to accept `TestingT` in order to support benchmarks and fuzz tests. (#6908)
- Change `SDKProcessorLogQueueCapacity`, `SDKProcessorLogQueueSize`, `SDKProcessorSpanQueueSize`, and `SDKProcessorSpanQueueCapacity` in `go.opentelemetry.io/otel/semconv/v1.36.0/otelconv` to use a `Int64ObservableUpDownCounter`. (#7041) - Change `SDKProcessorLogQueueCapacity`, `SDKProcessorLogQueueSize`, `SDKProcessorSpanQueueSize`, and `SDKProcessorSpanQueueCapacity` in `go.opentelemetry.io/otel/semconv/v1.36.0/otelconv` to use a `Int64ObservableUpDownCounter`. (#7041)
- Change `go.opentelemetry.io/otel/exporters/prometheus` to no longer deduplicate suffixes when UTF8 is enabled.
It is recommended to disable unit and counter suffixes in the exporter, and manually add suffixes if you rely on the existing behavior. (#7044)
### Fixed
- Fix `go.opentelemetry.io/otel/exporters/prometheus` to properly handle unit suffixes when the unit is in brackets.
E.g. `{spans}`. (#7044)
<!-- Released section --> <!-- Released section -->
<!-- Don't change this section unless doing release --> <!-- Don't change this section unless doing release -->

View File

@@ -4,11 +4,9 @@
package prometheus // import "go.opentelemetry.io/otel/exporters/prometheus" package prometheus // import "go.opentelemetry.io/otel/exporters/prometheus"
import ( import (
"strings"
"sync" "sync"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/internal/global" "go.opentelemetry.io/otel/internal/global"
@@ -139,17 +137,6 @@ func WithoutScopeInfo() Option {
// have special behavior based on their name. // have special behavior based on their name.
func WithNamespace(ns string) Option { func WithNamespace(ns string) Option {
return optionFunc(func(cfg config) config { return optionFunc(func(cfg config) config {
if model.NameValidationScheme != model.UTF8Validation { // nolint:staticcheck // We need this check to keep supporting the legacy scheme.
logDeprecatedLegacyScheme()
// Only sanitize if prometheus does not support UTF-8.
ns = model.EscapeName(ns, model.NameEscapingScheme)
}
if !strings.HasSuffix(ns, "_") {
// namespace and metric names should be separated with an underscore,
// adds a trailing underscore if there is not one already.
ns = ns + "_"
}
cfg.namespace = ns cfg.namespace = ns
return cfg return cfg
}) })

View File

@@ -113,17 +113,17 @@ func TestNewConfig(t *testing.T) {
}, },
wantConfig: config{ wantConfig: config{
registerer: prometheus.DefaultRegisterer, registerer: prometheus.DefaultRegisterer,
namespace: "test_", namespace: "test",
}, },
}, },
{ {
name: "with namespace with trailing underscore", name: "with namespace with trailing underscore",
options: []Option{ options: []Option{
WithNamespace("test_"), WithNamespace("test"),
}, },
wantConfig: config{ wantConfig: config{
registerer: prometheus.DefaultRegisterer, registerer: prometheus.DefaultRegisterer,
namespace: "test_", namespace: "test",
}, },
}, },
{ {
@@ -133,7 +133,7 @@ func TestNewConfig(t *testing.T) {
}, },
wantConfig: config{ wantConfig: config{
registerer: prometheus.DefaultRegisterer, registerer: prometheus.DefaultRegisterer,
namespace: "test/_", namespace: "test/",
}, },
}, },
} }

View File

@@ -16,6 +16,7 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
"github.com/prometheus/otlptranslator"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
@@ -27,16 +28,12 @@ import (
) )
const ( const (
targetInfoMetricName = "target_info"
targetInfoDescription = "Target metadata" targetInfoDescription = "Target metadata"
scopeLabelPrefix = "otel_scope_" scopeLabelPrefix = "otel_scope_"
scopeNameLabel = scopeLabelPrefix + "name" scopeNameLabel = scopeLabelPrefix + "name"
scopeVersionLabel = scopeLabelPrefix + "version" scopeVersionLabel = scopeLabelPrefix + "version"
scopeSchemaLabel = scopeLabelPrefix + "schema_url" scopeSchemaLabel = scopeLabelPrefix + "schema_url"
traceIDExemplarKey = "trace_id"
spanIDExemplarKey = "span_id"
) )
var metricsPool = sync.Pool{ var metricsPool = sync.Pool{
@@ -93,12 +90,11 @@ type collector struct {
targetInfo prometheus.Metric targetInfo prometheus.Metric
metricFamilies map[string]*dto.MetricFamily metricFamilies map[string]*dto.MetricFamily
resourceKeyVals keyVals resourceKeyVals keyVals
metricNamer otlptranslator.MetricNamer
labelNamer otlptranslator.LabelNamer
unitNamer otlptranslator.UnitNamer
} }
// prometheus counters MUST have a _total suffix by default:
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/compatibility/prometheus_and_openmetrics.md
const counterSuffix = "total"
// New returns a Prometheus Exporter. // New returns a Prometheus Exporter.
func New(opts ...Option) (*Exporter, error) { func New(opts ...Option) (*Exporter, error) {
cfg := newConfig(opts...) cfg := newConfig(opts...)
@@ -108,6 +104,12 @@ func New(opts ...Option) (*Exporter, error) {
// TODO (#3244): Enable some way to configure the reader, but not change temporality. // TODO (#3244): Enable some way to configure the reader, but not change temporality.
reader := metric.NewManualReader(cfg.readerOpts...) reader := metric.NewManualReader(cfg.readerOpts...)
utf8Allowed := model.NameValidationScheme == model.UTF8Validation // nolint:staticcheck // We need this check to keep supporting the legacy scheme.
if !utf8Allowed {
// Only sanitize if prometheus does not support UTF-8.
logDeprecatedLegacyScheme()
}
labelNamer := otlptranslator.LabelNamer{UTF8Allowed: utf8Allowed}
collector := &collector{ collector := &collector{
reader: reader, reader: reader,
disableTargetInfo: cfg.disableTargetInfo, disableTargetInfo: cfg.disableTargetInfo,
@@ -115,8 +117,18 @@ func New(opts ...Option) (*Exporter, error) {
withoutCounterSuffixes: cfg.withoutCounterSuffixes, withoutCounterSuffixes: cfg.withoutCounterSuffixes,
disableScopeInfo: cfg.disableScopeInfo, disableScopeInfo: cfg.disableScopeInfo,
metricFamilies: make(map[string]*dto.MetricFamily), metricFamilies: make(map[string]*dto.MetricFamily),
namespace: cfg.namespace, namespace: labelNamer.Build(cfg.namespace),
resourceAttributesFilter: cfg.resourceAttributesFilter, resourceAttributesFilter: cfg.resourceAttributesFilter,
metricNamer: otlptranslator.MetricNamer{
Namespace: cfg.namespace,
// We decide whether to pass type and unit to the netricNamer based
// on whether units or counter suffixes are enabled, and keep this
// always enabled.
WithMetricSuffixes: true,
UTF8Allowed: utf8Allowed,
},
unitNamer: otlptranslator.UnitNamer{UTF8Allowed: utf8Allowed},
labelNamer: labelNamer,
} }
if err := cfg.registerer.Register(collector); err != nil { if err := cfg.registerer.Register(collector); err != nil {
@@ -164,7 +176,11 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
defer c.mu.Unlock() defer c.mu.Unlock()
if c.targetInfo == nil && !c.disableTargetInfo { if c.targetInfo == nil && !c.disableTargetInfo {
targetInfo, err := createInfoMetric(targetInfoMetricName, targetInfoDescription, metrics.Resource) targetInfo, err := c.createInfoMetric(
otlptranslator.TargetInfoMetricName,
targetInfoDescription,
metrics.Resource,
)
if err != nil { if err != nil {
// If the target info metric is invalid, disable sending it. // If the target info metric is invalid, disable sending it.
c.disableTargetInfo = true c.disableTargetInfo = true
@@ -195,7 +211,7 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
kv.keys = append(kv.keys, scopeNameLabel, scopeVersionLabel, scopeSchemaLabel) kv.keys = append(kv.keys, scopeNameLabel, scopeVersionLabel, scopeSchemaLabel)
kv.vals = append(kv.vals, scopeMetrics.Scope.Name, scopeMetrics.Scope.Version, scopeMetrics.Scope.SchemaURL) kv.vals = append(kv.vals, scopeMetrics.Scope.Name, scopeMetrics.Scope.Version, scopeMetrics.Scope.SchemaURL)
attrKeys, attrVals := getAttrs(scopeMetrics.Scope.Attributes) attrKeys, attrVals := getAttrs(scopeMetrics.Scope.Attributes, c.labelNamer)
for i := range attrKeys { for i := range attrKeys {
attrKeys[i] = scopeLabelPrefix + attrKeys[i] attrKeys[i] = scopeLabelPrefix + attrKeys[i]
} }
@@ -211,7 +227,7 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
if typ == nil { if typ == nil {
continue continue
} }
name := c.getName(m, typ) name := c.getName(m)
drop, help := c.validateMetrics(name, m.Description, typ) drop, help := c.validateMetrics(name, m.Description, typ)
if drop { if drop {
@@ -224,21 +240,21 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
switch v := m.Data.(type) { switch v := m.Data.(type) {
case metricdata.Histogram[int64]: case metricdata.Histogram[int64]:
addHistogramMetric(ch, v, m, name, kv) addHistogramMetric(ch, v, m, name, kv, c.labelNamer)
case metricdata.Histogram[float64]: case metricdata.Histogram[float64]:
addHistogramMetric(ch, v, m, name, kv) addHistogramMetric(ch, v, m, name, kv, c.labelNamer)
case metricdata.ExponentialHistogram[int64]: case metricdata.ExponentialHistogram[int64]:
addExponentialHistogramMetric(ch, v, m, name, kv) addExponentialHistogramMetric(ch, v, m, name, kv, c.labelNamer)
case metricdata.ExponentialHistogram[float64]: case metricdata.ExponentialHistogram[float64]:
addExponentialHistogramMetric(ch, v, m, name, kv) addExponentialHistogramMetric(ch, v, m, name, kv, c.labelNamer)
case metricdata.Sum[int64]: case metricdata.Sum[int64]:
addSumMetric(ch, v, m, name, kv) addSumMetric(ch, v, m, name, kv, c.labelNamer)
case metricdata.Sum[float64]: case metricdata.Sum[float64]:
addSumMetric(ch, v, m, name, kv) addSumMetric(ch, v, m, name, kv, c.labelNamer)
case metricdata.Gauge[int64]: case metricdata.Gauge[int64]:
addGaugeMetric(ch, v, m, name, kv) addGaugeMetric(ch, v, m, name, kv, c.labelNamer)
case metricdata.Gauge[float64]: case metricdata.Gauge[float64]:
addGaugeMetric(ch, v, m, name, kv) addGaugeMetric(ch, v, m, name, kv, c.labelNamer)
} }
} }
} }
@@ -303,9 +319,10 @@ func addExponentialHistogramMetric[N int64 | float64](
m metricdata.Metrics, m metricdata.Metrics,
name string, name string,
kv keyVals, kv keyVals,
labelNamer otlptranslator.LabelNamer,
) { ) {
for _, dp := range histogram.DataPoints { for _, dp := range histogram.DataPoints {
keys, values := getAttrs(dp.Attributes) keys, values := getAttrs(dp.Attributes, labelNamer)
keys = append(keys, kv.keys...) keys = append(keys, kv.keys...)
values = append(values, kv.vals...) values = append(values, kv.vals...)
@@ -377,9 +394,10 @@ func addHistogramMetric[N int64 | float64](
m metricdata.Metrics, m metricdata.Metrics,
name string, name string,
kv keyVals, kv keyVals,
labelNamer otlptranslator.LabelNamer,
) { ) {
for _, dp := range histogram.DataPoints { for _, dp := range histogram.DataPoints {
keys, values := getAttrs(dp.Attributes) keys, values := getAttrs(dp.Attributes, labelNamer)
keys = append(keys, kv.keys...) keys = append(keys, kv.keys...)
values = append(values, kv.vals...) values = append(values, kv.vals...)
@@ -396,7 +414,7 @@ func addHistogramMetric[N int64 | float64](
otel.Handle(err) otel.Handle(err)
continue continue
} }
m = addExemplars(m, dp.Exemplars) m = addExemplars(m, dp.Exemplars, labelNamer)
ch <- m ch <- m
} }
} }
@@ -407,6 +425,7 @@ func addSumMetric[N int64 | float64](
m metricdata.Metrics, m metricdata.Metrics,
name string, name string,
kv keyVals, kv keyVals,
labelNamer otlptranslator.LabelNamer,
) { ) {
valueType := prometheus.CounterValue valueType := prometheus.CounterValue
if !sum.IsMonotonic { if !sum.IsMonotonic {
@@ -414,7 +433,7 @@ func addSumMetric[N int64 | float64](
} }
for _, dp := range sum.DataPoints { for _, dp := range sum.DataPoints {
keys, values := getAttrs(dp.Attributes) keys, values := getAttrs(dp.Attributes, labelNamer)
keys = append(keys, kv.keys...) keys = append(keys, kv.keys...)
values = append(values, kv.vals...) values = append(values, kv.vals...)
@@ -427,7 +446,7 @@ func addSumMetric[N int64 | float64](
// GaugeValues don't support Exemplars at this time // GaugeValues don't support Exemplars at this time
// https://github.com/prometheus/client_golang/blob/aef8aedb4b6e1fb8ac1c90790645169125594096/prometheus/metric.go#L199 // https://github.com/prometheus/client_golang/blob/aef8aedb4b6e1fb8ac1c90790645169125594096/prometheus/metric.go#L199
if valueType != prometheus.GaugeValue { if valueType != prometheus.GaugeValue {
m = addExemplars(m, dp.Exemplars) m = addExemplars(m, dp.Exemplars, labelNamer)
} }
ch <- m ch <- m
} }
@@ -439,9 +458,10 @@ func addGaugeMetric[N int64 | float64](
m metricdata.Metrics, m metricdata.Metrics,
name string, name string,
kv keyVals, kv keyVals,
labelNamer otlptranslator.LabelNamer,
) { ) {
for _, dp := range gauge.DataPoints { for _, dp := range gauge.DataPoints {
keys, values := getAttrs(dp.Attributes) keys, values := getAttrs(dp.Attributes, labelNamer)
keys = append(keys, kv.keys...) keys = append(keys, kv.keys...)
values = append(values, kv.vals...) values = append(values, kv.vals...)
@@ -457,12 +477,12 @@ func addGaugeMetric[N int64 | float64](
// getAttrs converts the attribute.Set to two lists of matching Prometheus-style // getAttrs converts the attribute.Set to two lists of matching Prometheus-style
// keys and values. // keys and values.
func getAttrs(attrs attribute.Set) ([]string, []string) { func getAttrs(attrs attribute.Set, labelNamer otlptranslator.LabelNamer) ([]string, []string) {
keys := make([]string, 0, attrs.Len()) keys := make([]string, 0, attrs.Len())
values := make([]string, 0, attrs.Len()) values := make([]string, 0, attrs.Len())
itr := attrs.Iter() itr := attrs.Iter()
if model.NameValidationScheme == model.UTF8Validation { // nolint:staticcheck // We need this check to keep supporting the legacy scheme. if labelNamer.UTF8Allowed {
// Do not perform sanitization if prometheus supports UTF-8. // Do not perform sanitization if prometheus supports UTF-8.
for itr.Next() { for itr.Next() {
kv := itr.Attribute() kv := itr.Attribute()
@@ -475,7 +495,7 @@ func getAttrs(attrs attribute.Set) ([]string, []string) {
keysMap := make(map[string][]string) keysMap := make(map[string][]string)
for itr.Next() { for itr.Next() {
kv := itr.Attribute() kv := itr.Attribute()
key := model.EscapeName(string(kv.Key), model.NameEscapingScheme) key := labelNamer.Build(string(kv.Key))
if _, ok := keysMap[key]; !ok { if _, ok := keysMap[key]; !ok {
keysMap[key] = []string{kv.Value.Emit()} keysMap[key] = []string{kv.Value.Emit()}
} else { } else {
@@ -492,91 +512,22 @@ func getAttrs(attrs attribute.Set) ([]string, []string) {
return keys, values return keys, values
} }
func createInfoMetric(name, description string, res *resource.Resource) (prometheus.Metric, error) { func (c *collector) createInfoMetric(name, description string, res *resource.Resource) (prometheus.Metric, error) {
keys, values := getAttrs(*res.Set()) keys, values := getAttrs(*res.Set(), c.labelNamer)
desc := prometheus.NewDesc(name, description, keys, nil) desc := prometheus.NewDesc(name, description, keys, nil)
return prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(1), values...) return prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(1), values...)
} }
func unitMapGetOrDefault(unit string) string {
if promUnit, ok := unitSuffixes[unit]; ok {
return promUnit
}
return unit
}
var unitSuffixes = map[string]string{
// Time
"d": "days",
"h": "hours",
"min": "minutes",
"s": "seconds",
"ms": "milliseconds",
"us": "microseconds",
"ns": "nanoseconds",
// Bytes
"By": "bytes",
"KiBy": "kibibytes",
"MiBy": "mebibytes",
"GiBy": "gibibytes",
"TiBy": "tibibytes",
"KBy": "kilobytes",
"MBy": "megabytes",
"GBy": "gigabytes",
"TBy": "terabytes",
// SI
"m": "meters",
"V": "volts",
"A": "amperes",
"J": "joules",
"W": "watts",
"g": "grams",
// Misc
"Cel": "celsius",
"Hz": "hertz",
"1": "ratio",
"%": "percent",
}
// getName returns the sanitized name, prefixed with the namespace and suffixed with unit. // getName returns the sanitized name, prefixed with the namespace and suffixed with unit.
func (c *collector) getName(m metricdata.Metrics, typ *dto.MetricType) string { func (c *collector) getName(m metricdata.Metrics) string {
name := m.Name translatorMetric := otlptranslator.Metric{
if model.NameValidationScheme != model.UTF8Validation { // nolint:staticcheck // We need this check to keep supporting the legacy scheme. Name: m.Name,
// Only sanitize if prometheus does not support UTF-8. Type: c.namingMetricType(m),
logDeprecatedLegacyScheme()
name = model.EscapeName(name, model.NameEscapingScheme)
} }
addCounterSuffix := !c.withoutCounterSuffixes && *typ == dto.MetricType_COUNTER if !c.withoutUnits {
if addCounterSuffix { translatorMetric.Unit = m.Unit
// Remove the _total suffix here, as we will re-add the total suffix
// later, and it needs to come after the unit suffix.
name = strings.TrimSuffix(name, counterSuffix)
// If the last character is an underscore, or would be converted to an underscore, trim it from the name.
// an underscore will be added back in later.
if convertsToUnderscore(rune(name[len(name)-1])) {
name = name[:len(name)-1]
} }
} return c.metricNamer.Build(translatorMetric)
if c.namespace != "" {
name = c.namespace + name
}
if suffix := unitMapGetOrDefault(m.Unit); suffix != "" && !c.withoutUnits && !strings.HasSuffix(name, suffix) {
name += "_" + suffix
}
if addCounterSuffix {
name += "_" + counterSuffix
}
return name
}
// convertsToUnderscore returns true if the character would be converted to an
// underscore when the escaping scheme is underscore escaping. This is meant to
// capture any character that should be considered a "delimiter".
func convertsToUnderscore(b rune) bool {
return (b < 'a' || b > 'z') && (b < 'A' || b > 'Z') && b != ':' && (b < '0' || b > '9')
} }
func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType { func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType {
@@ -601,12 +552,41 @@ func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType {
return nil return nil
} }
// namingMetricType provides the metric type for naming purposes.
func (c *collector) namingMetricType(m metricdata.Metrics) otlptranslator.MetricType {
switch v := m.Data.(type) {
case metricdata.ExponentialHistogram[int64], metricdata.ExponentialHistogram[float64]:
return otlptranslator.MetricTypeHistogram
case metricdata.Histogram[int64], metricdata.Histogram[float64]:
return otlptranslator.MetricTypeHistogram
case metricdata.Sum[float64]:
// If counter suffixes are disabled, treat them like non-monotonic
// suffixes for the purposes of naming.
if v.IsMonotonic && !c.withoutCounterSuffixes {
return otlptranslator.MetricTypeMonotonicCounter
}
return otlptranslator.MetricTypeNonMonotonicCounter
case metricdata.Sum[int64]:
// If counter suffixes are disabled, treat them like non-monotonic
// suffixes for the purposes of naming.
if v.IsMonotonic && !c.withoutCounterSuffixes {
return otlptranslator.MetricTypeMonotonicCounter
}
return otlptranslator.MetricTypeNonMonotonicCounter
case metricdata.Gauge[int64], metricdata.Gauge[float64]:
return otlptranslator.MetricTypeGauge
case metricdata.Summary:
return otlptranslator.MetricTypeSummary
}
return otlptranslator.MetricTypeUnknown
}
func (c *collector) createResourceAttributes(res *resource.Resource) { func (c *collector) createResourceAttributes(res *resource.Resource) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
resourceAttrs, _ := res.Set().Filter(c.resourceAttributesFilter) resourceAttrs, _ := res.Set().Filter(c.resourceAttributesFilter)
resourceKeys, resourceValues := getAttrs(resourceAttrs) resourceKeys, resourceValues := getAttrs(resourceAttrs, c.labelNamer)
c.resourceKeyVals = keyVals{keys: resourceKeys, vals: resourceValues} c.resourceKeyVals = keyVals{keys: resourceKeys, vals: resourceValues}
} }
@@ -648,16 +628,20 @@ func (c *collector) validateMetrics(name, description string, metricType *dto.Me
return false, "" return false, ""
} }
func addExemplars[N int64 | float64](m prometheus.Metric, exemplars []metricdata.Exemplar[N]) prometheus.Metric { func addExemplars[N int64 | float64](
m prometheus.Metric,
exemplars []metricdata.Exemplar[N],
labelNamer otlptranslator.LabelNamer,
) prometheus.Metric {
if len(exemplars) == 0 { if len(exemplars) == 0 {
return m return m
} }
promExemplars := make([]prometheus.Exemplar, len(exemplars)) promExemplars := make([]prometheus.Exemplar, len(exemplars))
for i, exemplar := range exemplars { for i, exemplar := range exemplars {
labels := attributesToLabels(exemplar.FilteredAttributes) labels := attributesToLabels(exemplar.FilteredAttributes, labelNamer)
// Overwrite any existing trace ID or span ID attributes // Overwrite any existing trace ID or span ID attributes
labels[traceIDExemplarKey] = hex.EncodeToString(exemplar.TraceID[:]) labels[otlptranslator.ExemplarTraceIDKey] = hex.EncodeToString(exemplar.TraceID[:])
labels[spanIDExemplarKey] = hex.EncodeToString(exemplar.SpanID[:]) labels[otlptranslator.ExemplarSpanIDKey] = hex.EncodeToString(exemplar.SpanID[:])
promExemplars[i] = prometheus.Exemplar{ promExemplars[i] = prometheus.Exemplar{
Value: float64(exemplar.Value), Value: float64(exemplar.Value),
Timestamp: exemplar.Time, Timestamp: exemplar.Time,
@@ -674,11 +658,10 @@ func addExemplars[N int64 | float64](m prometheus.Metric, exemplars []metricdata
return metricWithExemplar return metricWithExemplar
} }
func attributesToLabels(attrs []attribute.KeyValue) prometheus.Labels { func attributesToLabels(attrs []attribute.KeyValue, labelNamer otlptranslator.LabelNamer) prometheus.Labels {
labels := make(map[string]string) labels := make(map[string]string)
for _, attr := range attrs { for _, attr := range attrs {
key := model.EscapeName(string(attr.Key), model.NameEscapingScheme) labels[labelNamer.Build(string(attr.Key))] = attr.Value.Emit()
labels[key] = attr.Value.Emit()
} }
return labels return labels
} }

View File

@@ -16,6 +16,7 @@ import (
"github.com/prometheus/client_golang/prometheus/testutil" "github.com/prometheus/client_golang/prometheus/testutil"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
"github.com/prometheus/otlptranslator"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -43,6 +44,7 @@ func TestPrometheusExporter(t *testing.T) {
{ {
name: "counter", name: "counter",
expectedFile: "testdata/counter.txt", expectedFile: "testdata/counter.txt",
disableUTF8: true,
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) { recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
opt := otelmetric.WithAttributes( opt := otelmetric.WithAttributes(
attribute.Key("A").String("B"), attribute.Key("A").String("B"),
@@ -71,7 +73,8 @@ func TestPrometheusExporter(t *testing.T) {
}, },
{ {
name: "counter that already has the unit suffix", name: "counter that already has the unit suffix",
expectedFile: "testdata/counter_with_unit_suffix.txt", expectedFile: "testdata/counter_noutf8_with_unit_suffix.txt",
disableUTF8: true,
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) { recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
opt := otelmetric.WithAttributes( opt := otelmetric.WithAttributes(
attribute.Key("A").String("B"), attribute.Key("A").String("B"),
@@ -127,9 +130,39 @@ func TestPrometheusExporter(t *testing.T) {
counter.Add(ctx, 5, otelmetric.WithAttributeSet(attrs2)) counter.Add(ctx, 5, otelmetric.WithAttributeSet(attrs2))
}, },
}, },
{
name: "counter with bracketed unit",
expectedFile: "testdata/counter_no_unit.txt",
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
opt := otelmetric.WithAttributes(
attribute.Key("A").String("B"),
attribute.Key("C").String("D"),
attribute.Key("E").Bool(true),
attribute.Key("F").Int(42),
)
counter, err := meter.Float64Counter(
"foo",
otelmetric.WithDescription("a simple counter"),
otelmetric.WithUnit("{spans}"),
)
require.NoError(t, err)
counter.Add(ctx, 5, opt)
counter.Add(ctx, 10.3, opt)
counter.Add(ctx, 9, opt)
attrs2 := attribute.NewSet(
attribute.Key("A").String("D"),
attribute.Key("C").String("B"),
attribute.Key("E").Bool(true),
attribute.Key("F").Int(42),
)
counter.Add(ctx, 5, otelmetric.WithAttributeSet(attrs2))
},
},
{ {
name: "counter that already has a total suffix", name: "counter that already has a total suffix",
expectedFile: "testdata/counter.txt", expectedFile: "testdata/counter.txt",
disableUTF8: true,
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) { recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
opt := otelmetric.WithAttributes( opt := otelmetric.WithAttributes(
attribute.Key("A").String("B"), attribute.Key("A").String("B"),
@@ -194,14 +227,13 @@ func TestPrometheusExporter(t *testing.T) {
attribute.Key("A").String("B"), attribute.Key("A").String("B"),
attribute.Key("C").String("D"), attribute.Key("C").String("D"),
) )
gauge, err := meter.Float64UpDownCounter( gauge, err := meter.Float64Gauge(
"bar", "bar",
otelmetric.WithDescription("a fun little gauge"), otelmetric.WithDescription("a fun little gauge"),
otelmetric.WithUnit("1"), otelmetric.WithUnit("1"),
) )
require.NoError(t, err) require.NoError(t, err)
gauge.Add(ctx, 1.0, opt) gauge.Record(ctx, .75, opt)
gauge.Add(ctx, -.25, opt)
}, },
}, },
{ {
@@ -398,14 +430,13 @@ func TestPrometheusExporter(t *testing.T) {
attribute.Key("A").String("B"), attribute.Key("A").String("B"),
attribute.Key("C").String("D"), attribute.Key("C").String("D"),
) )
gauge, err := meter.Int64UpDownCounter( gauge, err := meter.Int64Gauge(
"bar", "bar",
otelmetric.WithDescription("a fun little gauge"), otelmetric.WithDescription("a fun little gauge"),
otelmetric.WithUnit("1"), otelmetric.WithUnit("1"),
) )
require.NoError(t, err) require.NoError(t, err)
gauge.Add(ctx, 2, opt) gauge.Record(ctx, 1, opt)
gauge.Add(ctx, -1, opt)
}, },
}, },
{ {
@@ -1011,16 +1042,16 @@ func TestExemplars(t *testing.T) {
attribute.Key("F.4").Int(42), attribute.Key("F.4").Int(42),
) )
expectedNonEscapedLabels := map[string]string{ expectedNonEscapedLabels := map[string]string{
traceIDExemplarKey: "01000000000000000000000000000000", otlptranslator.ExemplarTraceIDKey: "01000000000000000000000000000000",
spanIDExemplarKey: "0100000000000000", otlptranslator.ExemplarSpanIDKey: "0100000000000000",
"A.1": "B", "A.1": "B",
"C.2": "D", "C.2": "D",
"E.3": "true", "E.3": "true",
"F.4": "42", "F.4": "42",
} }
expectedEscapedLabels := map[string]string{ expectedEscapedLabels := map[string]string{
traceIDExemplarKey: "01000000000000000000000000000000", otlptranslator.ExemplarTraceIDKey: "01000000000000000000000000000000",
spanIDExemplarKey: "0100000000000000", otlptranslator.ExemplarSpanIDKey: "0100000000000000",
"A_1": "B", "A_1": "B",
"C_2": "D", "C_2": "D",
"E_3": "true", "E_3": "true",
@@ -1241,7 +1272,14 @@ func TestExponentialHistogramScaleValidation(t *testing.T) {
Description: "test", Description: "test",
} }
addExponentialHistogramMetric(ch, histogram, m, "test_histogram", keyVals{}) addExponentialHistogramMetric(
ch,
histogram,
m,
"test_histogram",
keyVals{},
otlptranslator.LabelNamer{},
)
assert.Error(t, capturedError) assert.Error(t, capturedError)
assert.Contains(t, capturedError.Error(), "scale -5 is below minimum") assert.Contains(t, capturedError.Error(), "scale -5 is below minimum")
select { select {
@@ -1398,7 +1436,14 @@ func TestExponentialHistogramHighScaleDownscaling(t *testing.T) {
} }
// This should not produce any errors and should properly downscale buckets // This should not produce any errors and should properly downscale buckets
addExponentialHistogramMetric(ch, histogram, m, "test_high_scale_histogram", keyVals{}) addExponentialHistogramMetric(
ch,
histogram,
m,
"test_high_scale_histogram",
keyVals{},
otlptranslator.LabelNamer{},
)
// Verify a metric was produced // Verify a metric was produced
select { select {
@@ -1453,7 +1498,14 @@ func TestExponentialHistogramHighScaleDownscaling(t *testing.T) {
} }
// This should not produce any errors and should properly downscale buckets // This should not produce any errors and should properly downscale buckets
addExponentialHistogramMetric(ch, histogram, m, "test_very_high_scale_histogram", keyVals{}) addExponentialHistogramMetric(
ch,
histogram,
m,
"test_very_high_scale_histogram",
keyVals{},
otlptranslator.LabelNamer{},
)
// Verify a metric was produced // Verify a metric was produced
select { select {
@@ -1508,7 +1560,14 @@ func TestExponentialHistogramHighScaleDownscaling(t *testing.T) {
} }
// This should handle negative buckets correctly // This should handle negative buckets correctly
addExponentialHistogramMetric(ch, histogram, m, "test_histogram_with_negative_buckets", keyVals{}) addExponentialHistogramMetric(
ch,
histogram,
m,
"test_histogram_with_negative_buckets",
keyVals{},
otlptranslator.LabelNamer{},
)
// Verify a metric was produced // Verify a metric was produced
select { select {
@@ -1557,7 +1616,14 @@ func TestExponentialHistogramHighScaleDownscaling(t *testing.T) {
} }
// This should handle int64 exponential histograms correctly // This should handle int64 exponential histograms correctly
addExponentialHistogramMetric(ch, histogram, m, "test_int64_exponential_histogram", keyVals{}) addExponentialHistogramMetric(
ch,
histogram,
m,
"test_int64_exponential_histogram",
keyVals{},
otlptranslator.LabelNamer{},
)
// Verify a metric was produced // Verify a metric was produced
select { select {

View File

@@ -10,6 +10,7 @@ require (
github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_golang v1.22.0
github.com/prometheus/client_model v0.6.2 github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.65.0 github.com/prometheus/common v0.65.0
github.com/prometheus/otlptranslator v0.0.0-20250717125610-8549f4ab4f8f
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
go.opentelemetry.io/otel v1.37.0 go.opentelemetry.io/otel v1.37.0
go.opentelemetry.io/otel/metric v1.37.0 go.opentelemetry.io/otel/metric v1.37.0
@@ -26,6 +27,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/kylelemons/godebug v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect

View File

@@ -13,6 +13,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -29,6 +31,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/otlptranslator v0.0.0-20250717125610-8549f4ab4f8f h1:QQB6SuvGZjK8kdc2YaLJpYhV8fxauOsjE6jgcL6YJ8Q=
github.com/prometheus/otlptranslator v0.0.0-20250717125610-8549f4ab4f8f/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=

View File

@@ -4,4 +4,4 @@ foo_seconds_total{A="B",C="D",E="true",F="42",otel_scope_fizz="buzz",otel_scope_
foo_seconds_total{A="D",C="B",E="true",F="42",otel_scope_fizz="buzz",otel_scope_name="testmeter",otel_scope_schema_url="",otel_scope_version="v0.1.0"} 5 foo_seconds_total{A="D",C="B",E="true",F="42",otel_scope_fizz="buzz",otel_scope_name="testmeter",otel_scope_schema_url="",otel_scope_version="v0.1.0"} 5
# HELP target_info Target metadata # HELP target_info Target metadata
# TYPE target_info gauge # TYPE target_info gauge
target_info{"service.name"="prometheus_test","telemetry.sdk.language"="go","telemetry.sdk.name"="opentelemetry","telemetry.sdk.version"="latest"} 1 target_info{service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 1

View File

@@ -0,0 +1,7 @@
# HELP foo_total a simple counter
# TYPE foo_total counter
foo_total{A="B",C="D",E="true",F="42",otel_scope_fizz="buzz",otel_scope_name="testmeter",otel_scope_schema_url="",otel_scope_version="v0.1.0"} 24.3
foo_total{A="D",C="B",E="true",F="42",otel_scope_fizz="buzz",otel_scope_name="testmeter",otel_scope_schema_url="",otel_scope_version="v0.1.0"} 5
# HELP target_info Target metadata
# TYPE target_info gauge
target_info{"service.name"="prometheus_test","telemetry.sdk.language"="go","telemetry.sdk.name"="opentelemetry","telemetry.sdk.version"="latest"} 1

View File

@@ -0,0 +1,7 @@
# HELP "foo_seconds_total" a simple counter
# TYPE "foo_seconds_total" counter
{"foo_seconds_total",A="B",C="D",E="true",F="42",otel_scope_fizz="buzz",otel_scope_name="testmeter",otel_scope_schema_url="",otel_scope_version="v0.1.0"} 24.3
{"foo_seconds_total",A="D",C="B",E="true",F="42",otel_scope_fizz="buzz",otel_scope_name="testmeter",otel_scope_schema_url="",otel_scope_version="v0.1.0"} 5
# HELP target_info Target metadata
# TYPE target_info gauge
target_info{service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 1