Skip to content

Commit 510910e

Browse files
dashpoleMrAlias
andauthored
Add unit suffixes to prometheus metric names (#3352)
* add unit suffixes to prometheus metric names * Update CHANGELOG.md Co-authored-by: Tyler Yahn <[email protected]> * remove unneccessary variable Co-authored-by: Tyler Yahn <[email protected]>
1 parent 1d9d4b2 commit 510910e

File tree

8 files changed

+111
-51
lines changed

8 files changed

+111
-51
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2121
- The `"go.opentelemetry.io/otel/exporters/prometheus".New` now also returns an error indicating the failure to register the exporter with Prometheus. (#3239)
2222
- The prometheus exporter will no longer try to enumerate the metrics it will send to prometheus on startup.
2323
This fixes the `reader is not registered` warning currently emitted on startup. (#3291 #3342)
24-
- The `go.opentelemetry.io/otel/exporters/prometheus` exporter now correctly adds _total suffixes to counter metrics. (#3360)
24+
- The `go.opentelemetry.io/otel/exporters/prometheus` exporter now adds a unit suffix to metric names.
25+
This can be disabled with the `WithoutUnits()` option. (#3352)
2526

2627
### Fixed
2728

exporters/prometheus/confg_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,20 @@ func TestNewConfig(t *testing.T) {
8989
disableTargetInfo: true,
9090
},
9191
},
92+
{
93+
name: "unit suffixes disabled",
94+
options: []Option{
95+
WithoutUnits(),
96+
},
97+
wantConfig: config{
98+
registerer: prometheus.DefaultRegisterer,
99+
withoutUnits: true,
100+
},
101+
},
92102
}
93103
for _, tt := range testCases {
94104
t.Run(tt.name, func(t *testing.T) {
95105
cfg := newConfig(tt.options...)
96-
97106
// tested by TestConfigManualReaderOptions
98107
cfg.aggregation = nil
99108

exporters/prometheus/config.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
type config struct {
2525
registerer prometheus.Registerer
2626
disableTargetInfo bool
27+
withoutUnits bool
2728
aggregation metric.AggregationSelector
2829
}
2930

@@ -89,3 +90,18 @@ func WithoutTargetInfo() Option {
8990
return cfg
9091
})
9192
}
93+
94+
// WithoutUnits disables exporter's addition of unit suffixes to metric names,
95+
// and will also prevent unit comments from being added in OpenMetrics once
96+
// unit comments are supported.
97+
//
98+
// By default, metric names include a unit suffix to follow Prometheus naming
99+
// conventions. For example, the counter metric request.duration, with unit
100+
// milliseconds would become request_duration_milliseconds_total.
101+
// With this option set, the name would instead be request_duration_total.
102+
func WithoutUnits() Option {
103+
return optionFunc(func(cfg config) config {
104+
cfg.withoutUnits = true
105+
return cfg
106+
})
107+
}

exporters/prometheus/exporter.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
"go.opentelemetry.io/otel"
2929
"go.opentelemetry.io/otel/attribute"
30+
"go.opentelemetry.io/otel/metric/unit"
3031
"go.opentelemetry.io/otel/sdk/metric"
3132
"go.opentelemetry.io/otel/sdk/metric/metricdata"
3233
"go.opentelemetry.io/otel/sdk/resource"
@@ -50,6 +51,7 @@ type collector struct {
5051
reader metric.Reader
5152

5253
disableTargetInfo bool
54+
withoutUnits bool
5355
targetInfo *metricData
5456
createTargetInfoOnce sync.Once
5557
}
@@ -70,6 +72,7 @@ func New(opts ...Option) (*Exporter, error) {
7072
collector := &collector{
7173
reader: reader,
7274
disableTargetInfo: cfg.disableTargetInfo,
75+
withoutUnits: cfg.withoutUnits,
7376
}
7477

7578
if err := cfg.registerer.Register(collector); err != nil {
@@ -151,28 +154,28 @@ func (c *collector) getMetricData(metrics metricdata.ResourceMetrics) []*metricD
151154
for _, m := range scopeMetrics.Metrics {
152155
switch v := m.Data.(type) {
153156
case metricdata.Histogram:
154-
allMetrics = append(allMetrics, getHistogramMetricData(v, m)...)
157+
allMetrics = append(allMetrics, getHistogramMetricData(v, m, c.getName(m))...)
155158
case metricdata.Sum[int64]:
156-
allMetrics = append(allMetrics, getSumMetricData(v, m)...)
159+
allMetrics = append(allMetrics, getSumMetricData(v, m, c.getName(m))...)
157160
case metricdata.Sum[float64]:
158-
allMetrics = append(allMetrics, getSumMetricData(v, m)...)
161+
allMetrics = append(allMetrics, getSumMetricData(v, m, c.getName(m))...)
159162
case metricdata.Gauge[int64]:
160-
allMetrics = append(allMetrics, getGaugeMetricData(v, m)...)
163+
allMetrics = append(allMetrics, getGaugeMetricData(v, m, c.getName(m))...)
161164
case metricdata.Gauge[float64]:
162-
allMetrics = append(allMetrics, getGaugeMetricData(v, m)...)
165+
allMetrics = append(allMetrics, getGaugeMetricData(v, m, c.getName(m))...)
163166
}
164167
}
165168
}
166169

167170
return allMetrics
168171
}
169172

170-
func getHistogramMetricData(histogram metricdata.Histogram, m metricdata.Metrics) []*metricData {
173+
func getHistogramMetricData(histogram metricdata.Histogram, m metricdata.Metrics, name string) []*metricData {
171174
// TODO(https://github.com/open-telemetry/opentelemetry-go/issues/3163): support exemplars
172175
dataPoints := make([]*metricData, 0, len(histogram.DataPoints))
173176
for _, dp := range histogram.DataPoints {
174177
keys, values := getAttrs(dp.Attributes)
175-
desc := prometheus.NewDesc(sanitizeName(m.Name), m.Description, keys, nil)
178+
desc := prometheus.NewDesc(name, m.Description, keys, nil)
176179
buckets := make(map[float64]uint64, len(dp.Bounds))
177180

178181
cumulativeCount := uint64(0)
@@ -194,15 +197,15 @@ func getHistogramMetricData(histogram metricdata.Histogram, m metricdata.Metrics
194197
return dataPoints
195198
}
196199

197-
func getSumMetricData[N int64 | float64](sum metricdata.Sum[N], m metricdata.Metrics) []*metricData {
200+
func getSumMetricData[N int64 | float64](sum metricdata.Sum[N], m metricdata.Metrics, name string) []*metricData {
198201
valueType := prometheus.CounterValue
199202
if !sum.IsMonotonic {
200203
valueType = prometheus.GaugeValue
201204
}
202205
dataPoints := make([]*metricData, 0, len(sum.DataPoints))
203206
for _, dp := range sum.DataPoints {
204-
name := sanitizeName(m.Name)
205207
if sum.IsMonotonic {
208+
// Add _total suffix for counters
206209
name += counterSuffix
207210
}
208211
keys, values := getAttrs(dp.Attributes)
@@ -219,11 +222,11 @@ func getSumMetricData[N int64 | float64](sum metricdata.Sum[N], m metricdata.Met
219222
return dataPoints
220223
}
221224

222-
func getGaugeMetricData[N int64 | float64](gauge metricdata.Gauge[N], m metricdata.Metrics) []*metricData {
225+
func getGaugeMetricData[N int64 | float64](gauge metricdata.Gauge[N], m metricdata.Metrics, name string) []*metricData {
223226
dataPoints := make([]*metricData, 0, len(gauge.DataPoints))
224227
for _, dp := range gauge.DataPoints {
225228
keys, values := getAttrs(dp.Attributes)
226-
desc := prometheus.NewDesc(sanitizeName(m.Name), m.Description, keys, nil)
229+
desc := prometheus.NewDesc(name, m.Description, keys, nil)
227230
md := &metricData{
228231
name: m.Name,
229232
description: desc,
@@ -289,6 +292,24 @@ func sanitizeRune(r rune) rune {
289292
return '_'
290293
}
291294

295+
var unitSuffixes = map[unit.Unit]string{
296+
unit.Dimensionless: "_ratio",
297+
unit.Bytes: "_bytes",
298+
unit.Milliseconds: "_milliseconds",
299+
}
300+
301+
// getName returns the sanitized name, including unit suffix.
302+
func (c *collector) getName(m metricdata.Metrics) string {
303+
name := sanitizeName(m.Name)
304+
if c.withoutUnits {
305+
return name
306+
}
307+
if suffix, ok := unitSuffixes[m.Unit]; ok {
308+
name += suffix
309+
}
310+
return name
311+
}
312+
292313
func sanitizeName(n string) string {
293314
// This algorithm is based on strings.Map from Go 1.19.
294315
const replacement = '_'

exporters/prometheus/exporter_test.go

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"go.opentelemetry.io/otel/attribute"
2727
otelmetric "go.opentelemetry.io/otel/metric"
2828
"go.opentelemetry.io/otel/metric/instrument"
29+
"go.opentelemetry.io/otel/metric/unit"
2930
"go.opentelemetry.io/otel/sdk/metric"
3031
"go.opentelemetry.io/otel/sdk/metric/aggregation"
3132
"go.opentelemetry.io/otel/sdk/metric/view"
@@ -39,7 +40,7 @@ func TestPrometheusExporter(t *testing.T) {
3940
emptyResource bool
4041
customResouceAttrs []attribute.KeyValue
4142
recordMetrics func(ctx context.Context, meter otelmetric.Meter)
42-
withoutTargetInfo bool
43+
options []Option
4344
expectedFile string
4445
}{
4546
{
@@ -52,7 +53,11 @@ func TestPrometheusExporter(t *testing.T) {
5253
attribute.Key("E").Bool(true),
5354
attribute.Key("F").Int(42),
5455
}
55-
counter, err := meter.SyncFloat64().Counter("foo", instrument.WithDescription("a simple counter"))
56+
counter, err := meter.SyncFloat64().Counter(
57+
"foo",
58+
instrument.WithDescription("a simple counter"),
59+
instrument.WithUnit(unit.Milliseconds),
60+
)
5661
require.NoError(t, err)
5762
counter.Add(ctx, 5, attrs...)
5863
counter.Add(ctx, 10.3, attrs...)
@@ -67,10 +72,14 @@ func TestPrometheusExporter(t *testing.T) {
6772
attribute.Key("A").String("B"),
6873
attribute.Key("C").String("D"),
6974
}
70-
gauge, err := meter.SyncFloat64().UpDownCounter("bar", instrument.WithDescription("a fun little gauge"))
75+
gauge, err := meter.SyncFloat64().UpDownCounter(
76+
"bar",
77+
instrument.WithDescription("a fun little gauge"),
78+
instrument.WithUnit(unit.Dimensionless),
79+
)
7180
require.NoError(t, err)
72-
gauge.Add(ctx, 100, attrs...)
73-
gauge.Add(ctx, -25, attrs...)
81+
gauge.Add(ctx, 1.0, attrs...)
82+
gauge.Add(ctx, -.25, attrs...)
7483
},
7584
},
7685
{
@@ -81,7 +90,11 @@ func TestPrometheusExporter(t *testing.T) {
8190
attribute.Key("A").String("B"),
8291
attribute.Key("C").String("D"),
8392
}
84-
histogram, err := meter.SyncFloat64().Histogram("histogram_baz", instrument.WithDescription("a very nice histogram"))
93+
histogram, err := meter.SyncFloat64().Histogram(
94+
"histogram_baz",
95+
instrument.WithDescription("a very nice histogram"),
96+
instrument.WithUnit(unit.Bytes),
97+
)
8598
require.NoError(t, err)
8699
histogram.Record(ctx, 23, attrs...)
87100
histogram.Record(ctx, 7, attrs...)
@@ -92,6 +105,7 @@ func TestPrometheusExporter(t *testing.T) {
92105
{
93106
name: "sanitized attributes to labels",
94107
expectedFile: "testdata/sanitized_labels.txt",
108+
options: []Option{WithoutUnits()},
95109
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
96110
attrs := []attribute.KeyValue{
97111
// exact match, value should be overwritten
@@ -102,7 +116,12 @@ func TestPrometheusExporter(t *testing.T) {
102116
attribute.Key("C.D").String("Y"),
103117
attribute.Key("C/D").String("Z"),
104118
}
105-
counter, err := meter.SyncFloat64().Counter("foo", instrument.WithDescription("a sanitary counter"))
119+
counter, err := meter.SyncFloat64().Counter(
120+
"foo",
121+
instrument.WithDescription("a sanitary counter"),
122+
// This unit is not added to
123+
instrument.WithUnit(unit.Bytes),
124+
)
106125
require.NoError(t, err)
107126
counter.Add(ctx, 5, attrs...)
108127
counter.Add(ctx, 10.3, attrs...)
@@ -177,9 +196,9 @@ func TestPrometheusExporter(t *testing.T) {
177196
},
178197
},
179198
{
180-
name: "without target_info",
181-
withoutTargetInfo: true,
182-
expectedFile: "testdata/without_target_info.txt",
199+
name: "without target_info",
200+
options: []Option{WithoutTargetInfo()},
201+
expectedFile: "testdata/without_target_info.txt",
183202
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
184203
attrs := []attribute.KeyValue{
185204
attribute.Key("A").String("B"),
@@ -200,13 +219,7 @@ func TestPrometheusExporter(t *testing.T) {
200219
t.Run(tc.name, func(t *testing.T) {
201220
ctx := context.Background()
202221
registry := prometheus.NewRegistry()
203-
204-
opts := []Option{WithRegisterer(registry)}
205-
if tc.withoutTargetInfo {
206-
opts = append(opts, WithoutTargetInfo())
207-
}
208-
209-
exporter, err := New(opts...)
222+
exporter, err := New(append(tc.options, WithRegisterer(registry))...)
210223
require.NoError(t, err)
211224

212225
customBucketsView, err := view.New(
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# HELP foo_total a simple counter
2-
# TYPE foo_total counter
3-
foo_total{A="B",C="D",E="true",F="42"} 24.3
1+
# HELP foo_milliseconds_total a simple counter
2+
# TYPE foo_milliseconds_total counter
3+
foo_milliseconds_total{A="B",C="D",E="true",F="42"} 24.3
44
# HELP target_info Target metadata
55
# TYPE target_info gauge
66
target_info{service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 1
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# HELP bar a fun little gauge
2-
# TYPE bar gauge
3-
bar{A="B",C="D"} 75
1+
# HELP bar_ratio a fun little gauge
2+
# TYPE bar_ratio gauge
3+
bar_ratio{A="B",C="D"} .75
44
# HELP target_info Target metadata
55
# TYPE target_info gauge
66
target_info{service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 1
Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
# HELP histogram_baz a very nice histogram
2-
# TYPE histogram_baz histogram
3-
histogram_baz_bucket{A="B",C="D",le="0"} 0
4-
histogram_baz_bucket{A="B",C="D",le="5"} 0
5-
histogram_baz_bucket{A="B",C="D",le="10"} 1
6-
histogram_baz_bucket{A="B",C="D",le="25"} 2
7-
histogram_baz_bucket{A="B",C="D",le="50"} 2
8-
histogram_baz_bucket{A="B",C="D",le="75"} 2
9-
histogram_baz_bucket{A="B",C="D",le="100"} 2
10-
histogram_baz_bucket{A="B",C="D",le="250"} 4
11-
histogram_baz_bucket{A="B",C="D",le="500"} 4
12-
histogram_baz_bucket{A="B",C="D",le="1000"} 4
13-
histogram_baz_bucket{A="B",C="D",le="+Inf"} 4
14-
histogram_baz_sum{A="B",C="D"} 236
15-
histogram_baz_count{A="B",C="D"} 4
1+
# HELP histogram_baz_bytes a very nice histogram
2+
# TYPE histogram_baz_bytes histogram
3+
histogram_baz_bytes_bucket{A="B",C="D",le="0"} 0
4+
histogram_baz_bytes_bucket{A="B",C="D",le="5"} 0
5+
histogram_baz_bytes_bucket{A="B",C="D",le="10"} 1
6+
histogram_baz_bytes_bucket{A="B",C="D",le="25"} 2
7+
histogram_baz_bytes_bucket{A="B",C="D",le="50"} 2
8+
histogram_baz_bytes_bucket{A="B",C="D",le="75"} 2
9+
histogram_baz_bytes_bucket{A="B",C="D",le="100"} 2
10+
histogram_baz_bytes_bucket{A="B",C="D",le="250"} 4
11+
histogram_baz_bytes_bucket{A="B",C="D",le="500"} 4
12+
histogram_baz_bytes_bucket{A="B",C="D",le="1000"} 4
13+
histogram_baz_bytes_bucket{A="B",C="D",le="+Inf"} 4
14+
histogram_baz_bytes_sum{A="B",C="D"} 236
15+
histogram_baz_bytes_count{A="B",C="D"} 4
1616
# HELP target_info Target metadata
1717
# TYPE target_info gauge
1818
target_info{service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 1

0 commit comments

Comments
 (0)