Skip to content
Open
9 changes: 9 additions & 0 deletions .chloggen/fix-prometheusreceiver-bucketless-nhcb.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
change_type: bug_fix

component: receiver/prometheus

note: Prevent the Prometheus receiver from dropping classic histograms without explicit bucket boundaries when `convert_classic_histograms_to_nhcb` is enabled and classic histograms are not retained.

issues: [49893]

change_logs: [user]
14 changes: 7 additions & 7 deletions receiver/prometheusreceiver/internal/metricfamily.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,25 +104,25 @@ func (mg *metricGroup) toDistributionPoint(dest pmetric.HistogramDataPointSlice)
return
}

pointIsStale := value.IsStaleNaN(mg.sum) || value.IsStaleNaN(mg.count)
// NHCB conversion loses whether _count was absent. Reject the inconsistent
// form that identifies a sum-only classic histogram.
if mg.isNHCB && !pointIsStale && mg.count == 0 && mg.sum != 0 {
return
}

mg.sortPoints()

var bounds []float64
var bucketCounts []uint64
pointIsStale := value.IsStaleNaN(mg.sum) || value.IsStaleNaN(mg.count)

if mg.isNHCB {
switch {
case mg.hValue != nil:
if len(mg.hValue.CustomValues) == 0 {
return
}
bounds = make([]float64, len(mg.hValue.CustomValues))
copy(bounds, mg.hValue.CustomValues)
bucketCounts = convertNHCBBDeltBuckets(mg.hValue)
case mg.fhValue != nil:
if len(mg.fhValue.CustomValues) == 0 {
return
}
bounds = make([]float64, len(mg.fhValue.CustomValues))
copy(bounds, mg.fhValue.CustomValues)
bucketCounts = convertNHCBAbsoluteBuckets(mg.fhValue)
Expand Down
158 changes: 158 additions & 0 deletions receiver/prometheusreceiver/internal/metricfamily_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,49 @@ func TestMetricGroupData_toNHCBDistributionUnitTest(t *testing.T) {
return point
},
},
{
name: "integer NHCB without explicit bounds",
metricName: "histogram",
intervalStartTimeMs: 11,
labels: labels.FromMap(map[string]string{"a": "A", "b": "B"}),
integerHistogram: &histogram.Histogram{
Schema: histogram.CustomBucketsSchema,
Count: 42,
Sum: 123.5,
PositiveSpans: []histogram.Span{{Offset: 0, Length: 1}},
PositiveBuckets: []int64{42},
},
want: func() pmetric.HistogramDataPoint {
point := pmetric.NewHistogramDataPoint()
point.SetCount(42)
point.SetSum(123.5)
point.SetTimestamp(pcommon.Timestamp(11 * time.Millisecond))
point.BucketCounts().FromRaw([]uint64{42})
attributes := point.Attributes()
attributes.PutStr("a", "A")
attributes.PutStr("b", "B")
return point
},
},
{
name: "integer NHCB without explicit bounds with zero count",
metricName: "histogram",
intervalStartTimeMs: 11,
labels: labels.FromMap(map[string]string{"a": "A", "b": "B"}),
integerHistogram: &histogram.Histogram{
Schema: histogram.CustomBucketsSchema,
},
want: func() pmetric.HistogramDataPoint {
point := pmetric.NewHistogramDataPoint()
point.SetSum(0)
point.SetTimestamp(pcommon.Timestamp(11 * time.Millisecond))
point.BucketCounts().FromRaw([]uint64{0})
attributes := point.Attributes()
attributes.PutStr("a", "A")
attributes.PutStr("b", "B")
return point
},
},
{
name: "integer NHCB that is stale",
metricName: "histogram",
Expand All @@ -351,6 +394,26 @@ func TestMetricGroupData_toNHCBDistributionUnitTest(t *testing.T) {
return point
},
},
{
name: "integer NHCB without explicit bounds that is stale",
metricName: "histogram",
intervalStartTimeMs: 11,
labels: labels.FromMap(map[string]string{"a": "A", "b": "B"}),
integerHistogram: &histogram.Histogram{
Schema: histogram.CustomBucketsSchema,
Sum: math.Float64frombits(value.StaleNaN),
},
want: func() pmetric.HistogramDataPoint {
point := pmetric.NewHistogramDataPoint()
point.SetTimestamp(pcommon.Timestamp(11 * time.Millisecond))
point.SetFlags(pmetric.DefaultDataPointFlags.WithNoRecordedValue(true))
point.BucketCounts().FromRaw([]uint64{0})
attributes := point.Attributes()
attributes.PutStr("a", "A")
attributes.PutStr("b", "B")
return point
},
},
{
name: "float NHCB",
metricName: "histogram",
Expand All @@ -376,6 +439,63 @@ func TestMetricGroupData_toNHCBDistributionUnitTest(t *testing.T) {
return point
},
},
{
name: "float NHCB without explicit bounds",
metricName: "histogram",
intervalStartTimeMs: 12,
labels: labels.FromMap(map[string]string{"a": "A"}),
floatHistogram: &histogram.FloatHistogram{
Schema: histogram.CustomBucketsSchema,
Count: 42,
Sum: 123.5,
PositiveSpans: []histogram.Span{{Offset: 0, Length: 1}},
PositiveBuckets: []float64{42},
},
want: func() pmetric.HistogramDataPoint {
point := pmetric.NewHistogramDataPoint()
point.SetCount(42)
point.SetSum(123.5)
point.SetTimestamp(pcommon.Timestamp(12 * time.Millisecond))
point.BucketCounts().FromRaw([]uint64{42})
point.Attributes().PutStr("a", "A")
return point
},
},
{
name: "float NHCB without explicit bounds with zero count",
metricName: "histogram",
intervalStartTimeMs: 12,
labels: labels.FromMap(map[string]string{"a": "A"}),
floatHistogram: &histogram.FloatHistogram{
Schema: histogram.CustomBucketsSchema,
},
want: func() pmetric.HistogramDataPoint {
point := pmetric.NewHistogramDataPoint()
point.SetSum(0)
point.SetTimestamp(pcommon.Timestamp(12 * time.Millisecond))
point.BucketCounts().FromRaw([]uint64{0})
point.Attributes().PutStr("a", "A")
return point
},
},
{
name: "float NHCB without explicit bounds that is stale",
metricName: "histogram",
intervalStartTimeMs: 12,
labels: labels.FromMap(map[string]string{"a": "A"}),
floatHistogram: &histogram.FloatHistogram{
Schema: histogram.CustomBucketsSchema,
Sum: math.Float64frombits(value.StaleNaN),
},
want: func() pmetric.HistogramDataPoint {
point := pmetric.NewHistogramDataPoint()
point.SetTimestamp(pcommon.Timestamp(12 * time.Millisecond))
point.SetFlags(pmetric.DefaultDataPointFlags.WithNoRecordedValue(true))
point.BucketCounts().FromRaw([]uint64{0})
point.Attributes().PutStr("a", "A")
return point
},
},
{
name: "integer NHCB with negative boundaries",
metricName: "histogram",
Expand Down Expand Up @@ -427,6 +547,44 @@ func TestMetricGroupData_toNHCBDistributionUnitTest(t *testing.T) {
}
}

func TestMetricGroupData_toNHCBDistributionRejectsNonzeroSumWithZeroCount(t *testing.T) {
tests := []struct {
name string
integerHistogram *histogram.Histogram
floatHistogram *histogram.FloatHistogram
}{
{
name: "integer NHCB",
integerHistogram: &histogram.Histogram{
Schema: histogram.CustomBucketsSchema,
Sum: 123.5,
},
},
{
name: "float NHCB",
floatHistogram: &histogram.FloatHistogram{
Schema: histogram.CustomBucketsSchema,
Sum: 123.5,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mp := newMetricFamily("histogram", mc, zap.NewNop(), false, false)
lbls := labels.FromMap(map[string]string{"a": "A"})
sRef, _ := getSeriesRefWithoutScopeLabels(nil, lbls, mp.mtype)

err := mp.addNHCBSeries(sRef, "histogram", lbls, 11, tt.integerHistogram, tt.floatHistogram)
require.NoError(t, err)

sl := pmetric.NewMetricSlice()
mp.appendMetric(sl, false)
require.Zero(t, sl.Len())
})
}
}

func TestMetricGroupData_toExponentialDistributionUnitTest(t *testing.T) {
type scrape struct {
at int64
Expand Down
101 changes: 101 additions & 0 deletions receiver/prometheusreceiver/metrics_receiver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1471,6 +1471,107 @@ func TestCoreMetricsEndToEnd(t *testing.T) {
testComponent(t, targets, nil)
}

func TestBucketlessHistogramNHCBConversion(t *testing.T) {
tests := []struct {
name string
alwaysScrapeClassic bool
expectedHistogramCount int
}{
{
name: "classic histogram not retained",
alwaysScrapeClassic: false,
expectedHistogramCount: 1,
},
{
name: "classic histogram retained",
alwaysScrapeClassic: true,
expectedHistogramCount: 2,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
targets := []*testData{
{
name: "bucketless-histogram",
pages: []mockPrometheusResponse{
{
code: 200,
data: `
# HELP demo_seconds A histogram without explicit buckets.
# TYPE demo_seconds histogram
demo_seconds_sum 123.5
demo_seconds_count 42
`,
},
},
validateFunc: func(t *testing.T, td *testData, resourceMetrics []pmetric.ResourceMetrics) {
verifyNumValidScrapeResults(t, td, resourceMetrics)
require.NotEmpty(t, resourceMetrics)
require.Equal(t, expectedScrapeMetricCount+tt.expectedHistogramCount, metricsCount(resourceMetrics[0]))

histogramCount := 0
for _, metric := range getMetrics(resourceMetrics[0]) {
if metric.Name() != "demo_seconds" {
continue
}
require.Equal(t, pmetric.MetricTypeHistogram, metric.Type())
require.Equal(t, 1, metric.Histogram().DataPoints().Len())
compareHistogram(42, 123.5, nil, []uint64{42})(t, metric.Histogram().DataPoints().At(0))
histogramCount++
}
require.Equal(t, tt.expectedHistogramCount, histogramCount)
},
},
}

testComponent(t, targets, nil, func(cfg *PromConfig) {
enabled := true
alwaysScrapeClassic := tt.alwaysScrapeClassic
for _, scrapeConfig := range cfg.ScrapeConfigs {
scrapeConfig.ConvertClassicHistogramsToNHCB = &enabled
scrapeConfig.AlwaysScrapeClassicHistograms = &alwaysScrapeClassic
}
})
})
}
}

func TestSumOnlyHistogramNHCBConversion(t *testing.T) {
targets := []*testData{
{
name: "sum-only-histogram",
pages: []mockPrometheusResponse{
{
code: 200,
data: `
# HELP demo_seconds A histogram without a count.
# TYPE demo_seconds histogram
demo_seconds_sum 123.5
`,
},
},
validateFunc: func(t *testing.T, td *testData, resourceMetrics []pmetric.ResourceMetrics) {
verifyNumValidScrapeResults(t, td, resourceMetrics)
require.NotEmpty(t, resourceMetrics)
require.Equal(t, expectedScrapeMetricCount, metricsCount(resourceMetrics[0]))
for _, metric := range getMetrics(resourceMetrics[0]) {
require.NotEqual(t, "demo_seconds", metric.Name())
}
},
},
}

testComponent(t, targets, nil, func(cfg *PromConfig) {
enabled := true
alwaysScrapeClassic := false
for _, scrapeConfig := range cfg.ScrapeConfigs {
scrapeConfig.ConvertClassicHistogramsToNHCB = &enabled
scrapeConfig.AlwaysScrapeClassicHistograms = &alwaysScrapeClassic
}
})
}

// metric type is defined as 'untyped' in the first metric
// and, type hint is missing in the 2nd metric
var untypedMetrics = `
Expand Down
Loading