Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Added

- Add exponential histogram support in `go.opentelemetry.io/otel/exporters/prometheus`. (#6421)

### Removed

- Drop support for [Go 1.22]. (#6381, #6418)
Expand Down
45 changes: 45 additions & 0 deletions exporters/prometheus/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"math"
"slices"
"strings"
"sync"
Expand Down Expand Up @@ -233,6 +234,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]:
Expand All @@ -246,6 +251,44 @@ 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
}

// NOTE(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], m metricdata.Metrics, name string, kv keyVals) {
for _, dp := range histogram.DataPoints {
keys, values := getAttrs(dp.Attributes)
Expand Down Expand Up @@ -441,6 +484,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]:
Expand Down
67 changes: 64 additions & 3 deletions exporters/prometheus/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -473,10 +518,10 @@ func TestPrometheusExporter(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
if tc.disableUTF8 {
model.NameValidationScheme = model.LegacyValidation
defer func() {
t.Cleanup(func() {
// Reset to defaults
model.NameValidationScheme = model.UTF8Validation
}()
})
}
ctx := context.Background()
registry := prometheus.NewRegistry()
Expand Down Expand Up @@ -508,7 +553,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",
Expand All @@ -524,6 +576,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)
})
}
}
Expand Down
10 changes: 10 additions & 0 deletions exporters/prometheus/testdata/counter_already_unit_suffix.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# HELP "foo.seconds_total" a simple counter
# TYPE "foo.seconds_total" counter
{"foo.seconds_total",A="B",C="D",E="true",F="42",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 24.3
{"foo.seconds_total",A="D",C="B",E="true",F="42",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 5
# 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
11 changes: 11 additions & 0 deletions exporters/prometheus/testdata/exponential_histogram.txt
Original file line number Diff line number Diff line change
@@ -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
Loading