Skip to content

Commit 75973ec

Browse files
GiedriusSXSAM
andauthored
exporters: prometheus: add exponential (native) histogram support (#6421)
Almost closes 5777. Adding native (exponential) histogram support to the Prometheus exporter. I tested it with a toy program, and the result looks good. I added a unit test. --------- Signed-off-by: Giedrius Statkevičius <[email protected]> Co-authored-by: Sam Xie <[email protected]>
1 parent dceb2cd commit 75973ec

File tree

4 files changed

+135
-1
lines changed

4 files changed

+135
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1818
1919
### Added
2020

21+
- Add exponential histogram support in `go.opentelemetry.io/otel/exporters/prometheus`. (#6421)
2122
- The `go.opentelemetry.io/otel/semconv/v1.31.0` package.
2223
The package contains semantic conventions from the `v1.31.0` version of the OpenTelemetry Semantic Conventions.
2324
See the [migration documentation](./semconv/v1.31.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.30.0`(#6479)

exporters/prometheus/exporter.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/hex"
99
"errors"
1010
"fmt"
11+
"math"
1112
"slices"
1213
"strings"
1314
"sync"
@@ -241,6 +242,10 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
241242
addHistogramMetric(ch, v, m, name, kv)
242243
case metricdata.Histogram[float64]:
243244
addHistogramMetric(ch, v, m, name, kv)
245+
case metricdata.ExponentialHistogram[int64]:
246+
addExponentialHistogramMetric(ch, v, m, name, kv)
247+
case metricdata.ExponentialHistogram[float64]:
248+
addExponentialHistogramMetric(ch, v, m, name, kv)
244249
case metricdata.Sum[int64]:
245250
addSumMetric(ch, v, m, name, kv)
246251
case metricdata.Sum[float64]:
@@ -254,6 +259,60 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
254259
}
255260
}
256261

262+
func addExponentialHistogramMetric[N int64 | float64](
263+
ch chan<- prometheus.Metric,
264+
histogram metricdata.ExponentialHistogram[N],
265+
m metricdata.Metrics,
266+
name string,
267+
kv keyVals,
268+
) {
269+
for _, dp := range histogram.DataPoints {
270+
keys, values := getAttrs(dp.Attributes)
271+
keys = append(keys, kv.keys...)
272+
values = append(values, kv.vals...)
273+
274+
desc := prometheus.NewDesc(name, m.Description, keys, nil)
275+
276+
// From spec: note that Prometheus Native Histograms buckets are indexed by upper boundary while Exponential Histograms are indexed by lower boundary, the result being that the Offset fields are different-by-one.
277+
positiveBuckets := make(map[int]int64)
278+
for i, c := range dp.PositiveBucket.Counts {
279+
if c > math.MaxInt64 {
280+
otel.Handle(fmt.Errorf("positive count %d is too large to be represented as int64", c))
281+
continue
282+
}
283+
positiveBuckets[int(dp.PositiveBucket.Offset)+i+1] = int64(c) // nolint: gosec // Size check above.
284+
}
285+
286+
negativeBuckets := make(map[int]int64)
287+
for i, c := range dp.NegativeBucket.Counts {
288+
if c > math.MaxInt64 {
289+
otel.Handle(fmt.Errorf("negative count %d is too large to be represented as int64", c))
290+
continue
291+
}
292+
negativeBuckets[int(dp.NegativeBucket.Offset)+i+1] = int64(c) // nolint: gosec // Size check above.
293+
}
294+
295+
m, err := prometheus.NewConstNativeHistogram(
296+
desc,
297+
dp.Count,
298+
float64(dp.Sum),
299+
positiveBuckets,
300+
negativeBuckets,
301+
dp.ZeroCount,
302+
dp.Scale,
303+
dp.ZeroThreshold,
304+
dp.StartTime,
305+
values...)
306+
if err != nil {
307+
otel.Handle(err)
308+
continue
309+
}
310+
311+
// TODO(GiedriusS): add exemplars here after https://github.com/prometheus/client_golang/pull/1654#pullrequestreview-2434669425 is done.
312+
ch <- m
313+
}
314+
}
315+
257316
func addHistogramMetric[N int64 | float64](
258317
ch chan<- prometheus.Metric,
259318
histogram metricdata.Histogram[N],
@@ -468,6 +527,8 @@ func convertsToUnderscore(b rune) bool {
468527

469528
func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType {
470529
switch v := m.Data.(type) {
530+
case metricdata.ExponentialHistogram[int64], metricdata.ExponentialHistogram[float64]:
531+
return dto.MetricType_HISTOGRAM.Enum()
471532
case metricdata.Histogram[int64], metricdata.Histogram[float64]:
472533
return dto.MetricType_HISTOGRAM.Enum()
473534
case metricdata.Sum[float64]:

exporters/prometheus/exporter_test.go

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func TestPrometheusExporter(t *testing.T) {
3636
options []Option
3737
expectedFile string
3838
disableUTF8 bool
39+
checkMetricFamilies func(t testing.TB, dtos []*dto.MetricFamily)
3940
}{
4041
{
4142
name: "counter",
@@ -172,6 +173,50 @@ func TestPrometheusExporter(t *testing.T) {
172173
gauge.Add(ctx, -.25, opt)
173174
},
174175
},
176+
{
177+
name: "exponential histogram",
178+
expectedFile: "testdata/exponential_histogram.txt",
179+
checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) {
180+
var hist *dto.MetricFamily
181+
182+
for _, mf := range mfs {
183+
if *mf.Name == `exponential_histogram_baz_bytes` {
184+
hist = mf
185+
break
186+
}
187+
}
188+
189+
if hist == nil {
190+
t.Fatal("expected to find histogram")
191+
}
192+
193+
m := hist.GetMetric()[0].Histogram
194+
195+
require.Equal(t, 236.0, *m.SampleSum)
196+
require.Equal(t, uint64(4), *m.SampleCount)
197+
require.Equal(t, []int64{1, -1, 1, -1, 2}, m.PositiveDelta)
198+
require.Equal(t, uint32(5), *m.PositiveSpan[0].Length)
199+
require.Equal(t, int32(3), *m.PositiveSpan[0].Offset)
200+
},
201+
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
202+
// NOTE(GiedriusS): there is no text format for exponential (native)
203+
// histograms so we don't expect any output.
204+
opt := otelmetric.WithAttributes(
205+
attribute.Key("A").String("B"),
206+
attribute.Key("C").String("D"),
207+
)
208+
histogram, err := meter.Float64Histogram(
209+
"exponential_histogram_baz",
210+
otelmetric.WithDescription("a very nice histogram"),
211+
otelmetric.WithUnit("By"),
212+
)
213+
require.NoError(t, err)
214+
histogram.Record(ctx, 23, opt)
215+
histogram.Record(ctx, 7, opt)
216+
histogram.Record(ctx, 101, opt)
217+
histogram.Record(ctx, 105, opt)
218+
},
219+
},
175220
{
176221
name: "histogram",
177222
expectedFile: "testdata/histogram.txt",
@@ -517,7 +562,14 @@ func TestPrometheusExporter(t *testing.T) {
517562
metric.Stream{Aggregation: metric.AggregationExplicitBucketHistogram{
518563
Boundaries: []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 1000},
519564
}},
520-
)),
565+
),
566+
metric.NewView(
567+
metric.Instrument{Name: "exponential_histogram_*"},
568+
metric.Stream{Aggregation: metric.AggregationBase2ExponentialHistogram{
569+
MaxSize: 10,
570+
}},
571+
),
572+
),
521573
)
522574
meter := provider.Meter(
523575
"testmeter",
@@ -533,6 +585,15 @@ func TestPrometheusExporter(t *testing.T) {
533585

534586
err = testutil.GatherAndCompare(registry, file)
535587
require.NoError(t, err)
588+
589+
if tc.checkMetricFamilies == nil {
590+
return
591+
}
592+
593+
mfs, err := registry.Gather()
594+
require.NoError(t, err)
595+
596+
tc.checkMetricFamilies(t, mfs)
536597
})
537598
}
538599
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# HELP exponential_histogram_baz_bytes a very nice histogram
2+
# TYPE exponential_histogram_baz_bytes histogram
3+
exponential_histogram_baz_bytes_bucket{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0",le="+Inf"} 4
4+
exponential_histogram_baz_bytes_sum{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 236
5+
exponential_histogram_baz_bytes_count{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 4
6+
# HELP otel_scope_info Instrumentation Scope metadata
7+
# TYPE otel_scope_info gauge
8+
otel_scope_info{fizz="buzz",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 1
9+
# HELP target_info Target metadata
10+
# TYPE target_info gauge
11+
target_info{"service.name"="prometheus_test","telemetry.sdk.language"="go","telemetry.sdk.name"="opentelemetry","telemetry.sdk.version"="latest"} 1

0 commit comments

Comments
 (0)