diff --git a/util/fmtutil/format.go b/util/fmtutil/format.go index 7a78df849c..377f4ece05 100644 --- a/util/fmtutil/format.go +++ b/util/fmtutil/format.go @@ -18,6 +18,7 @@ import ( "fmt" "io" "maps" + "math" "sort" "time" @@ -140,11 +141,23 @@ func makeTimeseries(wr *prompb.WriteRequest, labels map[string]string, m *dto.Me // Add Histogram bucket timeseries bucketLabels := make(map[string]string, len(labels)+1) maps.Copy(bucketLabels, labels) + var hasInf bool for _, b := range m.GetHistogram().Bucket { + if b.GetUpperBound() == math.Inf(1) { + hasInf = true + } bucketLabels[model.MetricNameLabel] = metricName + bucketStr bucketLabels[model.BucketLabel] = fmt.Sprint(b.GetUpperBound()) toTimeseries(wr, bucketLabels, timestamp, float64(b.GetCumulativeCount())) } + + // Add +Inf bucket if not present + if !hasInf { + bucketLabels[model.MetricNameLabel] = metricName + bucketStr + bucketLabels[model.BucketLabel] = "+Inf" + toTimeseries(wr, bucketLabels, timestamp, float64(m.GetHistogram().GetSampleCount())) + } + // Overwrite label model.MetricNameLabel for count and sum metrics // Add Histogram sum timeseries labels[model.MetricNameLabel] = metricName + sumStr diff --git a/util/fmtutil/format_test.go b/util/fmtutil/format_test.go index c592630fe8..f1d025806e 100644 --- a/util/fmtutil/format_test.go +++ b/util/fmtutil/format_test.go @@ -15,8 +15,11 @@ package fmtutil import ( "bytes" + "math" "testing" + "time" + dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/require" "github.com/prometheus/prometheus/prompb" @@ -231,3 +234,50 @@ func TestMetricTextToWriteRequestErrorParsingMetricType(t *testing.T) { _, err := MetricTextToWriteRequest(input, labels) require.Equal(t, "text format parsing error in line 3: unknown metric type \"info\"", err.Error()) } + +func TestMakeTimeseries_HistogramInfBucket(t *testing.T) { + tests := map[string]*dto.Histogram{ + "Histogram missing +Inf bucket": { + Bucket: []*dto.Bucket{ + {CumulativeCount: p[uint64](5), UpperBound: p(1.0)}, + {CumulativeCount: p[uint64](10), UpperBound: p(5.0)}, + }, + SampleCount: p[uint64](15), + }, + "Histogram already has +Inf bucket": { + Bucket: []*dto.Bucket{ + {CumulativeCount: p[uint64](5), UpperBound: p(1.0)}, + {CumulativeCount: p[uint64](10), UpperBound: p(5.0)}, + {CumulativeCount: p[uint64](15), UpperBound: p(math.Inf(1))}, + }, + SampleCount: p[uint64](15), + }, + } + + for name, histogram := range tests { + t.Run(name, func(t *testing.T) { + wr := &prompb.WriteRequest{} + labels := map[string]string{"__name__": "test_histogram"} + metric := &dto.Metric{ + Histogram: histogram, + TimestampMs: p(time.Now().UnixMilli()), + } + + require.NoError(t, makeTimeseries(wr, labels, metric)) + + var hasInf bool + for _, ts := range wr.Timeseries { + for _, lbl := range ts.Labels { + if lbl.Name == "le" && lbl.Value == "+Inf" { + hasInf = true + } + } + } + require.Truef(t, hasInf, "expected +Inf bucket in histogram") + }) + } +} + +func p[T any](v T) *T { + return &v +}