diff --git a/CHANGELOG.md b/CHANGELOG.md index 609a966f634..bf3c7adceb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/exporters/prometheus/config.go b/exporters/prometheus/config.go index 52183884029..4757b793d31 100644 --- a/exporters/prometheus/config.go +++ b/exporters/prometheus/config.go @@ -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 }) diff --git a/exporters/prometheus/config_test.go b/exporters/prometheus/config_test.go index c24ccd72e6d..68fae27cbd1 100644 --- a/exporters/prometheus/config_test.go +++ b/exporters/prometheus/config_test.go @@ -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/", }, }, } diff --git a/exporters/prometheus/exporter.go b/exporters/prometheus/exporter.go index 7b44c12c541..9f3e5414ac2 100644 --- a/exporters/prometheus/exporter.go +++ b/exporters/prometheus/exporter.go @@ -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) - } - 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.namespace != "" { - name = c.namespace + name +func (c *collector) getName(m metricdata.Metrics) string { + translatorMetric := otlptranslator.Metric{ + Name: m.Name, + Type: c.namingMetricType(m), } - if suffix := unitMapGetOrDefault(m.Unit); suffix != "" && !c.withoutUnits && !strings.HasSuffix(name, suffix) { - name += "_" + suffix + if !c.withoutUnits { + translatorMetric.Unit = m.Unit } - 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 } diff --git a/exporters/prometheus/exporter_test.go b/exporters/prometheus/exporter_test.go index be993fd892f..3dbfe94cdf8 100644 --- a/exporters/prometheus/exporter_test.go +++ b/exporters/prometheus/exporter_test.go @@ -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,20 +1042,20 @@ func TestExemplars(t *testing.T) { attribute.Key("F.4").Int(42), ) expectedNonEscapedLabels := map[string]string{ - traceIDExemplarKey: "01000000000000000000000000000000", - spanIDExemplarKey: "0100000000000000", - "A.1": "B", - "C.2": "D", - "E.3": "true", - "F.4": "42", + 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", - "A_1": "B", - "C_2": "D", - "E_3": "true", - "F_4": "42", + otlptranslator.ExemplarTraceIDKey: "01000000000000000000000000000000", + otlptranslator.ExemplarSpanIDKey: "0100000000000000", + "A_1": "B", + "C_2": "D", + "E_3": "true", + "F_4": "42", } for _, tc := range []struct { name string @@ -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 { diff --git a/exporters/prometheus/go.mod b/exporters/prometheus/go.mod index 8840b943976..b8366662d61 100644 --- a/exporters/prometheus/go.mod +++ b/exporters/prometheus/go.mod @@ -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 diff --git a/exporters/prometheus/go.sum b/exporters/prometheus/go.sum index bbf0e7e5fd7..9e01dcfa961 100644 --- a/exporters/prometheus/go.sum +++ b/exporters/prometheus/go.sum @@ -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= diff --git a/exporters/prometheus/testdata/counter.txt b/exporters/prometheus/testdata/counter.txt index 87893ad2ae5..878231f4bdd 100755 --- a/exporters/prometheus/testdata/counter.txt +++ b/exporters/prometheus/testdata/counter.txt @@ -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 diff --git a/exporters/prometheus/testdata/counter_no_unit.txt b/exporters/prometheus/testdata/counter_no_unit.txt new file mode 100755 index 00000000000..0d5bc97c064 --- /dev/null +++ b/exporters/prometheus/testdata/counter_no_unit.txt @@ -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 diff --git a/exporters/prometheus/testdata/counter_noutf8_with_unit_suffix.txt b/exporters/prometheus/testdata/counter_noutf8_with_unit_suffix.txt new file mode 100755 index 00000000000..1e1daab96c4 --- /dev/null +++ b/exporters/prometheus/testdata/counter_noutf8_with_unit_suffix.txt @@ -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