diff --git a/modules/ingester/instance_search.go b/modules/ingester/instance_search.go index df93bb2a419..a176fcb6b53 100644 --- a/modules/ingester/instance_search.go +++ b/modules/ingester/instance_search.go @@ -362,10 +362,10 @@ func (i *instance) SearchTagValuesV2(ctx context.Context, req *tempopb.SearchTag return nil, err } - query := util.ExtractMatchers(req.Query) + query := traceql.ExtractMatchers(req.Query) var searchBlock func(common.Searcher) error - if !i.autocompleteFilteringEnabled || isEmptyQuery(query) { + if !i.autocompleteFilteringEnabled || traceql.IsEmptyQuery(query) { // If filtering is disabled or query is empty, // we can use the more efficient SearchTagValuesV2 method. searchBlock = func(s common.Searcher) error { @@ -460,12 +460,6 @@ func (i *instance) SearchTagValuesV2(ctx context.Context, req *tempopb.SearchTag return resp, nil } -func isEmptyQuery(query string) bool { - return query == emptyQuery || len(query) == 0 -} - -const emptyQuery = "{}" - // includeBlock uses the provided time range to determine if the block should be included in the search. func includeBlock(b *backend.BlockMeta, req *tempopb.SearchRequest) bool { start := int64(req.Start) diff --git a/modules/ingester/instance_search_test.go b/modules/ingester/instance_search_test.go index c87ccfe80a3..7c7227bf1c9 100644 --- a/modules/ingester/instance_search_test.go +++ b/modules/ingester/instance_search_test.go @@ -11,6 +11,8 @@ import ( "testing" "time" + "github.com/grafana/tempo/pkg/traceql" + "github.com/google/uuid" "github.com/grafana/dskit/user" "github.com/stretchr/testify/assert" @@ -951,7 +953,7 @@ func TestExtractMatchers(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expected, util.ExtractMatchers(tc.query)) + assert.Equal(t, tc.expected, traceql.ExtractMatchers(tc.query)) }) } } @@ -970,7 +972,7 @@ func BenchmarkExtractMatchers(b *testing.B) { for _, query := range queries { b.Run(query, func(b *testing.B) { for i := 0; i < b.N; i++ { - _ = util.ExtractMatchers(query) + _ = traceql.ExtractMatchers(query) } }) } diff --git a/pkg/util/traceql_extractmatcher.go b/pkg/traceql/extractmatcher.go similarity index 57% rename from pkg/util/traceql_extractmatcher.go rename to pkg/traceql/extractmatcher.go index 7b046423713..5e996969d33 100644 --- a/pkg/util/traceql_extractmatcher.go +++ b/pkg/traceql/extractmatcher.go @@ -1,19 +1,24 @@ -package util +package traceql import ( "regexp" "strings" ) +// TODO: Support spaces, quotes +// See: https://github.com/grafana/grafana/issues/77394 + // Regex to extract matchers from a query string // This regular expression matches a string that contains three groups separated by operators. -// The first group is a string of alphabetical characters, dots, and underscores. +// The first group matches one or more Unicode letters, digits, underscores, or periods. It essentially matches variable names or identifiers // The second group is a comparison operator, which can be one of several possibilities, including =, >, <, and !=. -// The third group is one of several possible values: a string enclosed in double quotes, -// a number with an optional time unit (such as "ns", "ms", "s", "m", or "h"), -// a plain number, or the boolean values "true" or "false". +// The third group is one of several possible values: +// 1. A double-quoted string consisting of one or more Unicode characters, including letters, digits, punctuation, diacritical marks, and symbols, +// 2. A sequence of one or more digits, which can represent numeric values, possibly with units like 's', 'm', or 'h'. +// 3. The boolean values "true" or "false". +// // Example: "http.status_code = 200" from the query "{ .http.status_code = 200 && .http.method = }" -var matchersRegexp = regexp.MustCompile(`[a-zA-Z._]+\s*[=|<=|>=|=~|!=|>|<|!~]\s*(?:"[a-zA-Z./_0-9-]+"|[0-9smh]+|true|false)`) +var matchersRegexp = regexp.MustCompile(`[\p{L}\p{N}._]+\s*[=|<=|>=|=~|!=|>|<|!~]\s*(?:"[\p{L}\p{N}\p{P}\p{M}\p{S}]+"|true|false|[a-z]+|[0-9smh]+)`) // TODO: Merge into a single regular expression @@ -21,26 +26,28 @@ var matchersRegexp = regexp.MustCompile(`[a-zA-Z._]+\s*[=|<=|>=|=~|!=|>|<|!~]\s* // This regular expression matches a string that contains a single spanset filter and no OR `||` conditions. // Examples // -// Query | Match +// Query | Match // // { .bar = "foo" } | Yes // { .bar = "foo" && .foo = "bar" } | Yes // { .bar = "foo" || .foo = "bar" } | No // { .bar = "foo" } && { .foo = "bar" } | No // { .bar = "foo" } || { .foo = "bar" } | No -var singleFilterRegexp = regexp.MustCompile(`^{[a-zA-Z._\s\-()/&=<>~!0-9"]*}$`) +var singleFilterRegexp = regexp.MustCompile(`^\{[^|{}]*[^|{}]}?$`) + +const emptyQuery = "{}" // ExtractMatchers extracts matchers from a query string and returns a string that can be parsed by the storage layer. func ExtractMatchers(query string) string { query = strings.TrimSpace(query) if len(query) == 0 { - return "{}" + return emptyQuery } selector := singleFilterRegexp.FindString(query) if len(selector) == 0 { - return "{}" + return emptyQuery } matchers := matchersRegexp.FindAllString(query, -1) @@ -57,3 +64,7 @@ func ExtractMatchers(query string) string { return q.String() } + +func IsEmptyQuery(query string) bool { + return query == emptyQuery || len(query) == 0 +} diff --git a/tempodb/tempodb.go b/tempodb/tempodb.go index e7370ac4bd3..b1add5bfdab 100644 --- a/tempodb/tempodb.go +++ b/tempodb/tempodb.go @@ -443,8 +443,6 @@ func (rw *readerWriter) SearchForValuesTagsV2(ctx context.Context, meta *backend return nil, err } - var tagValues []*tempopb.TagValue - valueCollector := util.NewDistinctValueCollector[tempopb.TagValue](0, func(v tempopb.TagValue) int { return len(v.Type) + len(v.Value) }) engine := traceql.NewEngine() @@ -454,9 +452,9 @@ func (rw *readerWriter) SearchForValuesTagsV2(ctx context.Context, meta *backend return nil, err } - query := util.ExtractMatchers(req.Query) + query := traceql.ExtractMatchers(req.Query) - if len(query) > 0 { + if traceql.IsEmptyQuery(query) { fetcher := traceql.NewAutocompleteFetcherWrapper(func(ctx context.Context, req traceql.AutocompleteRequest, cb traceql.AutocompleteCallback) error { return block.FetchTagValues(ctx, req, cb, common.DefaultSearchOptions()) }) @@ -477,12 +475,6 @@ func (rw *readerWriter) SearchForValuesTagsV2(ctx context.Context, meta *backend err := block.SearchTagValuesV2(ctx, tag, traceql.MakeCollectTagValueFunc(valueCollector.Collect), opts) return nil, err } - - resp := &tempopb.SearchTagValuesV2Response{ - TagValues: tagValues, - } - - return resp, nil } func (rw *readerWriter) Fetch(ctx context.Context, meta *backend.BlockMeta, req traceql.FetchSpansRequest, opts common.SearchOptions) (traceql.FetchSpansResponse, error) {