Skip to content

Commit b0b9ac2

Browse files
prometheus: validate exponential histogram scale range (open-telemetry#6779)
1 parent a4afec7 commit b0b9ac2

File tree

3 files changed

+106
-1
lines changed

3 files changed

+106
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
3333
- The semantic conventions have been upgraded from `v1.26.0` to `v1.34.0` in `go.opentelemetry.io/otel/sdk/trace`. (#6835)
3434
- The semantic conventions have been upgraded from `v1.26.0` to `v1.34.0` in `go.opentelemetry.io/otel/trace`. (#6836)
3535

36+
### Fixed
37+
38+
- Validate exponential histogram scale range for Prometheus compatibility in `go.opentelemetry.io/otel/exporters/prometheus`. (#6779)
39+
3640
<!-- Released section -->
3741
<!-- Don't change this section unless doing release -->
3842

exporters/prometheus/exporter.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,19 @@ func addExponentialHistogramMetric[N int64 | float64](
258258

259259
desc := prometheus.NewDesc(name, m.Description, keys, nil)
260260

261+
// Prometheus native histograms support scales in the range [-4, 8]
262+
scale := dp.Scale
263+
if scale < -4 {
264+
// Reject scales below -4 as they cannot be represented in Prometheus
265+
otel.Handle(fmt.Errorf(
266+
"exponential histogram scale %d is below minimum supported scale -4, skipping data point",
267+
scale))
268+
continue
269+
}
270+
if scale > 8 {
271+
scale = 8
272+
}
273+
261274
// 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.
262275
positiveBuckets := make(map[int]int64)
263276
for i, c := range dp.PositiveBucket.Counts {
@@ -284,7 +297,7 @@ func addExponentialHistogramMetric[N int64 | float64](
284297
positiveBuckets,
285298
negativeBuckets,
286299
dp.ZeroCount,
287-
dp.Scale,
300+
scale,
288301
dp.ZeroThreshold,
289302
dp.StartTime,
290303
values...)

exporters/prometheus/exporter_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os"
1010
"sync"
1111
"testing"
12+
"time"
1213

1314
"github.com/prometheus/client_golang/prometheus"
1415
"github.com/prometheus/client_golang/prometheus/testutil"
@@ -21,6 +22,7 @@ import (
2122
"go.opentelemetry.io/otel/attribute"
2223
otelmetric "go.opentelemetry.io/otel/metric"
2324
"go.opentelemetry.io/otel/sdk/metric"
25+
"go.opentelemetry.io/otel/sdk/metric/metricdata"
2426
"go.opentelemetry.io/otel/sdk/resource"
2527
semconv "go.opentelemetry.io/otel/semconv/v1.34.0"
2628
"go.opentelemetry.io/otel/trace"
@@ -1134,3 +1136,89 @@ func TestExemplars(t *testing.T) {
11341136
})
11351137
}
11361138
}
1139+
1140+
func TestExponentialHistogramScaleValidation(t *testing.T) {
1141+
ctx := context.Background()
1142+
1143+
t.Run("normal_exponential_histogram_works", func(t *testing.T) {
1144+
registry := prometheus.NewRegistry()
1145+
exporter, err := New(WithRegisterer(registry), WithoutTargetInfo(), WithoutScopeInfo())
1146+
require.NoError(t, err)
1147+
1148+
provider := metric.NewMeterProvider(
1149+
metric.WithReader(exporter),
1150+
metric.WithResource(resource.Default()),
1151+
)
1152+
defer func() {
1153+
err := provider.Shutdown(ctx)
1154+
require.NoError(t, err)
1155+
}()
1156+
1157+
// Create a histogram with a valid scale
1158+
meter := provider.Meter("test")
1159+
hist, err := meter.Float64Histogram(
1160+
"test_exponential_histogram",
1161+
otelmetric.WithDescription("test histogram"),
1162+
)
1163+
require.NoError(t, err)
1164+
hist.Record(ctx, 1.0)
1165+
hist.Record(ctx, 10.0)
1166+
hist.Record(ctx, 100.0)
1167+
1168+
metricFamilies, err := registry.Gather()
1169+
require.NoError(t, err)
1170+
assert.NotEmpty(t, metricFamilies)
1171+
})
1172+
1173+
t.Run("error_handling_for_invalid_scales", func(t *testing.T) {
1174+
var capturedError error
1175+
originalHandler := otel.GetErrorHandler()
1176+
otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
1177+
capturedError = err
1178+
}))
1179+
defer otel.SetErrorHandler(originalHandler)
1180+
1181+
now := time.Now()
1182+
invalidScaleData := metricdata.ExponentialHistogramDataPoint[float64]{
1183+
Attributes: attribute.NewSet(),
1184+
StartTime: now,
1185+
Time: now,
1186+
Count: 1,
1187+
Sum: 10.0,
1188+
Scale: -5, // Invalid scale below -4
1189+
ZeroCount: 0,
1190+
ZeroThreshold: 0.0,
1191+
PositiveBucket: metricdata.ExponentialBucket{
1192+
Offset: 1,
1193+
Counts: []uint64{1},
1194+
},
1195+
NegativeBucket: metricdata.ExponentialBucket{
1196+
Offset: 1,
1197+
Counts: []uint64{},
1198+
},
1199+
}
1200+
1201+
ch := make(chan prometheus.Metric, 10)
1202+
defer close(ch)
1203+
1204+
histogram := metricdata.ExponentialHistogram[float64]{
1205+
Temporality: metricdata.CumulativeTemporality,
1206+
DataPoints: []metricdata.ExponentialHistogramDataPoint[float64]{invalidScaleData},
1207+
}
1208+
1209+
m := metricdata.Metrics{
1210+
Name: "test_histogram",
1211+
Description: "test",
1212+
}
1213+
1214+
addExponentialHistogramMetric(ch, histogram, m, "test_histogram", keyVals{})
1215+
assert.Error(t, capturedError)
1216+
assert.Contains(t, capturedError.Error(), "scale -5 is below minimum")
1217+
select {
1218+
case <-ch:
1219+
t.Error("Expected no metrics to be produced for invalid scale")
1220+
default:
1221+
// No metrics were produced for the invalid scale
1222+
}
1223+
})
1224+
}

0 commit comments

Comments
 (0)