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 `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 -->
<!-- Don't change this section unless doing release -->

View File

@@ -4,11 +4,9 @@
package prometheus // import "go.opentelemetry.io/otel/exporters/prometheus"
import (
"strings"
"sync"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/internal/global"
@@ -139,17 +137,6 @@ func WithoutScopeInfo() Option {
// have special behavior based on their name.
func WithNamespace(ns string) Option {
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
return cfg
})

View File

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

View File

@@ -16,6 +16,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/model"
"github.com/prometheus/otlptranslator"
"google.golang.org/protobuf/proto"
"go.opentelemetry.io/otel"
@@ -27,16 +28,12 @@ import (
)
const (
targetInfoMetricName = "target_info"
targetInfoDescription = "Target metadata"
scopeLabelPrefix = "otel_scope_"
scopeNameLabel = scopeLabelPrefix + "name"
scopeVersionLabel = scopeLabelPrefix + "version"
scopeSchemaLabel = scopeLabelPrefix + "schema_url"
traceIDExemplarKey = "trace_id"
spanIDExemplarKey = "span_id"
)
var metricsPool = sync.Pool{
@@ -93,12 +90,11 @@ type collector struct {
targetInfo prometheus.Metric
metricFamilies map[string]*dto.MetricFamily
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.
func New(opts ...Option) (*Exporter, error) {
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.
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{
reader: reader,
disableTargetInfo: cfg.disableTargetInfo,
@@ -115,8 +117,18 @@ func New(opts ...Option) (*Exporter, error) {
withoutCounterSuffixes: cfg.withoutCounterSuffixes,
disableScopeInfo: cfg.disableScopeInfo,
metricFamilies: make(map[string]*dto.MetricFamily),
namespace: cfg.namespace,
namespace: labelNamer.Build(cfg.namespace),
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 {
@@ -164,7 +176,11 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
defer c.mu.Unlock()
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 the target info metric is invalid, disable sending it.
c.disableTargetInfo = true
@@ -195,7 +211,7 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
kv.keys = append(kv.keys, scopeNameLabel, scopeVersionLabel, scopeSchemaLabel)
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 {
attrKeys[i] = scopeLabelPrefix + attrKeys[i]
}
@@ -211,7 +227,7 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
if typ == nil {
continue
}
name := c.getName(m, typ)
name := c.getName(m)
drop, help := c.validateMetrics(name, m.Description, typ)
if drop {
@@ -224,21 +240,21 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
switch v := m.Data.(type) {
case metricdata.Histogram[int64]:
addHistogramMetric(ch, v, m, name, kv)
addHistogramMetric(ch, v, m, name, kv, c.labelNamer)
case metricdata.Histogram[float64]:
addHistogramMetric(ch, v, m, name, kv)
addHistogramMetric(ch, v, m, name, kv, c.labelNamer)
case metricdata.ExponentialHistogram[int64]:
addExponentialHistogramMetric(ch, v, m, name, kv)
addExponentialHistogramMetric(ch, v, m, name, kv, c.labelNamer)
case metricdata.ExponentialHistogram[float64]:
addExponentialHistogramMetric(ch, v, m, name, kv)
addExponentialHistogramMetric(ch, v, m, name, kv, c.labelNamer)
case metricdata.Sum[int64]:
addSumMetric(ch, v, m, name, kv)
addSumMetric(ch, v, m, name, kv, c.labelNamer)
case metricdata.Sum[float64]:
addSumMetric(ch, v, m, name, kv)
addSumMetric(ch, v, m, name, kv, c.labelNamer)
case metricdata.Gauge[int64]:
addGaugeMetric(ch, v, m, name, kv)
addGaugeMetric(ch, v, m, name, kv, c.labelNamer)
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,
name string,
kv keyVals,
labelNamer otlptranslator.LabelNamer,
) {
for _, dp := range histogram.DataPoints {
keys, values := getAttrs(dp.Attributes)
keys, values := getAttrs(dp.Attributes, labelNamer)
keys = append(keys, kv.keys...)
values = append(values, kv.vals...)
@@ -377,9 +394,10 @@ func addHistogramMetric[N int64 | float64](
m metricdata.Metrics,
name string,
kv keyVals,
labelNamer otlptranslator.LabelNamer,
) {
for _, dp := range histogram.DataPoints {
keys, values := getAttrs(dp.Attributes)
keys, values := getAttrs(dp.Attributes, labelNamer)
keys = append(keys, kv.keys...)
values = append(values, kv.vals...)
@@ -396,7 +414,7 @@ func addHistogramMetric[N int64 | float64](
otel.Handle(err)
continue
}
m = addExemplars(m, dp.Exemplars)
m = addExemplars(m, dp.Exemplars, labelNamer)
ch <- m
}
}
@@ -407,6 +425,7 @@ func addSumMetric[N int64 | float64](
m metricdata.Metrics,
name string,
kv keyVals,
labelNamer otlptranslator.LabelNamer,
) {
valueType := prometheus.CounterValue
if !sum.IsMonotonic {
@@ -414,7 +433,7 @@ func addSumMetric[N int64 | float64](
}
for _, dp := range sum.DataPoints {
keys, values := getAttrs(dp.Attributes)
keys, values := getAttrs(dp.Attributes, labelNamer)
keys = append(keys, kv.keys...)
values = append(values, kv.vals...)
@@ -427,7 +446,7 @@ func addSumMetric[N int64 | float64](
// GaugeValues don't support Exemplars at this time
// https://github.com/prometheus/client_golang/blob/aef8aedb4b6e1fb8ac1c90790645169125594096/prometheus/metric.go#L199
if valueType != prometheus.GaugeValue {
m = addExemplars(m, dp.Exemplars)
m = addExemplars(m, dp.Exemplars, labelNamer)
}
ch <- m
}
@@ -439,9 +458,10 @@ func addGaugeMetric[N int64 | float64](
m metricdata.Metrics,
name string,
kv keyVals,
labelNamer otlptranslator.LabelNamer,
) {
for _, dp := range gauge.DataPoints {
keys, values := getAttrs(dp.Attributes)
keys, values := getAttrs(dp.Attributes, labelNamer)
keys = append(keys, kv.keys...)
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
// 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())
values := make([]string, 0, attrs.Len())
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.
for itr.Next() {
kv := itr.Attribute()
@@ -475,7 +495,7 @@ func getAttrs(attrs attribute.Set) ([]string, []string) {
keysMap := make(map[string][]string)
for itr.Next() {
kv := itr.Attribute()
key := model.EscapeName(string(kv.Key), model.NameEscapingScheme)
key := labelNamer.Build(string(kv.Key))
if _, ok := keysMap[key]; !ok {
keysMap[key] = []string{kv.Value.Emit()}
} else {
@@ -492,91 +512,22 @@ func getAttrs(attrs attribute.Set) ([]string, []string) {
return keys, values
}
func createInfoMetric(name, description string, res *resource.Resource) (prometheus.Metric, error) {
keys, values := getAttrs(*res.Set())
func (c *collector) createInfoMetric(name, description string, res *resource.Resource) (prometheus.Metric, error) {
keys, values := getAttrs(*res.Set(), c.labelNamer)
desc := prometheus.NewDesc(name, description, keys, nil)
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.
func (c *collector) getName(m metricdata.Metrics, typ *dto.MetricType) string {
name := m.Name
if model.NameValidationScheme != model.UTF8Validation { // nolint:staticcheck // We need this check to keep supporting the legacy scheme.
// Only sanitize if prometheus does not support UTF-8.
logDeprecatedLegacyScheme()
name = model.EscapeName(name, model.NameEscapingScheme)
func (c *collector) getName(m metricdata.Metrics) string {
translatorMetric := otlptranslator.Metric{
Name: m.Name,
Type: c.namingMetricType(m),
}
addCounterSuffix := !c.withoutCounterSuffixes && *typ == dto.MetricType_COUNTER
if addCounterSuffix {
// 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]
if !c.withoutUnits {
translatorMetric.Unit = m.Unit
}
}
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')
return c.metricNamer.Build(translatorMetric)
}
func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType {
@@ -601,12 +552,41 @@ func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType {
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) {
c.mu.Lock()
defer c.mu.Unlock()
resourceAttrs, _ := res.Set().Filter(c.resourceAttributesFilter)
resourceKeys, resourceValues := getAttrs(resourceAttrs)
resourceKeys, resourceValues := getAttrs(resourceAttrs, c.labelNamer)
c.resourceKeyVals = keyVals{keys: resourceKeys, vals: resourceValues}
}
@@ -648,16 +628,20 @@ func (c *collector) validateMetrics(name, description string, metricType *dto.Me
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 {
return m
}
promExemplars := make([]prometheus.Exemplar, len(exemplars))
for i, exemplar := range exemplars {
labels := attributesToLabels(exemplar.FilteredAttributes)
labels := attributesToLabels(exemplar.FilteredAttributes, labelNamer)
// Overwrite any existing trace ID or span ID attributes
labels[traceIDExemplarKey] = hex.EncodeToString(exemplar.TraceID[:])
labels[spanIDExemplarKey] = hex.EncodeToString(exemplar.SpanID[:])
labels[otlptranslator.ExemplarTraceIDKey] = hex.EncodeToString(exemplar.TraceID[:])
labels[otlptranslator.ExemplarSpanIDKey] = hex.EncodeToString(exemplar.SpanID[:])
promExemplars[i] = prometheus.Exemplar{
Value: float64(exemplar.Value),
Timestamp: exemplar.Time,
@@ -674,11 +658,10 @@ func addExemplars[N int64 | float64](m prometheus.Metric, exemplars []metricdata
return metricWithExemplar
}
func attributesToLabels(attrs []attribute.KeyValue) prometheus.Labels {
func attributesToLabels(attrs []attribute.KeyValue, labelNamer otlptranslator.LabelNamer) prometheus.Labels {
labels := make(map[string]string)
for _, attr := range attrs {
key := model.EscapeName(string(attr.Key), model.NameEscapingScheme)
labels[key] = attr.Value.Emit()
labels[labelNamer.Build(string(attr.Key))] = attr.Value.Emit()
}
return labels
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/prometheus/client_golang/prometheus/testutil"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/model"
"github.com/prometheus/otlptranslator"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -43,6 +44,7 @@ func TestPrometheusExporter(t *testing.T) {
{
name: "counter",
expectedFile: "testdata/counter.txt",
disableUTF8: true,
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
opt := otelmetric.WithAttributes(
attribute.Key("A").String("B"),
@@ -71,7 +73,8 @@ func TestPrometheusExporter(t *testing.T) {
},
{
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) {
opt := otelmetric.WithAttributes(
attribute.Key("A").String("B"),
@@ -127,9 +130,39 @@ func TestPrometheusExporter(t *testing.T) {
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",
expectedFile: "testdata/counter.txt",
disableUTF8: true,
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
opt := otelmetric.WithAttributes(
attribute.Key("A").String("B"),
@@ -194,14 +227,13 @@ func TestPrometheusExporter(t *testing.T) {
attribute.Key("A").String("B"),
attribute.Key("C").String("D"),
)
gauge, err := meter.Float64UpDownCounter(
gauge, err := meter.Float64Gauge(
"bar",
otelmetric.WithDescription("a fun little gauge"),
otelmetric.WithUnit("1"),
)
require.NoError(t, err)
gauge.Add(ctx, 1.0, opt)
gauge.Add(ctx, -.25, opt)
gauge.Record(ctx, .75, opt)
},
},
{
@@ -398,14 +430,13 @@ func TestPrometheusExporter(t *testing.T) {
attribute.Key("A").String("B"),
attribute.Key("C").String("D"),
)
gauge, err := meter.Int64UpDownCounter(
gauge, err := meter.Int64Gauge(
"bar",
otelmetric.WithDescription("a fun little gauge"),
otelmetric.WithUnit("1"),
)
require.NoError(t, err)
gauge.Add(ctx, 2, opt)
gauge.Add(ctx, -1, opt)
gauge.Record(ctx, 1, opt)
},
},
{
@@ -1011,16 +1042,16 @@ func TestExemplars(t *testing.T) {
attribute.Key("F.4").Int(42),
)
expectedNonEscapedLabels := map[string]string{
traceIDExemplarKey: "01000000000000000000000000000000",
spanIDExemplarKey: "0100000000000000",
otlptranslator.ExemplarTraceIDKey: "01000000000000000000000000000000",
otlptranslator.ExemplarSpanIDKey: "0100000000000000",
"A.1": "B",
"C.2": "D",
"E.3": "true",
"F.4": "42",
}
expectedEscapedLabels := map[string]string{
traceIDExemplarKey: "01000000000000000000000000000000",
spanIDExemplarKey: "0100000000000000",
otlptranslator.ExemplarTraceIDKey: "01000000000000000000000000000000",
otlptranslator.ExemplarSpanIDKey: "0100000000000000",
"A_1": "B",
"C_2": "D",
"E_3": "true",
@@ -1241,7 +1272,14 @@ func TestExponentialHistogramScaleValidation(t *testing.T) {
Description: "test",
}
addExponentialHistogramMetric(ch, histogram, m, "test_histogram", keyVals{})
addExponentialHistogramMetric(
ch,
histogram,
m,
"test_histogram",
keyVals{},
otlptranslator.LabelNamer{},
)
assert.Error(t, capturedError)
assert.Contains(t, capturedError.Error(), "scale -5 is below minimum")
select {
@@ -1398,7 +1436,14 @@ func TestExponentialHistogramHighScaleDownscaling(t *testing.T) {
}
// 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
select {
@@ -1453,7 +1498,14 @@ func TestExponentialHistogramHighScaleDownscaling(t *testing.T) {
}
// 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
select {
@@ -1508,7 +1560,14 @@ func TestExponentialHistogramHighScaleDownscaling(t *testing.T) {
}
// 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
select {
@@ -1557,7 +1616,14 @@ func TestExponentialHistogramHighScaleDownscaling(t *testing.T) {
}
// 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
select {

View File

@@ -10,6 +10,7 @@ require (
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.65.0
github.com/prometheus/otlptranslator v0.0.0-20250717125610-8549f4ab4f8f
github.com/stretchr/testify v1.10.0
go.opentelemetry.io/otel 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/stdr v1.2.2 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
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/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
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/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
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
# 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
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