You've already forked opentelemetry-go
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:
@@ -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 -->
|
||||
|
@@ -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
|
||||
})
|
||||
|
@@ -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/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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=
|
||||
|
2
exporters/prometheus/testdata/counter.txt
vendored
2
exporters/prometheus/testdata/counter.txt
vendored
@@ -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
|
||||
|
7
exporters/prometheus/testdata/counter_no_unit.txt
vendored
Executable file
7
exporters/prometheus/testdata/counter_no_unit.txt
vendored
Executable 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
|
7
exporters/prometheus/testdata/counter_noutf8_with_unit_suffix.txt
vendored
Executable file
7
exporters/prometheus/testdata/counter_noutf8_with_unit_suffix.txt
vendored
Executable 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
|
Reference in New Issue
Block a user