Files
cc86e98b04 Rewrite conditions on same attribute to equivalent array operation (#6353)
* 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]>
2026-02-22 00:55:23 -08:00

1780 lines
58 KiB
Go

package traceql
import (
"fmt"
"math"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestPipelineErrors(t *testing.T) {
tests := []struct {
in string
err error
}{
{in: "", err: newParseError("syntax error: unexpected $end", 0, 0)},
{in: "{ .a } | { .b", err: newParseError("syntax error: unexpected $end", 1, 14)},
{in: "{ .a | .b }", err: newParseError("syntax error: unexpected |", 1, 6)},
{in: "({ .a } | { .b }", err: newParseError("syntax error: unexpected $end, expecting ) or |", 1, 17)},
{in: "({ .a } | { .b }) + ({ .a } | { .b })", err: newParseError("syntax error: unexpected +, expecting with", 1, 19)},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
_, err := Parse(tc.in)
require.Equal(t, tc.err, err)
})
}
}
func TestPipelineOperatorPrecedence(t *testing.T) {
tests := []struct {
in string
expected SpansetOperation
}{
{
in: "({ .a } | { .b }) > ({ .a } | { .b }) && ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetAnd,
newSpansetOperation(OpSpansetChild,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) > (({ .a } | { .b }) && ({ .a } | { .b }))",
expected: newSpansetOperation(OpSpansetChild,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newSpansetOperation(OpSpansetAnd,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
),
},
{
in: "({ .a } | { .b }) < (({ .a } | { .b }) && ({ .a } | { .b }))",
expected: newSpansetOperation(OpSpansetParent,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newSpansetOperation(OpSpansetAnd,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
),
},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(tc.expected)), actual)
})
}
}
func TestPipelineSpansetOperators(t *testing.T) {
tests := []struct {
in string
expected SpansetOperation
}{
{
in: "({ .a } | { .b }) > ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetChild,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) < ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetParent,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) ~ ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetSibling,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) && ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetAnd,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) >> ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetDescendant,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) << ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetAncestor,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) !> ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetNotChild,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) !< ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetNotParent,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) !~ ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetNotSibling,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) !>> ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetNotDescendant,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) !<< ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetNotAncestor,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) &> ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetUnionChild,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) &< ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetUnionParent,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) &~ ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetUnionSibling,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) &>> ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetUnionDescendant,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
{
in: "({ .a } | { .b }) &<< ({ .a } | { .b })",
expected: newSpansetOperation(OpSpansetUnionAncestor,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
),
},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(tc.expected)), actual)
})
}
}
func TestPipelineScalarOperators(t *testing.T) {
tests := []struct {
in string
expected ScalarFilter
}{
{
in: "({ .a } | count()) = ({ .a } | count())",
expected: newScalarFilter(OpEqual,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newAggregate(aggregateCount, nil),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newAggregate(aggregateCount, nil),
),
),
},
{
in: "({ .a } | count()) != ({ .a } | count())",
expected: newScalarFilter(OpNotEqual,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newAggregate(aggregateCount, nil),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newAggregate(aggregateCount, nil),
),
),
},
{
in: "({ .a } | count()) < ({ .a } | count())",
expected: newScalarFilter(OpLess,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newAggregate(aggregateCount, nil),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newAggregate(aggregateCount, nil),
),
),
},
{
in: "({ .a } | count()) <= ({ .a } | count())",
expected: newScalarFilter(OpLessEqual,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newAggregate(aggregateCount, nil),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newAggregate(aggregateCount, nil),
),
),
},
{
in: "({ .a } | count()) >= ({ .a } | count())",
expected: newScalarFilter(OpGreaterEqual,
newPipeline(
newSpansetFilter(NewAttribute("a")),
newAggregate(aggregateCount, nil),
),
newPipeline(
newSpansetFilter(NewAttribute("a")),
newAggregate(aggregateCount, nil),
),
),
},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(tc.expected)), actual)
})
}
}
func TestPipelines(t *testing.T) {
tests := []struct {
in string
expected Pipeline
}{
{
in: "{ .a } | { .b }",
expected: newPipeline(
newSpansetFilter(NewAttribute("a")),
newSpansetFilter(NewAttribute("b")),
),
},
{
in: "{ .a } | count() > 1",
expected: newPipeline(
newSpansetFilter(NewAttribute("a")),
newScalarFilter(OpGreater, newAggregate(aggregateCount, nil), NewStaticInt(1)),
),
},
{
in: "{ .a } | by(.namespace) | coalesce() | avg(duration) = 1s ",
expected: newPipeline(
newSpansetFilter(NewAttribute("a")),
newGroupOperation(NewAttribute("namespace")),
newCoalesceOperation(),
newScalarFilter(OpEqual, newAggregate(aggregateAvg, NewIntrinsic(IntrinsicDuration)), NewStaticDuration(time.Second)),
),
},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, newRootExpr(tc.expected), actual)
})
}
}
func TestGroupCoalesceErrors(t *testing.T) {
tests := []struct {
in string
err error
}{
{in: "by(.a) && { .b }", err: newParseError("syntax error: unexpected &&, expecting with", 0, 8)},
{in: "by()", err: newParseError("syntax error: unexpected )", 1, 4)},
{in: "coalesce()", err: newParseError("syntax error: unexpected coalesce", 1, 1)},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
_, err := Parse(tc.in)
require.Equal(t, tc.err, err)
})
}
}
func TestGroupCoalesceOperation(t *testing.T) {
tests := []struct {
in string
expected Pipeline
}{
{in: "by(.a) | coalesce()", expected: newPipeline(newGroupOperation(NewAttribute("a")), newCoalesceOperation())},
{in: "by(.a + .b)", expected: newPipeline(newGroupOperation(newBinaryOperation(OpAdd, NewAttribute("a"), NewAttribute("b"))))},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, newRootExpr(tc.expected), actual)
})
}
}
func TestSelectErrors(t *testing.T) {
tests := []struct {
in string
err error
}{
{in: "select(.a) && { .b }", err: newParseError("syntax error: unexpected &&, expecting with", 0, 12)},
{in: "select()", err: newParseError("syntax error: unexpected )", 1, 8)},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
_, err := Parse(tc.in)
require.Equal(t, tc.err, err)
})
}
}
func TestSelectOperation(t *testing.T) {
tests := []struct {
in string
expected Pipeline
}{
{in: "select(.a)", expected: newPipeline(newSelectOperation([]Attribute{NewAttribute("a")}))},
{in: "select(.a,.b)", expected: newPipeline(newSelectOperation([]Attribute{NewAttribute("a"), NewAttribute("b")}))},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, newRootExpr(tc.expected), actual)
})
}
}
func TestSpansetExpressionErrors(t *testing.T) {
tests := []struct {
in string
err error
}{
{in: "{ true } &&", err: newParseError("syntax error: unexpected $end, expecting { or (", 1, 12)},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
_, err := Parse(tc.in)
require.Equal(t, tc.err, err)
})
}
}
func TestSpansetExpressionPrecedence(t *testing.T) {
tests := []struct {
in string
expected SpansetOperation
}{
{
in: "{ true } && { false } >> { `a` }",
expected: newSpansetOperation(OpSpansetAnd,
newSpansetFilter(NewStaticBool(true)),
newSpansetOperation(OpSpansetDescendant, newSpansetFilter(NewStaticBool(false)), newSpansetFilter(NewStaticString("a"))),
),
},
{
in: "{ true } >> { false } && { `a` }",
expected: newSpansetOperation(OpSpansetAnd,
newSpansetOperation(OpSpansetDescendant, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false))),
newSpansetFilter(NewStaticString("a")),
),
},
{
in: "({ true } >> { false }) && { `a` }",
expected: newSpansetOperation(OpSpansetAnd,
newSpansetOperation(OpSpansetDescendant, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false))),
newSpansetFilter(NewStaticString("a")),
),
},
{
in: "{ true } >> { false } ~ { `a` }",
expected: newSpansetOperation(OpSpansetSibling,
newSpansetOperation(OpSpansetDescendant, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false))),
newSpansetFilter(NewStaticString("a")),
),
},
{
in: "{ true } ~ { false } >> { `a` }",
expected: newSpansetOperation(OpSpansetDescendant,
newSpansetOperation(OpSpansetSibling, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false))),
newSpansetFilter(NewStaticString("a")),
),
},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(tc.expected)), actual)
})
}
}
func TestSpansetExpressionOperators(t *testing.T) {
tests := []struct {
in string
expected SpansetOperation
}{
{in: "{ true } && { false }", expected: newSpansetOperation(OpSpansetAnd, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } > { false }", expected: newSpansetOperation(OpSpansetChild, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } < { false }", expected: newSpansetOperation(OpSpansetParent, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } >> { false }", expected: newSpansetOperation(OpSpansetDescendant, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } << { false }", expected: newSpansetOperation(OpSpansetAncestor, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } || { false }", expected: newSpansetOperation(OpSpansetUnion, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } ~ { false }", expected: newSpansetOperation(OpSpansetSibling, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
// this test was added to highlight the one shift/reduce conflict in the grammar. this could also be parsed as two spanset pipelines &&ed together.
{in: "({ true }) && ({ false })", expected: newSpansetOperation(OpSpansetAnd, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } !> { false }", expected: newSpansetOperation(OpSpansetNotChild, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } !< { false }", expected: newSpansetOperation(OpSpansetNotParent, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } !>> { false }", expected: newSpansetOperation(OpSpansetNotDescendant, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } !<< { false }", expected: newSpansetOperation(OpSpansetNotAncestor, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } !~ { false }", expected: newSpansetOperation(OpSpansetNotSibling, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } &> { false }", expected: newSpansetOperation(OpSpansetUnionChild, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } &< { false }", expected: newSpansetOperation(OpSpansetUnionParent, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } &>> { false }", expected: newSpansetOperation(OpSpansetUnionDescendant, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } &<< { false }", expected: newSpansetOperation(OpSpansetUnionAncestor, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
{in: "{ true } &~ { false }", expected: newSpansetOperation(OpSpansetUnionSibling, newSpansetFilter(NewStaticBool(true)), newSpansetFilter(NewStaticBool(false)))},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(tc.expected)), actual)
})
}
}
func TestScalarExpressionErrors(t *testing.T) {
tests := []struct {
in string
err error
}{
{in: "(avg(.foo) > count()) + sum(.bar)", err: newParseError("syntax error: unexpected +, expecting with", 1, 23)},
{in: "count(", err: newParseError("syntax error: unexpected $end, expecting )", 1, 7)},
{in: "count(avg)", err: newParseError("syntax error: unexpected avg, expecting )", 1, 7)},
{in: "count(.thing)", err: newParseError("syntax error: unexpected ., expecting )", 1, 7)},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
_, err := Parse(tc.in)
require.Equal(t, tc.err, err)
})
}
}
func TestScalarExpressionPrecedence(t *testing.T) {
tests := []struct {
in string
expected ScalarFilter
}{
{
in: "avg(.foo) > count() + sum(.bar)",
expected: newScalarFilter(OpGreater,
newAggregate(aggregateAvg, NewAttribute("foo")),
newScalarOperation(OpAdd,
newAggregate(aggregateCount, nil),
newAggregate(aggregateSum, NewAttribute("bar")),
),
),
},
{
in: "avg(.foo) + count() > sum(.bar)",
expected: newScalarFilter(OpGreater,
newScalarOperation(OpAdd,
newAggregate(aggregateAvg, NewAttribute("foo")),
newAggregate(aggregateCount, nil),
),
newAggregate(aggregateSum, NewAttribute("bar")),
),
},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(tc.expected)), actual)
})
}
}
func TestScalarExpressionOperators(t *testing.T) {
tests := []struct {
in string
expected ScalarFilter
}{
{in: "count() > 1", expected: newScalarFilter(OpGreater, newAggregate(aggregateCount, nil), NewStaticInt(1))},
{in: "max(.a) > 1", expected: newScalarFilter(OpGreater, newAggregate(aggregateMax, NewAttribute("a")), NewStaticInt(1))},
{in: "min(1) > 1", expected: newScalarFilter(OpGreater, newAggregate(aggregateMin, NewStaticInt(1)), NewStaticInt(1))},
{in: "sum(true) > 1", expected: newScalarFilter(OpGreater, newAggregate(aggregateSum, NewStaticBool(true)), NewStaticInt(1))},
{in: "avg(`c`) > 1", expected: newScalarFilter(OpGreater, newAggregate(aggregateAvg, NewStaticString("c")), NewStaticInt(1))},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(tc.expected)), actual)
})
}
}
func TestSpansetFilterErrors(t *testing.T) {
tests := []struct {
in string
err error
}{
{in: "wharblgarbl", err: newParseError("syntax error: unexpected IDENTIFIER", 1, 1)},
{in: "{ 2 <> 3}", err: newParseError("syntax error: unexpected >", 1, 6)},
{in: "{ 2 = .b ", err: newParseError("syntax error: unexpected $end", 1, 10)},
{in: "{ + }", err: newParseError("syntax error: unexpected +", 1, 3)},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
_, err := Parse(tc.in)
require.Equal(t, tc.err, err)
})
}
}
func TestSpansetFilterOperatorPrecedence(t *testing.T) {
tests := []struct {
in string
expected FieldExpression
}{
{
in: "{ .a * .b + .c }",
expected: newBinaryOperation(OpAdd,
newBinaryOperation(OpMult, NewAttribute("a"), NewAttribute("b")),
NewAttribute("c")),
},
{
in: "{ .a + .b * .c }",
expected: newBinaryOperation(OpAdd,
NewAttribute("a"),
newBinaryOperation(OpMult, NewAttribute("b"), NewAttribute("c"))),
},
{
in: "{ ( .a + .b ) * .c }",
expected: newBinaryOperation(OpMult,
newBinaryOperation(OpAdd, NewAttribute("a"), NewAttribute("b")),
NewAttribute("c")),
},
{
in: "{ .a + .b ^ .c }",
expected: newBinaryOperation(OpAdd,
NewAttribute("a"),
newBinaryOperation(OpPower, NewAttribute("b"), NewAttribute("c"))),
},
{
in: "{ .a = .b + .c }",
expected: newBinaryOperation(OpEqual,
NewAttribute("a"),
newBinaryOperation(OpAdd, NewAttribute("b"), NewAttribute("c"))),
},
{
in: "{ .a + .b = .c }",
expected: newBinaryOperation(OpEqual,
newBinaryOperation(OpAdd, NewAttribute("a"), NewAttribute("b")),
NewAttribute("c")),
},
{
in: "{ .c - -.a + .b }",
expected: newBinaryOperation(OpAdd,
newBinaryOperation(OpSub, NewAttribute("c"), newUnaryOperation(OpSub, NewAttribute("a"))),
NewAttribute("b")),
},
{
in: "{ .c - -( .a + .b ) }",
expected: newBinaryOperation(OpSub,
NewAttribute("c"),
newUnaryOperation(OpSub, newBinaryOperation(OpAdd, NewAttribute("a"), NewAttribute("b")))),
},
{
in: "{ .a && .b = .c }",
expected: newBinaryOperation(OpAnd,
NewAttribute("a"),
newBinaryOperation(OpEqual, NewAttribute("b"), NewAttribute("c"))),
},
{
in: "{ .a = .b && .c }",
expected: newBinaryOperation(OpAnd,
newBinaryOperation(OpEqual, NewAttribute("a"), NewAttribute("b")),
NewAttribute("c")),
},
{
in: "{ .a = !.b && .c }",
expected: newBinaryOperation(OpAnd,
newBinaryOperation(OpEqual, NewAttribute("a"), newUnaryOperation(OpNot, NewAttribute("b"))),
NewAttribute("c")),
},
{
in: "{ .a = !( .b && .c ) }",
expected: newBinaryOperation(OpEqual,
NewAttribute("a"),
newUnaryOperation(OpNot, newBinaryOperation(OpAnd, NewAttribute("b"), NewAttribute("c")))),
},
{
in: "{ .a = .b || .c = .d}",
expected: newBinaryOperation(OpOr,
newBinaryOperation(OpEqual, NewAttribute("a"), NewAttribute("b")),
newBinaryOperation(OpEqual, NewAttribute("c"), NewAttribute("d"))),
},
{
in: "{ !.a = .b }",
expected: newBinaryOperation(OpEqual,
newUnaryOperation(OpNot, NewAttribute("a")),
NewAttribute("b")),
},
{
in: "{ !(.a = .b) }",
expected: newUnaryOperation(OpNot, newBinaryOperation(OpEqual,
NewAttribute("a"),
NewAttribute("b"))),
},
{
in: "{ -.a = .b }",
expected: newBinaryOperation(OpEqual,
newUnaryOperation(OpSub, NewAttribute("a")),
NewAttribute("b")),
},
{
in: "{ -(.a = .b) }",
expected: newUnaryOperation(OpSub, newBinaryOperation(OpEqual,
NewAttribute("a"),
NewAttribute("b"))),
},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(newSpansetFilter(tc.expected))), actual)
})
}
}
func TestSpansetFilterStatics(t *testing.T) {
tests := []struct {
in string
expected FieldExpression
}{
{in: "{ true }", expected: NewStaticBool(true)},
{in: "{ false }", expected: NewStaticBool(false)},
{in: `{ "true" }`, expected: NewStaticString("true")},
{in: `{ "true\"" }`, expected: NewStaticString("true\"")},
{in: "{ `foo` }", expected: NewStaticString("foo")},
{in: "{ .foo }", expected: NewAttribute("foo")},
{in: "{ duration }", expected: NewIntrinsic(IntrinsicDuration)},
{in: "{ name }", expected: NewIntrinsic(IntrinsicName)},
{in: "{ parent }", expected: NewIntrinsic(IntrinsicParent)},
{in: "{ status }", expected: NewIntrinsic(IntrinsicStatus)},
{in: "{ statusMessage }", expected: NewIntrinsic(IntrinsicStatusMessage)},
{in: "{ 4321 }", expected: NewStaticInt(4321)},
{in: "{ maxInt }", expected: NewStaticInt(math.MaxInt)},
{in: "{ minInt }", expected: NewStaticInt(math.MinInt)},
{in: "{ 1.234 }", expected: NewStaticFloat(1.234)},
{in: "{ 3h }", expected: NewStaticDuration(3 * time.Hour)},
{in: "{ 1.5m }", expected: NewStaticDuration(1*time.Minute + 30*time.Second)},
{in: "{ error }", expected: NewStaticStatus(StatusError)},
{in: "{ ok }", expected: NewStaticStatus(StatusOk)},
{in: "{ unset }", expected: NewStaticStatus(StatusUnset)},
{in: "{ unspecified }", expected: NewStaticKind(KindUnspecified)},
{in: "{ internal }", expected: NewStaticKind(KindInternal)},
{in: "{ client }", expected: NewStaticKind(KindClient)},
{in: "{ server }", expected: NewStaticKind(KindServer)},
{in: "{ producer }", expected: NewStaticKind(KindProducer)},
{in: "{ consumer }", expected: NewStaticKind(KindConsumer)},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(newSpansetFilter(tc.expected))), actual)
})
}
}
func TestSpansetFilterOperators(t *testing.T) {
tests := []struct {
in string
expected FieldExpression
alsoTestWithoutSpace bool
}{
{in: "{ .a + .b }", expected: newBinaryOperation(OpAdd, NewAttribute("a"), NewAttribute("b"))},
{in: "{ .a - .b }", expected: newBinaryOperation(OpSub, NewAttribute("a"), NewAttribute("b"))},
{in: "{ .a / .b }", expected: newBinaryOperation(OpDiv, NewAttribute("a"), NewAttribute("b"))},
{in: "{ .a % .b }", expected: newBinaryOperation(OpMod, NewAttribute("a"), NewAttribute("b"))},
{in: "{ .a * .b }", expected: newBinaryOperation(OpMult, NewAttribute("a"), NewAttribute("b"))},
{in: "{ .a = .b }", expected: newBinaryOperation(OpEqual, NewAttribute("a"), NewAttribute("b")), alsoTestWithoutSpace: true},
{in: "{ .a != .b }", expected: newBinaryOperation(OpNotEqual, NewAttribute("a"), NewAttribute("b")), alsoTestWithoutSpace: true},
{in: "{ .a =~ .b }", expected: newBinaryOperation(OpRegex, NewAttribute("a"), NewAttribute("b")), alsoTestWithoutSpace: true},
{in: "{ .a !~ .b }", expected: newBinaryOperation(OpNotRegex, NewAttribute("a"), NewAttribute("b")), alsoTestWithoutSpace: true},
{in: "{ .a > .b }", expected: newBinaryOperation(OpGreater, NewAttribute("a"), NewAttribute("b")), alsoTestWithoutSpace: true},
{in: "{ .a >= .b }", expected: newBinaryOperation(OpGreaterEqual, NewAttribute("a"), NewAttribute("b")), alsoTestWithoutSpace: true},
{in: "{ .a < .b }", expected: newBinaryOperation(OpLess, NewAttribute("a"), NewAttribute("b")), alsoTestWithoutSpace: true},
{in: "{ .a <= .b }", expected: newBinaryOperation(OpLessEqual, NewAttribute("a"), NewAttribute("b")), alsoTestWithoutSpace: true},
{in: "{ .a ^ .b }", expected: newBinaryOperation(OpPower, NewAttribute("a"), NewAttribute("b")), alsoTestWithoutSpace: true},
{in: "{ .a && .b }", expected: newBinaryOperation(OpAnd, NewAttribute("a"), NewAttribute("b")), alsoTestWithoutSpace: true},
{in: "{ .a || .b }", expected: newBinaryOperation(OpOr, NewAttribute("a"), NewAttribute("b")), alsoTestWithoutSpace: true},
{in: "{ !.b }", expected: newUnaryOperation(OpNot, NewAttribute("b"))},
{in: "{ -.b }", expected: newUnaryOperation(OpSub, NewAttribute("b"))},
// Against statics
{in: "{ .a = `foo` }", expected: newBinaryOperation(OpEqual, NewAttribute("a"), NewStaticString("foo")), alsoTestWithoutSpace: true},
{in: "{ .a = 3 }", expected: newBinaryOperation(OpEqual, NewAttribute("a"), NewStaticInt(3)), alsoTestWithoutSpace: true},
{in: "{ .a = 3.0 }", expected: newBinaryOperation(OpEqual, NewAttribute("a"), NewStaticFloat(3)), alsoTestWithoutSpace: true},
{in: "{ .a = true }", expected: newBinaryOperation(OpEqual, NewAttribute("a"), NewStaticBool(true)), alsoTestWithoutSpace: true},
// existence
{in: "{ .a != nil }", expected: newUnaryOperation(OpExists, NewAttribute("a")), alsoTestWithoutSpace: true},
{in: "{ .a = nil }", expected: newUnaryOperation(OpNotExists, NewAttribute("a")), alsoTestWithoutSpace: true},
// nil comparisons
{in: "{ nil != nil }", expected: NewStaticBool(false)},
{in: "{ nil = nil }", expected: NewStaticBool(false)},
}
test := func(q string, expected FieldExpression) {
actual, err := Parse(q)
require.NoError(t, err, q)
require.Equal(t, newRootExpr(newPipeline(newSpansetFilter(expected))), actual, q)
}
for _, tc := range tests {
t.Run(tc.in, func(_ *testing.T) {
test(tc.in, tc.expected)
if tc.alsoTestWithoutSpace {
test(strings.ReplaceAll(tc.in, " ", ""), tc.expected)
}
})
}
}
func TestAttributeNameErrors(t *testing.T) {
tests := []struct {
in string
err error
}{
{in: "{ . foo }", err: newParseError("syntax error: unexpected END_ATTRIBUTE, expecting IDENTIFIER", 1, 3)},
{in: `{ . "foo" }`, err: newParseError("syntax error: unexpected END_ATTRIBUTE, expecting IDENTIFIER", 1, 3)},
{in: "{ .foo .bar }", err: newParseError("syntax error: unexpected .", 1, 8)},
{in: "{ parent. }", err: newParseError("syntax error: unexpected END_ATTRIBUTE, expecting IDENTIFIER or resource. or span.", 0, 3)},
{in: ".3foo", err: newParseError("syntax error: unexpected IDENTIFIER", 1, 3)},
{in: `{ ."foo }`, err: newParseError(`unexpected EOF, expecting "`, 0, 3)},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
_, err := Parse(tc.in)
require.Equal(t, tc.err, err)
})
}
}
// TestBinaryAndUnaryOperationsRewrites tests code in the newBinaryOperation and newUnaryOperation functions
// that attempts to simplify combinations of static values where possible.
func TestBinaryAndUnaryOperationsRewrites(t *testing.T) {
tests := []struct {
in string
expected FieldExpression
}{
// collapse to statics
{in: "{ duration > 1 + 2}", expected: newBinaryOperation(OpGreater, NewIntrinsic(IntrinsicDuration), NewStaticInt(3))},
{in: "{ -1 }", expected: NewStaticInt(-1)},
{in: "{ 1 + 1 > 1 }", expected: NewStaticBool(true)},
{in: "{ `foo` = `bar` }", expected: NewStaticBool(false)},
{in: "{ 1 = 1. }", expected: NewStaticBool(true)}, // this is an interesting case, it returns true even though { span.foo = 1 } would be false if span.foo had the float value 1.0
{in: "{ .1 + 1 }", expected: NewStaticFloat(1.1)},
{in: "{ 1 * -1 = -1 }", expected: NewStaticBool(true)},
{in: "{ .foo * -1. = -1 }", expected: newBinaryOperation(OpEqual, newBinaryOperation(OpMult, NewAttribute("foo"), NewStaticFloat(-1)), NewStaticInt(-1))},
// rewrite != nil to existence
{in: "{ .foo != nil }", expected: newUnaryOperation(OpExists, NewAttribute("foo"))},
{in: "{ .foo = nil }", expected: newUnaryOperation(OpNotExists, NewAttribute("foo"))},
{in: "{ nil != .foo }", expected: newUnaryOperation(OpExists, NewAttribute("foo"))},
{in: "{ nil = .foo }", expected: newUnaryOperation(OpNotExists, NewAttribute("foo"))},
}
test := func(t *testing.T, q string, expected FieldExpression) {
actual, err := Parse(q)
require.NoError(t, err, q)
require.Equal(t, newRootExpr(newPipeline(newSpansetFilter(expected))), actual, q)
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
test(t, tc.in, tc.expected)
})
}
}
func TestAttributes(t *testing.T) {
tests := []struct {
in string
expected FieldExpression
}{
{in: "duration", expected: NewIntrinsic(IntrinsicDuration)},
{in: "kind", expected: NewIntrinsic(IntrinsicKind)},
{in: ".foo", expected: NewAttribute("foo")},
{in: ".max", expected: NewAttribute("max")},
{in: ".status", expected: NewAttribute("status")},
{in: ".kind", expected: NewAttribute("kind")},
{in: ".foo.bar", expected: NewAttribute("foo.bar")},
{in: ".foo.bar.baz", expected: NewAttribute("foo.bar.baz")},
{in: ".foo.3", expected: NewAttribute("foo.3")},
{in: ".foo3", expected: NewAttribute("foo3")},
{in: ".http_status", expected: NewAttribute("http_status")},
{in: ".http-status", expected: NewAttribute("http-status")},
{in: ".http+", expected: NewAttribute("http+")},
{in: ".😝", expected: NewAttribute("😝")},
{in: ".http-other", expected: NewAttribute("http-other")},
{in: "parent.duration", expected: NewScopedAttribute(AttributeScopeNone, true, "duration")},
{in: "parent.foo.bar.baz", expected: NewScopedAttribute(AttributeScopeNone, true, "foo.bar.baz")},
{in: "resource.foo.bar.baz", expected: NewScopedAttribute(AttributeScopeResource, false, "foo.bar.baz")},
{in: "span.foo.bar", expected: NewScopedAttribute(AttributeScopeSpan, false, "foo.bar")},
{in: "event.foo.bar", expected: NewScopedAttribute(AttributeScopeEvent, false, "foo.bar")},
{in: "link.foo.bar", expected: NewScopedAttribute(AttributeScopeLink, false, "foo.bar")},
{in: "instrumentation.foo.bar", expected: NewScopedAttribute(AttributeScopeInstrumentation, false, "foo.bar")},
{in: "parent.resource.foo", expected: NewScopedAttribute(AttributeScopeResource, true, "foo")},
{in: "parent.span.foo", expected: NewScopedAttribute(AttributeScopeSpan, true, "foo")},
{in: "parent.resource.foo.bar.baz", expected: NewScopedAttribute(AttributeScopeResource, true, "foo.bar.baz")},
{in: "parent.span.foo.bar", expected: NewScopedAttribute(AttributeScopeSpan, true, "foo.bar")},
{in: `."bar z".foo`, expected: NewAttribute("bar z.foo")},
{in: `span."bar z".foo`, expected: NewScopedAttribute(AttributeScopeSpan, false, "bar z.foo")},
{in: `."bar z".foo."bar"`, expected: NewAttribute("bar z.foo.bar")},
{in: `.foo."bar baz"`, expected: NewAttribute("foo.bar baz")},
{in: `.foo."bar baz".bar`, expected: NewAttribute("foo.bar baz.bar")},
{in: `.foo."bar \" baz"`, expected: NewAttribute(`foo.bar " baz`)},
{in: `.foo."bar \\ baz"`, expected: NewAttribute(`foo.bar \ baz`)},
{in: `.foo."bar \\"." baz"`, expected: NewAttribute(`foo.bar \. baz`)},
{in: `."foo.bar"`, expected: NewAttribute(`foo.bar`)},
{in: `."🤘"`, expected: NewAttribute(`🤘`)},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
s := "{ " + tc.in + " }"
actual, err := Parse(s)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(newSpansetFilter(tc.expected))), actual)
s = "{" + tc.in + "}"
actual, err = Parse(s)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(newSpansetFilter(tc.expected))), actual)
s = "{ (" + tc.in + ") }"
actual, err = Parse(s)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(newSpansetFilter(tc.expected))), actual)
s = "{ " + tc.in + " + " + tc.in + " }"
actual, err = Parse(s)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(newSpansetFilter(newBinaryOperation(OpAdd, tc.expected, tc.expected)))), actual)
})
}
}
func TestIntrinsics(t *testing.T) {
tests := []struct {
in string
expected Intrinsic
}{
{in: "duration", expected: IntrinsicDuration},
{in: "name", expected: IntrinsicName},
{in: "status", expected: IntrinsicStatus},
{in: "statusMessage", expected: IntrinsicStatusMessage},
{in: "kind", expected: IntrinsicKind},
{in: "parent", expected: IntrinsicParent},
{in: "traceDuration", expected: IntrinsicTraceDuration},
{in: "rootServiceName", expected: IntrinsicTraceRootService},
{in: "rootName", expected: IntrinsicTraceRootSpan},
{in: "nestedSetLeft", expected: IntrinsicNestedSetLeft},
{in: "nestedSetRight", expected: IntrinsicNestedSetRight},
{in: "nestedSetParent", expected: IntrinsicNestedSetParent},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
// as intrinsic e.g. duration
s := "{ " + tc.in + " }"
actual, err := Parse(s)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(
newSpansetFilter(Attribute{
Scope: AttributeScopeNone,
Parent: false,
Name: tc.in,
Intrinsic: tc.expected,
}))), actual)
// as attribute e.g .duration
s = "{ ." + tc.in + "}"
actual, err = Parse(s)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(
newSpansetFilter(Attribute{
Scope: AttributeScopeNone,
Parent: false,
Name: tc.in,
Intrinsic: IntrinsicNone,
}))), actual)
// as span scoped attribute e.g span.duration
s = "{ span." + tc.in + "}"
actual, err = Parse(s)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(
newSpansetFilter(Attribute{
Scope: AttributeScopeSpan,
Parent: false,
Name: tc.in,
Intrinsic: IntrinsicNone,
}))), actual)
// as resource scoped attribute e.g resource.duration
s = "{ resource." + tc.in + "}"
actual, err = Parse(s)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(
newSpansetFilter(Attribute{
Scope: AttributeScopeResource,
Parent: false,
Name: tc.in,
Intrinsic: IntrinsicNone,
}))), actual)
// as parent scoped intrinsic e.g parent.duration
s = "{ parent." + tc.in + "}"
actual, err = Parse(s)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(
newSpansetFilter(Attribute{
Scope: AttributeScopeNone,
Parent: true,
Name: tc.in,
Intrinsic: IntrinsicNone,
}))), actual)
// as nested parent scoped intrinsic e.g. parent.duration.foo
// this becomes lookup on attribute named "duration.foo"
s = "{ parent." + tc.in + ".foo }"
actual, err = Parse(s)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(
newSpansetFilter(Attribute{
Scope: AttributeScopeNone,
Parent: true,
Name: tc.in + ".foo",
Intrinsic: IntrinsicNone,
}))), actual)
// as parent resource scoped attribute e.g. parent.resource.duration
s = "{ parent.resource." + tc.in + "}"
actual, err = Parse(s)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(
newSpansetFilter(Attribute{
Scope: AttributeScopeResource,
Parent: true,
Name: tc.in,
Intrinsic: IntrinsicNone,
}))), actual)
// as parent span scoped attribute e.g. praent.span.duration
s = "{ parent.span." + tc.in + "}"
actual, err = Parse(s)
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(
newSpansetFilter(Attribute{
Scope: AttributeScopeSpan,
Parent: true,
Name: tc.in,
Intrinsic: IntrinsicNone,
}))), actual)
})
}
}
func TestScopedIntrinsics(t *testing.T) {
tests := []struct {
in string
expected Intrinsic
shouldError bool
}{
{in: "trace:duration", expected: IntrinsicTraceDuration},
{in: "trace:rootName", expected: IntrinsicTraceRootSpan},
{in: "trace:rootService", expected: IntrinsicTraceRootService},
{in: "trace:id", expected: IntrinsicTraceID},
{in: "span:duration", expected: IntrinsicDuration},
{in: "span:kind", expected: IntrinsicKind},
{in: "span:name", expected: IntrinsicName},
{in: "span:status", expected: IntrinsicStatus},
{in: "span:statusMessage", expected: IntrinsicStatusMessage},
{in: "span:id", expected: IntrinsicSpanID},
{in: "span:parentID", expected: IntrinsicParentID},
{in: "span:childCount", expected: IntrinsicChildCount},
{in: "event:name", expected: IntrinsicEventName},
{in: "event:timeSinceStart", expected: IntrinsicEventTimeSinceStart},
{in: "link:traceID", expected: IntrinsicLinkTraceID},
{in: "link:spanID", expected: IntrinsicLinkSpanID},
{in: "instrumentation:name", expected: IntrinsicInstrumentationName},
{in: "instrumentation:version", expected: IntrinsicInstrumentationVersion},
{in: ":duration", shouldError: true},
{in: ":statusMessage", shouldError: true},
{in: "trace:name", shouldError: true},
{in: "trace:rootServiceName", shouldError: true},
{in: "span:rootServiceName", shouldError: true},
{in: "parent:id", shouldError: true},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
// as scoped intrinsic e.g :duration
s := "{ " + tc.in + "}"
actual, err := Parse(s)
if tc.shouldError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, newRootExpr(newPipeline(
newSpansetFilter(Attribute{
Scope: AttributeScopeNone,
Parent: false,
Name: tc.expected.String(),
Intrinsic: tc.expected,
}))), actual)
}
})
}
}
func TestParseIdentifier(t *testing.T) {
testCases := map[string]Attribute{
// Basic intrinsics
"name": NewIntrinsic(IntrinsicName),
"status": NewIntrinsic(IntrinsicStatus),
"statusMessage": NewIntrinsic(IntrinsicStatusMessage),
"kind": NewIntrinsic(IntrinsicKind),
"duration": NewIntrinsic(IntrinsicDuration),
"parent": NewIntrinsic(IntrinsicParent),
"traceDuration": NewIntrinsic(IntrinsicTraceDuration),
"rootName": NewIntrinsic(IntrinsicTraceRootSpan),
"rootServiceName": NewIntrinsic(IntrinsicTraceRootService),
"nestedSetLeft": NewIntrinsic(IntrinsicNestedSetLeft),
"nestedSetRight": NewIntrinsic(IntrinsicNestedSetRight),
"nestedSetParent": NewIntrinsic(IntrinsicNestedSetParent),
// Scoped intrinsics - trace:
"trace:duration": NewIntrinsic(IntrinsicTraceDuration),
"trace:rootName": NewIntrinsic(IntrinsicTraceRootSpan),
"trace:rootService": NewIntrinsic(IntrinsicTraceRootService),
"trace:id": NewIntrinsic(IntrinsicTraceID),
// Scoped intrinsics - span:
"span:duration": NewIntrinsic(IntrinsicDuration),
"span:name": NewIntrinsic(IntrinsicName),
"span:kind": NewIntrinsic(IntrinsicKind),
"span:status": NewIntrinsic(IntrinsicStatus),
"span:statusMessage": NewIntrinsic(IntrinsicStatusMessage),
"span:id": NewIntrinsic(IntrinsicSpanID),
"span:parentID": NewIntrinsic(IntrinsicParentID),
"span:childCount": NewIntrinsic(IntrinsicChildCount),
// Scoped intrinsics - event:
"event:name": NewIntrinsic(IntrinsicEventName),
"event:timeSinceStart": NewIntrinsic(IntrinsicEventTimeSinceStart),
// Scoped intrinsics - link:
"link:traceID": NewIntrinsic(IntrinsicLinkTraceID),
"link:spanID": NewIntrinsic(IntrinsicLinkSpanID),
// Scoped intrinsics - instrumentation:
"instrumentation:name": NewIntrinsic(IntrinsicInstrumentationName),
"instrumentation:version": NewIntrinsic(IntrinsicInstrumentationVersion),
// Simple attributes
".name": NewAttribute("name"),
".status": NewAttribute("status"),
".foo": NewAttribute("foo"),
".foo.bar": NewAttribute("foo.bar"),
".foo.bar.baz": NewAttribute("foo.bar.baz"),
".http_status": NewAttribute("http_status"),
".http-status": NewAttribute("http-status"),
".http+": NewAttribute("http+"),
".foo3": NewAttribute("foo3"),
".foo.3": NewAttribute("foo.3"),
".\"0\"": NewAttribute("0"),
// Scoped attributes - resource
"resource.foo": NewScopedAttribute(AttributeScopeResource, false, "foo"),
"resource.foo.bar": NewScopedAttribute(AttributeScopeResource, false, "foo.bar"),
"resource.foo.bar.baz": NewScopedAttribute(AttributeScopeResource, false, "foo.bar.baz"),
// Scoped attributes - span
"span.foo": NewScopedAttribute(AttributeScopeSpan, false, "foo"),
"span.foo.bar": NewScopedAttribute(AttributeScopeSpan, false, "foo.bar"),
"span.foo.bar.baz": NewScopedAttribute(AttributeScopeSpan, false, "foo.bar.baz"),
// Scoped attributes - event
"event.foo": NewScopedAttribute(AttributeScopeEvent, false, "foo"),
"event.foo.bar": NewScopedAttribute(AttributeScopeEvent, false, "foo.bar"),
// Scoped attributes - link
"link.foo": NewScopedAttribute(AttributeScopeLink, false, "foo"),
"link.foo.bar": NewScopedAttribute(AttributeScopeLink, false, "foo.bar"),
// Scoped attributes - instrumentation
"instrumentation.foo": NewScopedAttribute(AttributeScopeInstrumentation, false, "foo"),
"instrumentation.foo.bar": NewScopedAttribute(AttributeScopeInstrumentation, false, "foo.bar"),
// Parent-scoped intrinsics
"parent.duration": NewScopedAttribute(AttributeScopeNone, true, "duration"),
"parent.name": NewScopedAttribute(AttributeScopeNone, true, "name"),
"parent.foo": NewScopedAttribute(AttributeScopeNone, true, "foo"),
"parent.foo.bar": NewScopedAttribute(AttributeScopeNone, true, "foo.bar"),
// Parent-scoped with resource/span
"parent.resource.foo": NewScopedAttribute(AttributeScopeResource, true, "foo"),
"parent.resource.foo.bar": NewScopedAttribute(AttributeScopeResource, true, "foo.bar"),
"parent.span.foo": NewScopedAttribute(AttributeScopeSpan, true, "foo"),
"parent.span.foo.bar": NewScopedAttribute(AttributeScopeSpan, true, "foo.bar"),
// Quoted identifiers with spaces
".\"foo bar\"": NewAttribute("foo bar"),
".\"bar z\".foo": NewAttribute("bar z.foo"),
".\"bar z\".foo.\"bar\"": NewAttribute("bar z.foo.bar"),
".foo.\"bar baz\"": NewAttribute("foo.bar baz"),
".foo.\"bar baz\".bar": NewAttribute("foo.bar baz.bar"),
"span.\"foo bar\"": NewScopedAttribute(AttributeScopeSpan, false, "foo bar"),
"span.\"bar z\".foo": NewScopedAttribute(AttributeScopeSpan, false, "bar z.foo"),
"resource.\"foo bar\"": NewScopedAttribute(AttributeScopeResource, false, "foo bar"),
"parent.\"foo bar\"": NewScopedAttribute(AttributeScopeNone, true, "foo bar"),
"parent.resource.\"foo\"": NewScopedAttribute(AttributeScopeResource, true, "foo"),
// Quoted identifiers with escape sequences
".foo.\"bar \\\" baz\"": NewAttribute("foo.bar \" baz"),
".foo.\"bar \\\\ baz\"": NewAttribute("foo.bar \\ baz"),
".foo.\"bar \\\\\".\" baz\"": NewAttribute("foo.bar \\. baz"),
".\"foo.bar\"": NewAttribute("foo.bar"),
// Unicode identifiers
".😝": NewAttribute("😝"),
".\"🤘\"": NewAttribute("🤘"),
}
for input, expected := range testCases {
t.Run(input, func(t *testing.T) {
actual, err := ParseIdentifier(input)
require.NoError(t, err, "input: %s", input)
require.Equal(t, expected, actual, "input: %s", input)
})
}
}
func TestParseIdentifierErrors(t *testing.T) {
errorCases := []struct {
input string
description string
}{
// Empty and whitespace
{"", "empty string"},
{" ", "only whitespace"},
// Invalid syntax
{"{ .foo }", "curly braces (already a filter)"},
{".foo &&", "incomplete expression with operator"},
{".foo && .bar", "multiple identifiers with AND"},
{".foo || .bar", "multiple identifiers with OR"},
{".foo > .bar", "comparison operator"},
// Expressions that aren't identifiers
{".foo = \"bar\"", "comparison expression"},
{".foo + .bar", "arithmetic expression"},
{".foo != nil", "existence check"},
{"true", "boolean literal"},
{"123", "numeric literal"},
{"\"string\"", "string literal"},
{"`backtick`", "backtick string literal"},
// Aggregate functions
{"count()", "aggregate function"},
{"max(.foo)", "aggregate with attribute"},
{"avg(duration)", "aggregate with intrinsic"},
// Invalid scoped intrinsics
{":duration", "missing scope prefix"},
{"trace:name", "invalid trace intrinsic"},
{"parent:id", "invalid parent scope"},
// Incomplete attributes
{".", "dot without identifier"},
{"span.", "scope without attribute"},
{"resource.", "resource without attribute"},
{"parent.", "parent without attribute"},
// Invalid quoted strings
{".\"foo", "unclosed quote"},
{".\"foo\\", "unclosed quote with escape"},
// Complex queries
{"{ .foo } | { .bar }", "pipeline expression"},
{"({ .foo })", "wrapped spanset"},
}
for _, tc := range errorCases {
t.Run(tc.description, func(t *testing.T) {
_, err := ParseIdentifier(tc.input)
require.Error(t, err, "expected error for input: %s", tc.input)
require.Contains(t, err.Error(), "failed to parse identifier", "error should have context")
})
}
}
func TestEmptyQuery(t *testing.T) {
tests := []struct {
in string
}{
{in: "{}"},
{in: "{ }"},
{in: "{ }"},
{in: "{ true }"},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err, tc.in)
require.Equal(t, newRootExpr(newPipeline(newSpansetFilter(NewStaticBool(true)))), actual, tc.in)
})
}
}
func TestHints(t *testing.T) {
tests := []struct {
in string
expected *RootExpr
}{
{
in: `{ } | rate() with(foo="bar")`,
expected: newRootExprWithMetrics(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregate(metricsAggregateRate, nil),
).withHints(newHints([]*Hint{
newHint("foo", NewStaticString("bar")),
})),
},
{
in: `{ } | rate() with(foo=0.5)`,
expected: newRootExprWithMetrics(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregate(metricsAggregateRate, nil),
).withHints(newHints([]*Hint{
newHint("foo", NewStaticFloat(0.5)),
})),
},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
})
}
}
func TestReallyLongQuery(t *testing.T) {
for i := 1000; i < 1050; i++ {
longVal := strings.Repeat("a", i)
// static value
query := fmt.Sprintf("{ .a = `%s` }", longVal)
expected := newBinaryOperation(OpEqual, NewAttribute("a"), NewStaticString(longVal))
actual, err := Parse(query)
require.NoError(t, err, "i=%d", i)
require.Equal(t, newRootExpr(newPipeline(newSpansetFilter(expected))), actual, "i=%d", i)
// attr name
query = fmt.Sprintf("{ .%s = `foo` }", longVal)
expected = newBinaryOperation(OpEqual, NewAttribute(longVal), NewStaticString("foo"))
actual, err = Parse(query)
require.NoError(t, err, "i=%d", i)
require.Equal(t, newRootExpr(newPipeline(newSpansetFilter(expected))), actual, "i=%d", i)
}
}
func TestMetrics(t *testing.T) {
tests := []struct {
in string
expected *RootExpr
}{
{
in: `{ } | rate()`,
expected: newRootExprWithMetrics(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregate(metricsAggregateRate, nil),
),
},
{
in: `{ } | count_over_time() by(name, span.http.status_code)`,
expected: newRootExprWithMetrics(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregate(metricsAggregateCountOverTime, []Attribute{
NewIntrinsic(IntrinsicName),
NewScopedAttribute(AttributeScopeSpan, false, "http.status_code"),
}),
),
},
{
in: `{ } | min_over_time(duration) by(name, span.http.status_code)`,
expected: newRootExprWithMetrics(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregateWithAttr(metricsAggregateMinOverTime,
NewIntrinsic(IntrinsicDuration),
[]Attribute{
NewIntrinsic(IntrinsicName),
NewScopedAttribute(AttributeScopeSpan, false, "http.status_code"),
}),
),
},
{
in: `{ } | max_over_time(duration) by(name, span.http.status_code)`,
expected: newRootExprWithMetrics(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregateWithAttr(metricsAggregateMaxOverTime,
NewIntrinsic(IntrinsicDuration),
[]Attribute{
NewIntrinsic(IntrinsicName),
NewScopedAttribute(AttributeScopeSpan, false, "http.status_code"),
}),
),
},
{
in: `{ } | avg_over_time(duration) by(name, span.http.status_code)`,
expected: newRootExprWithMetrics(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newAverageOverTimeMetricsAggregator(
NewIntrinsic(IntrinsicDuration),
[]Attribute{
NewIntrinsic(IntrinsicName),
NewScopedAttribute(AttributeScopeSpan, false, "http.status_code"),
}),
),
},
{
in: `{ } | sum_over_time(duration) by(name, span.http.status_code)`,
expected: newRootExprWithMetrics(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregateWithAttr(metricsAggregateSumOverTime,
NewIntrinsic(IntrinsicDuration),
[]Attribute{
NewIntrinsic(IntrinsicName),
NewScopedAttribute(AttributeScopeSpan, false, "http.status_code"),
}),
),
},
{
in: `{ } | quantile_over_time(duration, 0, 0.90, 0.95, 1) by(name, span.http.status_code)`,
expected: newRootExprWithMetrics(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregateQuantileOverTime(
NewIntrinsic(IntrinsicDuration),
[]float64{0, 0.9, 0.95, 1.0},
[]Attribute{
NewIntrinsic(IntrinsicName),
NewScopedAttribute(AttributeScopeSpan, false, "http.status_code"),
}),
),
},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
})
}
}
func TestMetricsSecondStage(t *testing.T) {
tests := []struct {
in string
expected *RootExpr
}{
{
in: `{ } | rate() | topk(10)`,
expected: newRootExprWithMetricsTwoStage(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregate(metricsAggregateRate, nil),
newTopKBottomK(OpTopK, 10),
),
},
{
in: `{ } | count_over_time() by(name, span.http.status_code) | topk(10)`,
expected: newRootExprWithMetricsTwoStage(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregate(metricsAggregateCountOverTime, []Attribute{
NewIntrinsic(IntrinsicName),
NewScopedAttribute(AttributeScopeSpan, false, "http.status_code"),
}),
newTopKBottomK(OpTopK, 10),
),
},
{
in: `{ } | rate() | topk(10) with(foo="bar")`,
expected: newRootExprWithMetricsTwoStage(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregate(metricsAggregateRate, nil),
newTopKBottomK(OpTopK, 10),
).withHints(newHints([]*Hint{
newHint("foo", NewStaticString("bar")),
})),
},
{
in: `{ } | rate() | bottomk(10)`,
expected: newRootExprWithMetricsTwoStage(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregate(metricsAggregateRate, nil),
newTopKBottomK(OpBottomK, 10),
),
},
{
in: `{ } | count_over_time() by(name, span.http.status_code) | bottomk(10)`,
expected: newRootExprWithMetricsTwoStage(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregate(metricsAggregateCountOverTime, []Attribute{
NewIntrinsic(IntrinsicName),
NewScopedAttribute(AttributeScopeSpan, false, "http.status_code"),
}),
newTopKBottomK(OpBottomK, 10),
),
},
{
in: `{ } | rate() | bottomk(10) with(foo="bar")`,
expected: newRootExprWithMetricsTwoStage(
newPipeline(newSpansetFilter(NewStaticBool(true))),
newMetricsAggregate(metricsAggregateRate, nil),
newTopKBottomK(OpBottomK, 10),
).withHints(newHints([]*Hint{
newHint("foo", NewStaticString("bar")),
})),
},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
actual, err := Parse(tc.in)
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
})
}
}
func TestMetricsSecondStageErrors(t *testing.T) {
tests := []struct {
in string
err error
}{
{
in: "{} | topk(10)",
err: newParseError("syntax error: unexpected topk", 1, 6),
},
{
in: "{} | topk(10) with(sample=0.1)",
err: newParseError("syntax error: unexpected topk", 1, 6),
},
{
in: "{} | rate() | topk(-1)",
err: newParseError("syntax error: unexpected -, expecting INTEGER", 1, 20),
},
{
in: "{} | bottomk(10)",
err: newParseError("syntax error: unexpected bottomk", 1, 6),
},
{
in: "{} | bottomk(10) with(sample=0.1)",
err: newParseError("syntax error: unexpected bottomk", 1, 6),
},
{
in: "{} | rate() | bottomk(-1)",
err: newParseError("syntax error: unexpected -, expecting INTEGER", 1, 23),
},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
_, err := Parse(tc.in)
require.Equal(t, tc.err, err)
})
}
}
func TestParseRewrites(t *testing.T) {
tests := []struct {
name string
query string
want string
}{
{
name: "no rewrites",
query: "{ .attr = `foo` }",
want: "{ .attr = `foo` }",
},
{
name: "query with rewrite OR",
query: "{ .attr = `foo` || .attr = `bar` } | rate()",
want: "{ .attr IN [`foo`, `bar`] } | rate()",
},
{
name: "query with rewrite AND",
query: "{ .attr != `foo` && .attr != `bar` }",
want: "{ .attr NOT IN [`foo`, `bar`] }",
},
{
name: "query with rewrite OR regex",
query: "{ .attr =~ `foo` || .attr =~ `bar` }",
want: "{ .attr MATCH ANY [`foo`, `bar`] }",
},
{
name: "query with rewrite AND regex",
query: "{ .attr !~ `foo` && .attr !~ `bar` }",
want: "{ .attr MATCH NONE [`foo`, `bar`] }",
},
{
name: "skip rewrites hint",
query: "{ .attr = `foo` || .attr = `bar` } | rate() with(skip_optimization=true)",
want: "{ (.attr = `foo`) || (.attr = `bar`) } | rate() with(skip_optimization=true)",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
actual, err := Parse(tc.query)
require.NoError(t, err)
require.Equal(t, tc.want, actual.String())
})
}
}