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
|
### Added
|
||||||
|
|
||||||
|
- Add exponential histogram support in `go.opentelemetry.io/otel/exporters/prometheus`. (#6421)
|
||||||
- The `go.opentelemetry.io/otel/semconv/v1.31.0` package.
|
- 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.
|
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)
|
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"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -241,6 +242,10 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
|
|||||||
addHistogramMetric(ch, v, m, name, kv)
|
addHistogramMetric(ch, v, m, name, kv)
|
||||||
case metricdata.Histogram[float64]:
|
case metricdata.Histogram[float64]:
|
||||||
addHistogramMetric(ch, v, m, name, kv)
|
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]:
|
case metricdata.Sum[int64]:
|
||||||
addSumMetric(ch, v, m, name, kv)
|
addSumMetric(ch, v, m, name, kv)
|
||||||
case metricdata.Sum[float64]:
|
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](
|
func addHistogramMetric[N int64 | float64](
|
||||||
ch chan<- prometheus.Metric,
|
ch chan<- prometheus.Metric,
|
||||||
histogram metricdata.Histogram[N],
|
histogram metricdata.Histogram[N],
|
||||||
@@ -468,6 +527,8 @@ func convertsToUnderscore(b rune) bool {
|
|||||||
|
|
||||||
func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType {
|
func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType {
|
||||||
switch v := m.Data.(type) {
|
switch v := m.Data.(type) {
|
||||||
|
case metricdata.ExponentialHistogram[int64], metricdata.ExponentialHistogram[float64]:
|
||||||
|
return dto.MetricType_HISTOGRAM.Enum()
|
||||||
case metricdata.Histogram[int64], metricdata.Histogram[float64]:
|
case metricdata.Histogram[int64], metricdata.Histogram[float64]:
|
||||||
return dto.MetricType_HISTOGRAM.Enum()
|
return dto.MetricType_HISTOGRAM.Enum()
|
||||||
case metricdata.Sum[float64]:
|
case metricdata.Sum[float64]:
|
||||||
|
@@ -36,6 +36,7 @@ func TestPrometheusExporter(t *testing.T) {
|
|||||||
options []Option
|
options []Option
|
||||||
expectedFile string
|
expectedFile string
|
||||||
disableUTF8 bool
|
disableUTF8 bool
|
||||||
|
checkMetricFamilies func(t testing.TB, dtos []*dto.MetricFamily)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "counter",
|
name: "counter",
|
||||||
@@ -172,6 +173,50 @@ func TestPrometheusExporter(t *testing.T) {
|
|||||||
gauge.Add(ctx, -.25, opt)
|
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",
|
name: "histogram",
|
||||||
expectedFile: "testdata/histogram.txt",
|
expectedFile: "testdata/histogram.txt",
|
||||||
@@ -517,7 +562,14 @@ func TestPrometheusExporter(t *testing.T) {
|
|||||||
metric.Stream{Aggregation: metric.AggregationExplicitBucketHistogram{
|
metric.Stream{Aggregation: metric.AggregationExplicitBucketHistogram{
|
||||||
Boundaries: []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 1000},
|
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(
|
meter := provider.Meter(
|
||||||
"testmeter",
|
"testmeter",
|
||||||
@@ -533,6 +585,15 @@ func TestPrometheusExporter(t *testing.T) {
|
|||||||
|
|
||||||
err = testutil.GatherAndCompare(registry, file)
|
err = testutil.GatherAndCompare(registry, file)
|
||||||
require.NoError(t, err)
|
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