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
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:
committed by
GitHub
parent
dceb2cd512
commit
75973eccbf
@@ -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)
|
||||
|
@@ -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]:
|
||||
|
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
11
exporters/prometheus/testdata/exponential_histogram.txt
vendored
Normal file
11
exporters/prometheus/testdata/exponential_histogram.txt
vendored
Normal 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
|
Reference in New Issue
Block a user