mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
* Add operators IN, NOT IN, MATCH ANY, and MATCH NONE
* Implement Static.append()
* Print string arrays with '`' instead of '"'
Example ["aa", "bb"] -> [`aa`, `bb`]
* Rewrite binary operations to array equivalent
The rewriter handles the following cases:
- { .a = "a" || .a = "b" } => { .a IN ["a", "b"] }
- { .a != "a" && .a != "b" } => { .a NOT IN ["a", "b"] }
- { .a =~ "a" || .a =~ "b" } => { .a MATCH ANY ["a", "b"] }
- { .a !~ "a" && .a !~ "b" } => { .a MATCH NONE ["a", "b"] }
* Add test cases with arrays to TestBinOp
* traceql.Parse() applies AST optimizations by default
* Rename predicate_test.go to predicates_test.go
* Add array predicates to parquetquery
* BREAKING CHANGE: more precise regex validation
BinaryOperation.validate() considered everythin a valid regex where
FieldExpression.EncodeToString(false) happened to return a valid regex.
With this change the RHS of a regex operation is required to be a string
or string array
* vp4: handle predicates with arrays correctly
* vp5: handle predicates with arrays correctly
* vp3: handle predicates with arrays correctly
* Add test cases for optimized queries
* Update query test examples with optimizable queries
* vp3: fix handling of array types
* CHANGELOG.md
* Pass array operators down to fetch layer
* Read to first virtual row after seeking on a low definition level
This helps avoid additional spans/op in queries with only
resource attributes
---------
Co-authored-by: Martin Disibio <[email protected]>
483 lines
14 KiB
Go
483 lines
14 KiB
Go
package traceql
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestSpansetFilter_extractConditions(t *testing.T) {
|
|
tests := []struct {
|
|
query string
|
|
conditions []Condition
|
|
allConditions bool
|
|
}{
|
|
{
|
|
query: `{ .foo = "bar" && "bzz" = .fzz }`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticString("bar")),
|
|
newCondition(NewAttribute("fzz"), OpEqual, NewStaticString("bzz")),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ .foo = "bar" || "bzz" = .fzz }`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticString("bar")),
|
|
newCondition(NewAttribute("fzz"), OpEqual, NewStaticString("bzz")),
|
|
},
|
|
allConditions: false,
|
|
},
|
|
{
|
|
query: `{ "foo" = "bar" }`,
|
|
conditions: []Condition{},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ .foo = .bar }`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpNone),
|
|
newCondition(NewAttribute("bar"), OpNone),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ (.foo = "bar") = true }`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticString("bar")),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ true = (.foo = "bar") }`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticString("bar")),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ (.foo = "bar") = .bar }`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticString("bar")),
|
|
newCondition(NewAttribute("bar"), OpNone),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ .bar = (.foo = "bar") }`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("bar"), OpNone),
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticString("bar")),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
// TODO we need a smarter engine to handle this - we can either negate OpEqual or just fetch .fzz
|
|
// {
|
|
// query: `{ (.foo = "bar") = !(.fzz = "bzz") }`,
|
|
// conditions: []Condition{
|
|
// newCondition(NewAttribute("foo"), OpEqual, NewStaticString("bar")),
|
|
// newCondition(NewAttribute("fzz"), OpNotEqual, NewStaticString("bzz")),
|
|
// },
|
|
// allConditions: true,
|
|
// },
|
|
{
|
|
query: `{ (.foo = "bar") = !.bar }`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticString("bar")),
|
|
newCondition(NewAttribute("bar"), OpNone),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ .foo = .bar + 1 }`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpNone),
|
|
newCondition(NewAttribute("bar"), OpNone),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ (.foo = 2) && (.bar / 1 > 3) }`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticInt(2)),
|
|
newCondition(NewAttribute("bar"), OpNone),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ .foo && true }`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpNone),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ true || .foo }`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpNone),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ span.foo = nil }`,
|
|
conditions: []Condition{
|
|
newCondition(NewScopedAttribute(AttributeScopeSpan, false, "foo"), OpNotExists),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.query, func(t *testing.T) {
|
|
expr, err := Parse(tt.query)
|
|
require.NoError(t, err)
|
|
|
|
spansetFilter := expr.Pipeline.Elements[0].(*SpansetFilter)
|
|
|
|
req := &FetchSpansRequest{
|
|
Conditions: []Condition{},
|
|
AllConditions: true,
|
|
}
|
|
spansetFilter.extractConditions(req)
|
|
|
|
assert.Equal(t, tt.conditions, req.Conditions)
|
|
assert.Nil(t, req.SecondPassConditions)
|
|
assert.Equal(t, tt.allConditions, req.AllConditions, "FetchSpansRequest.AllConditions")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScalarFilter_extractConditions(t *testing.T) {
|
|
tests := []struct {
|
|
query string
|
|
conditions []Condition
|
|
allConditions bool
|
|
}{
|
|
{
|
|
query: `{ .foo = "a" } | count() > 10`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticString("a")),
|
|
},
|
|
allConditions: false,
|
|
},
|
|
{
|
|
query: `{ .foo = "a" } | avg(duration) > 10ms`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticString("a")),
|
|
newCondition(NewIntrinsic(IntrinsicDuration), OpNone),
|
|
},
|
|
allConditions: false,
|
|
},
|
|
{
|
|
query: `({ span.http.status_code = 200 } | count()) > ({ span.http.status_code = 500 } | count())`,
|
|
conditions: []Condition{
|
|
newCondition(NewScopedAttribute(AttributeScopeSpan, false, "http.status_code"), OpEqual, NewStaticInt(200)),
|
|
newCondition(NewScopedAttribute(AttributeScopeSpan, false, "http.status_code"), OpEqual, NewStaticInt(500)),
|
|
},
|
|
allConditions: false,
|
|
},
|
|
{
|
|
query: `{ .foo = "a" } | 3 > 2`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticString("a")),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.query, func(t *testing.T) {
|
|
expr, err := Parse(tt.query)
|
|
require.NoError(t, err)
|
|
|
|
req := &FetchSpansRequest{
|
|
Conditions: []Condition{},
|
|
AllConditions: true,
|
|
}
|
|
expr.Pipeline.extractConditions(req)
|
|
|
|
assert.Equal(t, tt.conditions, req.Conditions)
|
|
assert.Nil(t, req.SecondPassConditions)
|
|
assert.Equal(t, tt.allConditions, req.AllConditions, "FetchSpansRequest.AllConditions")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStructuralNestedSet_extractConditions(t *testing.T) {
|
|
tests := []struct {
|
|
query string
|
|
conditions []Condition
|
|
allConditions bool
|
|
}{
|
|
{
|
|
query: `{} >> {}`,
|
|
conditions: []Condition{
|
|
newCondition(NewIntrinsic(IntrinsicStructuralDescendant), OpNone),
|
|
},
|
|
allConditions: false,
|
|
},
|
|
{
|
|
query: `{ nestedSetRight = 2 }`,
|
|
conditions: []Condition{
|
|
newCondition(NewIntrinsic(IntrinsicNestedSetRight), OpEqual, NewStaticInt(2)),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
|
|
{
|
|
query: `{ nestedSetParent = 1 } > { nestedSetLeft < 3 }`,
|
|
conditions: []Condition{
|
|
newCondition(NewIntrinsic(IntrinsicStructuralChild), OpNone),
|
|
newCondition(NewIntrinsic(IntrinsicNestedSetParent), OpEqual, NewStaticInt(1)),
|
|
newCondition(NewIntrinsic(IntrinsicNestedSetLeft), OpLess, NewStaticInt(3)),
|
|
},
|
|
allConditions: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.query, func(t *testing.T) {
|
|
expr, err := Parse(tt.query)
|
|
require.NoError(t, err)
|
|
|
|
req := &FetchSpansRequest{
|
|
Conditions: []Condition{},
|
|
AllConditions: true,
|
|
}
|
|
expr.Pipeline.extractConditions(req)
|
|
|
|
assert.Equal(t, tt.conditions, req.Conditions)
|
|
assert.Nil(t, req.SecondPassConditions)
|
|
assert.Equal(t, tt.allConditions, req.AllConditions, "FetchSpansRequest.AllConditions")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSelect_extractConditions(t *testing.T) {
|
|
tests := []struct {
|
|
query string
|
|
conditions []Condition
|
|
secondPassConditions []Condition
|
|
allConditions bool
|
|
}{
|
|
{
|
|
query: `{ .foo = "a" } | select(resource.service.name)`,
|
|
conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticString("a")),
|
|
},
|
|
secondPassConditions: []Condition{
|
|
newCondition(NewScopedAttribute(AttributeScopeResource, false, "service.name"), OpNone),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ } | select(.name,name)`,
|
|
conditions: []Condition{
|
|
newCondition(NewIntrinsic(IntrinsicSpanStartTime), OpNone),
|
|
},
|
|
secondPassConditions: []Condition{
|
|
newCondition(NewAttribute("name"), OpNone),
|
|
newCondition(NewIntrinsic(IntrinsicName), OpNone),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
// Pipleline elements after a select are always directed to the second pass
|
|
query: `{ } | select(span.foo) | { span.foo = "a" && span.bar = "b"}`,
|
|
conditions: []Condition{
|
|
newCondition(NewIntrinsic(IntrinsicSpanStartTime), OpNone),
|
|
},
|
|
secondPassConditions: []Condition{
|
|
// span.foo=a has no effect because it's already covered by the select statement
|
|
newCondition(NewScopedAttribute(AttributeScopeSpan, false, "foo"), OpNone),
|
|
newCondition(NewScopedAttribute(AttributeScopeSpan, false, "bar"), OpEqual, NewStaticString("b")),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.query, func(t *testing.T) {
|
|
expr, err := Parse(tt.query)
|
|
require.NoError(t, err)
|
|
|
|
req := &FetchSpansRequest{
|
|
Conditions: []Condition{},
|
|
AllConditions: true,
|
|
}
|
|
expr.Pipeline.extractConditions(req)
|
|
|
|
assert.Equal(t, tt.conditions, req.Conditions)
|
|
assert.Equal(t, tt.secondPassConditions, req.SecondPassConditions)
|
|
assert.Equal(t, tt.allConditions, req.AllConditions, "FetchSpansRequest.AllConditions")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMetricsAggregate_extractConditions(t *testing.T) {
|
|
tests := map[string]struct {
|
|
first []Condition
|
|
second []Condition
|
|
all bool
|
|
}{
|
|
`{} | rate() by (name)`: {
|
|
// Empty spanset implies start time
|
|
[]Condition{newCondition(IntrinsicSpanStartTimeAttribute, OpNone)},
|
|
[]Condition{newCondition(IntrinsicNameAttribute, OpNone)},
|
|
true,
|
|
},
|
|
`{name="foo"} | rate() by (name)`: {
|
|
// by() clause doesn't overwrite existing condition
|
|
[]Condition{newCondition(IntrinsicNameAttribute, OpEqual, NewStaticString("foo"))},
|
|
nil,
|
|
true,
|
|
},
|
|
}
|
|
for q, tt := range tests {
|
|
t.Run(q, func(t *testing.T) {
|
|
expr, err := Parse(q)
|
|
require.NoError(t, err)
|
|
|
|
req := &FetchSpansRequest{
|
|
AllConditions: true,
|
|
}
|
|
expr.extractConditions(req)
|
|
|
|
require.Equal(t, tt.first, req.Conditions)
|
|
require.Equal(t, tt.second, req.SecondPassConditions)
|
|
require.Equal(t, tt.all, req.AllConditions, "FetchSpansRequest.AllConditions")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBinaryOperation_extractConditions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
op *BinaryOperation
|
|
want []Condition
|
|
}{
|
|
{
|
|
name: "string array equal",
|
|
op: &BinaryOperation{Op: OpEqual, LHS: NewAttribute("attr"), RHS: NewStaticStringArray([]string{"a", "b"})},
|
|
want: []Condition{
|
|
newCondition(NewAttribute("attr"), OpEqual, NewStaticStringArray([]string{"a", "b"})),
|
|
},
|
|
},
|
|
{
|
|
name: "string IN array",
|
|
op: &BinaryOperation{Op: OpIn, LHS: NewAttribute("attr"), RHS: NewStaticStringArray([]string{"a", "b"})},
|
|
want: []Condition{
|
|
newCondition(NewAttribute("attr"), OpIn, NewStaticStringArray([]string{"a", "b"})),
|
|
},
|
|
},
|
|
{
|
|
name: "int array equal",
|
|
op: &BinaryOperation{Op: OpEqual, LHS: NewAttribute("attr"), RHS: NewStaticIntArray([]int{1, 2, 3})},
|
|
want: []Condition{
|
|
newCondition(NewAttribute("attr"), OpEqual, NewStaticIntArray([]int{1, 2, 3})),
|
|
},
|
|
},
|
|
{
|
|
name: "int IN array",
|
|
op: &BinaryOperation{Op: OpIn, LHS: NewAttribute("attr"), RHS: NewStaticIntArray([]int{1, 2, 3})},
|
|
want: []Condition{
|
|
newCondition(NewAttribute("attr"), OpIn, NewStaticIntArray([]int{1, 2, 3})),
|
|
},
|
|
},
|
|
{
|
|
name: "string array not regex",
|
|
op: &BinaryOperation{Op: OpNotRegex, LHS: NewAttribute("attr"), RHS: NewStaticStringArray([]string{"a.*", "b.*"})},
|
|
want: []Condition{
|
|
newCondition(NewAttribute("attr"), OpNotRegex, NewStaticStringArray([]string{"a.*", "b.*"})),
|
|
},
|
|
},
|
|
{
|
|
name: "string array MATCH ANY regex",
|
|
op: &BinaryOperation{Op: OpRegexMatchAny, LHS: NewAttribute("attr"), RHS: NewStaticStringArray([]string{"a.*", "b.*"})},
|
|
want: []Condition{
|
|
newCondition(NewAttribute("attr"), OpRegexMatchAny, NewStaticStringArray([]string{"a.*", "b.*"})),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := &FetchSpansRequest{
|
|
AllConditions: true,
|
|
}
|
|
tt.op.extractConditions(req)
|
|
require.Equal(t, tt.want, req.Conditions)
|
|
require.Equal(t, true, req.AllConditions, "allConditions should be true")
|
|
})
|
|
|
|
t.Run(tt.name+" inverted", func(t *testing.T) {
|
|
req := &FetchSpansRequest{
|
|
AllConditions: true,
|
|
}
|
|
tt.op.RHS, tt.op.LHS = tt.op.LHS, tt.op.RHS
|
|
tt.op.extractConditions(req)
|
|
require.Equal(t, tt.want, req.Conditions)
|
|
require.Equal(t, true, req.AllConditions, "allConditions should be true")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUnaryOperation_extractConditions(t *testing.T) {
|
|
tests := []struct {
|
|
query string
|
|
conditions []Condition
|
|
allConditions bool
|
|
}{
|
|
{
|
|
query: `{ span.foo != nil }`,
|
|
conditions: []Condition{
|
|
newCondition(NewScopedAttribute(AttributeScopeSpan, false, "foo"), OpExists),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ span.foo = nil }`,
|
|
conditions: []Condition{
|
|
newCondition(NewScopedAttribute(AttributeScopeSpan, false, "foo"), OpNotExists),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ span.foo }`,
|
|
conditions: []Condition{
|
|
newCondition(NewScopedAttribute(AttributeScopeSpan, false, "foo"), OpNone),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ !span.foo }`,
|
|
conditions: []Condition{
|
|
newCondition(NewScopedAttribute(AttributeScopeSpan, false, "foo"), OpNone),
|
|
},
|
|
allConditions: true,
|
|
},
|
|
{
|
|
query: `{ !(span.a = "a" || span.a = "c") }`,
|
|
conditions: []Condition{
|
|
newCondition(NewScopedAttribute(AttributeScopeSpan, false, "a"), OpEqual, NewStaticString("a")),
|
|
newCondition(NewScopedAttribute(AttributeScopeSpan, false, "a"), OpEqual, NewStaticString("c")),
|
|
},
|
|
allConditions: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.query, func(t *testing.T) {
|
|
expr, err := Parse(tt.query)
|
|
require.NoError(t, err)
|
|
|
|
req := &FetchSpansRequest{
|
|
Conditions: []Condition{},
|
|
AllConditions: true,
|
|
}
|
|
expr.extractConditions(req)
|
|
|
|
assert.Equal(t, tt.conditions, req.Conditions)
|
|
assert.Nil(t, req.SecondPassConditions)
|
|
assert.Equal(t, tt.allConditions, req.AllConditions, "FetchSpansRequest.AllConditions")
|
|
})
|
|
}
|
|
}
|