diff --git a/CHANGELOG.md b/CHANGELOG.md index d34d39d7816..64c8d797dde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## main / unreleased +* [ENHANCEMENT] TraceQL: Attribute iterators collect matched array values [#3867](https://github.com/grafana/tempo/pull/3867) (@electron0zero, @stoewer) * [ENHANCEMENT] Add bytes and spans received to usage stats [#3983](https://github.com/grafana/tempo/pull/3983) (@joe-elliott) # v2.6.0-rc.0 diff --git a/pkg/traceql/ast.go b/pkg/traceql/ast.go index 5c98f609029..0538c0e96d1 100644 --- a/pkg/traceql/ast.go +++ b/pkg/traceql/ast.go @@ -731,6 +731,52 @@ func (s Static) compare(o *Static) int { } } +type VisitFunc func(Static) bool // Return false to stop iteration + +// GetElements turns arrays into slice of Static elements to iterate over. +func (s Static) GetElements(fn VisitFunc) error { + switch s.Type { + case TypeIntArray: + ints, _ := s.IntArray() + for _, n := range ints { + if !fn(NewStaticInt(n)) { + break // stop early if the callback returns false + } + } + return nil + + case TypeFloatArray: + floats, _ := s.FloatArray() + for _, f := range floats { + if !fn(NewStaticFloat(f)) { + break + } + } + return nil + + case TypeStringArray: + strs, _ := s.StringArray() + for _, str := range strs { + if !fn(NewStaticString(str)) { + break + } + } + return nil + + case TypeBooleanArray: + bools, _ := s.BooleanArray() + for _, b := range bools { + if !fn(NewStaticBool(b)) { + break + } + } + return nil + + default: + return fmt.Errorf("unsupported type") + } +} + func (s Static) Int() (int, bool) { if s.Type != TypeInt { return 0, false diff --git a/pkg/traceql/ast_execute.go b/pkg/traceql/ast_execute.go index f3553bf28f8..6f2bc000362 100644 --- a/pkg/traceql/ast_execute.go +++ b/pkg/traceql/ast_execute.go @@ -381,7 +381,7 @@ func (o *BinaryOperation) execute(span Span) (Static, error) { } // if both sides are integers then do integer math, otherwise we can drop to the - // catch all below + // catch-all below if lhsT == TypeInt && rhsT == TypeInt { lhsN, _ := lhs.Int() rhsN, _ := rhs.Int() @@ -422,6 +422,39 @@ func (o *BinaryOperation) execute(span Span) (Static, error) { } } + if lhsT.isMatchingArrayElement(rhsT) { + // we only support boolean op in the arrays + if !o.Op.isBoolean() { + return NewStaticNil(), errors.ErrUnsupported + } + + elemOp := &BinaryOperation{Op: o.Op, LHS: lhs, RHS: rhs} + arraySide := lhs + // to support symmetric operations + if rhsT.isArray() { + // for regex operations, TraceQL makes an assumption that RHS is the regex, and compiles it. + // we can support symmetric array operations by flipping the sides and executing the binary operation. + elemOp = &BinaryOperation{Op: getFlippedOp(o.Op), LHS: rhs, RHS: lhs} + arraySide = rhs + } + + var res Static + err := arraySide.GetElements(func(elem Static) bool { + elemOp.LHS = elem + res, err = elemOp.execute(span) + if err != nil { + return false // stop iteration early if there's an error + } + match, ok := res.Bool() + return !(ok && match) // stop if a match is found + }) + if err != nil { + return NewStaticNil(), err + } + + return res, err + } + switch o.Op { case OpAdd: return NewStaticFloat(lhs.Float() + rhs.Float()), nil @@ -452,6 +485,22 @@ func (o *BinaryOperation) execute(span Span) (Static, error) { } } +// getFlippedOp will return the flipped op, used when flipping the LHS and RHS of a BinaryOperation +func getFlippedOp(op Operator) Operator { + switch op { + case OpGreater: + return OpLess + case OpGreaterEqual: + return OpLessEqual + case OpLess: + return OpGreater + case OpLessEqual: + return OpGreaterEqual + default: + return op + } +} + // why does this and the above exist? func binOp(op Operator, lhs, rhs Static) (bool, error) { lhsT := lhs.impliedType() diff --git a/pkg/traceql/ast_execute_test.go b/pkg/traceql/ast_execute_test.go index cd6d28d237b..4a2345a96b2 100644 --- a/pkg/traceql/ast_execute_test.go +++ b/pkg/traceql/ast_execute_test.go @@ -2,6 +2,7 @@ package traceql import ( "bytes" + "errors" "fmt" "sort" "testing" @@ -527,6 +528,774 @@ func TestSpansetOperationEvaluate(t *testing.T) { } } +// test cases for array +func TestSpansetOperationEvaluateArray(t *testing.T) { + testCases := []evalTC{ + // string arrays + { + "{ .foo = `bar` }", // match string array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticString("b")}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + }}, + }, + }, + { + "{ .foo = `bar` || .bat = `baz` }", // match string array with or + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticString("b")}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + }}, + }, + }, + { + "{ .foo != `baz` }", // match string array not equal + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"baz"})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + }}, + }, + }, + { + "{ .foo =~ `ba` }", // match string array with regex + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"foo", "baz"})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"dog", "cat"})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"foo", "baz"})}}, + }}, + }, + }, + { + "{ .foo !~ `ba` }", // regex non-matching + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"foo", "baz"})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"cat"})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"foo", "baz"})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"cat"})}}, + }}, + }, + }, + // int arrays + { + "{ .foo = 2 }", // match int array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticString("b")}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + }}, + }, + }, + { + "{ .foo != 3 }", // match int array not equal + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 4})}}, + // this is filtered out as expected?? + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 3})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 4})}}, + }}, + }, + }, + { + "{ .foo = 2.5 }", // match float array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 2.5})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticString("b")}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 2.5})}}, + }}, + }, + }, + { + "{ .foo = 3.14 }", // match another float array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{3.14, 6.28})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.23, 4.56})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{3.14, 6.28})}}, + }}, + }, + }, + { + "{ .foo > 1 }", // match int array greater than + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{0, 1})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + }}, + }, + }, + { + "{ .foo >= 1 }", // match int array greater equal than + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{0, 1})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{0, -1})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{0, 1})}}, + }}, + }, + }, + { + "{ .foo < 2 }", // match int array less than + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 3})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 4})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 3})}}, + }}, + }, + }, + { + "{ .foo <= 2 }", // match int array less than equal + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 3})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 4})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{2, 4})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 3})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{2, 4})}}, + }}, + }, + }, + // match float arrays + { + "{ .foo != 2.5 }", // match float array not equal + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 3.0})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.5, 4.0})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.5})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 3.0})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.5, 4.0})}}, + }}, + }, + }, + { + "{ .foo < 2.0 }", // match float array less than + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 3.0})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 4.0})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 3.0})}}, + }}, + }, + }, + { + "{ .foo <= 2.0 }", // match float array less than + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 3.0})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 4.0})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{3.0, 4.0})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 3.0})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 4.0})}}, + }}, + }, + }, + { + "{ .foo > 2.5 }", // match float array greater than + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 3.0})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.0, 2.0})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 3.0})}}, + }}, + }, + }, + { + "{ .foo >= 2.5 }", // match float array greater than + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 3.0})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.0, 2.0})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.0, 2.5})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 3.0})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.0, 2.5})}}, + }}, + }, + }, + // match bool arrays + { + "{ .foo = true }", // match boolean array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticString("b")}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + }}, + }, + }, + { + "{ .foo = false }", // match another boolean array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, true})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + }}, + }, + }, + { + "{ .foo != true }", // match boolean array not equal to true + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, true})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + }}, + }, + }, + { + "{ .foo = !true }", // match boolean array not equal to true + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, true})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + }}, + }, + }, + { + "{ .foo = true && .foo = false }", + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, true})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + }}, + }, + }, + { + "{ .foo = true || .foo = false }", + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, true})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, true})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + }}, + }, + }, + // empty arrays + { + "{ .foo = 1 }", + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{})}}, + }}, + }, + []*Spanset{}, + }, + { + "{ .foo = `test` }", + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{})}}, + }}, + }, + []*Spanset{}, + }, + } + for _, tc := range testCases { + testEvaluator(t, tc) + } +} + +// tests to make sure symmetric operations are supported for arrays... +func TestSpansetOperationEvaluateArraySymmetric(t *testing.T) { + testCases := []evalTC{ + // string arrays + { + "{ `bar` = .foo }", // Symmetric match for string array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + }}, + }, + }, + { + "{ `baz` != .foo }", // Symmetric not equal for string array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"baz"})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + }}, + }, + }, + { + "{ `ba` =~ .foo }", // Symmetric regex match for string array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"foo", "baz"})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"cat", "dog"})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"foo", "baz"})}}, + }}, + }, + }, + { + "{ `ba` !~ .foo }", // Symmetric regex non-match for string array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"foo", "baz"})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"bar", "baz"})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticStringArray([]string{"foo", "baz"})}}, + }}, + }, + }, + // int arrays + { + "{ 2 = .foo }", // Symmetric match for int array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + }}, + }, + }, + { + "{ 3 != .foo }", // Symmetric not equal for int array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 4})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 4})}}, + }}, + }, + }, + { + "{ 2 < .foo }", // Symmetric less-than for int array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 2})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{0, 1})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + // spans with any array elements grater then 2 will not be filtered out. + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 2})}}, + }}, + }, + }, + { + "{ 2 <= .foo }", // Symmetric less-than equal for int array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 2})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{0, 1})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 2})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + }}, + }, + }, + { + "{ 3 > .foo }", // Symmetric grater-than for int array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 3})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 4})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 3})}}, + }}, + }, + }, + { + "{ 3 >= .foo }", // Symmetric grater-than-equal for int array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 3})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 4})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + &mockSpan{id: []byte{4}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{4, 6})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 3})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{3, 4})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + }}, + }, + }, + // match float arrays + { + "{ 2.5 = .foo }", // Symmetric match for float array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 2.5})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 3.5})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 2.5})}}, + }}, + }, + }, + { + "{ 3.14 != .foo }", // Symmetric not equal for float array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.23, 4.56})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{3.14, 6.28})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{3.14})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.23, 4.56})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{3.14, 6.28})}}, + }}, + }, + }, + { + "{ 2.0 > .foo }", // Symmetric grater-than for float array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 4.0})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 3.0})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 3.0})}}, + }}, + }, + }, + { + "{ 2.0 >= .foo }", // Symmetric grater-than-equal for float array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 4.0})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 3.0})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{3.5, 3.0})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 4.0})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{1.5, 3.0})}}, + }}, + }, + }, + { + "{ 3.5 < .foo }", // Symmetric less-than for float array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 3.5})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{4.0, 3.5})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{4.0, 5.5})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{4.0, 3.5})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{4.0, 5.5})}}, + }}, + }, + }, + { + "{ 3.5 <= .foo }", // Symmetric less-than-equal for float array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 3.5})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{4.0, 3.5})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{4.0, 5.5})}}, + &mockSpan{id: []byte{4}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 1.5})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{2.0, 3.5})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{4.0, 3.5})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticFloatArray([]float64{4.0, 5.5})}}, + }}, + }, + }, + // match bool arrays + { + "{ true = .foo }", // Symmetric match for boolean array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + }}, + }, + }, + { + "{ false != .foo }", // Symmetric not equal for boolean array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, true})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, true})}}, + }}, + }, + }, + { + "{ !false = .foo }", // Symmetric not equal for boolean array + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, true})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, true})}}, + }}, + }, + }, + { + "{ true = .foo && false = .foo }", + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, true})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + }}, + }, + }, + { + "{ true = .foo || false = .foo }", + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, true})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + }}, + }, + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, false})}}, + &mockSpan{id: []byte{2}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{true, true})}}, + &mockSpan{id: []byte{3}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticBooleanArray([]bool{false, false})}}, + }}, + }, + }, + } + for _, tc := range testCases { + testEvaluator(t, tc) + } +} + +func TestSpansetOperationEvaluateArrayUnsupported(t *testing.T) { + testCases := []evalTC{ + { + "{ .foo + 3 = 4 }", + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + }}, + }, + []*Spanset{}, + }, + { + "{ 4 = .foo + 3 }", + []*Spanset{ + {Spans: []Span{ + &mockSpan{id: []byte{1}, attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticIntArray([]int{1, 2})}}, + }}, + }, + []*Spanset{}, + }, + } + for _, tc := range testCases { + t.Helper() + + t.Run(tc.query, func(t *testing.T) { + ast, err := Parse(tc.query) + require.NoError(t, err) + + // clone input to confirm it doesn't get modified + cloneIn := make([]*Spanset, len(tc.input)) + for i := range tc.input { + cloneIn[i] = tc.input[i].clone() + cloneIn[i].Spans = append([]Span(nil), tc.input[i].Spans...) + } + + _, err = ast.Pipeline.evaluate(tc.input) + require.Error(t, err, errors.ErrUnsupported) + }) + } +} + func TestScalarFilterEvaluate(t *testing.T) { testCases := []evalTC{ { diff --git a/pkg/traceql/engine.go b/pkg/traceql/engine.go index 1ead1bd70dd..55ab2803b33 100644 --- a/pkg/traceql/engine.go +++ b/pkg/traceql/engine.go @@ -387,6 +387,66 @@ func (s Static) AsAnyValue() *common_v1.AnyValue { StringValue: s.EncodeToString(false), }, } + case TypeIntArray: + ints, _ := s.IntArray() + + anyInts := make([]common_v1.AnyValue_IntValue, len(ints)) + anyVals := make([]common_v1.AnyValue, len(ints)) + anyArray := common_v1.ArrayValue{ + Values: make([]*common_v1.AnyValue, len(ints)), + } + for i, n := range ints { + anyInts[i].IntValue = int64(n) + anyVals[i].Value = &anyInts[i] + anyArray.Values[i] = &anyVals[i] + } + + return &common_v1.AnyValue{Value: &common_v1.AnyValue_ArrayValue{ArrayValue: &anyArray}} + case TypeFloatArray: + floats, _ := s.FloatArray() + + anyDouble := make([]common_v1.AnyValue_DoubleValue, len(floats)) + anyVals := make([]common_v1.AnyValue, len(floats)) + anyArray := common_v1.ArrayValue{ + Values: make([]*common_v1.AnyValue, len(floats)), + } + for i, f := range floats { + anyDouble[i].DoubleValue = f + anyVals[i].Value = &anyDouble[i] + anyArray.Values[i] = &anyVals[i] + } + + return &common_v1.AnyValue{Value: &common_v1.AnyValue_ArrayValue{ArrayValue: &anyArray}} + case TypeStringArray: + strs, _ := s.StringArray() + + anyStrs := make([]common_v1.AnyValue_StringValue, len(strs)) + anyVals := make([]common_v1.AnyValue, len(strs)) + anyArray := common_v1.ArrayValue{ + Values: make([]*common_v1.AnyValue, len(strs)), + } + for i, str := range strs { + anyStrs[i].StringValue = str + anyVals[i].Value = &anyStrs[i] + anyArray.Values[i] = &anyVals[i] + } + + return &common_v1.AnyValue{Value: &common_v1.AnyValue_ArrayValue{ArrayValue: &anyArray}} + case TypeBooleanArray: + bools, _ := s.BooleanArray() + + anyBools := make([]common_v1.AnyValue_BoolValue, len(bools)) + anyVals := make([]common_v1.AnyValue, len(bools)) + anyArray := common_v1.ArrayValue{ + Values: make([]*common_v1.AnyValue, len(bools)), + } + for i, b := range bools { + anyBools[i].BoolValue = b + anyVals[i].Value = &anyBools[i] + anyArray.Values[i] = &anyVals[i] + } + + return &common_v1.AnyValue{Value: &common_v1.AnyValue_ArrayValue{ArrayValue: &anyArray}} default: return &common_v1.AnyValue{ Value: &common_v1.AnyValue_StringValue{ diff --git a/pkg/traceql/engine_test.go b/pkg/traceql/engine_test.go index 6c799d7be8f..2721b87d409 100644 --- a/pkg/traceql/engine_test.go +++ b/pkg/traceql/engine_test.go @@ -482,6 +482,39 @@ func TestStatic_AsAnyValue(t *testing.T) { {NewStaticStatus(StatusOk), &v1.AnyValue{Value: &v1.AnyValue_StringValue{StringValue: "ok"}}}, {NewStaticKind(KindInternal), &v1.AnyValue{Value: &v1.AnyValue_StringValue{StringValue: "internal"}}}, {NewStaticNil(), &v1.AnyValue{Value: &v1.AnyValue_StringValue{StringValue: "nil"}}}, + // Test for arrays + { + NewStaticIntArray([]int{1, 2}), + &v1.AnyValue{ + Value: &v1.AnyValue_ArrayValue{ + ArrayValue: &v1.ArrayValue{Values: []*v1.AnyValue{{Value: &v1.AnyValue_IntValue{IntValue: 1}}, {Value: &v1.AnyValue_IntValue{IntValue: 2}}}}, + }, + }, + }, + { + NewStaticFloatArray([]float64{1.1, 2.2}), + &v1.AnyValue{ + Value: &v1.AnyValue_ArrayValue{ + ArrayValue: &v1.ArrayValue{Values: []*v1.AnyValue{{Value: &v1.AnyValue_DoubleValue{DoubleValue: 1.1}}, {Value: &v1.AnyValue_DoubleValue{DoubleValue: 2.2}}}}, + }, + }, + }, + { + NewStaticStringArray([]string{"foo", "bar"}), + &v1.AnyValue{ + Value: &v1.AnyValue_ArrayValue{ + ArrayValue: &v1.ArrayValue{Values: []*v1.AnyValue{{Value: &v1.AnyValue_StringValue{StringValue: "foo"}}, {Value: &v1.AnyValue_StringValue{StringValue: "bar"}}}}, + }, + }, + }, + { + NewStaticBooleanArray([]bool{true, false}), + &v1.AnyValue{ + Value: &v1.AnyValue_ArrayValue{ + ArrayValue: &v1.ArrayValue{Values: []*v1.AnyValue{{Value: &v1.AnyValue_BoolValue{BoolValue: true}}, {Value: &v1.AnyValue_BoolValue{BoolValue: false}}}}, + }, + }, + }, } for _, tc := range tt { t.Run(fmt.Sprintf("%v", tc.s), func(t *testing.T) { diff --git a/pkg/traceql/enum_operators.go b/pkg/traceql/enum_operators.go index 5a797574e11..993bffb3a0c 100644 --- a/pkg/traceql/enum_operators.go +++ b/pkg/traceql/enum_operators.go @@ -66,16 +66,12 @@ func binaryTypeValid(op Operator, t StaticType) bool { } switch t { - case TypeBoolean: + case TypeBoolean, TypeBooleanArray: return op == OpAnd || op == OpOr || op == OpEqual || op == OpNotEqual - case TypeFloat: - fallthrough - case TypeInt: - fallthrough - case TypeDuration: + case TypeFloat, TypeFloatArray, TypeInt, TypeIntArray, TypeDuration: return op == OpAdd || op == OpSub || op == OpMult || @@ -88,7 +84,7 @@ func binaryTypeValid(op Operator, t StaticType) bool { op == OpGreaterEqual || op == OpLess || op == OpLessEqual - case TypeString: + case TypeString, TypeStringArray: return op == OpEqual || op == OpNotEqual || op == OpRegex || @@ -97,11 +93,7 @@ func binaryTypeValid(op Operator, t StaticType) bool { op == OpGreaterEqual || op == OpLess || op == OpLessEqual - case TypeNil: - fallthrough - case TypeStatus: - return op == OpEqual || op == OpNotEqual - case TypeKind: + case TypeNil, TypeStatus, TypeKind: return op == OpEqual || op == OpNotEqual } diff --git a/pkg/traceql/enum_statics.go b/pkg/traceql/enum_statics.go index df5c93e794b..c1c6050720c 100644 --- a/pkg/traceql/enum_statics.go +++ b/pkg/traceql/enum_statics.go @@ -1,6 +1,8 @@ package traceql -import "fmt" +import ( + "fmt" +) type StaticType int @@ -41,13 +43,84 @@ func (t StaticType) isMatchingOperand(otherT StaticType) bool { return true } - return false + return t.isMatchingArrayElement(otherT) } func (t StaticType) isNumeric() bool { return t == TypeInt || t == TypeFloat || t == TypeDuration } +// isMatchingArrayElement is like isMatchingOperand but for arrays +func (t StaticType) isMatchingArrayElement(otherT StaticType) bool { + switch t { + case TypeIntArray: + return TypeInt.isMatchingOperand(otherT) + case TypeFloatArray: + return TypeFloat.isMatchingOperand(otherT) + case TypeStringArray: + return TypeString.isMatchingOperand(otherT) + case TypeBooleanArray: + return TypeBoolean.isMatchingOperand(otherT) + } + + // make it symmetric + switch otherT { + case TypeIntArray: + return TypeInt.isMatchingOperand(t) + case TypeFloatArray: + return TypeFloat.isMatchingOperand(t) + case TypeStringArray: + return TypeString.isMatchingOperand(t) + case TypeBooleanArray: + return TypeBoolean.isMatchingOperand(t) + } + // either t or otherT are non-array types + return false +} + +// isArray used to test if a type is ArrayType +func (t StaticType) isArray() bool { + if t == TypeIntArray || t == TypeFloatArray || t == TypeStringArray || t == TypeBooleanArray { + return true + } + return false +} + +func (t StaticType) String() string { + switch t { + case TypeNil: + return "TypeNil" + case TypeSpanset: + return "TypeSpanset" + case TypeAttribute: + return "TypeAttribute" + case TypeInt: + return "TypeInt" + case TypeFloat: + return "TypeFloat" + case TypeString: + return "TypeString" + case TypeBoolean: + return "TypeBoolean" + case TypeIntArray: + return "TypeIntArray" + case TypeFloatArray: + return "TypeFloatArray" + case TypeStringArray: + return "TypeStringArray" + case TypeBooleanArray: + return "TypeBooleanArray" + case TypeDuration: + return "TypeDuration" + case TypeStatus: + return "TypeStatus" + case TypeKind: + return "TypeKind" + default: + return fmt.Sprintf("StaticType(%d)", int(t)) + } +} + // Status represents valid static values of typeStatus type Status int diff --git a/pkg/traceql/enum_statics_test.go b/pkg/traceql/enum_statics_test.go new file mode 100644 index 00000000000..13c1be76511 --- /dev/null +++ b/pkg/traceql/enum_statics_test.go @@ -0,0 +1,159 @@ +package traceql + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStaticType_isMatchingOperand(t *testing.T) { + tests := []struct { + t StaticType + otherT StaticType + want bool + }{ + // same type on both sides + {t: TypeNil, otherT: TypeNil, want: true}, + {t: TypeSpanset, otherT: TypeSpanset, want: true}, + {t: TypeAttribute, otherT: TypeAttribute, want: true}, + {t: TypeInt, otherT: TypeInt, want: true}, + {t: TypeFloat, otherT: TypeFloat, want: true}, + {t: TypeString, otherT: TypeString, want: true}, + {t: TypeBoolean, otherT: TypeBoolean, want: true}, + {t: TypeIntArray, otherT: TypeIntArray, want: true}, + {t: TypeFloatArray, otherT: TypeFloatArray, want: true}, + {t: TypeStringArray, otherT: TypeStringArray, want: true}, + {t: TypeBooleanArray, otherT: TypeBooleanArray, want: true}, + {t: TypeDuration, otherT: TypeDuration, want: true}, + {t: TypeStatus, otherT: TypeStatus, want: true}, + {t: TypeKind, otherT: TypeKind, want: true}, + + // TypeAttribute with any other type + {t: TypeAttribute, otherT: TypeNil, want: true}, + {t: TypeAttribute, otherT: TypeSpanset, want: true}, + {t: TypeAttribute, otherT: TypeInt, want: true}, + {t: TypeAttribute, otherT: TypeFloat, want: true}, + {t: TypeAttribute, otherT: TypeString, want: true}, + {t: TypeAttribute, otherT: TypeBoolean, want: true}, + {t: TypeAttribute, otherT: TypeIntArray, want: true}, + {t: TypeAttribute, otherT: TypeFloatArray, want: true}, + {t: TypeAttribute, otherT: TypeStringArray, want: true}, + {t: TypeAttribute, otherT: TypeBooleanArray, want: true}, + {t: TypeAttribute, otherT: TypeDuration, want: true}, + {t: TypeAttribute, otherT: TypeStatus, want: true}, + {t: TypeAttribute, otherT: TypeKind, want: true}, + + // any type with TypeAttribute + {t: TypeNil, otherT: TypeAttribute, want: true}, + {t: TypeSpanset, otherT: TypeAttribute, want: true}, + {t: TypeInt, otherT: TypeAttribute, want: true}, + {t: TypeFloat, otherT: TypeAttribute, want: true}, + {t: TypeString, otherT: TypeAttribute, want: true}, + {t: TypeBoolean, otherT: TypeAttribute, want: true}, + {t: TypeIntArray, otherT: TypeAttribute, want: true}, + {t: TypeFloatArray, otherT: TypeAttribute, want: true}, + {t: TypeStringArray, otherT: TypeAttribute, want: true}, + {t: TypeBooleanArray, otherT: TypeAttribute, want: true}, + {t: TypeDuration, otherT: TypeAttribute, want: true}, + {t: TypeStatus, otherT: TypeAttribute, want: true}, + {t: TypeKind, otherT: TypeAttribute, want: true}, + + // both numeric + {t: TypeInt, otherT: TypeFloat, want: true}, + {t: TypeFloat, otherT: TypeInt, want: true}, + {t: TypeNil, otherT: TypeIntArray, want: true}, + {t: TypeNil, otherT: TypeFloatArray, want: true}, + {t: TypeNil, otherT: TypeStringArray, want: true}, + {t: TypeNil, otherT: TypeBooleanArray, want: true}, + {t: TypeNil, otherT: TypeInt, want: false}, + {t: TypeNil, otherT: TypeFloat, want: false}, + {t: TypeNil, otherT: TypeString, want: false}, + {t: TypeNil, otherT: TypeBoolean, want: false}, + {t: TypeNil, otherT: TypeDuration, want: false}, + {t: TypeNil, otherT: TypeStatus, want: false}, + {t: TypeNil, otherT: TypeKind, want: false}, + + // array types + {t: TypeIntArray, otherT: TypeIntArray, want: true}, + {t: TypeIntArray, otherT: TypeFloatArray, want: true}, + {t: TypeFloatArray, otherT: TypeFloatArray, want: true}, + {t: TypeStringArray, otherT: TypeStringArray, want: true}, + {t: TypeBooleanArray, otherT: TypeBooleanArray, want: true}, + {t: TypeStringArray, otherT: TypeBooleanArray, want: false}, + {t: TypeIntArray, otherT: TypeStringArray, want: false}, + {t: TypeIntArray, otherT: TypeBooleanArray, want: false}, + {t: TypeFloatArray, otherT: TypeStringArray, want: false}, + {t: TypeFloatArray, otherT: TypeBooleanArray, want: false}, + + // other edge cases + {t: TypeInt, otherT: TypeString, want: false}, + {t: TypeBoolean, otherT: TypeString, want: false}, + {t: TypeDuration, otherT: TypeStatus, want: false}, + {t: TypeSpanset, otherT: TypeKind, want: false}, + } + for _, tt := range tests { + name := fmt.Sprintf("%s with %s", tt.t, tt.otherT) + t.Run(name, func(t *testing.T) { + assert.Equalf(t, tt.want, tt.t.isMatchingOperand(tt.otherT), "isMatchingOperand: %s", name) + }) + } +} + +func TestStaticType_isMatchingArrayElement(t *testing.T) { + tests := []struct { + t StaticType + otherT StaticType + want bool + }{ + // IntArray cases + {t: TypeIntArray, otherT: TypeInt, want: true}, + {t: TypeIntArray, otherT: TypeFloat, want: true}, + {t: TypeIntArray, otherT: TypeNil, want: true}, + {t: TypeIntArray, otherT: TypeAttribute, want: true}, + {t: TypeIntArray, otherT: TypeString, want: false}, + {t: TypeIntArray, otherT: TypeBoolean, want: false}, + + // FloatArray cases + {t: TypeFloatArray, otherT: TypeFloat, want: true}, + {t: TypeFloatArray, otherT: TypeInt, want: true}, + {t: TypeFloatArray, otherT: TypeNil, want: true}, + {t: TypeFloatArray, otherT: TypeAttribute, want: true}, + {t: TypeFloatArray, otherT: TypeString, want: false}, + {t: TypeFloatArray, otherT: TypeBoolean, want: false}, + + // StringArray cases + {t: TypeStringArray, otherT: TypeString, want: true}, + {t: TypeStringArray, otherT: TypeNil, want: true}, + {t: TypeStringArray, otherT: TypeAttribute, want: true}, + {t: TypeStringArray, otherT: TypeInt, want: false}, + {t: TypeStringArray, otherT: TypeFloat, want: false}, + {t: TypeStringArray, otherT: TypeBoolean, want: false}, + + // BooleanArray cases + {t: TypeBooleanArray, otherT: TypeBoolean, want: true}, + {t: TypeBooleanArray, otherT: TypeNil, want: true}, + {t: TypeBooleanArray, otherT: TypeAttribute, want: true}, + {t: TypeBooleanArray, otherT: TypeInt, want: false}, + {t: TypeBooleanArray, otherT: TypeFloat, want: false}, + {t: TypeBooleanArray, otherT: TypeString, want: false}, + + // non array types on both sides + {t: TypeInt, otherT: TypeInt, want: false}, + {t: TypeFloat, otherT: TypeFloat, want: false}, + {t: TypeString, otherT: TypeString, want: false}, + {t: TypeBoolean, otherT: TypeBoolean, want: false}, + {t: TypeNil, otherT: TypeBoolean, want: false}, + {t: TypeNil, otherT: TypeString, want: false}, + {t: TypeInt, otherT: TypeNil, want: false}, + {t: TypeFloat, otherT: TypeNil, want: false}, + } + for _, tt := range tests { + name := fmt.Sprintf("%s with %s", tt.t, tt.otherT) + t.Run(name, func(t *testing.T) { + assert.Equalf(t, tt.want, tt.t.isMatchingArrayElement(tt.otherT), "isMatchingArrayElement(%s)", name) + // test symmetric case + assert.Equalf(t, tt.want, tt.otherT.isMatchingArrayElement(tt.t), "isMatchingArrayElement(%s) [symmetric]", name) + }) + } +} diff --git a/tempodb/encoding/vparquet4/block_search.go b/tempodb/encoding/vparquet4/block_search.go index c488eb22973..2f1c8df78c0 100644 --- a/tempodb/encoding/vparquet4/block_search.go +++ b/tempodb/encoding/vparquet4/block_search.go @@ -349,7 +349,10 @@ func rawToResults(ctx context.Context, pf *parquet.File, rgs []parquet.RowGroup, return results, nil } -func makeIterFunc(ctx context.Context, rgs []parquet.RowGroup, pf *parquet.File) func(name string, predicate pq.Predicate, selectAs string) pq.Iterator { +// makeIterFn is a helper to create an iterator, that abstracts away context like file and row groups. +type makeIterFn func(columnName string, predicate pq.Predicate, selectAs string) pq.Iterator + +func makeIterFunc(ctx context.Context, rgs []parquet.RowGroup, pf *parquet.File) makeIterFn { async := os.Getenv(EnvVarAsyncIteratorName) == EnvVarAsyncIteratorValue return func(name string, predicate pq.Predicate, selectAs string) pq.Iterator { diff --git a/tempodb/encoding/vparquet4/block_traceql.go b/tempodb/encoding/vparquet4/block_traceql.go index 75c8353abf0..96958adfb04 100644 --- a/tempodb/encoding/vparquet4/block_traceql.go +++ b/tempodb/encoding/vparquet4/block_traceql.go @@ -832,10 +832,6 @@ func putSpansetAndSpans(ss *traceql.Spanset) { } } -// Helper function to create an iterator, that abstracts away -// context like file and rowgroups. -type makeIterFn func(columnName string, predicate parquetquery.Predicate, selectAs string) parquetquery.Iterator - const ( columnPathTraceID = "TraceID" columnPathStartTimeUnixNano = "StartTimeUnixNano" @@ -2862,7 +2858,12 @@ func (c *serviceStatsCollector) KeepGroup(res *parquetquery.IteratorResult) bool // attributeCollector receives rows from the individual key/string/int/etc // columns and joins them together into map[key]value entries with the // right type. -type attributeCollector struct{} +type attributeCollector struct { + strBuffer []string + intBuffer []int + floatBuffer []float64 + boolBuffer []bool +} var _ parquetquery.GroupPredicate = (*attributeCollector)(nil) @@ -2874,27 +2875,54 @@ func (c *attributeCollector) KeepGroup(res *parquetquery.IteratorResult) bool { var key string var val traceql.Static + // Reset buffers to reuse them without reallocating + c.strBuffer = c.strBuffer[:0] + c.intBuffer = c.intBuffer[:0] + c.floatBuffer = c.floatBuffer[:0] + c.boolBuffer = c.boolBuffer[:0] + for _, e := range res.Entries { // Ignore nulls, this leaves val as the remaining found value, // or nil if the key was found but no matching values if e.Value.Kind() < 0 { continue } - switch e.Key { case "key": key = unsafeToString(e.Value.Bytes()) case "string": - val = traceql.NewStaticString(unsafeToString(e.Value.Bytes())) + c.strBuffer = append(c.strBuffer, unsafeToString(e.Value.Bytes())) case "int": - val = traceql.NewStaticInt(int(e.Value.Int64())) + c.intBuffer = append(c.intBuffer, int(e.Value.Int64())) case "float": - val = traceql.NewStaticFloat(e.Value.Double()) + c.floatBuffer = append(c.floatBuffer, e.Value.Double()) case "bool": - val = traceql.NewStaticBool(e.Value.Boolean()) - } - } - + c.boolBuffer = append(c.boolBuffer, e.Value.Boolean()) + } + } + + // TODO: maybe pull IsArray here, and decide that to see if we have an array or not and make this go faster + switch { + // keep len == 1 cases first so we short-circuit early for non-array case + case len(c.strBuffer) == 1: + val = traceql.NewStaticString(c.strBuffer[0]) + case len(c.intBuffer) == 1: + val = traceql.NewStaticInt(c.intBuffer[0]) + case len(c.floatBuffer) == 1: + val = traceql.NewStaticFloat(c.floatBuffer[0]) + case len(c.boolBuffer) == 1: + val = traceql.NewStaticBool(c.boolBuffer[0]) + case len(c.strBuffer) > 1: + val = traceql.NewStaticStringArray(c.strBuffer) + case len(c.intBuffer) > 1: + val = traceql.NewStaticIntArray(c.intBuffer) + case len(c.floatBuffer) > 1: + val = traceql.NewStaticFloatArray(c.floatBuffer) + case len(c.boolBuffer) > 1: + val = traceql.NewStaticBooleanArray(c.boolBuffer) + } + + // reset the slices res.Entries = res.Entries[:0] res.OtherEntries = res.OtherEntries[:0] res.AppendOtherValue(key, val) diff --git a/tempodb/encoding/vparquet4/block_traceql_test.go b/tempodb/encoding/vparquet4/block_traceql_test.go index 87eb6113d3e..8bf59fa2c76 100644 --- a/tempodb/encoding/vparquet4/block_traceql_test.go +++ b/tempodb/encoding/vparquet4/block_traceql_test.go @@ -35,6 +35,7 @@ func TestOne(t *testing.T) { b := makeBackendBlockWithTraces(t, []*Trace{wantTr}) ctx := context.Background() q := `{ resource.region != nil && resource.service.name = "bar" }` + // q := `{ resource.str-array =~ "value.*" }` req := traceql.MustExtractFetchSpansRequestWithMetadata(q) req.StartTimeUnixNanos = uint64(1000 * time.Second) @@ -149,6 +150,11 @@ func TestBackendBlockSearchTraceQL(t *testing.T) { // Span dedicated attributes {"span.dedicated.span.2", traceql.MustExtractFetchSpansRequestWithMetadata(`{span.dedicated.span.2 = "dedicated-span-attr-value-2"}`)}, {"span.dedicated.span.4", traceql.MustExtractFetchSpansRequestWithMetadata(`{span.dedicated.span.4 = "dedicated-span-attr-value-4"}`)}, + // Arrays + {"resource.str-array", traceql.MustExtractFetchSpansRequestWithMetadata(`{resource.str-array = "value-three"}`)}, + {"resource.int-array", traceql.MustExtractFetchSpansRequestWithMetadata(`{resource.int-array = 11}`)}, + {"span.str-array", traceql.MustExtractFetchSpansRequestWithMetadata(`{span.str-array = "value-two"}`)}, + {"span.int-array", traceql.MustExtractFetchSpansRequestWithMetadata(`{span.int-array = 222}`)}, // Events {"event:name", traceql.MustExtractFetchSpansRequestWithMetadata(`{event:name = "e1"}`)}, {"event:timeSinceStart", traceql.MustExtractFetchSpansRequestWithMetadata(`{event:timeSinceStart > 2ms}`)}, @@ -191,13 +197,11 @@ func TestBackendBlockSearchTraceQL(t *testing.T) { parse(t, `{.foo = "def"}`), )}, {"Multiple conditions on same well-known attribute, matches either", makeReq( - // parse(t, `{.`+LabelHTTPStatusCode+` = 500}`), parse(t, `{.`+LabelHTTPStatusCode+` > 500}`), )}, { "Mix of duration with other conditions", makeReq( - // parse(t, `{`+LabelName+` = "hello"}`), // Match parse(t, `{`+LabelDuration+` < 100s }`), // No match ), @@ -518,7 +522,8 @@ func fullyPopulatedTestTrace(id common.ID) *Trace { K8sContainerName: ptr("k8scontainer"), Attrs: []Attribute{ attr("foo", "abc"), - attr("str-array", []string{"value-one", "value-two"}), + attr("str-array", []string{"value-one", "value-two", "value-three", "value-four"}), + attr("int-array", []int64{11, 22, 33}), attr(LabelServiceName, 123), // Different type than dedicated column // Unsupported attributes {Key: "unsupported-mixed-array", ValueUnsupported: &mixedArrayAttrValue, IsArray: false}, @@ -567,8 +572,8 @@ func fullyPopulatedTestTrace(id common.ID) *Trace { attr("bar", 123), attr("float", 456.78), attr("bool", false), - attr("string-array", []string{"value-one"}), - attr("int-array", []int64{11, 22}), + attr("str-array", []string{"value-one", "value-two"}), + attr("int-array", []int64{111, 222, 333, 444}), attr("double-array", []float64{1.1, 2.2, 3.3}), attr("bool-array", []bool{true, false, true, false}), // Edge-cases @@ -922,11 +927,13 @@ func BenchmarkBackendBlockTraceQL(b *testing.B) { ctx := context.TODO() tenantID := "1" // blockID := uuid.MustParse("06ebd383-8d4e-4289-b0e9-cf2197d611d5") - blockID := uuid.MustParse("0008e57d-069d-4510-a001-b9433b2da08c") + // blockID := uuid.MustParse("0008e57d-069d-4510-a001-b9433b2da08c") + blockID := uuid.MustParse("257e3a56-224a-4ebe-9696-1b304f456ac2") r, _, _, err := local.New(&local.Config{ // Path: path.Join("/Users/marty/src/tmp"), - Path: path.Join("/Users/mapno/workspace/testblock"), + // Path: path.Join("/Users/mapno/workspace/testblock"), + Path: path.Join("/Users/suraj/wd/grafana/testblock"), }) require.NoError(b, err) @@ -972,16 +979,18 @@ func BenchmarkBackendBlockGetMetrics(b *testing.B) { query string groupby string }{ - //{"{ resource.service.name = `gme-ingester` }", "resource.cluster"}, + // {"{ resource.service.name = `gme-ingester` }", "resource.cluster"}, {"{}", "name"}, } ctx := context.TODO() tenantID := "1" - blockID := uuid.MustParse("06ebd383-8d4e-4289-b0e9-cf2197d611d5") + // blockID := uuid.MustParse("06ebd383-8d4e-4289-b0e9-cf2197d611d5") + blockID := uuid.MustParse("257e3a56-224a-4ebe-9696-1b304f456ac2") r, _, _, err := local.New(&local.Config{ - Path: path.Join("/Users/marty/src/tmp/"), + // Path: path.Join("/Users/marty/src/tmp/"), + Path: path.Join("/Users/suraj/wd/grafana/testblock"), }) require.NoError(b, err) @@ -1031,9 +1040,11 @@ func BenchmarkBackendBlockQueryRange(b *testing.B) { e = traceql.NewEngine() tenantID = "1" // blockID = uuid.MustParse("06ebd383-8d4e-4289-b0e9-cf2197d611d5") - blockID = uuid.MustParse("0008e57d-069d-4510-a001-b9433b2da08c") - path = "/Users/mapno/workspace/testblock" - // path = "/Users/marty/src/tmp" + // blockID = uuid.MustParse("0008e57d-069d-4510-a001-b9433b2da08c") + blockID = uuid.MustParse("257e3a56-224a-4ebe-9696-1b304f456ac2") + // path = "/Users/marty/src/tmp/" + // path = "/Users/mapno/workspace/testblock" + path = "/Users/suraj/wd/grafana/testblock" ) r, _, _, err := local.New(&local.Config{ @@ -1191,8 +1202,10 @@ func TestTraceIDShardingQuality(t *testing.T) { opts = common.DefaultSearchOptions() tenantID = "1" // blockID = uuid.MustParse("06ebd383-8d4e-4289-b0e9-cf2197d611d5") - blockID = uuid.MustParse("18364616-f80d-45a6-b2a3-cb63e203edff") - path = "/Users/marty/src/tmp/" + // blockID = uuid.MustParse("18364616-f80d-45a6-b2a3-cb63e203edff") + blockID = uuid.MustParse("257e3a56-224a-4ebe-9696-1b304f456ac2") + // path = "/Users/marty/src/tmp/" + path = "/Users/suraj/wd/grafana/testblock" ) r, _, _, err := local.New(&local.Config{