Files
traces/pkg/traceql/engine_metrics_test.go
Martin Disibioandzeekay d037886dde Align traceql metrics default steps to match vp5 timestamp columns (#6413)
* Align traceql metrics default steps to match vp5 timestamp columns

* changelog

* Fix edge case for millisecond granularities
2026-02-22 00:55:22 -08:00

3617 lines
132 KiB
Go

package traceql
import (
"fmt"
"math"
"math/rand/v2"
"strconv"
"testing"
"time"
"github.com/grafana/tempo/pkg/tempopb"
commonv1proto "github.com/grafana/tempo/pkg/tempopb/common/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDefaultQueryRangeStep(t *testing.T) {
day := 24 * time.Hour
tc := []struct {
window time.Duration
expected time.Duration
}{
{59 * time.Second, 200 * time.Millisecond},
{time.Minute, time.Second},
{5 * time.Minute, 1 * time.Second},
{15 * time.Minute, 3 * time.Second},
{30 * time.Minute, 5 * time.Second},
{time.Hour, 15 * time.Second},
{3 * time.Hour, 45 * time.Second},
{6 * time.Hour, time.Minute},
{12 * time.Hour, 3 * time.Minute},
{day - time.Second, 5 * time.Minute}, // 23:59:59 - edge case - ensure this is treated the same as 24 hours
{day, 5 * time.Minute},
{2 * day, 10 * time.Minute},
{7 * day, 40 * time.Minute},
{14 * day, time.Hour},
{30 * day, 3 * time.Hour},
}
for _, c := range tc {
t.Run(c.window.String(), func(t *testing.T) {
require.Equal(t, c.expected, time.Duration(DefaultQueryRangeStep(0, uint64(c.window.Nanoseconds()))))
})
}
}
func TestStepRangeToIntervals(t *testing.T) {
tc := []struct {
start, end, step uint64
instant bool
expected int
}{
{
start: 10,
end: 11,
step: 1,
instant: false,
expected: 1,
},
{
start: 0,
end: 3,
step: 1,
instant: false,
expected: 3, // 1, 2, 3
},
{
start: 0,
end: 10,
step: 3,
instant: false,
expected: 4, // 3, 6, 9, 12
},
{
start: 0,
end: 10,
step: 3,
instant: true,
expected: 1, // for instant queries, always only one interval
},
}
for _, c := range tc {
mapper := NewIntervalMapper(c.start, c.end, c.step, c.instant)
require.Equal(t, c.expected, mapper.IntervalCount())
}
}
func TestTimestampOf(t *testing.T) {
tc := []struct {
interval int
start, end, step uint64
instant bool
expected uint64
}{
{
interval: 2,
start: 10, // aligned to 9
step: 3,
end: 100,
instant: false,
expected: 18, // 12, 15, 18 <-- intervals
},
// start <= step
{
interval: 0,
start: 1,
end: 10,
step: 1,
instant: false,
expected: 2,
},
{
interval: 1,
start: 1,
end: 5,
step: 1,
instant: false,
expected: 3,
},
{
interval: 4,
start: 1,
end: 5,
step: 1,
instant: false,
expected: 6,
},
// start > step
{
interval: 0,
start: 10,
end: 50,
step: 10,
instant: false,
expected: 20,
},
{
interval: 2,
start: 10,
end: 50,
step: 10,
instant: false,
expected: 40, // 3rd interval: (10;20] (20;30] (30;40]
},
{
interval: 3,
start: 10,
end: 50,
step: 10,
instant: false,
expected: 50,
},
{
interval: 0,
start: 10,
end: 20,
step: 10,
instant: false,
expected: 20,
},
{
interval: 0,
start: 10,
end: 20,
step: 10,
instant: true,
expected: 20,
},
{
interval: 0,
start: 10,
end: 15,
step: 100,
instant: true,
expected: 15, // not aligned
},
}
for _, c := range tc {
mapper := NewIntervalMapper(c.start, c.end, c.step, c.instant)
assert.Equal(t, c.expected, mapper.TimestampOf(c.interval), "interval: %d, start: %d, end: %d, step: %d", c.interval, c.start, c.end, c.step)
}
}
// TestTimestampOfIntervals checks that bucket timestamps are within the range.
func TestTimestampOfIntervals(t *testing.T) {
start := uint64(10)
end := uint64(100)
step := uint64(10)
mapper := NewIntervalMapper(start, end, step, false)
intervals := mapper.IntervalCount()
for i := range intervals {
ts := mapper.TimestampOf(i)
assert.True(t, ts > start && ts <= end, "ts: %d, start: %d, end: %d", ts, start, end)
}
}
func TestIntervalOf(t *testing.T) {
tc := []struct {
ts, start, end, step uint64
instant bool
expected int
}{
// start <= step
{
start: 1,
step: 1,
expected: -1,
},
{
start: 9,
step: 10,
expected: -1,
},
{
ts: 0,
end: 1,
step: 1,
instant: false,
expected: -1,
},
{
ts: 0,
start: 0,
end: 1,
step: 1,
instant: true,
expected: 0, // start and end are inclusive for instant queries
},
{
ts: 10,
start: 0,
end: 10,
step: 100,
instant: true,
expected: 0, // start and end are inclusive for instant queries
},
{
ts: 98,
start: 0,
end: 10,
step: 99,
instant: true,
expected: -1, // outside of range, range is not aligned
},
{
ts: 10,
start: 1,
end: 10,
step: 1,
instant: false,
expected: 8, // 9th interval: (9;10]
},
{
ts: 1,
start: 1,
end: 5,
step: 1,
instant: false,
expected: -1, // should be excluded
},
{
ts: 2,
start: 1,
end: 5,
step: 1,
instant: false,
expected: 0, // 2nd interval: (1;2]
},
// start > step
{
ts: 15,
start: 10,
end: 50,
step: 10,
instant: false,
expected: 0, // first interval: (10;20]
},
{
ts: 5,
start: 10,
end: 50,
step: 10,
instant: false,
expected: -1, // should be excluded
},
{
ts: 10,
start: 10,
end: 50,
step: 10,
instant: false,
expected: -1, // should be excluded
},
{
ts: 20,
start: 10,
end: 50,
step: 10,
instant: false,
expected: 0, // first interval: (10;20]
},
{
ts: 25,
start: 10,
end: 50,
step: 10,
instant: false,
expected: 1, // second interval: (20;30]
},
{
ts: 50,
start: 10,
end: 50,
step: 10,
instant: false,
expected: 3, // 4th interval: (40;50]
},
}
for _, c := range tc {
mapper := NewIntervalMapper(c.start, c.end, c.step, c.instant)
assert.Equal(t, c.expected, mapper.Interval(c.ts), "ts: %d, start: %d, end: %d, step: %d", c.ts, c.start, c.end, c.step)
}
}
func TestTrimToBlockOverlap(t *testing.T) {
tc := []struct {
start1, end1 string
step time.Duration
start2, end2 string
expectedStart, expectedEnd string
expectedStep time.Duration
}{
{
// Block fully within range
// Left border is extended to the next step.
// Right border is extended to the next step.
"2024-01-01T01:00:00Z", "2024-01-01T02:00:00Z", 5 * time.Minute,
"2024-01-01T01:33:00Z", "2024-01-01T01:38:00Z",
"2024-01-01T01:30:00Z", "2024-01-01T01:40:00Z", 5 * time.Minute,
},
{
// Block overlapping right border.
// Left border is extended to the next step.
// Right border preserved.
"2024-01-01T01:01:00Z", "2024-01-01T02:01:00.123Z", 5 * time.Minute,
"2024-01-01T01:31:00Z", "2024-01-01T02:31:00Z",
"2024-01-01T01:30:00Z", "2024-01-01T02:01:00.123Z", 5 * time.Minute,
},
{
// Block overlapping left border.
// Left border preserved.
// Right border extended to the next step.
"2024-01-01T01:01:00.123Z", "2024-01-01T02:00:00Z", 5 * time.Minute,
"2024-01-01T00:31:00Z", "2024-01-01T01:31:00Z",
"2024-01-01T01:01:00.123Z", "2024-01-01T01:35:00Z", 5 * time.Minute,
},
{
// Block larger than range
// Neither border is extended. Nanoseconds preserved.
"2024-01-01T01:00:01.123Z", "2024-01-01T01:15:01.123Z", 5 * time.Minute,
"2024-01-01T00:00:00Z", "2024-01-01T02:00:00Z",
"2024-01-01T01:00:01.123Z", "2024-01-01T01:15:01.123Z", 5 * time.Minute,
},
{
// Instant query, block overlaps right border.
// Original range is 1h
// Right border isn't extended past request range.
// Left border is able to be extended.
"2024-01-01T01:00:00.123Z", "2024-01-01T02:00:00.123Z", time.Hour,
"2024-01-01T01:30:00.123Z", "2024-01-01T02:30:00.123Z",
// we subtract a nanosecond to the start to make sure it's before the block start
"2024-01-01T01:30:00.122999999Z", "2024-01-01T02:00:00.123Z", 30*time.Minute + time.Nanosecond,
},
}
for _, c := range tc {
start1, _ := time.Parse(time.RFC3339Nano, c.start1)
end1, _ := time.Parse(time.RFC3339Nano, c.end1)
start2, _ := time.Parse(time.RFC3339Nano, c.start2)
end2, _ := time.Parse(time.RFC3339Nano, c.end2)
req := &tempopb.QueryRangeRequest{
Start: uint64(start1.UnixNano()),
End: uint64(end1.UnixNano()),
Step: uint64(c.step.Nanoseconds()),
}
actualStart, actualEnd, actualStep := TrimToBlockOverlap(
req,
start2,
end2,
)
assert.Equal(t, c.expectedStart, time.Unix(0, int64(actualStart)).UTC().Format(time.RFC3339Nano))
assert.Equal(t, c.expectedEnd, time.Unix(0, int64(actualEnd)).UTC().Format(time.RFC3339Nano))
assert.Equal(t, c.expectedStep, time.Duration(actualStep))
}
}
func TestTimeRangeOverlap(t *testing.T) {
tc := []struct {
reqStart, reqEnd, dataStart, dataEnd uint64
expected float64
}{
{1, 2, 3, 4, 0.0}, // No overlap
{0, 10, 0, 10, 1.0}, // Perfect overlap
{0, 10, 1, 2, 1.0}, // Request covers 100% of data
{3, 8, 0, 10, 0.5}, // 50% in the middle
{0, 10, 5, 15, 0.5}, // 50% of the start
{5, 15, 0, 10, 0.5}, // 50% of the end
}
for _, c := range tc {
actual := timeRangeOverlap(c.reqStart, c.reqEnd, c.dataStart, c.dataEnd)
require.Equal(t, c.expected, actual)
}
}
func TestCompileMetricsQueryRange(t *testing.T) {
tc := map[string]struct {
q string
start, end uint64
step uint64
expectedErr error
}{
"start": {
expectedErr: fmt.Errorf("start required"),
},
"end": {
start: 1,
expectedErr: fmt.Errorf("end required"),
},
"range": {
start: 2,
end: 1,
expectedErr: fmt.Errorf("end must be greater than start"),
},
"step": {
start: 1,
end: 2,
expectedErr: fmt.Errorf("step required"),
},
"notmetrics": {
start: 1,
end: 2,
step: 3,
q: "{}",
expectedErr: fmt.Errorf("not a metrics query"),
},
"notsupported": {
start: 1,
end: 2,
step: 3,
q: "{} | rate() by (.a,.b,.c,.d,.e,.f)",
expectedErr: fmt.Errorf("compiling query: metrics group by 6 values not yet supported"),
},
"ok": {
start: 1,
end: 2,
step: 3,
q: "{} | rate()",
},
}
for n, c := range tc {
t.Run(n, func(t *testing.T) {
_, err := NewEngine().CompileMetricsQueryRange(&tempopb.QueryRangeRequest{
Query: c.q,
Start: c.start,
End: c.end,
Step: c.step,
}, 0, 0, false)
if c.expectedErr != nil {
require.EqualError(t, err, c.expectedErr.Error())
}
})
}
}
func TestCompileMetricsQueryRangeExemplarsHint(t *testing.T) {
defaultExempalars := 5
tcs := []struct {
q string
expectedCount int
}{
{
q: "{} | rate() with(exemplars=10)",
expectedCount: 10,
},
{
q: "{} | rate() with(exemplars=false)",
expectedCount: 0,
},
{
q: "{} | rate() with(exemplars=true)",
expectedCount: defaultExempalars,
},
{
q: "{} | rate()",
expectedCount: defaultExempalars,
},
}
for _, tc := range tcs {
eval, err := NewEngine().CompileMetricsQueryRange(&tempopb.QueryRangeRequest{
Query: tc.q,
Start: 1,
End: 2,
Step: 1,
}, 5, 0, false)
require.NoError(t, err)
require.NotNil(t, eval)
require.Equal(t, tc.expectedCount, eval.maxExemplars)
}
}
func TestCompileMetricsQueryRangeFetchSpansRequest(t *testing.T) {
tc := map[string]struct {
q string
expectedReq FetchSpansRequest
}{
"minimal": {
q: "{} | rate()",
expectedReq: FetchSpansRequest{
AllConditions: true,
Conditions: []Condition{
{
// In this case start time is in the first pass
Attribute: IntrinsicSpanStartTimeAttribute,
},
},
},
},
"dedupe": {
q: "{} | rate()",
expectedReq: FetchSpansRequest{
AllConditions: true,
Conditions: []Condition{
{
Attribute: IntrinsicSpanStartTimeAttribute,
},
},
},
},
"secondPass": {
q: "{duration > 10s} | rate() by (resource.cluster)",
expectedReq: FetchSpansRequest{
AllConditions: true,
Conditions: []Condition{
{
Attribute: IntrinsicDurationAttribute,
Op: OpGreater,
Operands: Operands{NewStaticDuration(10 * time.Second)},
},
},
SecondPassConditions: []Condition{
{
// Group-by attributes (non-intrinsic) must be in the second pass
Attribute: NewScopedAttribute(AttributeScopeResource, false, "cluster"),
},
{
// Since there is already a second pass then span start time isn't optimized to the first pass.
Attribute: IntrinsicSpanStartTimeAttribute,
},
},
},
},
"optimizations": {
q: "{duration > 10s} | rate() by (name, resource.service.name)",
expectedReq: FetchSpansRequest{
AllConditions: true,
Conditions: []Condition{
{
Attribute: IntrinsicDurationAttribute,
Op: OpGreater,
Operands: Operands{NewStaticDuration(10 * time.Second)},
},
{
// Intrinsic moved to first pass
Attribute: IntrinsicNameAttribute,
},
{
// Resource service name is treated as an intrinsic and moved to the first pass
Attribute: NewScopedAttribute(AttributeScopeResource, false, "service.name"),
},
{
Attribute: IntrinsicSpanStartTimeAttribute,
},
},
},
},
"structural_rate_by": {
q: "{name=`foo`} > {} | rate() by (name)",
expectedReq: FetchSpansRequest{
AllConditions: false,
Conditions: []Condition{
{
Attribute: NewIntrinsic(IntrinsicStructuralChild),
},
{
Attribute: IntrinsicNameAttribute,
Op: OpEqual,
Operands: Operands{NewStaticString("foo")},
},
},
SecondPassConditions: []Condition{
{
Attribute: IntrinsicNameAttribute,
},
{
// Since there is already a second pass then span start time isn't optimized to the first pass.
Attribute: IntrinsicSpanStartTimeAttribute,
},
},
},
},
}
for n, tc := range tc {
t.Run(n, func(t *testing.T) {
eval, err := NewEngine().CompileMetricsQueryRange(&tempopb.QueryRangeRequest{
Query: tc.q,
Start: 1,
End: 2,
Step: 3,
}, 0, 0, false)
require.NoError(t, err)
// Nil out func to Equal works
eval.storageReq.SecondPass = nil
require.Equal(t, tc.expectedReq, *eval.storageReq)
})
}
}
func TestOptimizeFetchSpansRequest(t *testing.T) {
secondPass := func(_ *Spanset) ([]*Spanset, error) {
return nil, nil
}
tc := []struct {
name string
input FetchSpansRequest
expected FetchSpansRequest
}{
{
name: "Not able to be optimized because not all conditions",
input: FetchSpansRequest{
SecondPass: secondPass,
SecondPassConditions: []Condition{
{Attribute: IntrinsicNameAttribute},
{Attribute: NewScopedAttribute(AttributeScopeResource, false, "service.name")},
},
},
expected: FetchSpansRequest{
SecondPass: secondPass,
SecondPassConditions: []Condition{
{Attribute: IntrinsicNameAttribute},
{Attribute: NewScopedAttribute(AttributeScopeResource, false, "service.name")},
},
},
},
{
name: "Intrinsics moved to first pass and second pass eliminated",
input: FetchSpansRequest{
AllConditions: true,
SecondPass: secondPass,
SecondPassConditions: []Condition{
{Attribute: IntrinsicNameAttribute},
{Attribute: NewScopedAttribute(AttributeScopeResource, false, "service.name")},
},
},
expected: FetchSpansRequest{
AllConditions: true,
Conditions: []Condition{
{Attribute: IntrinsicNameAttribute},
{Attribute: NewScopedAttribute(AttributeScopeResource, false, "service.name")},
},
},
},
{
name: "Unscoped cannot be optimized",
input: FetchSpansRequest{
AllConditions: true,
Conditions: []Condition{
{Attribute: NewScopedAttribute(AttributeScopeNone, false, "http.status_code")},
},
SecondPass: secondPass,
},
expected: FetchSpansRequest{
AllConditions: true,
Conditions: []Condition{
{Attribute: NewScopedAttribute(AttributeScopeNone, false, "http.status_code")},
},
SecondPass: secondPass,
},
},
{
name: "Single scoped non-intrinsic can still elminiate second pass",
input: FetchSpansRequest{
AllConditions: true,
Conditions: []Condition{
{Attribute: NewScopedAttribute(AttributeScopeSpan, false, "http.status_code")},
},
SecondPass: secondPass,
},
expected: FetchSpansRequest{
AllConditions: true,
Conditions: []Condition{
{Attribute: NewScopedAttribute(AttributeScopeSpan, false, "http.status_code")},
},
},
},
}
for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
// Make a copy
actual := &c.input
optimize(actual)
// Instead of comparing func pointers, check if they are both set or not
if c.expected.SecondPass != nil {
require.NotNil(t, actual.SecondPass)
} else {
require.Nil(t, actual.SecondPass)
}
// Now nil out func and compare the rest
c.expected.SecondPass = nil
actual.SecondPass = nil
require.Equal(t, c.expected, *actual)
})
}
}
func TestLog2Bucketize(t *testing.T) {
tc := []struct {
input uint64
output float64
}{
{0, -1},
{1, -1},
{2, 2.0},
{3, 4.0},
{4, 4.0},
{5, 8.0},
{1023, 1024.0},
{1024, 1024.0},
{1025, 2048.0},
}
for _, c := range tc {
require.Equal(t, c.output, Log2Bucketize(c.input))
}
}
func BenchmarkLog2Bucketize(b *testing.B) {
for i := 0; i < b.N; i++ {
Log2Bucketize(1024)
}
}
func TestQuantileOverTime(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | quantile_over_time(duration, 0, 0.5, 1) by (span.foo)",
}
// intervals: (0;1], (1;2], (2;3]
var (
_128ns = 0.000000128
_256ns = 0.000000256
_512ns = 0.000000512
)
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
// 1st interval: (0;1]
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(512),
// 2nd interval: (1;2]
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
// 3rd interval: (2;3]
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(512),
}
// Output series with quantiles per foo
// Prom labels are sorted alphabetically, traceql labels maintain original order.
out := []TimeSeries{
{
Labels: []Label{
{Name: "span.foo", Value: NewStaticString("bar")},
{Name: "p", Value: NewStaticFloat(0)},
},
Values: []float64{
_128ns,
percentileHelper(0, _256ns, _256ns, _256ns, _256ns),
0,
},
},
{
Labels: []Label{
{Name: "span.foo", Value: NewStaticString("bar")},
{Name: "p", Value: NewStaticFloat(0.5)},
},
Values: []float64{
_256ns,
percentileHelper(0.5, _256ns, _256ns, _256ns, _256ns),
0,
},
},
{
Labels: []Label{
{Name: "span.foo", Value: NewStaticString("bar")},
{Name: "p", Value: NewStaticFloat(1)},
},
Values: []float64{_512ns, _256ns, 0},
},
{
Labels: []Label{
{Name: "span.foo", Value: NewStaticString("baz")},
{Name: "p", Value: NewStaticFloat(0)},
},
Values: []float64{
0, 0,
percentileHelper(0, _512ns, _512ns, _512ns),
},
},
{
Labels: []Label{
{Name: "span.foo", Value: NewStaticString("baz")},
{Name: "p", Value: NewStaticFloat(0.5)},
},
Values: []float64{
0, 0,
percentileHelper(0.5, _512ns, _512ns, _512ns),
},
},
{
Labels: []Label{
{Name: "span.foo", Value: NewStaticString("baz")},
{Name: "p", Value: NewStaticFloat(1)},
},
Values: []float64{0, 0, _512ns},
},
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
requireEqualSeriesSets(t, out, result)
require.Equal(t, len(result), seriesCount)
}
func percentileHelper(q float64, values ...float64) float64 {
h := Histogram{}
for _, v := range values {
h.Record(v, 1)
}
return Log2Quantile(q, h.Buckets)
}
func TestCountOverTime(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | count_over_time() by (span.foo)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(512),
}
// Output series with quantiles per foo
// Prom labels are sorted alphabetically, traceql labels maintain original order.
out := []TimeSeries{
{
Labels: []Label{
{Name: "span.foo", Value: NewStaticString("baz")},
},
Values: []float64{0, 0, 3},
Exemplars: make([]Exemplar, 0),
},
{
Labels: []Label{
{Name: "span.foo", Value: NewStaticString("bar")},
},
Values: []float64{3, 4, 0},
Exemplars: make([]Exemplar, 0),
},
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
requireEqualSeriesSets(t, out, result)
}
func TestCountOverTimeInstantNs(t *testing.T) {
// not rounded values to simulate real world data
start := 1*time.Second - 9*time.Nanosecond
end := 3*time.Second + 9*time.Nanosecond
step := end - start // for instant queries step == end-start
req := &tempopb.QueryRangeRequest{
Start: uint64(start),
End: uint64(end),
Step: uint64(step),
Query: "{ } | count_over_time()",
}
req.SetInstant(true)
in := []Span{
// outside of the range but within the range for ms. Should be ignored.
newMockSpan(nil).WithStartTime(uint64(start - 20*time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(start - time.Nanosecond)).WithDuration(1),
// within the range
newMockSpan(nil).WithStartTime(uint64(start)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(start + time.Nanosecond)).WithDuration(1),
// within the range
newMockSpan(nil).WithStartTime(uint64(end - time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(end)).WithDuration(10),
// outside of the range but within the range for ms. Should be ignored.
newMockSpan(nil).WithStartTime(uint64(end + time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(end + 20*time.Nanosecond)).WithDuration(1),
}
out := []TimeSeries{
{
Labels: []Label{
{Name: "__name__", Value: NewStaticString("count_over_time")},
},
Values: []float64{4},
Exemplars: make([]Exemplar, 0),
},
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
requireEqualSeriesSets(t, out, result)
}
func TestAvgOverTimeInstantNs(t *testing.T) {
// not rounded values to simulate real world data
start := 1*time.Second - 9*time.Nanosecond
end := 3*time.Second + 9*time.Nanosecond
step := end - start // for instant queries step == end-start
req := &tempopb.QueryRangeRequest{
Start: uint64(start),
End: uint64(end),
Step: uint64(step),
Query: "{ } | avg_over_time(span:duration)",
}
req.SetInstant(true)
in := []Span{
// outside of the range but within the range for ms. Should be ignored.
newMockSpan(nil).WithStartTime(uint64(start - 20*time.Nanosecond)).WithDuration(uint64(1 * time.Second)),
newMockSpan(nil).WithStartTime(uint64(start - time.Nanosecond)).WithDuration(uint64(2 * time.Second)),
// within the range
newMockSpan(nil).WithStartTime(uint64(start)).WithDuration(uint64(3 * time.Second)),
newMockSpan(nil).WithStartTime(uint64(start + time.Nanosecond)).WithDuration(uint64(4 * time.Second)),
// within the range
newMockSpan(nil).WithStartTime(uint64(end - time.Nanosecond)).WithDuration(uint64(5 * time.Second)),
newMockSpan(nil).WithStartTime(uint64(end)).WithDuration(uint64(6 * time.Second)),
// outside of the range but within the range for ms. Should be ignored.
newMockSpan(nil).WithStartTime(uint64(end + time.Nanosecond)).WithDuration(uint64(7 * time.Second)),
newMockSpan(nil).WithStartTime(uint64(end + 20*time.Nanosecond)).WithDuration(uint64(8 * time.Second)),
}
out := []TimeSeries{
{
Labels: []Label{
{Name: "__name__", Value: NewStaticString("avg_over_time")},
},
Values: []float64{(3 + 4 + 5 + 6) / 4.},
Exemplars: make([]Exemplar, 0),
},
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
requireEqualSeriesSets(t, out, result)
}
// TestCountOverTimeInstantNsWithCutoff simulates merge behavior in L2 and L3.
func TestCountOverTimeInstantNsWithCutoff(t *testing.T) {
start := 1*time.Second + 300*time.Nanosecond // additional 300ns that can be accidentally dropped by ms conversion
end := 3 * time.Second
step := end - start // for instant queries step == end-start
req := &tempopb.QueryRangeRequest{
Start: uint64(start),
End: uint64(end),
Step: uint64(step),
Query: "{ } | count_over_time()",
}
req.SetInstant(true)
cutoff := 2*time.Second + 300*time.Nanosecond
// from start to cutoff
req1 := *req
req1.End = uint64(cutoff)
req1.Step = req1.End - req1.Start
// from cutoff to end
req2 := *req
req2.Start = uint64(cutoff)
req2.Step = req2.End - req2.Start
in1 := []Span{
// outside of the range but within the range for ms. Should be ignored.
newMockSpan(nil).WithStartTime(uint64(start - 20*time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(start - time.Nanosecond)).WithDuration(1),
// within the range
newMockSpan(nil).WithStartTime(uint64(start)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(start + time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(cutoff - time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(cutoff)).WithDuration(1),
// outside of cutoff
newMockSpan(nil).WithStartTime(uint64(cutoff + time.Nanosecond)).WithDuration(1),
}
in2 := []Span{
// outside of cutoff
newMockSpan(nil).WithStartTime(uint64(cutoff - time.Nanosecond)).WithDuration(1),
// within the range
newMockSpan(nil).WithStartTime(uint64(cutoff)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(cutoff + time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(end - time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(end)).WithDuration(10),
// outside of the range but within the range for ms. Should be ignored.
newMockSpan(nil).WithStartTime(uint64(end + time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(end + 20*time.Nanosecond)).WithDuration(1),
}
out := []TimeSeries{
{
Labels: []Label{
{Name: "__name__", Value: NewStaticString("count_over_time")},
},
Values: []float64{8},
Exemplars: make([]Exemplar, 0),
},
}
t.Run("merge in L2", func(t *testing.T) {
e := NewEngine()
layer2, err := e.CompileMetricsQueryRangeNonRaw(req, AggregateModeSum)
require.NoError(t, err)
// process different series in L1
layer1, err := e.CompileMetricsQueryRange(&req1, 0, 0, false)
require.NoError(t, err)
for _, s := range in1 {
layer1.metricsPipeline.observe(s)
}
res1 := layer1.Results().ToProto(&req1)
layer1, err = e.CompileMetricsQueryRange(&req2, 0, 0, false)
require.NoError(t, err)
for _, s := range in2 {
layer1.metricsPipeline.observe(s)
}
res2 := layer1.Results().ToProto(&req2)
// merge in L2
layer2.metricsPipeline.observeSeries(res1)
layer2.metricsPipeline.observeSeries(res2)
result, seriesCount, err := processLayer3(req, layer2.Results())
require.NoError(t, err)
requireEqualSeriesSets(t, out, result)
require.Equal(t, len(result), seriesCount)
})
t.Run("merge in L3", func(t *testing.T) {
// process different series in L1+L2
res1, err := processLayer1AndLayer2(&req1, in1)
require.NoError(t, err)
res2, err := processLayer1AndLayer2(&req2, in2)
require.NoError(t, err)
// merge in L3
e := NewEngine()
layer3, err := e.CompileMetricsQueryRangeNonRaw(req, AggregateModeFinal)
require.NoError(t, err)
layer3.ObserveSeries(res1.ToProto(req))
layer3.ObserveSeries(res2.ToProto(req))
require.NoError(t, err)
requireEqualSeriesSets(t, out, layer3.Results())
require.Equal(t, len(layer3.Results()), layer3.Length())
})
}
func TestRateCutoff(t *testing.T) {
start := 1*time.Second + 300*time.Nanosecond // additional 300ns that can be accidentally dropped by ms conversion
end := 3 * time.Second
step := end - start
cutoff := 2*time.Second + 300*time.Nanosecond
req := tempopb.QueryRangeRequest{
Start: uint64(start),
End: uint64(end),
Step: uint64(step),
Query: "{ } | rate()",
}
for _, tc := range []struct {
name string
setInstant func(req tempopb.QueryRangeRequest) tempopb.QueryRangeRequest
expectedResult []float64
}{
{
"legacy request (no instant param)",
func(req tempopb.QueryRangeRequest) tempopb.QueryRangeRequest {
req.Step = req.End - req.Start
return req
},
// BUG: without instant query provided, instant rate is calculated incorrectly as sum of rates for each sub-interval
[]float64{(4 / (cutoff - start).Seconds()) + (4 / (end - cutoff).Seconds())},
},
{
"instant query",
func(req tempopb.QueryRangeRequest) tempopb.QueryRangeRequest {
req.SetInstant(true)
return req
},
[]float64{8 / step.Seconds()},
},
{
"not instant query",
func(req tempopb.QueryRangeRequest) tempopb.QueryRangeRequest {
req.SetInstant(false)
return req
},
// consumes all spans due to step alignment
[]float64{11 / step.Seconds(), 3 / step.Seconds()},
},
} {
t.Run(tc.name, func(t *testing.T) {
req = tc.setInstant(req)
// from start to cutoff
req1 := req
req1.End = uint64(cutoff)
req1 = tc.setInstant(req1)
// from cutoff to end
req2 := req
req2.Start = uint64(cutoff)
req2 = tc.setInstant(req2)
in1 := []Span{
// outside of the range but within the range for ms. Should be ignored for instant queries.
newMockSpan(nil).WithStartTime(uint64(start - 20*time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(start - time.Nanosecond)).WithDuration(1),
// within the range
newMockSpan(nil).WithStartTime(uint64(start)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(start + time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(cutoff - time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(cutoff)).WithDuration(1),
// outside of cutoff
newMockSpan(nil).WithStartTime(uint64(cutoff + time.Nanosecond)).WithDuration(1),
}
in2 := []Span{
// outside of cutoff
newMockSpan(nil).WithStartTime(uint64(cutoff - time.Nanosecond)).WithDuration(1),
// within the range
newMockSpan(nil).WithStartTime(uint64(cutoff)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(cutoff + time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(end - time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(end)).WithDuration(10),
// outside of the range but within the range for ms. Should be ignored for instant queries.
newMockSpan(nil).WithStartTime(uint64(end + time.Nanosecond)).WithDuration(1),
newMockSpan(nil).WithStartTime(uint64(end + 20*time.Nanosecond)).WithDuration(1),
}
// expectedValue := 8 * float64(time.Second.Nanoseconds()) / float64(step.Nanoseconds())
out := []TimeSeries{
{
Labels: []Label{
{Name: "__name__", Value: NewStaticString("rate")},
},
Values: tc.expectedResult,
Exemplars: make([]Exemplar, 0),
},
}
res1, err := processLayer1AndLayer2(&req1, in1)
require.NoError(t, err)
res2, err := processLayer1AndLayer2(&req2, in2)
require.NoError(t, err)
res, seriesCount, err := processLayer3(&req, res1, res2)
require.NoError(t, err)
require.Equal(t, len(res), seriesCount)
requireEqualSeriesSets(t, out, res)
})
}
}
func TestMinOverTimeForDuration(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | min_over_time(duration) by (span.foo)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(512),
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
fooBaz := result[LabelsFromArgs("span.foo", "baz").MapKey()]
fooBar := result[LabelsFromArgs("span.foo", "bar").MapKey()]
// We cannot compare with require.Equal because NaN != NaN
// foo.baz = (NaN, NaN, 0.000000512)
assert.True(t, math.IsNaN(fooBaz.Values[0]))
assert.True(t, math.IsNaN(fooBaz.Values[1]))
assert.Equal(t, 512/float64(time.Second), fooBaz.Values[2])
// foo.bar = (0.000000128, 0.000000128, NaN)
assert.Equal(t, 128/float64(time.Second), fooBar.Values[0])
assert.Equal(t, 8/float64(time.Second), fooBar.Values[1])
assert.True(t, math.IsNaN(fooBar.Values[2]))
require.Equal(t, len(result), seriesCount)
}
func TestMinOverTimeWithNoMatch(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | min_over_time(span.buu)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 404).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 201).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 401).WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 500).WithDuration(512),
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
// Test that empty timeseries are not included
ts := result.ToProto(req)
assert.True(t, len(ts) == 0)
require.Equal(t, 0, seriesCount)
}
func TestMinOverTimeForSpanAttribute(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | min_over_time(span.http.status_code) by (span.foo)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 404).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 201).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 401).WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 500).WithDuration(512),
}
in2 := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 100).WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 300).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 204).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 400).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 401).WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 402).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 403).WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 300).WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 400).WithDuration(512),
}
result, seriesCount, err := runTraceQLMetric(req, in, in2)
require.NoError(t, err)
fooBaz := result[LabelsFromArgs("span.foo", "baz").MapKey()]
fooBar := result[LabelsFromArgs("span.foo", "bar").MapKey()]
// Alas,we cannot compare with require.Equal because NaN != NaN
// foo.baz = (204, NaN, 200)
assert.Equal(t, 204.0, fooBaz.Values[0])
assert.True(t, math.IsNaN(fooBaz.Values[1]))
assert.Equal(t, 200.0, fooBaz.Values[2])
require.Equal(t, len(result), seriesCount)
// foo.bar = (100,200, NaN)
assert.Equal(t, 100.0, fooBar.Values[0])
assert.Equal(t, 200.0, fooBar.Values[1])
assert.True(t, math.IsNaN(fooBar.Values[2]))
// Test that NaN values are not included in the samples after casting to proto
ts := result.ToProto(req)
fooBarSamples := []tempopb.Sample{{TimestampMs: 1000, Value: 100}, {TimestampMs: 2000, Value: 200}}
fooBazSamples := []tempopb.Sample{{TimestampMs: 1000, Value: 204}, {TimestampMs: 3000, Value: 200}}
for _, s := range ts {
if LabelsFromProto(s.Labels).String() == "{\"span.foo\"=\"bar\"}" {
assert.Equal(t, fooBarSamples, s.Samples)
} else {
assert.Equal(t, fooBazSamples, s.Samples)
}
}
}
func TestAvgOverTimeForDuration(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | avg_over_time(duration) by (span.foo)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(500),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(200),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(300),
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
fooBaz := result[LabelsFromArgs("span.foo", "baz").MapKey()]
fooBar := result[LabelsFromArgs("span.foo", "bar").MapKey()]
// We cannot compare with require.Equal because NaN != NaN
assert.True(t, math.IsNaN(fooBaz.Values[0]))
assert.True(t, math.IsNaN(fooBaz.Values[1]))
assert.Equal(t, 200., fooBaz.Values[2]*float64(time.Second))
assert.Equal(t, 100., fooBar.Values[0]*float64(time.Second))
assert.Equal(t, 200., fooBar.Values[1]*float64(time.Second))
assert.True(t, math.IsNaN(fooBar.Values[2]))
}
func TestAvgOverTimeForDurationWithSecondStage(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | avg_over_time(duration) by (span.foo) | topk(1)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(500),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(200),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(300),
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
fooBaz := result[LabelsFromArgs("span.foo", "baz").MapKey()]
fooBar := result[LabelsFromArgs("span.foo", "bar").MapKey()]
// We cannot compare with require.Equal because NaN != NaN
assert.True(t, math.IsNaN(fooBaz.Values[0]))
assert.True(t, math.IsNaN(fooBaz.Values[1]))
assert.Equal(t, 200., fooBaz.Values[2]*float64(time.Second))
assert.Equal(t, 100., fooBar.Values[0]*float64(time.Second))
assert.Equal(t, 200., fooBar.Values[1]*float64(time.Second))
assert.True(t, math.IsNaN(fooBar.Values[2]))
}
func TestAvgOverTimeForDurationWithoutAggregation(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | avg_over_time(duration)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(500),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(200),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "bar").WithDuration(300),
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
avg := result[LabelsFromArgs("__name__", "avg_over_time").MapKey()]
assert.Equal(t, 100., avg.Values[0]*float64(time.Second))
assert.Equal(t, 200., avg.Values[1]*float64(time.Second))
assert.Equal(t, 200., avg.Values[2]*float64(time.Second))
}
func TestAvgOverTimeCombine(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | avg_over_time(duration)",
}
// Three set of spans with different data for a single time interval
// Each set will be passed to level 2 independently
in1 := []Span{
newMockSpan(nil).WithStartTime(uint64(1 * time.Second)).WithDuration(uint64(100 * time.Second)),
}
in2 := []Span{
newMockSpan(nil).WithStartTime(uint64(1 * time.Second)).WithDuration(uint64(200 * time.Second)),
newMockSpan(nil).WithStartTime(uint64(1 * time.Second)).WithDuration(uint64(400 * time.Second)),
}
in3 := []Span{
newMockSpan(nil).WithStartTime(uint64(1 * time.Second)).WithDuration(uint64(500 * time.Second)),
newMockSpan(nil).WithStartTime(uint64(1 * time.Second)).WithDuration(uint64(600 * time.Second)),
newMockSpan(nil).WithStartTime(uint64(1 * time.Second)).WithDuration(uint64(700 * time.Second)),
}
result, seriesCount, err := runTraceQLMetric(req, in1, in2, in3)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
avg := result[LabelsFromArgs("__name__", "avg_over_time").MapKey()]
expectedResult := (100 + 200 + 400 + 500 + 600 + 700) / 6.
assert.Equal(t, expectedResult, avg.Values[0])
assert.True(t, math.IsNaN(avg.Values[1]), "expected NaN, got %f", avg.Values[1])
assert.True(t, math.IsNaN(avg.Values[2]), "expected NaN, got %f", avg.Values[2])
}
func TestAvgOverTimeForSpanAttribute(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | avg_over_time(span.http.status_code) by (span.foo)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 404).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 400).WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 300).WithDuration(512),
}
in2 := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 200).WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 200).WithDuration(512),
}
result, seriesCount, err := runTraceQLMetric(req, in, in2)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
fooBaz := result[LabelsFromArgs("span.foo", "baz").MapKey()]
fooBar := result[LabelsFromArgs("span.foo", "bar").MapKey()]
// Alas,we cannot compare with require.Equal because NaN != NaN
// foo.baz = (NaN, NaN, 250)
assert.True(t, math.IsNaN(fooBaz.Values[0]))
assert.True(t, math.IsNaN(fooBaz.Values[1]))
assert.Equal(t, 250.0, fooBaz.Values[2])
// foo.bar = (234,200, NaN)
assert.Equal(t, 234.0, fooBar.Values[0])
assert.Equal(t, 200.0, fooBar.Values[1])
assert.True(t, math.IsNaN(fooBar.Values[2]))
// Test that NaN values are not included in the samples after casting to proto
ts := result.ToProto(req)
fooBarSamples := []tempopb.Sample{{TimestampMs: 1000, Value: 234}, {TimestampMs: 2000, Value: 200}}
fooBazSamples := []tempopb.Sample{{TimestampMs: 3000, Value: 250}}
for _, s := range ts {
if LabelsFromProto(s.Labels).String() == "{\"span.foo\"=\"bar\"}" {
assert.Equal(t, fooBarSamples, s.Samples)
} else {
assert.Equal(t, fooBazSamples, s.Samples)
}
}
}
func TestAvgOverTimeWithNoMatch(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | avg_over_time(span.buu)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 404).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 201).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 401).WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 500).WithDuration(512),
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
// Test that empty timeseries are not included
ts := result.ToProto(req)
assert.True(t, len(ts) == 0)
}
func TestObserveSeriesAverageOverTimeForSpanAttribute(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | avg_over_time(span.http.status_code) by (span.foo)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 300),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 400),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 100),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 200),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 400),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 500),
}
in2 := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 100),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 300),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 400),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 100),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 100),
}
e := NewEngine()
layer1A, _ := e.CompileMetricsQueryRange(req, 0, 0, false)
layer1B, _ := e.CompileMetricsQueryRange(req, 0, 0, false)
layer2A, _ := e.CompileMetricsQueryRangeNonRaw(req, AggregateModeSum)
layer2B, _ := e.CompileMetricsQueryRangeNonRaw(req, AggregateModeSum)
layer3, _ := e.CompileMetricsQueryRangeNonRaw(req, AggregateModeFinal)
for _, s := range in {
layer1A.metricsPipeline.observe(s)
}
layer2A.ObserveSeries(layer1A.Results().ToProto(req))
for _, s := range in2 {
layer1B.metricsPipeline.observe(s)
}
layer2B.ObserveSeries(layer1B.Results().ToProto(req))
layer3.ObserveSeries(layer2A.Results().ToProto(req))
layer3.ObserveSeries(layer2B.Results().ToProto(req))
result := layer3.Results()
fooBaz := result[LabelsFromArgs("span.foo", "baz").MapKey()]
fooBar := result[LabelsFromArgs("span.foo", "bar").MapKey()]
// Alas,we cannot compare with require.Equal because NaN != NaN
// foo.baz = (NaN, NaN, 300)
assert.True(t, math.IsNaN(fooBaz.Values[0]))
assert.True(t, math.IsNaN(fooBaz.Values[1]))
// 300 = (200 + 400 + 500 + 100) / 4
assert.Equal(t, 300.0, fooBaz.Values[2])
// foo.bar = (260,200, 100)
assert.Equal(t, 260.0, fooBar.Values[0])
assert.Equal(t, 200.0, fooBar.Values[1])
assert.Equal(t, 100.0, fooBar.Values[2])
}
func TestObserveSeriesAverageOverTimeForSpanAttributeWithTruncation(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: uint64(1 * time.Second),
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | avg_over_time(span.http.status_code) by (span.foo)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 300),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 400),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 100),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 100),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 200),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 400),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 500),
}
in2 := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 100),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 300),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 400),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 100),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 100),
}
e := NewEngine()
layer1A, _ := e.CompileMetricsQueryRange(req, 0, 0, false)
layer1B, _ := e.CompileMetricsQueryRange(req, 0, 0, false)
layer2A, _ := e.CompileMetricsQueryRangeNonRaw(req, AggregateModeSum)
layer2B, _ := e.CompileMetricsQueryRangeNonRaw(req, AggregateModeSum)
layer3, _ := e.CompileMetricsQueryRangeNonRaw(req, AggregateModeFinal)
for _, s := range in {
layer1A.metricsPipeline.observe(s)
}
layer2A.ObserveSeries(layer1A.Results().ToProto(req))
for _, s := range in2 {
layer1B.metricsPipeline.observe(s)
}
layer2B.ObserveSeries(layer1B.Results().ToProto(req))
layer3.ObserveSeries(layer2A.Results().ToProto(req))
layer2bResults := layer2B.Results().ToProto(req)
truncated2bResults := make([]*tempopb.TimeSeries, 0, len(layer2bResults)-1)
for _, ts := range layer2bResults {
if !LabelsFromProto(ts.Labels).Has(internalLabelMetaType) {
// add all values series
truncated2bResults = append(truncated2bResults, ts)
} else if len(ts.Samples) != 3 {
// the panic appears when the count series with 3 samples is missing
truncated2bResults = append(truncated2bResults, ts)
}
}
assert.NotPanics(t, func() {
layer3.ObserveSeries(truncated2bResults)
}, "should not panic on truncation")
}
func TestMaxOverTimeForDuration(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | max_over_time(duration) by (span.foo)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(512),
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
fooBaz := result[LabelsFromArgs("span.foo", "baz").MapKey()]
fooBar := result[LabelsFromArgs("span.foo", "bar").MapKey()]
// We cannot compare with require.Equal because NaN != NaN
// foo.baz = (NaN, NaN, 0.000000512)
assert.True(t, math.IsNaN(fooBaz.Values[0]))
assert.True(t, math.IsNaN(fooBaz.Values[1]))
assert.Equal(t, 1024/float64(time.Second), fooBaz.Values[2])
// foo.bar = (0.000000128, 0.000000128, NaN)
assert.Equal(t, 512/float64(time.Second), fooBar.Values[0])
assert.Equal(t, 256/float64(time.Second), fooBar.Values[1])
assert.True(t, math.IsNaN(fooBar.Values[2]))
}
func TestMaxOverTimeWithNoMatch(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | max_over_time(span.buu)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 404).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 201).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 401).WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 500).WithDuration(512),
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
// Test that empty timeseries are not included
ts := result.ToProto(req)
assert.True(t, len(ts) == 0)
}
func TestMaxOverTimeForSpanAttribute(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | max_over_time(span.http.status_code) by (span.foo)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 404).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 201).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 401).WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 500).WithDuration(512),
}
in2 := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 100).WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 300).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 204).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 400).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 401).WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 402).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 403).WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 300).WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 400).WithDuration(512),
}
result, seriesCount, err := runTraceQLMetric(req, in, in2)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
fooBaz := result[LabelsFromArgs("span.foo", "baz").MapKey()]
fooBar := result[LabelsFromArgs("span.foo", "bar").MapKey()]
// Alas,we cannot compare with require.Equal because NaN != NaN
// foo.baz = (204, NaN, 500)
assert.Equal(t, 204.0, fooBaz.Values[0])
assert.True(t, math.IsNaN(fooBaz.Values[1]))
assert.Equal(t, 500.0, fooBaz.Values[2])
// foo.bar = (404,403, NaN)
assert.Equal(t, 404.0, fooBar.Values[0])
assert.Equal(t, 403.0, fooBar.Values[1])
assert.True(t, math.IsNaN(fooBar.Values[2]))
// Test that NaN values are not included in the samples after casting to proto
ts := result.ToProto(req)
fooBarSamples := []tempopb.Sample{{TimestampMs: 1000, Value: 404}, {TimestampMs: 2000, Value: 403}}
fooBazSamples := []tempopb.Sample{{TimestampMs: 1000, Value: 204}, {TimestampMs: 3000, Value: 500}}
for _, s := range ts {
if LabelsFromProto(s.Labels).String() == "{\"span.foo\"=\"bar\"}" {
assert.Equal(t, fooBarSamples, s.Samples)
} else {
assert.Equal(t, fooBazSamples, s.Samples)
}
}
}
func TestSumOverTimeForDuration(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | sum_over_time(duration) by (span.foo)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(10),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(20),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(30),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(40),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(50),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(60),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(70),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(80),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(90),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(100),
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
fooBaz := result[LabelsFromArgs("span.foo", "baz").MapKey()]
fooBar := result[LabelsFromArgs("span.foo", "bar").MapKey()]
// We cannot compare with require.Equal because NaN != NaN
// foo.baz = (NaN, NaN, 0.00000027)
assert.True(t, math.IsNaN(fooBaz.Values[0]))
assert.True(t, math.IsNaN(fooBaz.Values[1]))
assert.Equal(t, (80+90+100)/float64(time.Second), fooBaz.Values[2])
// foo.bar = (0.000000128, 0.000000128, NaN)
assert.InEpsilon(t, (10+20+30)/float64(time.Second), fooBar.Values[0], 1e-9)
assert.InEpsilon(t, (40+50+60+70)/float64(time.Second), fooBar.Values[1], 1e-9)
assert.True(t, math.IsNaN(fooBar.Values[2]))
}
func TestSumOverTimeForSpanAttribute(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | sum_over_time(span.kafka.lag) by (span.foo)",
}
// A variety of spans across times, durations, and series.
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 100).WithDuration(100),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 300).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 200).WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 200).WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("kafka.lag", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("kafka.lag", 400).WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("kafka.lag", 200).WithDuration(512),
}
in2 := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 100).WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 300).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "baz").WithSpanInt("kafka.lag", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 400).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 400).WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 400).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("kafka.lag", 400).WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("kafka.lag", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("kafka.lag", 300).WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("kafka.lag", 400).WithDuration(512),
}
result, seriesCount, err := runTraceQLMetric(req, in, in2)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
fooBaz := result[LabelsFromArgs("span.foo", "baz").MapKey()]
fooBar := result[LabelsFromArgs("span.foo", "bar").MapKey()]
// Alas,we cannot compare with require.Equal because NaN != NaN
// foo.baz = (200, NaN, 1700)
assert.Equal(t, 200.0, fooBaz.Values[0])
assert.True(t, math.IsNaN(fooBaz.Values[1]))
assert.Equal(t, 1700.0, fooBaz.Values[2])
// foo.bar = (1200,2400, NaN)
assert.Equal(t, 1200.0, fooBar.Values[0])
assert.Equal(t, 2400.0, fooBar.Values[1])
assert.True(t, math.IsNaN(fooBar.Values[2]))
// Test that NaN values are not included in the samples after casting to proto
ts := result.ToProto(req)
fooBarSamples := []tempopb.Sample{{TimestampMs: 1000, Value: 1200}, {TimestampMs: 2000, Value: 2400}}
fooBazSamples := []tempopb.Sample{{TimestampMs: 1000, Value: 200}, {TimestampMs: 3000, Value: 1700}}
for _, s := range ts {
if LabelsFromProto(s.Labels).String() == "{\"span.foo\"=\"bar\"}" {
assert.Equal(t, fooBarSamples, s.Samples)
} else {
assert.Equal(t, fooBazSamples, s.Samples)
}
}
}
func TestSumOverTimeWithNoMatch(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | sum_over_time(span.buu)",
}
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 404).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(64),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithSpanInt("http.status_code", 200).WithDuration(8),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 201).WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 401).WithDuration(1024),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithSpanInt("http.status_code", 500).WithDuration(512),
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
// Test that empty timeseries are not included
ts := result.ToProto(req)
assert.True(t, len(ts) == 0)
}
func TestHistogramOverTime(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | histogram_over_time(duration) by (span.foo)",
}
var (
_128ns = NewStaticFloat(0.000000128)
_256ns = NewStaticFloat(0.000000256)
_512ns = NewStaticFloat(0.000000512)
)
// A variety of spans across times, durations, and series. All durations are powers of 2 for simplicity
in := []Span{
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(128),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(1*time.Second)).WithSpanString("foo", "bar").WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(2*time.Second)).WithSpanString("foo", "bar").WithDuration(256),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(512),
newMockSpan(nil).WithStartTime(uint64(3*time.Second)).WithSpanString("foo", "baz").WithDuration(512),
}
// Output series with buckets per foo
// Prom labels are sorted alphabetically, traceql labels maintain original order.
out := []TimeSeries{
{
Labels: []Label{
{Name: "span.foo", Value: NewStaticString("bar")},
{Name: internalLabelBucket, Value: _128ns},
},
Values: []float64{1, 0, 0},
Exemplars: make([]Exemplar, 0),
},
{
Labels: []Label{
{Name: "span.foo", Value: NewStaticString("bar")},
{Name: internalLabelBucket, Value: _256ns},
},
Values: []float64{1, 4, 0},
Exemplars: make([]Exemplar, 0),
},
{
Labels: []Label{
{Name: "span.foo", Value: NewStaticString("bar")},
{Name: internalLabelBucket, Value: _512ns},
},
Values: []float64{1, 0, 0},
Exemplars: make([]Exemplar, 0),
},
{
Labels: []Label{
{Name: "span.foo", Value: NewStaticString("baz")},
{Name: internalLabelBucket, Value: _512ns},
},
Values: []float64{0, 0, 3},
Exemplars: make([]Exemplar, 0),
},
}
result, seriesCount, err := runTraceQLMetric(req, in)
require.NoError(t, err)
require.Equal(t, len(result), seriesCount)
requireEqualSeriesSets(t, out, result)
}
func TestSecondStageTopK(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(8 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | rate() by (span.foo) | topk(2)",
}
in := make([]Span, 0)
// 15 spans, at different start times across 3 series
in = append(in, generateSpans(7, []int{1, 2, 3, 4, 5, 6, 7, 8}, "bar")...)
in = append(in, generateSpans(5, []int{1, 2, 3, 4, 5, 6, 7, 8}, "baz")...)
in = append(in, generateSpans(3, []int{1, 2, 3, 4, 5, 6, 7, 8}, "quax")...)
result, _, err := runTraceQLMetric(req, in)
require.NoError(t, err)
// bar and baz have more spans so they should be the top 2
resultBar := result[LabelsFromArgs("span.foo", "bar").MapKey()]
require.Equal(t, []float64{7, 7, 7, 7, 7, 7, 7, 7}, resultBar.Values)
resultBaz := result[LabelsFromArgs("span.foo", "baz").MapKey()]
require.Equal(t, []float64{5, 5, 5, 5, 5, 5, 5, 5}, resultBaz.Values)
}
func TestSecondStageTopKInstant(t *testing.T) {
// Instant Queries are just Range Queries with a step that spans the entire range (end - start) + start
req := &tempopb.QueryRangeRequest{
Start: uint64(1 * time.Second),
End: uint64(8 * time.Second),
Step: uint64(7 * time.Second),
Query: "{ } | rate() by (span.foo) | topk(2)",
}
req.SetInstant(true)
in := make([]Span, 0)
// 15 spans, at different start times across 3 series
in = append(in, generateSpans(7, []int{1, 2, 3, 4, 5, 6, 7, 8}, "bar")...)
in = append(in, generateSpans(5, []int{1, 2, 3, 4, 5, 6, 7, 8}, "baz")...)
in = append(in, generateSpans(3, []int{1, 2, 3, 4, 5, 6, 7, 8}, "quax")...)
result, _, err := runTraceQLMetric(req, in)
require.NoError(t, err)
// Instant queries return a single value for each series.
// so there should be only one value in each series and only two series in the result.
require.Equal(t, 2, len(result))
// bar and baz have more spans so they should be the top 2
require.Equal(t, 1, len(result[LabelsFromArgs("span.foo", "bar").MapKey()].Values))
require.Equal(t, 1, len(result[LabelsFromArgs("span.foo", "baz").MapKey()].Values))
}
func TestSecondStageTopKAverage(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(8 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | avg_over_time(duration) by (span.foo) | topk(2)",
}
in := make([]Span, 0)
// 15 spans, at different start times across 3 series
in = append(in, generateSpans(7, []int{1, 2, 3, 4, 5, 6, 7, 8}, "bar")...)
in = append(in, generateSpans(5, []int{1, 2, 3, 4, 5, 6, 7, 8}, "baz")...)
in = append(in, generateSpans(3, []int{1, 2, 3, 4, 5, 6, 7, 8}, "quax")...)
result, _, err := runTraceQLMetric(req, in)
require.NoError(t, err)
resultBar := result[LabelsFromArgs("span.foo", "bar").MapKey()]
val1 := 0.000000512
require.Equal(t, []float64{val1, val1, val1, val1, val1, val1, val1, val1}, resultBar.Values)
resultBaz := result[LabelsFromArgs("span.foo", "baz").MapKey()]
val2 := 0.00000038400000000000005
require.Equal(t, []float64{val2, val2, val2, val2, val2, val2, val2, val2}, resultBaz.Values)
}
func TestSecondStageBottomK(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: 1,
End: uint64(8 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | rate() by (span.foo) | bottomk(2)",
}
in := make([]Span, 0)
// 15 spans, at different start times across 3 series
in = append(in, generateSpans(7, []int{1, 2, 3, 4, 5, 6, 7, 8}, "bar")...)
in = append(in, generateSpans(5, []int{1, 2, 3, 4, 5, 6, 7, 8}, "baz")...)
in = append(in, generateSpans(3, []int{1, 2, 3, 4, 5, 6, 7, 8}, "quax")...)
result, _, err := runTraceQLMetric(req, in)
require.NoError(t, err)
// quax and baz have the lowest spans so they should be the bottom 2
resultQuax := result[LabelsFromArgs("span.foo", "quax").MapKey()]
require.Equal(t, []float64{3, 3, 3, 3, 3, 3, 3, 3}, resultQuax.Values)
resultBaz := result[LabelsFromArgs("span.foo", "baz").MapKey()]
require.Equal(t, []float64{5, 5, 5, 5, 5, 5, 5, 5}, resultBaz.Values)
}
func TestSecondStageBottomKInstant(t *testing.T) {
// Instant Queries are just Range Queries with a step that spans the entire range (end - start)
start := uint64(1 * time.Second)
end := uint64(8 * time.Second)
req := &tempopb.QueryRangeRequest{
Start: start,
End: end,
Step: end - start,
Query: "{ } | rate() by (span.foo) | bottomk(2)",
}
req.SetInstant(true)
in := make([]Span, 0)
// 15 spans, at different start times across 3 series
in = append(in, generateSpans(7, []int{1, 2, 3, 4, 5, 6, 7, 8}, "bar")...)
in = append(in, generateSpans(5, []int{1, 2, 3, 4, 5, 6, 7, 8}, "baz")...)
in = append(in, generateSpans(3, []int{1, 2, 3, 4, 5, 6, 7, 8}, "quax")...)
result, _, err := runTraceQLMetric(req, in)
require.NoError(t, err)
// Instant queries return a single value for each series.
// so there should be only one value in each series and only two series in the result.
require.Equal(t, 2, len(result))
// quax and baz have the lowest spans so they should be the bottom 2
require.Equal(t, 1, len(result[LabelsFromArgs("span.foo", "quax").MapKey()].Values))
require.Equal(t, 1, len(result[LabelsFromArgs("span.foo", "baz").MapKey()].Values))
}
func TestProcessTopK(t *testing.T) {
tests := []struct {
name string
input SeriesSet
limit int
expected SeriesSet
}{
{
name: "topk selection",
input: createSeriesSet(map[string][]float64{
"a": {1, 5, 3},
"b": {2, 6, 2},
"c": {3, 1, 1},
"d": {4, 2, 4},
}),
limit: 2,
expected: createSeriesSet(map[string][]float64{
"a": {math.NaN(), 5, 3}, // Top-2 at timestamp 1, 2
"b": {math.NaN(), 6, math.NaN()}, // Top-2 at timestamp 1
"c": {3, math.NaN(), math.NaN()}, // Top-2 at timestamp 0
"d": {4, math.NaN(), 4}, // Top-2 at timestamps 0, 2
}),
},
{
name: "topk selection at each timestamp",
input: createSeriesSet(map[string][]float64{
"a": {1, 2, 3},
"b": {2, 3, 4},
"c": {3, 4, 5},
"d": {1, 1, 1},
"e": {0.5, 2, 1},
}),
limit: 2,
expected: createSeriesSet(map[string][]float64{
"b": {2, 3, 4}, // Top-2 at all timestamps
"c": {3, 4, 5}, // Top-2 at all timestamps
}),
},
{
name: "select single highest value at specific timestamp",
input: createSeriesSet(map[string][]float64{
"a": {1, 6, 3},
"b": {4, 5, 1},
"c": {2, 3, 7},
}),
limit: 1,
expected: createSeriesSet(map[string][]float64{
"a": {math.NaN(), 6, math.NaN()}, // top at timestamp 1
"b": {4, math.NaN(), math.NaN()}, // top at timestamp 0
"c": {math.NaN(), math.NaN(), 7}, // top at timestamp 2
}),
},
{
name: "with NaN values",
input: createSeriesSet(map[string][]float64{
"a": {1, math.NaN(), 3},
"b": {2, 6, math.NaN()},
"c": {3, 1, 1},
}),
limit: 2,
expected: createSeriesSet(map[string][]float64{
"a": {math.NaN(), math.NaN(), 3}, // Top-2 at timestamp 2
"b": {2, 6, math.NaN()}, // Top-2 at timestamp 0, 1
"c": {3, 1, 1}, // Top-2 at timestamp 0, 2
}),
},
{
name: "series with all NaN values is skipped",
input: createSeriesSet(map[string][]float64{
"a": {math.NaN(), math.NaN(), math.NaN()},
"b": {2, 6, math.NaN()},
"c": {3, 1, 1},
}),
limit: 2,
expected: createSeriesSet(map[string][]float64{
"b": {2, 6, math.NaN()},
"c": {3, 1, 1},
}),
},
{
name: "all series with all NaN values",
input: createSeriesSet(map[string][]float64{
"a": {math.NaN(), math.NaN(), math.NaN()},
"b": {math.NaN(), math.NaN(), math.NaN()},
}),
limit: 2,
expected: SeriesSet{},
},
{
name: "empty input",
input: SeriesSet{},
limit: 2,
expected: SeriesSet{},
},
{
name: "limit larger than series count",
input: createSeriesSet(map[string][]float64{
"a": {1, 5, 3},
"b": {2, 6, 2},
}),
limit: 5,
expected: createSeriesSet(map[string][]float64{
"a": {1, 5, 3},
"b": {2, 6, 2},
}),
},
{
name: "negative and infinity values",
input: createSeriesSet(map[string][]float64{
"a": {-1, 5, math.Inf(-1)},
"b": {-2, 6, math.Inf(1)},
"c": {-3, 7, 1},
}),
limit: 2,
expected: createSeriesSet(map[string][]float64{
"a": {-1, math.NaN(), math.NaN()}, // Top-2 at timestamp 0
"b": {-2, 6, math.Inf(1)}, // Top-2 at timestamps 1, 2
"c": {math.NaN(), 7, 1}, // Top-2 at timestamp 1
}),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := processTopK(tt.input, 3, tt.limit)
expectSeriesSet(t, tt.expected, result)
})
}
}
func TestProcessBottomK(t *testing.T) {
tests := []struct {
name string
input SeriesSet
limit int
expected SeriesSet
}{
{
name: "bottomk selection",
input: createSeriesSet(map[string][]float64{
"a": {1, 5, 3},
"b": {2, 6, 2},
"c": {3, 1, 1},
"d": {4, 2, 4},
}),
limit: 2,
expected: createSeriesSet(map[string][]float64{
"a": {1, math.NaN(), math.NaN()}, // Bottom-2 at timestamp 0
"b": {2, math.NaN(), 2}, // Bottom-2 at timestamps 0, 2
"c": {math.NaN(), 1, 1}, // Bottom-2 at timestamps 1, 2
"d": {math.NaN(), 2, math.NaN()}, // Bottom-2 at timestamp 1
}),
},
{
name: "bottomk selection at each timestamp",
input: createSeriesSet(map[string][]float64{
"a": {5, 4, 3},
"b": {6, 5, 4},
"c": {7, 6, 1},
"d": {3, 3, 3},
"e": {4, 2, 2},
}),
limit: 2,
expected: createSeriesSet(map[string][]float64{
"c": {math.NaN(), math.NaN(), 1}, // bottom 2 at timestamp 2
"d": {3, 3, math.NaN()}, // bottom 2 at timestamp 0, 1
"e": {4, 2, 2}, // bottom 2 at timestamp 0, 1, 2
}),
},
{
name: "select single lowest value at specific timestamp",
input: createSeriesSet(map[string][]float64{
"a": {3, 1, 5},
"b": {1, 2, 3},
"c": {4, 5, 1},
}),
limit: 1,
expected: createSeriesSet(map[string][]float64{
"a": {math.NaN(), 1, math.NaN()}, // Lowest at timestamp 1
"b": {1, math.NaN(), math.NaN()}, // Lowest at timestamp 0
"c": {math.NaN(), math.NaN(), 1}, // Lowest at timestamp 2
}),
},
{
name: "with NaN values",
input: createSeriesSet(map[string][]float64{
"a": {1, math.NaN(), 3},
"b": {4, 6, math.NaN()},
"c": {5, 1, 1},
}),
limit: 2,
expected: createSeriesSet(map[string][]float64{
"a": {1, math.NaN(), 3}, // Bottom-2 at timestamp 0
"b": {4, 6, math.NaN()}, // NaN values are skipped in comparison
"c": {math.NaN(), 1, 1}, // Bottom-2 at timestamps 1, 2
}),
},
{
name: "all series with NaN values",
input: createSeriesSet(map[string][]float64{
"a": {math.NaN(), math.NaN(), math.NaN()},
"b": {math.NaN(), math.NaN(), math.NaN()},
"c": {math.NaN(), math.NaN(), math.NaN()},
}),
limit: 2,
expected: SeriesSet{},
},
{
name: "empty input",
input: SeriesSet{},
limit: 2,
expected: SeriesSet{},
},
{
name: "limit larger than series count",
input: createSeriesSet(map[string][]float64{
"a": {1, 5, 3},
"b": {2, 6, 2},
}),
limit: 5,
expected: createSeriesSet(map[string][]float64{
"a": {1, 5, 3},
"b": {2, 6, 2},
}),
},
{
name: "negative and infinity values",
input: createSeriesSet(map[string][]float64{
"a": {-1, 5, math.Inf(-1)},
"b": {-2, 6, math.Inf(1)},
"c": {-3, 7, 1},
}),
limit: 2,
expected: createSeriesSet(map[string][]float64{
"a": {math.NaN(), 5, math.Inf(-1)}, // Bottom-2 at timestamp 2
"b": {-2, 6, math.NaN()}, // Bottom-2 at timestamp 0
"c": {-3, math.NaN(), 1}, // Bottom-2 at timestamps 0, 2
}),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := processBottomK(tt.input, 3, tt.limit)
expectSeriesSet(t, tt.expected, result)
})
}
}
func TestTiesInTopK(t *testing.T) {
t.Run("three-way tie at first position", func(t *testing.T) {
input := createSeriesSet(map[string][]float64{
"a": {10, 5, 1},
"b": {10, 4, 2},
"c": {10, 3, 3},
})
result := processTopK(input, 3, 2)
// When there are ties, series are selected deterministically using
// alphabetical order of labels as the tiebreaker (alphabetically earlier wins).
// At index 0: all have value 10, so "a" and "b" are selected (alphabetically first)
// At index 1: top 2 are {a:5, b:4} (c:3 is excluded)
// At index 2: top 2 are {b:2, c:3} (a:1 is excluded)
expectSeriesValues(t, result[LabelsFromArgs("label", "a").MapKey()].Values, []float64{10, 5, math.NaN()})
expectSeriesValues(t, result[LabelsFromArgs("label", "b").MapKey()].Values, []float64{10, 4, 2})
expectSeriesValues(t, result[LabelsFromArgs("label", "c").MapKey()].Values, []float64{math.NaN(), math.NaN(), 3})
})
t.Run("all values tied", func(t *testing.T) {
input := createSeriesSet(map[string][]float64{
"x": {5, 5, 5},
"y": {5, 5, 5},
"z": {5, 5, 5},
})
result := processTopK(input, 3, 2)
// All values are equal (5), so topk should consistently select "x" and "y" (alphabetically first)
expectSeriesValues(t, result[LabelsFromArgs("label", "x").MapKey()].Values, []float64{5, 5, 5})
expectSeriesValues(t, result[LabelsFromArgs("label", "y").MapKey()].Values, []float64{5, 5, 5})
// "z" should be excluded at all positions
_, exists := result[LabelsFromArgs("label", "z").MapKey()]
require.False(t, exists, "series z should not be in result")
})
t.Run("partial ties with different values", func(t *testing.T) {
input := createSeriesSet(map[string][]float64{
"alpha": {100, 50, 50},
"beta": {100, 50, 40},
"gamma": {90, 50, 30},
"delta": {80, 40, 20},
})
result := processTopK(input, 3, 2)
// At index 0: top 2 are {alpha:100, beta:100} (tied at top, alphabetically first)
// At index 1: top 2 are {alpha:50, beta:50} (three-way tie at 50, alphabetically first two)
// At index 2: top 2 are {alpha:50, beta:40} (alpha:50 is highest, beta:40 is second)
expectSeriesValues(t, result[LabelsFromArgs("label", "alpha").MapKey()].Values, []float64{100, 50, 50})
expectSeriesValues(t, result[LabelsFromArgs("label", "beta").MapKey()].Values, []float64{100, 50, 40})
// gamma and delta should not be in the result (all NaN)
_, exists := result[LabelsFromArgs("label", "gamma").MapKey()]
require.False(t, exists, "series gamma should not be in result")
_, exists = result[LabelsFromArgs("label", "delta").MapKey()]
require.False(t, exists, "series delta should not be in result")
})
t.Run("k=1 with ties", func(t *testing.T) {
input := createSeriesSet(map[string][]float64{
"service1": {100, 80, 60},
"service2": {100, 70, 60},
"service3": {90, 60, 60},
})
result := processTopK(input, 3, 1)
// With k=1, only one series should be selected at each position
// At index 0: top 1 is {service1:100} (tied with service2, but alphabetically first)
// At index 1: top 1 is {service1:80} (highest value)
// At index 2: top 1 is {service1:60} (three-way tie, alphabetically first)
expectSeriesValues(t, result[LabelsFromArgs("label", "service1").MapKey()].Values, []float64{100, 80, 60})
_, exists := result[LabelsFromArgs("label", "service2").MapKey()]
require.False(t, exists, "series service2 should not be in result")
_, exists = result[LabelsFromArgs("label", "service3").MapKey()]
require.False(t, exists, "series service3 should not be in result")
})
t.Run("ties with NaN values", func(t *testing.T) {
input := createSeriesSet(map[string][]float64{
"m": {math.NaN(), 10, 10},
"n": {10, 10, math.NaN()},
"o": {10, math.NaN(), 10},
})
result := processTopK(input, 3, 2)
// NaN values should be ignored, ties broken alphabetically
// At index 0: top 2 are {n:10, o:10} (m has NaN, alphabetically first two with values)
// At index 1: top 2 are {m:10, n:10} (o has NaN, alphabetically first two with values)
// At index 2: top 2 are {m:10, o:10} (n has NaN, alphabetically first two with values)
expectSeriesValues(t, result[LabelsFromArgs("label", "m").MapKey()].Values, []float64{math.NaN(), 10, 10})
expectSeriesValues(t, result[LabelsFromArgs("label", "n").MapKey()].Values, []float64{10, 10, math.NaN()})
expectSeriesValues(t, result[LabelsFromArgs("label", "o").MapKey()].Values, []float64{10, math.NaN(), 10})
})
}
func TestTiesInBottomK(t *testing.T) {
t.Run("three-way tie at first position", func(t *testing.T) {
input := createSeriesSet(map[string][]float64{
"a": {10, 5, 1},
"b": {10, 4, 2},
"c": {10, 3, 3},
})
result := processBottomK(input, 3, 2)
// When there are ties, series are selected deterministically using
// alphabetical order of labels as the tiebreaker.
// With reversed comparison, bottomk selects alphabetically later series when tied.
// At index 0: all have value 10, so "b" and "c" are selected (alphabetically later)
// At index 1: bottom 2 are {c:3, b:4} (a:5 is excluded)
// At index 2: bottom 2 are {a:1, b:2} (c:3 is excluded)
expectSeriesValues(t, result[LabelsFromArgs("label", "a").MapKey()].Values, []float64{math.NaN(), math.NaN(), 1})
expectSeriesValues(t, result[LabelsFromArgs("label", "b").MapKey()].Values, []float64{10, 4, 2})
expectSeriesValues(t, result[LabelsFromArgs("label", "c").MapKey()].Values, []float64{10, 3, math.NaN()})
})
t.Run("all values tied", func(t *testing.T) {
input := createSeriesSet(map[string][]float64{
"x": {5, 5, 5},
"y": {5, 5, 5},
"z": {5, 5, 5},
})
result := processBottomK(input, 3, 2)
// All values are equal (5), so bottomk should consistently select "y" and "z" (alphabetically later)
expectSeriesValues(t, result[LabelsFromArgs("label", "y").MapKey()].Values, []float64{5, 5, 5})
expectSeriesValues(t, result[LabelsFromArgs("label", "z").MapKey()].Values, []float64{5, 5, 5})
// "x" should be excluded at all positions
_, exists := result[LabelsFromArgs("label", "x").MapKey()]
require.False(t, exists, "series x should not be in result")
})
t.Run("partial ties with different values", func(t *testing.T) {
input := createSeriesSet(map[string][]float64{
"alpha": {10, 20, 30},
"beta": {10, 20, 40},
"gamma": {10, 20, 50},
"delta": {20, 30, 60},
})
result := processBottomK(input, 3, 2)
// At index 0: bottom 2 are {beta:10, gamma:10} (three-way tie at 10, alphabetically later two)
// At index 1: bottom 2 are {beta:20, gamma:20} (three-way tie at 20, alphabetically later two)
// At index 2: bottom 2 are {alpha:30, beta:40} (alpha:30 is lowest, beta:40 is second lowest)
expectSeriesValues(t, result[LabelsFromArgs("label", "alpha").MapKey()].Values, []float64{math.NaN(), math.NaN(), 30})
expectSeriesValues(t, result[LabelsFromArgs("label", "beta").MapKey()].Values, []float64{10, 20, 40})
expectSeriesValues(t, result[LabelsFromArgs("label", "gamma").MapKey()].Values, []float64{10, 20, math.NaN()})
// delta should not be in the result (all NaN)
_, exists := result[LabelsFromArgs("label", "delta").MapKey()]
require.False(t, exists, "series delta should not be in result")
})
t.Run("k=1 with ties", func(t *testing.T) {
input := createSeriesSet(map[string][]float64{
"service1": {10, 20, 30},
"service2": {10, 25, 30},
"service3": {15, 30, 30},
})
result := processBottomK(input, 3, 1)
// With k=1, only one series should be selected at each position
// At index 0: bottom 1 is {service2:10} (tied with service1, but alphabetically later)
// At index 1: bottom 1 is {service1:20} (lowest value)
// At index 2: bottom 1 is {service3:30} (three-way tie, alphabetically later)
expectSeriesValues(t, result[LabelsFromArgs("label", "service1").MapKey()].Values, []float64{math.NaN(), 20, math.NaN()})
expectSeriesValues(t, result[LabelsFromArgs("label", "service2").MapKey()].Values, []float64{10, math.NaN(), math.NaN()})
expectSeriesValues(t, result[LabelsFromArgs("label", "service3").MapKey()].Values, []float64{math.NaN(), math.NaN(), 30})
})
t.Run("ties with NaN values", func(t *testing.T) {
input := createSeriesSet(map[string][]float64{
"m": {math.NaN(), 5, 5},
"n": {5, 5, math.NaN()},
"o": {5, math.NaN(), 5},
})
result := processBottomK(input, 3, 2)
// NaN values should be ignored, ties broken alphabetically (later for bottomk)
// At index 0: bottom 2 are {n:5, o:5} (m has NaN, alphabetically later two with values)
// At index 1: bottom 2 are {m:5, n:5} (o has NaN, alphabetically later two with values)
// At index 2: bottom 2 are {m:5, o:5} (n has NaN, alphabetically later two with values)
expectSeriesValues(t, result[LabelsFromArgs("label", "m").MapKey()].Values, []float64{math.NaN(), 5, 5})
expectSeriesValues(t, result[LabelsFromArgs("label", "n").MapKey()].Values, []float64{5, 5, math.NaN()})
expectSeriesValues(t, result[LabelsFromArgs("label", "o").MapKey()].Values, []float64{5, math.NaN(), 5})
})
t.Run("edge case with larger k than series", func(t *testing.T) {
input := createSeriesSet(map[string][]float64{
"a": {1, 2, 3},
"b": {1, 2, 3},
})
result := processBottomK(input, 3, 5)
// k=5 is larger than the number of series (2), so all series should be included
expectSeriesValues(t, result[LabelsFromArgs("label", "a").MapKey()].Values, []float64{1, 2, 3})
expectSeriesValues(t, result[LabelsFromArgs("label", "b").MapKey()].Values, []float64{1, 2, 3})
})
}
func TestHistogramAggregator(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: uint64(time.Now().Add(-1 * time.Hour).UnixNano()),
End: uint64(time.Now().UnixNano()),
Step: uint64(15 * time.Second.Nanoseconds()),
Exemplars: maxExemplars,
}
const seriesCount = 6
cases := []struct {
name string
samplesCount int
exemplarCount int
}{
{"Small", 10, 5},
{"Medium", 100, 20},
{"Large", 1000, 100},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
series := generateTestTimeSeries(seriesCount, tc.samplesCount, tc.exemplarCount, req.Start, req.End)
quantiles := []float64{0.5, 0.9, 0.99}
agg := NewHistogramAggregator(req, quantiles, uint32(tc.exemplarCount))
agg.Combine(series)
results := agg.Results()
require.NotNil(t, results, "Expected non-nil SeriesSet from HistogramAggregator")
require.Greater(t, len(results), 0, "Expected non-empty SeriesSet from HistogramAggregator")
// Check that exemplars are distributed across quantile series
totalExemplars := 0
for _, ts := range results {
totalExemplars += len(ts.Exemplars)
}
require.Greater(t, totalExemplars, 0, "Expected at least some exemplars across all quantile series")
for _, ts := range results {
// With aggregated semantic matching, total exemplars can be higher since
// we're more efficiently distributing exemplars across quantile series
require.LessOrEqual(t, len(ts.Exemplars), tc.exemplarCount*5, "Per-series exemplars should be reasonable")
// Note: With aggregated semantic matching, exemplars are distributed more efficiently
// across quantile series based on comprehensive quantile thresholds
// t.Logf("Series: %s", ts.Labels)
// t.Logf("Values: %v", ts.Values)
// t.Logf("Exemplars: %v", ts.Exemplars)
// check that the values are within the expected histogram buckets
require.Greater(t, len(ts.Values), 0, "Expected non-empty histogram values")
for _, value := range ts.Values {
require.GreaterOrEqual(t, value, 0.0, "Histogram values should be non-negative")
}
// check that the exemplars are within the expected histogram Buckets
for _, ex := range ts.Exemplars {
require.GreaterOrEqual(t, ex.Value, 0.0, "Exemplar values should be non-negative")
// Convert nanoseconds to milliseconds for comparison
startMs := req.Start / uint64(time.Millisecond)
endMs := req.End / uint64(time.Millisecond)
require.GreaterOrEqual(t, ex.TimestampMs, startMs)
require.LessOrEqual(t, ex.TimestampMs, endMs)
}
// With semantic matching, some quantile series may legitimately have zero exemplars
// if no exemplars fall within their quantile range - this is correct behavior
}
})
}
}
func runTraceQLMetric(req *tempopb.QueryRangeRequest, inSpans ...[]Span) (SeriesSet, int, error) {
res, err := processLayer1AndLayer2(req, inSpans...)
if err != nil {
return nil, 0, err
}
// Pass layer 2 to layer 3
// These are summed counts over time by bucket
return processLayer3(req, res)
}
func processLayer1AndLayer2(req *tempopb.QueryRangeRequest, in ...[]Span) (SeriesSet, error) {
e := NewEngine()
layer2, err := e.CompileMetricsQueryRangeNonRaw(req, AggregateModeSum)
if err != nil {
return nil, err
}
for _, spanSet := range in {
layer1, err := e.CompileMetricsQueryRange(req, 0, 0, false)
if err != nil {
return nil, err
}
for _, s := range spanSet {
layer1.metricsPipeline.observe(s)
}
res := layer1.Results()
// Pass layer 1 to layer 2
// These are partial counts over time by bucket
layer2.metricsPipeline.observeSeries(res.ToProto(req))
}
return layer2.Results(), nil
}
func processLayer3(req *tempopb.QueryRangeRequest, results ...SeriesSet) (SeriesSet, int, error) {
e := NewEngine()
layer3, err := e.CompileMetricsQueryRangeNonRaw(req, AggregateModeFinal)
if err != nil {
return nil, 0, err
}
for _, res := range results {
layer3.ObserveSeries(res.ToProto(req))
}
return layer3.Results(), layer3.Length(), nil
}
func randInt(minimum, maximum int) int {
return rand.IntN(maximum-minimum) + minimum
}
func randFloat(minimum, maximum float64) float64 {
return rand.Float64()*(maximum-minimum) + minimum
}
func generateSpans(count int, startTimes []int, value string) []Span {
spans := make([]Span, 0)
for i := 0; i < count; i++ {
for _, t := range startTimes {
sTime := uint64(time.Duration(t) * time.Second)
spans = append(spans, newMockSpan(nil).WithStartTime(sTime).WithSpanString("foo", value).WithDuration(128*uint64(i+1)))
}
}
return spans
}
// createSeriesSet to create a SeriesSet from a map of values
func createSeriesSet(data map[string][]float64) SeriesSet {
seriesSet := SeriesSet{}
labelName := "label"
for key, values := range data {
labels := Labels{Label{Name: labelName, Value: NewStaticString(key)}}
seriesSet[labels.MapKey()] = TimeSeries{
Values: values,
Labels: labels,
}
}
return seriesSet
}
// expectSeriesSet validates SeriesSet equality, and also considers NaN values
func expectSeriesSet(t *testing.T, expected, result SeriesSet) {
for expectedKey, expectedSeries := range expected {
resultSeries, ok := result[expectedKey]
require.True(t, ok, "expected series %s to be in result", expectedKey)
require.Equal(t, expectedSeries.Labels, resultSeries.Labels)
// check values, including NaN values
require.Equal(t, len(expectedSeries.Values), len(resultSeries.Values))
for i, expectedValue := range expectedSeries.Values {
if math.IsNaN(expectedValue) {
require.True(t, math.IsNaN(resultSeries.Values[i]), "expected NaN at index %d", i)
} else {
require.Equal(t, expectedValue, resultSeries.Values[i])
}
}
}
}
func expectSeriesValues(t *testing.T, result, expected []float64) {
require.Len(t, result, len(expected))
for i := range result {
if math.IsNaN(expected[i]) {
require.True(t, math.IsNaN(result[i]), "index %d: expected NaN, got %v", i, result[i])
} else {
require.Equal(t, expected[i], result[i], "index %d: expected %v, got %v", i, expected[i], result[i])
}
}
}
func BenchmarkSumOverTime(b *testing.B) {
totalSpans := 1_000_000
in := make([]Span, 0, totalSpans)
in2 := make([]Span, 0, totalSpans)
minimum := 1e10 // 10 billion
maximun := 1e20 // 100 quintillion
for range totalSpans {
s := time.Duration(randInt(1, 3)) * time.Second
v := randFloat(minimum, maximun)
in = append(in, newMockSpan(nil).WithStartTime(uint64(s)).WithSpanString("foo", "bar").WithSpanFloat("kafka.lag", v).WithDuration(100))
s = time.Duration(randInt(1, 3)) * time.Second
v = randFloat(minimum, maximun)
in2 = append(in2, newMockSpan(nil).WithStartTime(uint64(s)).WithSpanString("foo", "bar").WithSpanFloat("kafka.lag", v).WithDuration(100))
}
req := &tempopb.QueryRangeRequest{
Start: uint64(1 * time.Second),
End: uint64(3 * time.Second),
Step: uint64(1 * time.Second),
Query: "{ } | sum_over_time(span.kafka.lag) by (span.foo)",
}
for b.Loop() {
_, _, _ = runTraceQLMetric(req, in, in2)
}
}
func BenchmarkHistogramAggregator_Combine(b *testing.B) {
// nolint:gosec // G115
req := &tempopb.QueryRangeRequest{
Start: uint64(time.Now().Add(-1 * time.Hour).UnixNano()),
End: uint64(time.Now().UnixNano()),
Step: uint64(15 * time.Second.Nanoseconds()),
Exemplars: maxExemplars,
}
const seriesCount = 6
benchmarks := []struct {
name string
samplesCount int
exemplarCount int
}{
{"Small", 10, 5},
{"Medium", 100, 20},
{"Large", 1000, 100},
}
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
series := generateTestTimeSeries(seriesCount, bm.samplesCount, bm.exemplarCount, req.Start, req.End)
for b.Loop() {
agg := NewHistogramAggregator(req, []float64{0.5, 0.9, 0.99}, uint32(bm.exemplarCount)) // nolint: gosec // G115
agg.Combine(series)
}
})
}
}
func BenchmarkHistogramAggregator_Results(b *testing.B) {
// nolint:gosec // G115
req := &tempopb.QueryRangeRequest{
Start: uint64(time.Now().Add(-1 * time.Hour).UnixNano()),
End: uint64(time.Now().UnixNano()),
Step: uint64(15 * time.Second.Nanoseconds()),
Exemplars: maxExemplars,
}
benchmarks := []struct {
name string
seriesCount int
samplesCount int
exemplarCount int
quantiles []float64
}{
{"Small_3Quantiles", 6, 10, 5, []float64{0.5, 0.9, 0.99}},
{"Medium_3Quantiles", 10, 100, 20, []float64{0.5, 0.9, 0.99}},
{"Large_3Quantiles", 20, 1000, 100, []float64{0.5, 0.9, 0.99}},
// These test the bucket rescanning optimization specifically
{"Small_5Quantiles", 6, 10, 5, []float64{0.5, 0.75, 0.9, 0.95, 0.99}},
{"Medium_5Quantiles", 10, 100, 20, []float64{0.5, 0.75, 0.9, 0.95, 0.99}},
{"Large_5Quantiles", 20, 1000, 100, []float64{0.5, 0.75, 0.9, 0.95, 0.99}},
// High exemplar density to test caching benefits
{"High_Exemplars", 10, 100, 200, []float64{0.5, 0.9, 0.99}},
}
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
// Generate test data
series := generateTestTimeSeries(bm.seriesCount, bm.samplesCount, bm.exemplarCount, req.Start, req.End)
// Create histogram aggregator
h := NewHistogramAggregator(req, bm.quantiles, req.Exemplars)
// Combine the series (this is setup, not benchmarked)
h.Combine(series)
// Benchmark the Results method
b.ResetTimer()
for i := 0; i < b.N; i++ {
results := h.Results()
_ = results // Prevent optimization
}
})
}
}
// generateTestTimeSeries creates test time series data for benchmarking
// nolint:gosec // G115
func generateTestTimeSeries(seriesCount, samplesCount, exemplarCount int, start, end uint64) []*tempopb.TimeSeries {
result := make([]*tempopb.TimeSeries, seriesCount)
timeRange := end - start
for i := 0; i < seriesCount; i++ {
// Create unique labels for each series
labels := []commonv1proto.KeyValue{
{
Key: "service",
Value: &commonv1proto.AnyValue{
Value: &commonv1proto.AnyValue_StringValue{
StringValue: "service-" + fmt.Sprintf("%d", i),
},
},
},
{
Key: internalLabelBucket,
Value: &commonv1proto.AnyValue{
Value: &commonv1proto.AnyValue_DoubleValue{
DoubleValue: math.Pow(2, float64(i%20)), // Power of 2 as bucket
},
},
},
}
samples := make([]tempopb.Sample, samplesCount)
for j := 0; j < samplesCount; j++ {
// Distribute samples evenly across the time range
offset := (uint64(j) * timeRange) / uint64(samplesCount)
ts := time.Unix(0, int64(start+offset)).UnixMilli()
samples[j] = tempopb.Sample{
TimestampMs: ts,
Value: float64(j % 100), // Simple pattern for test data
}
}
// Create exemplars
exemplars := make([]tempopb.Exemplar, exemplarCount)
for j := 0; j < exemplarCount; j++ {
// Distribute exemplars evenly across the time range
offset := (uint64(j) * timeRange) / uint64(exemplarCount)
ts := time.Unix(0, int64(start+offset)).UnixMilli()
exemplarLabels := []commonv1proto.KeyValue{
{
Key: "trace_id",
Value: &commonv1proto.AnyValue{
Value: &commonv1proto.AnyValue_StringValue{
StringValue: fmt.Sprintf("trace-%d", i*1000+j),
},
},
},
{
Key: "span_id",
Value: &commonv1proto.AnyValue{
Value: &commonv1proto.AnyValue_StringValue{
StringValue: fmt.Sprintf("span-%d", j),
},
},
},
}
exemplars[j] = tempopb.Exemplar{
Labels: exemplarLabels,
Value: float64(j % 100), // Simple pattern for test data
TimestampMs: ts,
}
}
result[i] = &tempopb.TimeSeries{
Labels: labels,
Samples: samples,
Exemplars: exemplars,
}
}
return result
}
func TestHistogramAggregator_ExemplarBucketSelection(t *testing.T) {
req := &tempopb.QueryRangeRequest{
Start: uint64(time.Now().Add(-1 * time.Hour).UnixNano()),
End: uint64(time.Now().UnixNano()),
Step: uint64(15 * time.Second.Nanoseconds()),
Exemplars: 10,
}
tests := []struct {
name string
quantiles []float64
timeSeries []*tempopb.TimeSeries
expectedExemplarBuckets map[string][]float64 // quantile label -> expected bucket values
}{
{
name: "exemplars match correct quantile buckets",
quantiles: []float64{0.5, 0.9},
timeSeries: []*tempopb.TimeSeries{
{
Labels: []commonv1proto.KeyValue{
{Key: "service", Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_StringValue{StringValue: "test"}}},
{Key: internalLabelBucket, Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_DoubleValue{DoubleValue: 1.0}}},
},
Samples: []tempopb.Sample{
{TimestampMs: time.Unix(0, int64(req.Start)).UnixMilli(), Value: 5}, // 5 samples in 1s bucket (p50)
},
Exemplars: []tempopb.Exemplar{
{
Labels: []commonv1proto.KeyValue{
{Key: "trace_id", Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_StringValue{StringValue: "trace1"}}},
},
Value: 0.8, // Should go to p50 (< quantile threshold)
TimestampMs: time.Unix(0, int64(req.Start)).UnixMilli(),
},
},
},
{
Labels: []commonv1proto.KeyValue{
{Key: "service", Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_StringValue{StringValue: "test"}}},
{Key: internalLabelBucket, Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_DoubleValue{DoubleValue: 4.0}}},
},
Samples: []tempopb.Sample{
{TimestampMs: time.Unix(0, int64(req.Start)).UnixMilli(), Value: 1}, // 1 sample in 4s bucket (p90)
},
Exemplars: []tempopb.Exemplar{
{
Labels: []commonv1proto.KeyValue{
{Key: "trace_id", Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_StringValue{StringValue: "trace2"}}},
},
Value: 3.5, // Should go to p90 (> quantile threshold)
TimestampMs: time.Unix(0, int64(req.Start)).UnixMilli(),
},
},
},
},
expectedExemplarBuckets: map[string][]float64{
`{p="0.5", service="test"}`: {1.0}, // p50 should get fast exemplars
`{p="0.9", service="test"}`: {4.0}, // p90 should get slow exemplars
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
agg := NewHistogramAggregator(req, tt.quantiles, 10)
agg.Combine(tt.timeSeries)
results := agg.Results()
// Verify semantic matching behavior - exemplars should be assigned based on quantile ranges
p50Series, p50Exists := results[LabelsFromArgs("service", "test", "p", 0.5).MapKey()]
p90Series, p90Exists := results[LabelsFromArgs("service", "test", "p", 0.9).MapKey()]
require.True(t, p50Exists, "p50 series should exist")
require.True(t, p90Exists, "p90 series should exist")
// Check that we have exemplars distributed appropriately
totalExemplars := len(p50Series.Exemplars) + len(p90Series.Exemplars)
require.Greater(t, totalExemplars, 0, "Should have some exemplars")
// Verify exemplars are assigned correctly based on semantic matching
// Exemplars should be distributed across quantile ranges appropriately
})
}
}
func TestHistogramAggregator_ExemplarDistribution(t *testing.T) {
// Use fixed timestamps to ensure deterministic behavior
baseTime := time.Unix(1257894000, 0) // Fixed timestamp
req := &tempopb.QueryRangeRequest{
Start: uint64(baseTime.UnixNano()),
End: uint64(baseTime.Add(1 * time.Hour).UnixNano()),
Step: uint64(15 * time.Second.Nanoseconds()),
Exemplars: 12, // Will be distributed: 12/3 = 4 per quantile
}
// Create test data with multiple exemplars
timeSeries := []*tempopb.TimeSeries{
{
Labels: []commonv1proto.KeyValue{
{Key: "service", Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_StringValue{StringValue: "test"}}},
{Key: internalLabelBucket, Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_DoubleValue{DoubleValue: 2.0}}},
},
Samples: []tempopb.Sample{
{TimestampMs: baseTime.UnixMilli(), Value: 10},
},
Exemplars: []tempopb.Exemplar{
{Value: 1.5, TimestampMs: baseTime.UnixMilli()},
{Value: 1.6, TimestampMs: baseTime.UnixMilli()},
{Value: 1.7, TimestampMs: baseTime.UnixMilli()},
{Value: 1.8, TimestampMs: baseTime.UnixMilli()},
{Value: 1.9, TimestampMs: baseTime.UnixMilli()},
},
},
}
quantiles := []float64{0.5, 0.9, 0.99}
agg := NewHistogramAggregator(req, quantiles, 12)
agg.Combine(timeSeries)
results := agg.Results()
totalExemplars := 0
for _, series := range results {
totalExemplars += len(series.Exemplars)
}
// Verify total exemplars doesn't exceed limit
require.LessOrEqual(t, totalExemplars, 12, "Total exemplars should not exceed limit")
// Verify each quantile gets roughly equal distribution (allowing for rounding)
expectedPerQuantile := 12 / len(quantiles)
for seriesLabel, series := range results {
require.LessOrEqual(t, len(series.Exemplars), expectedPerQuantile+1,
"Series %s has too many exemplars: %d", seriesLabel, len(series.Exemplars))
}
}
func TestLog2Quantile(t *testing.T) {
tests := []struct {
name string
quantile float64
buckets []HistogramBucket
expectedValue float64
}{
{
name: "p50 in middle bucket",
quantile: 0.5,
buckets: []HistogramBucket{
{Max: 1.0, Count: 2},
{Max: 2.0, Count: 4}, // p50 should land here
{Max: 4.0, Count: 2},
},
},
{
name: "p90 in last bucket",
quantile: 0.9,
buckets: []HistogramBucket{
{Max: 1.0, Count: 1},
{Max: 2.0, Count: 1},
{Max: 4.0, Count: 8}, // p90 should land here
},
},
{
name: "p100 exact match",
quantile: 1.0,
buckets: []HistogramBucket{
{Max: 1.0, Count: 5},
{Max: 2.0, Count: 5},
},
expectedValue: 2.0,
},
{
name: "empty buckets",
quantile: 0.5,
buckets: []HistogramBucket{},
expectedValue: 0.0,
},
{
name: "single bucket",
quantile: 0.5,
buckets: []HistogramBucket{
{Max: 2.0, Count: 10},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
value := Log2Quantile(tt.quantile, tt.buckets)
if tt.expectedValue != 0 {
require.Equal(t, tt.expectedValue, value, "Quantile value mismatch")
}
// Verify value is reasonable (non-negative)
require.GreaterOrEqual(t, value, 0.0, "Quantile values should be non-negative")
})
}
}
func TestHistogramAggregator_EdgeCases(t *testing.T) {
// Use fixed timestamps to ensure deterministic behavior
baseTime := time.Unix(1257894000, 0) // Fixed timestamp
req := &tempopb.QueryRangeRequest{
Start: uint64(baseTime.UnixNano()),
End: uint64(baseTime.Add(1 * time.Hour).UnixNano()),
Step: uint64(15 * time.Second.Nanoseconds()),
Exemplars: 5,
}
tests := []struct {
name string
timeSeries []*tempopb.TimeSeries
quantiles []float64
expectFunc func(t *testing.T, results SeriesSet)
}{
{
name: "no exemplars",
quantiles: []float64{0.5, 0.9},
timeSeries: []*tempopb.TimeSeries{
{
Labels: []commonv1proto.KeyValue{
{Key: "service", Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_StringValue{StringValue: "test"}}},
{Key: internalLabelBucket, Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_DoubleValue{DoubleValue: 2.0}}},
},
Samples: []tempopb.Sample{
{TimestampMs: baseTime.UnixMilli(), Value: 5},
},
Exemplars: []tempopb.Exemplar{}, // No exemplars
},
},
expectFunc: func(t *testing.T, results SeriesSet) {
for _, series := range results {
require.Empty(t, series.Exemplars, "Should have no exemplars")
}
},
},
{
name: "exemplars outside bucket ranges",
quantiles: []float64{0.5},
timeSeries: []*tempopb.TimeSeries{
{
Labels: []commonv1proto.KeyValue{
{Key: "service", Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_StringValue{StringValue: "test"}}},
{Key: internalLabelBucket, Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_DoubleValue{DoubleValue: 2.0}}},
},
Samples: []tempopb.Sample{
{TimestampMs: baseTime.UnixMilli(), Value: 5},
},
Exemplars: []tempopb.Exemplar{
{
Value: 10.0, // Much larger than bucket, should not match
TimestampMs: baseTime.UnixMilli(),
},
},
},
},
expectFunc: func(t *testing.T, results SeriesSet) {
for _, series := range results {
// May have no exemplars if the value doesn't match any bucket
// This is acceptable behavior
if len(series.Exemplars) > 0 {
// If exemplars exist, they should be reasonable
for _, ex := range series.Exemplars {
bucketValue := Log2Bucketize(uint64(ex.Value * float64(time.Second)))
require.True(t, bucketValue > 0, "Bucket value should be positive")
}
}
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
agg := NewHistogramAggregator(req, tt.quantiles, 5)
agg.Combine(tt.timeSeries)
results := agg.Results()
require.NotNil(t, results, "Results should not be nil")
tt.expectFunc(t, results)
})
}
}
func createBucketSeries(bucketValue string, count int, timestampMs int64) *tempopb.TimeSeries {
bucketFloat, _ := strconv.ParseFloat(bucketValue, 64)
return &tempopb.TimeSeries{
Labels: []commonv1proto.KeyValue{
{Key: "service", Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_StringValue{StringValue: "test"}}},
{Key: internalLabelBucket, Value: &commonv1proto.AnyValue{Value: &commonv1proto.AnyValue_DoubleValue{DoubleValue: bucketFloat}}},
},
Samples: []tempopb.Sample{
{TimestampMs: timestampMs, Value: float64(count)},
},
}
}
// requireEqualSeriesSets is like require.Equal for SeriesSets and supports NaN.
func requireEqualSeriesSets(t *testing.T, expected []TimeSeries, actual SeriesSet) {
require.Equal(t, len(expected), len(actual))
for _, eTS := range expected {
k := eTS.Labels.MapKey()
aTS, ok := actual[k]
require.True(t, ok, "expected series %s to be in result", k)
require.Equal(t, eTS.Labels, aTS.Labels, "expected labels %v, got %v", eTS.Labels, aTS.Labels)
eSamples := eTS.Values
aSamples := aTS.Values
require.Equal(t, len(eSamples), len(aSamples), "expected %d samples for %s, got %d", len(eSamples), k, len(aSamples))
for i := range eSamples {
if math.IsNaN(eSamples[i]) {
require.True(t, math.IsNaN(aSamples[i]))
} else {
require.InDelta(t, eSamples[i], aSamples[i], 0.001, "expected %v, got %v, for %s[%d]", eSamples[i], aSamples[i], k, i)
}
}
}
}
func TestHistogramAggregator_LatencySpike(t *testing.T) {
// Simulate a latency spike: normal traffic, then a spike, then normal again
// Use fixed timestamps to ensure deterministic behavior
baseTime := time.Unix(1640995200, 0) // Fixed timestamp: 2022-01-01 00:00:00 UTC
start := baseTime
req := &tempopb.QueryRangeRequest{
Start: uint64(start.UnixNano()),
End: uint64(start.Add(45 * time.Minute).UnixNano()),
Step: uint64(15 * time.Minute), // 3 intervals: normal, spike, normal
}
quantiles := []float64{0.5, 0.9, 0.99}
agg := NewHistogramAggregator(req, quantiles, 20)
// Interval 1: Normal latency (0-15min) - p50=100ms, p90=200ms, p99=500ms
normal1Time := start.Add(7 * time.Minute)
normalSeries1 := []*tempopb.TimeSeries{
createBucketSeries("0.125", 70, normal1Time.UnixMilli()), // 70 fast requests
createBucketSeries("0.25", 20, normal1Time.UnixMilli()), // 20 medium requests
createBucketSeries("0.5", 8, normal1Time.UnixMilli()), // 8 slow requests
createBucketSeries("1.0", 2, normal1Time.UnixMilli()), // 2 very slow requests
}
// Add exemplars for normal period
normalSeries1[0].Exemplars = []tempopb.Exemplar{
{Value: 0.08, TimestampMs: normal1Time.UnixMilli()}, // Fast - should go to p50
{Value: 0.12, TimestampMs: normal1Time.UnixMilli()}, // Fast - should go to p50
}
normalSeries1[1].Exemplars = []tempopb.Exemplar{
{Value: 0.18, TimestampMs: normal1Time.UnixMilli()}, // Medium - should go to p90
}
normalSeries1[2].Exemplars = []tempopb.Exemplar{
{Value: 0.35, TimestampMs: normal1Time.UnixMilli()}, // Slow - should go to p99
}
// Interval 2: Latency spike (15-30min) - p50=800ms, p90=2000ms, p99=4000ms
spikeTime := start.Add(22 * time.Minute)
spikeSeries := []*tempopb.TimeSeries{
createBucketSeries("1.0", 50, spikeTime.UnixMilli()), // 50 slow requests (now "fast" for spike)
createBucketSeries("2.0", 30, spikeTime.UnixMilli()), // 30 very slow requests
createBucketSeries("4.0", 15, spikeTime.UnixMilli()), // 15 extremely slow requests
createBucketSeries("8.0", 5, spikeTime.UnixMilli()), // 5 timeout requests
}
// Add exemplars during spike - these should be assigned contextually
spikeSeries[0].Exemplars = []tempopb.Exemplar{
{Value: 0.9, TimestampMs: spikeTime.UnixMilli()}, // During spike, this is p50!
{Value: 1.1, TimestampMs: spikeTime.UnixMilli()}, // During spike, this is p50!
}
spikeSeries[1].Exemplars = []tempopb.Exemplar{
{Value: 1.8, TimestampMs: spikeTime.UnixMilli()}, // During spike, this is p90!
}
spikeSeries[2].Exemplars = []tempopb.Exemplar{
{Value: 3.5, TimestampMs: spikeTime.UnixMilli()}, // During spike, this is p99!
}
// Interval 3: Back to normal (30-45min) - p50=100ms, p90=200ms, p99=500ms
normal2Time := start.Add(37 * time.Minute)
normalSeries2 := []*tempopb.TimeSeries{
createBucketSeries("0.125", 75, normal2Time.UnixMilli()),
createBucketSeries("0.25", 20, normal2Time.UnixMilli()),
createBucketSeries("0.5", 4, normal2Time.UnixMilli()),
createBucketSeries("1.0", 1, normal2Time.UnixMilli()),
}
normalSeries2[0].Exemplars = []tempopb.Exemplar{
{Value: 0.09, TimestampMs: normal2Time.UnixMilli()}, // Fast - should go to p50
}
normalSeries2[1].Exemplars = []tempopb.Exemplar{
{Value: 0.19, TimestampMs: normal2Time.UnixMilli()}, // Medium - should go to p90
}
// Combine all time series in correct temporal order
allSeries := append([]*tempopb.TimeSeries(nil), normalSeries1...) // Copy normalSeries1
allSeries = append(allSeries, spikeSeries...)
allSeries = append(allSeries, normalSeries2...)
agg.Combine(allSeries)
results := agg.Results()
// Verify we have the expected quantile series
require.Len(t, results, 3, "Should have 3 quantile series")
var p50Series, p90Series, p99Series TimeSeries
var found50, found90, found99 bool
for _, series := range results {
for _, label := range series.Labels {
if label.Name == "p" {
switch label.Value.Float() {
case 0.5:
p50Series = series
found50 = true
case 0.9:
p90Series = series
found90 = true
case 0.99:
p99Series = series
found99 = true
}
}
}
}
require.True(t, found50, "Should find p50 series")
require.True(t, found90, "Should find p90 series")
require.True(t, found99, "Should find p99 series")
// Verify quantile values reflect the spike pattern
require.Greater(t, p50Series.Values[1], p50Series.Values[0], "P50 should spike in interval 1")
require.Greater(t, p50Series.Values[1], p50Series.Values[2], "P50 should be higher during spike than after")
// Verify exemplar distribution makes sense with per-interval context
totalExemplars := len(p50Series.Exemplars) + len(p90Series.Exemplars) + len(p99Series.Exemplars)
require.Greater(t, totalExemplars, 0, "Should have exemplars distributed")
t.Logf("Quantile values across intervals:")
t.Logf("P50: [%.3f, %.3f, %.3f]", p50Series.Values[0], p50Series.Values[1], p50Series.Values[2])
t.Logf("P90: [%.3f, %.3f, %.3f]", p90Series.Values[0], p90Series.Values[1], p90Series.Values[2])
t.Logf("P99: [%.3f, %.3f, %.3f]", p99Series.Values[0], p99Series.Values[1], p99Series.Values[2])
t.Logf("Exemplar distribution:")
t.Logf("P50 exemplars: %d", len(p50Series.Exemplars))
t.Logf("P90 exemplars: %d", len(p90Series.Exemplars))
t.Logf("P99 exemplars: %d", len(p99Series.Exemplars))
// Log individual exemplar values to see the assignment
for _, ex := range p50Series.Exemplars {
t.Logf("P50 exemplar: %.3f", ex.Value)
}
for _, ex := range p90Series.Exemplars {
t.Logf("P90 exemplar: %.3f", ex.Value)
}
for _, ex := range p99Series.Exemplars {
t.Logf("P99 exemplar: %.3f", ex.Value)
}
}
func TestLog2QuantileWithBucket(t *testing.T) {
buckets := []HistogramBucket{
{Max: 1.0, Count: 10}, // bucket 0
{Max: 2.0, Count: 20}, // bucket 1
{Max: 4.0, Count: 30}, // bucket 2
{Max: 8.0, Count: 40}, // bucket 3
}
tests := []struct {
name string
quantile float64
expectedBucket int
}{
{"p10", 0.1, 0}, // 10% of 100 = 10, should be in bucket 0
{"p30", 0.3, 1}, // 30% of 100 = 30, should be in bucket 1
{"p60", 0.6, 2}, // 60% of 100 = 60, should be in bucket 2
{"p90", 0.9, 3}, // 90% of 100 = 90, should be in bucket 3
{"p100", 1.0, 3}, // 100% should be in last bucket
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
value, bucketIdx := Log2QuantileWithBucket(tt.quantile, buckets)
// Verify we get a valid quantile value
require.Greater(t, value, 0.0, "Quantile value should be positive")
// Verify we get the expected bucket index
require.Equal(t, tt.expectedBucket, bucketIdx,
"Quantile %f should fall in bucket %d, got %d", tt.quantile, tt.expectedBucket, bucketIdx)
// Verify consistency with original Log2Quantile function
originalValue := Log2Quantile(tt.quantile, buckets)
require.Equal(t, originalValue, value,
"Log2QuantileWithBucket should return same value as Log2Quantile")
})
}
}