mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
* Ensure leading zero handling on "both" histograms implementaions * Add test to cover native and "both" modes in native implementation * Update changelog
965 lines
47 KiB
Go
965 lines
47 KiB
Go
package registry
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/prometheus/prometheus/model/exemplar"
|
|
"github.com/prometheus/prometheus/model/labels"
|
|
"github.com/prometheus/prometheus/storage"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var testTenant = "test-tenant"
|
|
|
|
// Duplicate labels should not grow the series count.
|
|
func Test_ObserveWithExemplar_duplicate(t *testing.T) {
|
|
var seriesAdded int
|
|
lifecycler := &mockLimiter{
|
|
onAddFunc: func(_ uint64, count uint32, lbls labels.Labels) (labels.Labels, uint64) {
|
|
seriesAdded += int(count)
|
|
return lbls, lbls.Hash()
|
|
},
|
|
}
|
|
|
|
h := newNativeHistogram("my_histogram", []float64{0.1, 0.2}, lifecycler, "trace_id", HistogramModeBoth, nil, testTenant, &mockOverrides{}, 15*time.Minute)
|
|
|
|
lv := buildTestLabels([]string{"label"}, []string{"value-1"})
|
|
|
|
h.ObserveWithExemplar(lv, 1.0, "trace-1", 1.0)
|
|
h.ObserveWithExemplar(lv, 1.1, "trace-1", 1.0)
|
|
// In BOTH mode, a single histogram series contributes classic (sum+count+buckets including +Inf)
|
|
// plus one native series.
|
|
assert.Equal(t, int(h.activeSeriesPerHistogramSerie()), seriesAdded)
|
|
}
|
|
|
|
func Test_Histograms(t *testing.T) {
|
|
// A single observation has a labelset, a value, and a multiplier.
|
|
type observations []struct {
|
|
lbls labels.Labels
|
|
value float64
|
|
multiplier float64
|
|
traceID string
|
|
}
|
|
|
|
// A single collection has a few observations, and some expectations. This
|
|
// allows the test to track the accumulation of observations over a series of
|
|
// collections.
|
|
type collections []struct {
|
|
observations observations
|
|
expectedSamples []sample
|
|
expectedExemplars []exemplarSample
|
|
}
|
|
|
|
collectionTimeMs := time.Now().UnixMilli()
|
|
endOfLastMinuteMs := getEndOfLastMinuteMs(collectionTimeMs)
|
|
|
|
cases := []struct {
|
|
name string
|
|
buckets []float64
|
|
collections collections
|
|
// native histogram does not support all features yet
|
|
skipNativeHistogram bool
|
|
}{
|
|
{
|
|
name: "single collection single observation",
|
|
buckets: []float64{1, 2},
|
|
collections: collections{
|
|
{
|
|
observations: observations{
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-1"}),
|
|
value: 1.0,
|
|
multiplier: 1.0,
|
|
traceID: "trace-1",
|
|
},
|
|
},
|
|
expectedSamples: []sample{
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-1"}, endOfLastMinuteMs, 0), // zero count at the beginning
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "+Inf"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "+Inf"}, collectionTimeMs, 1),
|
|
},
|
|
expectedExemplars: []exemplarSample{
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-1"}),
|
|
Value: 1.0,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "single collection double observation",
|
|
buckets: []float64{1, 2},
|
|
collections: collections{
|
|
{
|
|
observations: observations{
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-1"}),
|
|
value: 1.0,
|
|
multiplier: 1.0,
|
|
traceID: "trace-1",
|
|
},
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-2"}),
|
|
value: 1.5,
|
|
multiplier: 1.0,
|
|
traceID: "trace-2",
|
|
},
|
|
},
|
|
expectedSamples: []sample{
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-1"}, endOfLastMinuteMs, 0), // zero count at the beginning
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "+Inf"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "+Inf"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-2"}, endOfLastMinuteMs, 0), // zero count at the beginning
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-2"}, collectionTimeMs, 1.5),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "1"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "1"}, collectionTimeMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, collectionTimeMs, 1),
|
|
},
|
|
expectedExemplars: []exemplarSample{
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-1"}),
|
|
Value: 1.0,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-2"}),
|
|
Value: 1.5,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "double collection double observation",
|
|
buckets: []float64{1, 2},
|
|
collections: collections{
|
|
{
|
|
observations: observations{
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-1"}),
|
|
value: 1.0,
|
|
multiplier: 1.0,
|
|
traceID: "trace-1",
|
|
},
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-2"}),
|
|
value: 1.5,
|
|
multiplier: 1.0,
|
|
traceID: "trace-2",
|
|
},
|
|
},
|
|
expectedSamples: []sample{
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-1"}, endOfLastMinuteMs, 0), // zero count at the beginning
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "+Inf"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "+Inf"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-2"}, endOfLastMinuteMs, 0), // zero count at the beginning
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-2"}, collectionTimeMs, 1.5),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "1"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "1"}, collectionTimeMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, collectionTimeMs, 1),
|
|
},
|
|
expectedExemplars: []exemplarSample{
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-1"}),
|
|
Value: 1.0,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-2"}),
|
|
Value: 1.5,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
observations: observations{
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-2"}),
|
|
value: 2.5,
|
|
multiplier: 1.0,
|
|
traceID: "trace-2",
|
|
},
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-3"}),
|
|
value: 3.0,
|
|
multiplier: 1.0,
|
|
traceID: "trace-3",
|
|
},
|
|
},
|
|
expectedSamples: []sample{
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "+Inf"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-2"}, collectionTimeMs, 2),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-2"}, collectionTimeMs, 4),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "1"}, collectionTimeMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, collectionTimeMs, 2),
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-3"}, endOfLastMinuteMs, 0), // zero count at the beginning
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-3"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-3"}, collectionTimeMs, 3),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "1"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "1"}, collectionTimeMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "2"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "2"}, collectionTimeMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "+Inf"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "+Inf"}, collectionTimeMs, 1),
|
|
},
|
|
expectedExemplars: []exemplarSample{
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-2"}),
|
|
Value: 2.5,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "+Inf"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-3"}),
|
|
Value: 3,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "integer multiplier",
|
|
buckets: []float64{1, 2},
|
|
collections: collections{
|
|
{
|
|
observations: observations{
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-1"}),
|
|
value: 1.5,
|
|
multiplier: 20.0,
|
|
traceID: "trace-1",
|
|
},
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-2"}),
|
|
value: 3.0,
|
|
multiplier: 13,
|
|
traceID: "trace-2",
|
|
},
|
|
},
|
|
expectedSamples: []sample{
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-1"}, endOfLastMinuteMs, 0), // zero count at the beginning
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-1"}, collectionTimeMs, 20),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-1"}, collectionTimeMs, 20*1.5),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, collectionTimeMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, collectionTimeMs, 20),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "+Inf"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "+Inf"}, collectionTimeMs, 20),
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-2"}, endOfLastMinuteMs, 0), // zero count at the beginning
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-2"}, collectionTimeMs, 13),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-2"}, collectionTimeMs, 13*3),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "1"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "1"}, collectionTimeMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, collectionTimeMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, collectionTimeMs, 13),
|
|
},
|
|
expectedExemplars: []exemplarSample{
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-1"}),
|
|
Value: 1.5,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-2"}),
|
|
Value: 3,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "many observations with floating point multiplier",
|
|
skipNativeHistogram: true,
|
|
buckets: []float64{1, 2},
|
|
collections: collections{
|
|
{
|
|
observations: observations{
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-1"}),
|
|
value: 1.0,
|
|
multiplier: 1.0,
|
|
traceID: "trace-1",
|
|
},
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-2"}),
|
|
value: 1.5,
|
|
multiplier: 1.0,
|
|
traceID: "trace-2",
|
|
},
|
|
},
|
|
expectedSamples: []sample{
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-1"}, endOfLastMinuteMs, 0), // zero count at the beginning
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "+Inf"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "+Inf"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-2"}, endOfLastMinuteMs, 0), // zero count at the beginning
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-2"}, collectionTimeMs, 1.5),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "1"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "1"}, collectionTimeMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, collectionTimeMs, 1),
|
|
},
|
|
expectedExemplars: []exemplarSample{
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-1"}),
|
|
Value: 1.0,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-2"}),
|
|
Value: 1.5,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
observations: observations{
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-2"}),
|
|
value: 2.5,
|
|
multiplier: 1.0,
|
|
traceID: "trace-2",
|
|
},
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-3"}),
|
|
value: 3.0,
|
|
multiplier: 1.0,
|
|
traceID: "trace-3",
|
|
},
|
|
},
|
|
expectedSamples: []sample{
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "+Inf"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-2"}, collectionTimeMs, 2),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-2"}, collectionTimeMs, 4),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "1"}, collectionTimeMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, collectionTimeMs, 2),
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-3"}, endOfLastMinuteMs, 0), // zero count at the beginning
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-3"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-3"}, collectionTimeMs, 3),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "1"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "1"}, collectionTimeMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "2"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "2"}, collectionTimeMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "+Inf"}, endOfLastMinuteMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "+Inf"}, collectionTimeMs, 1),
|
|
},
|
|
expectedExemplars: []exemplarSample{
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-2"}),
|
|
Value: 2.5,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "+Inf"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-3"}),
|
|
Value: 3,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
observations: observations{
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-2"}),
|
|
value: 2.5,
|
|
multiplier: 20.0,
|
|
traceID: "trace-2.2",
|
|
},
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-3"}),
|
|
value: 3.0,
|
|
multiplier: 13.5,
|
|
traceID: "trace-3",
|
|
},
|
|
{
|
|
lbls: buildTestLabels([]string{"label"}, []string{"value-3"}),
|
|
value: 1.0,
|
|
multiplier: 7.5,
|
|
traceID: "trace-3.3",
|
|
},
|
|
},
|
|
expectedSamples: []sample{
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "1"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-1", "le": "+Inf"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-2"}, collectionTimeMs, 22),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-2"}, collectionTimeMs, 54.0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "1"}, collectionTimeMs, 0),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "2"}, collectionTimeMs, 1),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, collectionTimeMs, 22),
|
|
newSample(map[string]string{"__name__": "test_histogram_count", "label": "value-3"}, collectionTimeMs, 22),
|
|
newSample(map[string]string{"__name__": "test_histogram_sum", "label": "value-3"}, collectionTimeMs, 51),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "1"}, collectionTimeMs, 7.5),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "2"}, collectionTimeMs, 7.5),
|
|
newSample(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "+Inf"}, collectionTimeMs, 22.0),
|
|
},
|
|
expectedExemplars: []exemplarSample{
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-2", "le": "+Inf"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-2.2"}),
|
|
Value: 2.5,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "+Inf"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-3"}),
|
|
Value: 3,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
newExemplar(map[string]string{"__name__": "test_histogram_bucket", "label": "value-3", "le": "1"}, exemplar.Exemplar{
|
|
Labels: labels.FromMap(map[string]string{"trace_id": "trace-3.3"}),
|
|
Value: 1,
|
|
Ts: collectionTimeMs,
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Tests a single histogram implementation.
|
|
testHistogram := func(t *testing.T, h Histogram, collections collections) {
|
|
for _, c := range collections {
|
|
appender := &capturingAppender{}
|
|
for _, obs := range c.observations {
|
|
h.ObserveWithExemplar(obs.lbls, obs.value, obs.traceID, obs.multiplier)
|
|
}
|
|
|
|
expected := expectedSeriesLen(c.expectedSamples)
|
|
// If we're testing the native histogram in BOTH or NATIVE mode, the active
|
|
// series includes an additional native series per base labelset.
|
|
if _, ok := h.(*nativeHistogram); ok {
|
|
expected += expectedBaseSeriesCount(c.expectedSamples)
|
|
}
|
|
|
|
collectMetricsAndAssertSeries(t, h, collectionTimeMs, expected, appender)
|
|
if len(c.expectedSamples) > 0 {
|
|
assertAppenderSamples(t, appender, c.expectedSamples)
|
|
}
|
|
if len(c.expectedExemplars) > 0 {
|
|
assertAppenderExemplars(t, appender, c.expectedExemplars)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tests both classic and native histograms.
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Run("classic", func(t *testing.T) {
|
|
h := newHistogram("test_histogram", tc.buckets, noopLimiter, "trace_id", nil, 15*time.Minute)
|
|
testHistogram(t, h, tc.collections)
|
|
})
|
|
t.Run("native", func(t *testing.T) {
|
|
if tc.skipNativeHistogram {
|
|
t.SkipNow()
|
|
}
|
|
|
|
h := newNativeHistogram("test_histogram", tc.buckets, noopLimiter, "trace_id", HistogramModeBoth, nil, testTenant, &mockOverrides{}, 15*time.Minute)
|
|
testHistogram(t, h, tc.collections)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func collectMetricsAndAssertSeries(t *testing.T, m metric, collectionTimeMs int64, expectedSeries int, appender storage.Appender) {
|
|
err := m.collectMetrics(appender, collectionTimeMs)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func assertAppenderSamples(t *testing.T, appender *capturingAppender, expectedSamples []sample) {
|
|
t.Run("Samples", func(t *testing.T) {
|
|
fmt.Println("Expected samples:")
|
|
for _, expectedSample := range expectedSamples {
|
|
fmt.Println(" - ", expectedSample.l, expectedSample.v)
|
|
}
|
|
fmt.Println("Appender samples:")
|
|
for _, sample := range appender.samples {
|
|
fmt.Println(" - ", sample.l, sample.v)
|
|
}
|
|
|
|
require.Len(t, appender.samples, len(expectedSamples))
|
|
// for i, expected := range expectedSamples {
|
|
// assert.Equal(t, expected, appender.samples[i])
|
|
// }
|
|
|
|
sort.Slice(expectedSamples[:], func(i, j int) bool {
|
|
return expectedSamples[i].l.String() < expectedSamples[j].l.String()
|
|
})
|
|
// t.Log("Expected samples:")
|
|
// for _, expectedSample := range expectedSamples {
|
|
// t.Log(" - ", expectedSample.l, expectedSample.v, expectedSample.t)
|
|
// }
|
|
|
|
sort.Slice(appender.samples[:], func(i, j int) bool {
|
|
return appender.samples[i].l.String() < appender.samples[j].l.String()
|
|
})
|
|
|
|
// t.Log("Appender samples:")
|
|
// for _, sample := range appender.samples {
|
|
// t.Log(" - ", sample.l, sample.v, sample.t)
|
|
// }
|
|
|
|
matched := assert.ElementsMatchf(t, expectedSamples, appender.samples, "samples mismatch")
|
|
if !matched {
|
|
for i, expected := range expectedSamples {
|
|
if expected.l.String() != appender.samples[i].l.String() || expected.v != appender.samples[i].v || expected.t != appender.samples[i].t {
|
|
t.Log("Mismatch at index", i)
|
|
t.Log("Expected: ", expected.l, expected.v, expected.t)
|
|
t.Log("Actual: ", appender.samples[i].l, appender.samples[i].v, appender.samples[i].t)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func assertAppenderExemplars(t *testing.T, appender *capturingAppender, expected []exemplarSample) {
|
|
t.Run("Exemplars", func(t *testing.T) {
|
|
require.Len(t, appender.exemplars, len(expected))
|
|
sort.Slice(expected[:], func(i, j int) bool {
|
|
return expected[i].l.String() < expected[j].l.String()
|
|
})
|
|
|
|
sort.Slice(appender.exemplars[:], func(i, j int) bool {
|
|
return appender.exemplars[i].l.String() < appender.exemplars[j].l.String()
|
|
})
|
|
|
|
matched := assert.ElementsMatchf(t, expected, appender.exemplars, "exemplars mismatch")
|
|
if !matched {
|
|
for i, expected := range expected {
|
|
if expected.l.String() != appender.exemplars[i].l.String() || !expected.e.Equals(appender.exemplars[i].e) {
|
|
t.Log("Mismatch at index", i)
|
|
t.Log("Expected: ", expected.l, expected.e)
|
|
t.Log("Actual: ", appender.exemplars[i].l, appender.exemplars[i].e)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func expectedSeriesLen(samples []sample) int {
|
|
series := make(map[string]struct{})
|
|
for _, s := range samples {
|
|
series[s.l.String()] = struct{}{}
|
|
}
|
|
return len(series)
|
|
}
|
|
|
|
// expectedBaseSeriesCount returns the number of distinct base labelsets in the
|
|
// expected classic samples by collapsing away metric name and bucket labels.
|
|
// This approximates the number of native histogram series emitted in addition
|
|
// to the classic series when using BOTH or NATIVE modes.
|
|
func expectedBaseSeriesCount(samples []sample) int {
|
|
base := make(map[string]struct{})
|
|
for _, s := range samples {
|
|
lb := labels.NewBuilder(s.l)
|
|
lb.Del(labels.MetricName)
|
|
lb.Del(labels.BucketLabel)
|
|
key := lb.Labels().String()
|
|
base[key] = struct{}{}
|
|
}
|
|
return len(base)
|
|
}
|
|
|
|
// Test specifically for native-only mode to ensure exemplars work
|
|
func Test_NativeOnlyExemplars(t *testing.T) {
|
|
buckets := []float64{1, 2}
|
|
collectionTimeMs := time.Now().UnixMilli()
|
|
|
|
t.Run("native_only_with_exemplars", func(t *testing.T) {
|
|
overrides := &mockOverrides{
|
|
nativeHistogramBucketFactor: 1.5,
|
|
nativeHistogramMaxBucketNumber: 10,
|
|
nativeHistogramMinResetDuration: time.Minute,
|
|
}
|
|
|
|
// Use HistogramModeNative to test native-only behavior
|
|
h := newNativeHistogram("test_native_histogram", buckets, noopLimiter, "trace_id", HistogramModeNative, nil, testTenant, overrides, 15*time.Minute)
|
|
|
|
// Add some observations with exemplars
|
|
lbls := buildTestLabels([]string{"service"}, []string{"test-service"})
|
|
h.ObserveWithExemplar(lbls, 1.5, "trace-123", 1.0)
|
|
h.ObserveWithExemplar(lbls, 0.5, "trace-456", 1.0)
|
|
|
|
// Collect metrics
|
|
appender := &capturingAppender{}
|
|
err := h.collectMetrics(appender, collectionTimeMs)
|
|
require.NoError(t, err)
|
|
t.Logf("Captured samples: %d", len(appender.samples))
|
|
t.Logf("Captured exemplars: %d", len(appender.exemplars))
|
|
|
|
for i, ex := range appender.exemplars {
|
|
t.Logf("Exemplar %d: labels=%v, value=%f, trace=%v", i, ex.l, ex.e.Value, ex.e.Labels)
|
|
}
|
|
|
|
// We should have exemplars
|
|
require.Greater(t, len(appender.exemplars), 0, "Native-only mode should capture exemplars")
|
|
|
|
// Verify that exemplars have the correct format for native histograms
|
|
for _, ex := range appender.exemplars {
|
|
// Exemplars should be attached to the main histogram metric, not bucket metrics
|
|
require.Equal(t, "test_native_histogram", ex.l.Get(labels.MetricName), "Exemplar should be attached to main histogram metric")
|
|
require.Contains(t, ex.e.Labels.String(), "trace_id", "Exemplar should contain trace_id")
|
|
require.Greater(t, ex.e.Value, 0.0, "Exemplar should have a value")
|
|
}
|
|
|
|
require.Len(t, appender.exemplars, 2, "Should have exactly 2 exemplars for 2 observations")
|
|
|
|
require.Equal(t, "trace-456", appender.exemplars[0].e.Labels.Get("trace_id"))
|
|
require.Equal(t, 0.5, appender.exemplars[0].e.Value)
|
|
require.Equal(t, "trace-123", appender.exemplars[1].e.Labels.Get("trace_id"))
|
|
require.Equal(t, 1.5, appender.exemplars[1].e.Value)
|
|
})
|
|
|
|
t.Run("native_only_histogram_exemplars", func(t *testing.T) {
|
|
overrides := &mockOverrides{
|
|
nativeHistogramBucketFactor: 1.5,
|
|
nativeHistogramMaxBucketNumber: 10,
|
|
nativeHistogramMinResetDuration: time.Minute,
|
|
}
|
|
|
|
// Create a native histogram with empty buckets to force native-only mode
|
|
h := newNativeHistogram("test_native_only", []float64{}, noopLimiter, "trace_id", HistogramModeNative, nil, testTenant, overrides, 15*time.Minute)
|
|
|
|
// Add some observations with exemplars
|
|
lbls := buildTestLabels([]string{"service"}, []string{"native-only-xyz"})
|
|
h.ObserveWithExemplar(lbls, 1.5, "trace-native-123", 1.0)
|
|
h.ObserveWithExemplar(lbls, 0.5, "trace-native-456", 1.0)
|
|
|
|
// Collect metrics
|
|
appender := &capturingAppender{}
|
|
err := h.collectMetrics(appender, collectionTimeMs)
|
|
require.NoError(t, err)
|
|
t.Logf("Native-only - Captured samples: %d", len(appender.samples))
|
|
t.Logf("Native-only - Captured exemplars: %d", len(appender.exemplars))
|
|
|
|
// This test might still use bucket exemplars due to Prometheus's default behavior,
|
|
// but it helps us understand the difference
|
|
|
|
require.Len(t, appender.exemplars, 2, "Should have exactly 2 exemplars for 2 observations")
|
|
|
|
for i, ex := range appender.exemplars {
|
|
t.Logf("Native-only exemplar %d: labels=%v, value=%f, trace=%v", i, ex.l, ex.e.Value, ex.e.Labels)
|
|
}
|
|
|
|
require.Equal(t, "trace-native-456", appender.exemplars[0].e.Labels.Get("trace_id"))
|
|
require.Equal(t, 0.5, appender.exemplars[0].e.Value)
|
|
require.Equal(t, "trace-native-123", appender.exemplars[1].e.Labels.Get("trace_id"))
|
|
require.Equal(t, 1.5, appender.exemplars[1].e.Value)
|
|
})
|
|
}
|
|
|
|
func Test_nativeHistogram_demandTracking(t *testing.T) {
|
|
h := newNativeHistogram("my_histogram", []float64{1.0, 2.0}, noopLimiter, "", HistogramModeBoth, nil, testTenant, &mockOverrides{}, 15*time.Minute)
|
|
|
|
// Initially, demand should be 0
|
|
assert.Equal(t, 0, h.countSeriesDemand())
|
|
|
|
// Add some histogram series
|
|
for i := 0; i < 20; i++ {
|
|
lbls := buildTestLabels([]string{"label"}, []string{fmt.Sprintf("value-%d", i)})
|
|
h.ObserveWithExemplar(lbls, 1.5, "", 1.0)
|
|
}
|
|
|
|
// In BOTH mode: sum, count, 3 buckets (classic) + 1 native = 6 series per histogram
|
|
// 20 histograms * 6 = 120 total series (within HLL error)
|
|
demand := h.countSeriesDemand()
|
|
assert.Greater(t, demand, 100, "demand should be close to 120")
|
|
assert.Less(t, demand, 140, "demand should be close to 120")
|
|
|
|
// Active series should exactly match
|
|
expectedActive := 20 * int(h.activeSeriesPerHistogramSerie())
|
|
assert.Equal(t, expectedActive, h.countActiveSeries())
|
|
}
|
|
|
|
func Test_nativeHistogram_activeSeriesPerHistogramSerie(t *testing.T) {
|
|
// Test BOTH mode with 2 buckets: sum, count, bucket1, bucket2, +Inf, native = 6
|
|
h := newNativeHistogram("my_histogram", []float64{1.0, 2.0}, noopLimiter, "", HistogramModeBoth, nil, testTenant, &mockOverrides{}, 15*time.Minute)
|
|
assert.Equal(t, uint32(6), h.activeSeriesPerHistogramSerie(), "BOTH mode should be classic + native")
|
|
|
|
// Test NATIVE mode only: 1 native histogram series
|
|
h2 := newNativeHistogram("my_histogram", []float64{1.0, 2.0}, noopLimiter, "", HistogramModeNative, nil, testTenant, &mockOverrides{}, 15*time.Minute)
|
|
assert.Equal(t, uint32(1), h2.activeSeriesPerHistogramSerie(), "NATIVE mode should be 1 series")
|
|
|
|
// Test CLASSIC mode with 2 buckets: sum, count, bucket1, bucket2, +Inf = 5
|
|
h3 := newNativeHistogram("my_histogram", []float64{1.0, 2.0}, noopLimiter, "", HistogramModeClassic, nil, testTenant, &mockOverrides{}, 15*time.Minute)
|
|
assert.Equal(t, uint32(5), h3.activeSeriesPerHistogramSerie(), "CLASSIC mode should be sum + count + buckets")
|
|
|
|
// Test BOTH mode with 3 buckets: sum, count, 4 buckets, native = 7
|
|
h4 := newNativeHistogram("my_histogram", []float64{1.0, 2.0, 3.0}, noopLimiter, "", HistogramModeBoth, nil, testTenant, &mockOverrides{}, 15*time.Minute)
|
|
assert.Equal(t, uint32(7), h4.activeSeriesPerHistogramSerie(), "BOTH mode with 3 buckets")
|
|
}
|
|
|
|
func Test_nativeHistogram_demandVsActiveSeries(t *testing.T) {
|
|
limitReached := false
|
|
overflowLabels := labels.FromStrings("metric_overflow", "true")
|
|
overflowHash := overflowLabels.Hash()
|
|
lifecycler := &mockLimiter{
|
|
onAddFunc: func(hash uint64, _ uint32, lbls labels.Labels) (labels.Labels, uint64) {
|
|
if !limitReached {
|
|
return lbls, hash
|
|
}
|
|
return overflowLabels, overflowHash
|
|
},
|
|
}
|
|
|
|
h := newNativeHistogram("my_histogram", []float64{1.0, 2.0}, lifecycler, "", HistogramModeNative, nil, testTenant, &mockOverrides{}, 15*time.Minute)
|
|
|
|
// Add some histogram series
|
|
for i := 0; i < 10; i++ {
|
|
lbls := buildTestLabels([]string{"label"}, []string{fmt.Sprintf("value-%d", i)})
|
|
h.ObserveWithExemplar(lbls, 1.5, "", 1.0)
|
|
}
|
|
|
|
expectedActive := 10 * int(h.activeSeriesPerHistogramSerie())
|
|
assert.Equal(t, expectedActive, h.countActiveSeries())
|
|
|
|
// Hit the limit
|
|
limitReached = true
|
|
|
|
// Try to add more series (they should be mapped to overflow)
|
|
for i := 10; i < 20; i++ {
|
|
lbls := buildTestLabels([]string{"label"}, []string{fmt.Sprintf("value-%d", i)})
|
|
h.ObserveWithExemplar(lbls, 1.5, "", 1.0)
|
|
}
|
|
|
|
// Active series should have increased by 1 overflow series (1 series for native histogram)
|
|
expectedOverflowSeries := int(h.activeSeriesPerHistogramSerie())
|
|
assert.Equal(t, expectedActive+expectedOverflowSeries, h.countActiveSeries())
|
|
|
|
// But demand should show all attempted series
|
|
demand := h.countSeriesDemand()
|
|
expectedDemand := 20 * int(h.activeSeriesPerHistogramSerie())
|
|
assert.Greater(t, demand, expectedDemand-10, "demand should track all attempted series")
|
|
assert.Greater(t, demand, h.countActiveSeries(), "demand should exceed active series")
|
|
}
|
|
|
|
func Test_nativeHistogram_onUpdate(t *testing.T) {
|
|
// Test BOTH mode (classic + native)
|
|
t.Run("both_mode", func(t *testing.T) {
|
|
var seriesUpdated int
|
|
lifecycler := &mockLimiter{
|
|
onAddFunc: func(_ uint64, count uint32, lbls labels.Labels) (labels.Labels, uint64) {
|
|
// BOTH mode with 2 buckets: sum, count, bucket1, bucket2, +Inf, native = 6
|
|
assert.Equal(t, uint32(6), count)
|
|
return lbls, lbls.Hash()
|
|
},
|
|
onUpdateFunc: func(_ uint64, count uint32) {
|
|
// BOTH mode with 2 buckets: sum, count, bucket1, bucket2, +Inf, native = 6
|
|
assert.Equal(t, uint32(6), count)
|
|
seriesUpdated++
|
|
},
|
|
}
|
|
|
|
h := newNativeHistogram("my_histogram", []float64{1.0, 2.0}, lifecycler, "", HistogramModeBoth, nil, testTenant, &mockOverrides{}, 15*time.Minute)
|
|
|
|
// Add initial series (first observation triggers both OnAdd and OnUpdate)
|
|
h.ObserveWithExemplar(buildTestLabels([]string{"label"}, []string{"value-1"}), 1.0, "", 1.0)
|
|
h.ObserveWithExemplar(buildTestLabels([]string{"label"}, []string{"value-2"}), 1.5, "", 1.0)
|
|
|
|
initialUpdates := seriesUpdated
|
|
assert.Equal(t, 2, initialUpdates, "First observations should trigger OnUpdate")
|
|
|
|
// Update existing series
|
|
h.ObserveWithExemplar(buildTestLabels([]string{"label"}, []string{"value-1"}), 1.5, "", 1.0)
|
|
assert.Equal(t, initialUpdates+1, seriesUpdated)
|
|
|
|
// Update both series
|
|
h.ObserveWithExemplar(buildTestLabels([]string{"label"}, []string{"value-1"}), 2.0, "", 1.0)
|
|
h.ObserveWithExemplar(buildTestLabels([]string{"label"}, []string{"value-2"}), 2.5, "", 1.0)
|
|
assert.Equal(t, initialUpdates+3, seriesUpdated)
|
|
})
|
|
|
|
// Test NATIVE mode only
|
|
t.Run("native_mode", func(t *testing.T) {
|
|
var seriesUpdated int
|
|
lifecycler := &mockLimiter{
|
|
onAddFunc: func(_ uint64, count uint32, lbls labels.Labels) (labels.Labels, uint64) {
|
|
// NATIVE mode: only 1 native histogram series
|
|
assert.Equal(t, uint32(1), count)
|
|
return lbls, lbls.Hash()
|
|
},
|
|
onUpdateFunc: func(_ uint64, count uint32) {
|
|
assert.Equal(t, uint32(1), count)
|
|
seriesUpdated++
|
|
},
|
|
}
|
|
|
|
h := newNativeHistogram("my_histogram", []float64{1.0, 2.0}, lifecycler, "", HistogramModeNative, nil, testTenant, &mockOverrides{}, 15*time.Minute)
|
|
|
|
// Add initial series (first observation triggers both OnAdd and OnUpdate)
|
|
h.ObserveWithExemplar(buildTestLabels([]string{"label"}, []string{"value-1"}), 1.0, "", 1.0)
|
|
|
|
initialUpdates := seriesUpdated
|
|
assert.Equal(t, 1, initialUpdates, "First observation should trigger OnUpdate")
|
|
|
|
// Update existing series multiple times
|
|
h.ObserveWithExemplar(buildTestLabels([]string{"label"}, []string{"value-1"}), 1.5, "", 1.0)
|
|
assert.Equal(t, initialUpdates+1, seriesUpdated)
|
|
|
|
h.ObserveWithExemplar(buildTestLabels([]string{"label"}, []string{"value-1"}), 2.0, "", 1.0)
|
|
assert.Equal(t, initialUpdates+2, seriesUpdated)
|
|
|
|
h.ObserveWithExemplar(buildTestLabels([]string{"label"}, []string{"value-1"}), 2.5, "", 1.0)
|
|
assert.Equal(t, initialUpdates+3, seriesUpdated)
|
|
})
|
|
|
|
// Test CLASSIC mode
|
|
t.Run("classic_mode", func(t *testing.T) {
|
|
var seriesUpdated int
|
|
lifecycler := &mockLimiter{
|
|
onAddFunc: func(_ uint64, count uint32, lbls labels.Labels) (labels.Labels, uint64) {
|
|
// CLASSIC mode with 2 buckets: sum, count, bucket1, bucket2, +Inf = 5
|
|
assert.Equal(t, uint32(5), count)
|
|
return lbls, lbls.Hash()
|
|
},
|
|
onUpdateFunc: func(_ uint64, count uint32) {
|
|
// CLASSIC mode with 2 buckets: sum, count, bucket1, bucket2, +Inf = 5
|
|
assert.Equal(t, uint32(5), count)
|
|
seriesUpdated++
|
|
},
|
|
}
|
|
|
|
h := newNativeHistogram("my_histogram", []float64{1.0, 2.0}, lifecycler, "", HistogramModeClassic, nil, testTenant, &mockOverrides{}, 15*time.Minute)
|
|
|
|
// Add initial series (first observation triggers both OnAdd and OnUpdate)
|
|
h.ObserveWithExemplar(buildTestLabels([]string{"label"}, []string{"value-1"}), 1.0, "", 1.0)
|
|
|
|
initialUpdates := seriesUpdated
|
|
assert.Equal(t, 1, initialUpdates, "First observation should trigger OnUpdate")
|
|
|
|
// Update existing series
|
|
h.ObserveWithExemplar(buildTestLabels([]string{"label"}, []string{"value-1"}), 1.5, "", 1.0)
|
|
assert.Equal(t, initialUpdates+1, seriesUpdated)
|
|
|
|
h.ObserveWithExemplar(buildTestLabels([]string{"label"}, []string{"value-1"}), 2.0, "", 1.0)
|
|
assert.Equal(t, initialUpdates+2, seriesUpdated)
|
|
})
|
|
}
|
|
|
|
// Test that native histograms emit a leading zero for new series.
|
|
// This avoids Prometheus discarding the first value when transitioning from null -> x.
|
|
func Test_nativeHistogram_leadingZero(t *testing.T) {
|
|
collectionTimeMs := time.Now().UnixMilli()
|
|
endOfLastMinuteMs := getEndOfLastMinuteMs(collectionTimeMs)
|
|
|
|
t.Run("native_mode", func(t *testing.T) {
|
|
h := newNativeHistogram("test_histogram", []float64{}, noopLimiter, "trace_id", HistogramModeNative, nil, testTenant, &mockOverrides{}, 15*time.Minute)
|
|
|
|
lbls := buildTestLabels([]string{"label"}, []string{"value-1"})
|
|
h.ObserveWithExemplar(lbls, 1.5, "trace-1", 1.0)
|
|
|
|
// First collection: leading zero + actual value
|
|
appender1 := &capturingAppender{}
|
|
err := h.collectMetrics(appender1, collectionTimeMs)
|
|
require.NoError(t, err)
|
|
require.Len(t, appender1.histograms, 2, "First collection should have 2 histograms")
|
|
|
|
assert.Equal(t, endOfLastMinuteMs, appender1.histograms[0].t)
|
|
assert.Equal(t, uint64(0), appender1.histograms[0].h.Count)
|
|
assert.Equal(t, collectionTimeMs, appender1.histograms[1].t)
|
|
assert.Equal(t, uint64(1), appender1.histograms[1].h.Count)
|
|
|
|
// Second collection: no leading zero
|
|
h.ObserveWithExemplar(lbls, 2.5, "trace-2", 1.0)
|
|
appender2 := &capturingAppender{}
|
|
err = h.collectMetrics(appender2, collectionTimeMs+1000)
|
|
require.NoError(t, err)
|
|
require.Len(t, appender2.histograms, 1, "Second collection should have only 1 histogram")
|
|
assert.Equal(t, uint64(2), appender2.histograms[0].h.Count)
|
|
})
|
|
|
|
t.Run("both_mode", func(t *testing.T) {
|
|
h := newNativeHistogram("test_histogram", []float64{1, 2}, noopLimiter, "trace_id", HistogramModeBoth, nil, testTenant, &mockOverrides{}, 15*time.Minute)
|
|
|
|
lbls := buildTestLabels([]string{"label"}, []string{"value-1"})
|
|
h.ObserveWithExemplar(lbls, 1.5, "trace-1", 1.0)
|
|
|
|
// First collection: both native and classic should have leading zeros
|
|
appender1 := &capturingAppender{}
|
|
err := h.collectMetrics(appender1, collectionTimeMs)
|
|
require.NoError(t, err)
|
|
|
|
// Native: leading zero + actual
|
|
require.Len(t, appender1.histograms, 2)
|
|
assert.Equal(t, endOfLastMinuteMs, appender1.histograms[0].t)
|
|
assert.Equal(t, uint64(0), appender1.histograms[0].h.Count)
|
|
assert.Equal(t, collectionTimeMs, appender1.histograms[1].t)
|
|
assert.Equal(t, uint64(1), appender1.histograms[1].h.Count)
|
|
|
|
// Classic: verify leading zeros exist
|
|
hasClassicLeadingZero := false
|
|
for _, s := range appender1.samples {
|
|
if s.t == endOfLastMinuteMs && s.v == 0 {
|
|
hasClassicLeadingZero = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasClassicLeadingZero, "Classic histograms should have leading zeros")
|
|
|
|
// Second collection: no leading zeros for either
|
|
h.ObserveWithExemplar(lbls, 2.5, "trace-2", 1.0)
|
|
appender2 := &capturingAppender{}
|
|
err = h.collectMetrics(appender2, collectionTimeMs+1000)
|
|
require.NoError(t, err)
|
|
require.Len(t, appender2.histograms, 1, "Second collection should have only 1 native histogram")
|
|
|
|
for _, s := range appender2.samples {
|
|
assert.NotEqual(t, endOfLastMinuteMs, s.t, "Second collection should have no classic leading zeros")
|
|
}
|
|
})
|
|
}
|