From dc3e6af91a9b57d469546c304909920bdacb323f Mon Sep 17 00:00:00 2001
From: Patryk Prus
Date: Tue, 30 Sep 2025 12:49:54 -0400
Subject: [PATCH] tsdb: Fix appended sample count metrics when converting float
staleness markers to histograms (#17241)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
tsdb: Fix appended sample count metrics when converting histogram staleness markers
Signed-off-by: Patryk Prus
Signed-off-by: Björn Rabenstein
Co-authored-by: Björn Rabenstein
---
tsdb/head_append.go | 6 +++
tsdb/head_test.go | 90 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 96 insertions(+)
diff --git a/tsdb/head_append.go b/tsdb/head_append.go
index 80357fdf13..659c09f7e7 100644
--- a/tsdb/head_append.go
+++ b/tsdb/head_append.go
@@ -1331,6 +1331,9 @@ func (a *headAppender) commitFloats(b *appendBatch, acc *appenderCommitContext)
H: &histogram.Histogram{Sum: s.V},
})
b.histogramSeries = append(b.histogramSeries, series)
+ // This sample was counted as a float but is now a histogram.
+ acc.floatsAppended--
+ acc.histogramsAppended++
series.Unlock()
continue
case series.lastFloatHistogramValue != nil:
@@ -1340,6 +1343,9 @@ func (a *headAppender) commitFloats(b *appendBatch, acc *appenderCommitContext)
FH: &histogram.FloatHistogram{Sum: s.V},
})
b.floatHistogramSeries = append(b.floatHistogramSeries, series)
+ // This sample was counted as a float but is now a float histogram.
+ acc.floatsAppended--
+ acc.histogramsAppended++
series.Unlock()
continue
}
diff --git a/tsdb/head_test.go b/tsdb/head_test.go
index d2470952c2..e150797660 100644
--- a/tsdb/head_test.go
+++ b/tsdb/head_test.go
@@ -34,6 +34,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/prometheus/client_golang/prometheus"
prom_testutil "github.com/prometheus/client_golang/prometheus/testutil"
+ dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
@@ -7256,3 +7257,92 @@ func TestHead_NumStaleSeries(t *testing.T) {
appendFloatHistogram(series8, 400, staleFH)
verifySeriesCounts(4, 5)
}
+
+// TestHistogramStalenessConversionMetrics verifies that staleness marker conversion correctly
+// increments the right appender metrics for both histogram and float histogram scenarios.
+func TestHistogramStalenessConversionMetrics(t *testing.T) {
+ testCases := []struct {
+ name string
+ setupHistogram func(app storage.Appender, lbls labels.Labels) error
+ }{
+ {
+ name: "float_staleness_to_histogram",
+ setupHistogram: func(app storage.Appender, lbls labels.Labels) error {
+ _, err := app.AppendHistogram(0, lbls, 1000, tsdbutil.GenerateTestHistograms(1)[0], nil)
+ return err
+ },
+ },
+ {
+ name: "float_staleness_to_float_histogram",
+ setupHistogram: func(app storage.Appender, lbls labels.Labels) error {
+ _, err := app.AppendHistogram(0, lbls, 1000, nil, tsdbutil.GenerateTestFloatHistograms(1)[0])
+ return err
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ head, _ := newTestHead(t, 1000, compression.None, false)
+ defer func() {
+ require.NoError(t, head.Close())
+ }()
+
+ lbls := labels.FromStrings("name", tc.name)
+
+ // Helper to get counter values
+ getSampleCounter := func(sampleType string) float64 {
+ metric := &dto.Metric{}
+ err := head.metrics.samplesAppended.WithLabelValues(sampleType).Write(metric)
+ require.NoError(t, err)
+ return metric.GetCounter().GetValue()
+ }
+
+ // Step 1: Establish a series with histogram data
+ app := head.Appender(context.Background())
+ err := tc.setupHistogram(app, lbls)
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Step 2: Add a float staleness marker
+ app = head.Appender(context.Background())
+ _, err = app.Append(0, lbls, 2000, math.Float64frombits(value.StaleNaN))
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Count what was actually stored by querying the series
+ q, err := NewBlockQuerier(head, 0, 3000)
+ require.NoError(t, err)
+ defer q.Close()
+
+ ss := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "name", tc.name))
+ require.True(t, ss.Next())
+ series := ss.At()
+
+ it := series.Iterator(nil)
+
+ actualFloatSamples := 0
+ actualHistogramSamples := 0
+
+ for valType := it.Next(); valType != chunkenc.ValNone; valType = it.Next() {
+ switch valType {
+ case chunkenc.ValFloat:
+ actualFloatSamples++
+ case chunkenc.ValHistogram, chunkenc.ValFloatHistogram:
+ actualHistogramSamples++
+ }
+ }
+ require.NoError(t, it.Err())
+
+ // Verify what was actually stored - should be 0 floats, 2 histograms (original + converted staleness marker)
+ require.Equal(t, 0, actualFloatSamples, "Should have 0 float samples stored")
+ require.Equal(t, 2, actualHistogramSamples, "Should have 2 histogram samples: original + converted staleness marker")
+
+ // The metrics should match what was actually stored
+ require.Equal(t, float64(actualFloatSamples), getSampleCounter(sampleMetricTypeFloat),
+ "Float counter should match actual float samples stored")
+ require.Equal(t, float64(actualHistogramSamples), getSampleCounter(sampleMetricTypeHistogram),
+ "Histogram counter should match actual histogram samples stored")
+ })
+ }
+}