1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-08-10 22:31:50 +02:00

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 <giedrius.statkevicius@vinted.com>
Co-authored-by: Sam Xie <sam@samxie.me>
This commit is contained in:
Giedrius Statkevičius
2025-04-04 05:35:14 +03:00
committed by GitHub
parent dceb2cd512
commit 75973eccbf
4 changed files with 135 additions and 1 deletions

View File

@@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Added
- Add exponential histogram support in `go.opentelemetry.io/otel/exporters/prometheus`. (#6421)
- The `go.opentelemetry.io/otel/semconv/v1.31.0` package.
The package contains semantic conventions from the `v1.31.0` version of the OpenTelemetry Semantic Conventions.
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)

View File

@@ -8,6 +8,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"math"
"slices"
"strings"
"sync"
@@ -241,6 +242,10 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
addHistogramMetric(ch, v, m, name, kv)
case metricdata.Histogram[float64]:
addHistogramMetric(ch, v, m, name, kv)
case metricdata.ExponentialHistogram[int64]:
addExponentialHistogramMetric(ch, v, m, name, kv)
case metricdata.ExponentialHistogram[float64]:
addExponentialHistogramMetric(ch, v, m, name, kv)
case metricdata.Sum[int64]:
addSumMetric(ch, v, m, name, kv)
case metricdata.Sum[float64]:
@@ -254,6 +259,60 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
}
}
func addExponentialHistogramMetric[N int64 | float64](
ch chan<- prometheus.Metric,
histogram metricdata.ExponentialHistogram[N],
m metricdata.Metrics,
name string,
kv keyVals,
) {
for _, dp := range histogram.DataPoints {
keys, values := getAttrs(dp.Attributes)
keys = append(keys, kv.keys...)
values = append(values, kv.vals...)
desc := prometheus.NewDesc(name, m.Description, keys, nil)
// 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.
positiveBuckets := make(map[int]int64)
for i, c := range dp.PositiveBucket.Counts {
if c > math.MaxInt64 {
otel.Handle(fmt.Errorf("positive count %d is too large to be represented as int64", c))
continue
}
positiveBuckets[int(dp.PositiveBucket.Offset)+i+1] = int64(c) // nolint: gosec // Size check above.
}
negativeBuckets := make(map[int]int64)
for i, c := range dp.NegativeBucket.Counts {
if c > math.MaxInt64 {
otel.Handle(fmt.Errorf("negative count %d is too large to be represented as int64", c))
continue
}
negativeBuckets[int(dp.NegativeBucket.Offset)+i+1] = int64(c) // nolint: gosec // Size check above.
}
m, err := prometheus.NewConstNativeHistogram(
desc,
dp.Count,
float64(dp.Sum),
positiveBuckets,
negativeBuckets,
dp.ZeroCount,
dp.Scale,
dp.ZeroThreshold,
dp.StartTime,
values...)
if err != nil {
otel.Handle(err)
continue
}
// TODO(GiedriusS): add exemplars here after https://github.com/prometheus/client_golang/pull/1654#pullrequestreview-2434669425 is done.
ch <- m
}
}
func addHistogramMetric[N int64 | float64](
ch chan<- prometheus.Metric,
histogram metricdata.Histogram[N],
@@ -468,6 +527,8 @@ func convertsToUnderscore(b rune) bool {
func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType {
switch v := m.Data.(type) {
case metricdata.ExponentialHistogram[int64], metricdata.ExponentialHistogram[float64]:
return dto.MetricType_HISTOGRAM.Enum()
case metricdata.Histogram[int64], metricdata.Histogram[float64]:
return dto.MetricType_HISTOGRAM.Enum()
case metricdata.Sum[float64]:

View File

@@ -36,6 +36,7 @@ func TestPrometheusExporter(t *testing.T) {
options []Option
expectedFile string
disableUTF8 bool
checkMetricFamilies func(t testing.TB, dtos []*dto.MetricFamily)
}{
{
name: "counter",
@@ -172,6 +173,50 @@ func TestPrometheusExporter(t *testing.T) {
gauge.Add(ctx, -.25, opt)
},
},
{
name: "exponential histogram",
expectedFile: "testdata/exponential_histogram.txt",
checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) {
var hist *dto.MetricFamily
for _, mf := range mfs {
if *mf.Name == `exponential_histogram_baz_bytes` {
hist = mf
break
}
}
if hist == nil {
t.Fatal("expected to find histogram")
}
m := hist.GetMetric()[0].Histogram
require.Equal(t, 236.0, *m.SampleSum)
require.Equal(t, uint64(4), *m.SampleCount)
require.Equal(t, []int64{1, -1, 1, -1, 2}, m.PositiveDelta)
require.Equal(t, uint32(5), *m.PositiveSpan[0].Length)
require.Equal(t, int32(3), *m.PositiveSpan[0].Offset)
},
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
// NOTE(GiedriusS): there is no text format for exponential (native)
// histograms so we don't expect any output.
opt := otelmetric.WithAttributes(
attribute.Key("A").String("B"),
attribute.Key("C").String("D"),
)
histogram, err := meter.Float64Histogram(
"exponential_histogram_baz",
otelmetric.WithDescription("a very nice histogram"),
otelmetric.WithUnit("By"),
)
require.NoError(t, err)
histogram.Record(ctx, 23, opt)
histogram.Record(ctx, 7, opt)
histogram.Record(ctx, 101, opt)
histogram.Record(ctx, 105, opt)
},
},
{
name: "histogram",
expectedFile: "testdata/histogram.txt",
@@ -517,7 +562,14 @@ func TestPrometheusExporter(t *testing.T) {
metric.Stream{Aggregation: metric.AggregationExplicitBucketHistogram{
Boundaries: []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 1000},
}},
)),
),
metric.NewView(
metric.Instrument{Name: "exponential_histogram_*"},
metric.Stream{Aggregation: metric.AggregationBase2ExponentialHistogram{
MaxSize: 10,
}},
),
),
)
meter := provider.Meter(
"testmeter",
@@ -533,6 +585,15 @@ func TestPrometheusExporter(t *testing.T) {
err = testutil.GatherAndCompare(registry, file)
require.NoError(t, err)
if tc.checkMetricFamilies == nil {
return
}
mfs, err := registry.Gather()
require.NoError(t, err)
tc.checkMetricFamilies(t, mfs)
})
}
}

View File

@@ -0,0 +1,11 @@
# HELP exponential_histogram_baz_bytes a very nice histogram
# TYPE exponential_histogram_baz_bytes histogram
exponential_histogram_baz_bytes_bucket{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0",le="+Inf"} 4
exponential_histogram_baz_bytes_sum{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 236
exponential_histogram_baz_bytes_count{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 4
# HELP otel_scope_info Instrumentation Scope metadata
# TYPE otel_scope_info gauge
otel_scope_info{fizz="buzz",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 1
# 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