diff --git a/app/vtselect/common/extra_filters.go b/app/vtselect/common/extra_filters.go new file mode 100644 index 000000000..85fceb85c --- /dev/null +++ b/app/vtselect/common/extra_filters.go @@ -0,0 +1,122 @@ +package common + +import ( + "fmt" + "regexp" + "strings" + + "github.com/VictoriaMetrics/VictoriaLogs/lib/logstorage" + "github.com/valyala/fastjson" +) + +// ParseExtraFilters parses extra_filters from either LogsQL or JSON format. +func ParseExtraFilters(s string) (*logstorage.Filter, error) { + if s == "" { + return nil, nil + } + if !strings.HasPrefix(s, `{"`) { + return logstorage.ParseFilter(s) + } + + // Extra filters in the form {"field":"value",...}. + filters, err := parseExtraFiltersJSON(s) + if err != nil { + return nil, err + } + + result := make([]string, len(filters)) + for i, f := range filters { + if len(f.values) == 1 { + result[i] = fmt.Sprintf("%q:=%q", f.key, f.values[0]) + } else { + orValues := make([]string, len(f.values)) + for j, v := range f.values { + orValues[j] = fmt.Sprintf("%q", v) + } + result[i] = fmt.Sprintf("%q:in(%s)", f.key, strings.Join(orValues, ",")) + } + } + return logstorage.ParseFilter(strings.Join(result, " ")) +} + +// ParseExtraStreamFilters parses extra_stream_filters from either LogsQL or JSON format. +func ParseExtraStreamFilters(s string) (*logstorage.Filter, error) { + if s == "" { + return nil, nil + } + if !strings.HasPrefix(s, `{"`) { + return logstorage.ParseFilter(s) + } + + // Extra stream filters in the form {"field":"value",...}. + filters, err := parseExtraFiltersJSON(s) + if err != nil { + return nil, err + } + + result := make([]string, len(filters)) + for i, f := range filters { + if len(f.values) == 1 { + result[i] = fmt.Sprintf("%q=%q", f.key, f.values[0]) + } else { + orValues := make([]string, len(f.values)) + for j, v := range f.values { + orValues[j] = regexp.QuoteMeta(v) + } + result[i] = fmt.Sprintf("%q=~%q", f.key, strings.Join(orValues, "|")) + } + } + return logstorage.ParseFilter("{" + strings.Join(result, ",") + "}") +} + +type extraFilter struct { + key string + values []string +} + +func parseExtraFiltersJSON(s string) ([]extraFilter, error) { + v, err := fastjson.Parse(s) + if err != nil { + return nil, err + } + o := v.GetObject() + + var errOuter error + var filters []extraFilter + o.Visit(func(k []byte, v *fastjson.Value) { + if errOuter != nil { + return + } + switch v.Type() { + case fastjson.TypeString: + filters = append(filters, extraFilter{ + key: string(k), + values: []string{string(v.GetStringBytes())}, + }) + case fastjson.TypeArray: + a := v.GetArray() + if len(a) == 0 { + return + } + orValues := make([]string, len(a)) + for i, av := range a { + ov, err := av.StringBytes() + if err != nil { + errOuter = fmt.Errorf("cannot obtain string item at the array for key %q; item: %s", k, av) + return + } + orValues[i] = string(ov) + } + filters = append(filters, extraFilter{ + key: string(k), + values: orValues, + }) + default: + errOuter = fmt.Errorf("unexpected type of value for key %q: %s; value: %s", k, v.Type(), v) + } + }) + if errOuter != nil { + return nil, errOuter + } + return filters, nil +} diff --git a/app/vtselect/common/extra_filters_test.go b/app/vtselect/common/extra_filters_test.go new file mode 100644 index 000000000..41d88f916 --- /dev/null +++ b/app/vtselect/common/extra_filters_test.go @@ -0,0 +1,62 @@ +package common + +import "testing" + +func TestParseExtraFilters(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + {"empty", "", ""}, + {"json string", `{"foo":"bar"}`, `foo:=bar`}, + {"json array", `{"foo":["bar","baz"]}`, `foo:in(bar,baz)`}, + {"json mixed", `{"z":"=b ","c":["d","e,"],"a":[],"_msg":"x"}`, `z:="=b " c:in(d,"e,") =x`}, + {"logsql", `foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `{foo="bar",baz="z"} (foo:bar or foo:baz) error _time:5m`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := ParseExtraFilters(tt.input) + if err != nil { + t.Fatal(err) + } + if got := f.String(); got != tt.want { + t.Fatalf("got %q; want %q", got, tt.want) + } + }) + } + + for _, input := range []string{`{"foo"}`, `[1,2]`, `{"foo":[1]}`, `foo:(bar`, `foo | count()`} { + if _, err := ParseExtraFilters(input); err == nil { + t.Fatalf("expected error for %q", input) + } + } +} + +func TestParseExtraStreamFilters(t *testing.T) { + tests := []struct { + input string + want string + }{ + {"", ""}, + {`{"foo":"bar"}`, `{foo="bar"}`}, + {`{"foo":["bar","baz"]}`, `{foo=~"bar|baz"}`}, + {`{"z":"b","c":["d","e|\""],"a":[],"_msg":"x"}`, `{z="b",c=~"d|e\\|\"",_msg="x"}`}, + {`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `{foo="bar",baz="z"} (foo:bar or foo:baz) error _time:5m`}, + } + for _, tt := range tests { + f, err := ParseExtraStreamFilters(tt.input) + if err != nil { + t.Fatal(err) + } + if got := f.String(); got != tt.want { + t.Fatalf("got %q; want %q", got, tt.want) + } + } + + for _, input := range []string{`{"foo"}`, `[1,2]`, `{"foo":[1]}`, `foo:(bar`, `foo | count()`} { + if _, err := ParseExtraStreamFilters(input); err == nil { + t.Fatalf("expected error for %q", input) + } + } +} diff --git a/app/vtselect/logsql/logsql.go b/app/vtselect/logsql/logsql.go index d1e23090e..524e0ae7d 100644 --- a/app/vtselect/logsql/logsql.go +++ b/app/vtselect/logsql/logsql.go @@ -8,7 +8,6 @@ import ( "io" "math" "net/http" - "regexp" "slices" "sort" "strconv" @@ -26,9 +25,9 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/logger" "github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil" "github.com/VictoriaMetrics/metrics" - "github.com/valyala/fastjson" "github.com/valyala/quicktemplate" + "github.com/VictoriaMetrics/VictoriaTraces/app/vtselect/common" "github.com/VictoriaMetrics/VictoriaTraces/app/vtstorage" ) @@ -1553,7 +1552,7 @@ func parseCommonArgsWithConfig(r *http.Request, skipMaxRangeCheck bool) (*common // Parse optional extra_filters for _, extraFiltersStr := range r.Form["extra_filters"] { - extraFilters, err := parseExtraFilters(extraFiltersStr) + extraFilters, err := common.ParseExtraFilters(extraFiltersStr) if err != nil { return nil, err } @@ -1562,7 +1561,7 @@ func parseCommonArgsWithConfig(r *http.Request, skipMaxRangeCheck bool) (*common // Parse optional extra_stream_filters for _, extraStreamFiltersStr := range r.Form["extra_stream_filters"] { - extraStreamFilters, err := parseExtraStreamFilters(extraStreamFiltersStr) + extraStreamFilters, err := common.ParseExtraStreamFilters(extraStreamFiltersStr) if err != nil { return nil, err } @@ -1652,118 +1651,6 @@ func getTimeNsec(r *http.Request, argName string) (int64, bool, error) { return nsecs, true, nil } -func parseExtraFilters(s string) (*logstorage.Filter, error) { - if s == "" { - return nil, nil - } - if !strings.HasPrefix(s, `{"`) { - return logstorage.ParseFilter(s) - } - - // Extra filters in the form {"field":"value",...}. - kvs, err := parseExtraFiltersJSON(s) - if err != nil { - return nil, err - } - - filters := make([]string, len(kvs)) - for i, kv := range kvs { - if len(kv.values) == 1 { - filters[i] = fmt.Sprintf("%q:=%q", kv.key, kv.values[0]) - } else { - orValues := make([]string, len(kv.values)) - for j, v := range kv.values { - orValues[j] = fmt.Sprintf("%q", v) - } - filters[i] = fmt.Sprintf("%q:in(%s)", kv.key, strings.Join(orValues, ",")) - } - } - s = strings.Join(filters, " ") - return logstorage.ParseFilter(s) -} - -func parseExtraStreamFilters(s string) (*logstorage.Filter, error) { - if s == "" { - return nil, nil - } - if !strings.HasPrefix(s, `{"`) { - return logstorage.ParseFilter(s) - } - - // Extra stream filters in the form {"field":"value",...}. - kvs, err := parseExtraFiltersJSON(s) - if err != nil { - return nil, err - } - - filters := make([]string, len(kvs)) - for i, kv := range kvs { - if len(kv.values) == 1 { - filters[i] = fmt.Sprintf("%q=%q", kv.key, kv.values[0]) - } else { - orValues := make([]string, len(kv.values)) - for j, v := range kv.values { - orValues[j] = regexp.QuoteMeta(v) - } - filters[i] = fmt.Sprintf("%q=~%q", kv.key, strings.Join(orValues, "|")) - } - } - s = "{" + strings.Join(filters, ",") + "}" - return logstorage.ParseFilter(s) -} - -type extraFilter struct { - key string - values []string -} - -func parseExtraFiltersJSON(s string) ([]extraFilter, error) { - v, err := fastjson.Parse(s) - if err != nil { - return nil, err - } - o := v.GetObject() - - var errOuter error - var filters []extraFilter - o.Visit(func(k []byte, v *fastjson.Value) { - if errOuter != nil { - return - } - switch v.Type() { - case fastjson.TypeString: - filters = append(filters, extraFilter{ - key: string(k), - values: []string{string(v.GetStringBytes())}, - }) - case fastjson.TypeArray: - a := v.GetArray() - if len(a) == 0 { - return - } - orValues := make([]string, len(a)) - for i, av := range a { - ov, err := av.StringBytes() - if err != nil { - errOuter = fmt.Errorf("cannot obtain string item at the array for key %q; item: %s", k, av) - return - } - orValues[i] = string(ov) - } - filters = append(filters, extraFilter{ - key: string(k), - values: orValues, - }) - default: - errOuter = fmt.Errorf("unexpected type of value for key %q: %s; value: %s", k, v.Type(), v) - } - }) - if errOuter != nil { - return nil, errOuter - } - return filters, nil -} - func getPositiveInt(r *http.Request, argName string) (int, error) { n, err := httputil.GetInt(r, argName) if err != nil { diff --git a/app/vtselect/logsql/logsql_test.go b/app/vtselect/logsql/logsql_test.go deleted file mode 100644 index 0cb30c62a..000000000 --- a/app/vtselect/logsql/logsql_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package logsql - -import ( - "testing" -) - -func TestParseExtraFilters_Success(t *testing.T) { - f := func(s, resultExpected string) { - t.Helper() - - f, err := parseExtraFilters(s) - if err != nil { - t.Fatalf("unexpected error in parseExtraFilters: %s", err) - } - result := f.String() - if result != resultExpected { - t.Fatalf("unexpected result\ngot\n%s\nwant\n%s", result, resultExpected) - } - } - - f("", "") - - // JSON string - f(`{"foo":"bar"}`, `foo:=bar`) - f(`{"foo":["bar","baz"]}`, `foo:in(bar,baz)`) - f(`{"z":"=b ","c":["d","e,"],"a":[],"_msg":"x"}`, `z:="=b " c:in(d,"e,") =x`) - - // LogsQL filter - f(`foobar`, `foobar`) - f(`foo:bar`, `foo:bar`) - f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `{foo="bar",baz="z"} (foo:bar or foo:baz) error _time:5m`) -} - -func TestParseExtraFilters_Failure(t *testing.T) { - f := func(s string) { - t.Helper() - - _, err := parseExtraFilters(s) - if err == nil { - t.Fatalf("expecting non-nil error") - } - } - - // Invalid JSON - f(`{"foo"}`) - f(`[1,2]`) - f(`{"foo":[1]}`) - - // Invalid LogsQL filter - f(`foo:(bar`) - - // excess pipe - f(`foo | count()`) -} - -func TestParseExtraStreamFilters_Success(t *testing.T) { - f := func(s, resultExpected string) { - t.Helper() - - f, err := parseExtraStreamFilters(s) - if err != nil { - t.Fatalf("unexpected error in parseExtraStreamFilters: %s", err) - } - result := f.String() - if result != resultExpected { - t.Fatalf("unexpected result;\ngot\n%s\nwant\n%s", result, resultExpected) - } - } - - f("", "") - - // JSON string - f(`{"foo":"bar"}`, `{foo="bar"}`) - f(`{"foo":["bar","baz"]}`, `{foo=~"bar|baz"}`) - f(`{"z":"b","c":["d","e|\""],"a":[],"_msg":"x"}`, `{z="b",c=~"d|e\\|\"",_msg="x"}`) - - // LogsQL filter - f(`foobar`, `foobar`) - f(`foo:bar`, `foo:bar`) - f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `{foo="bar",baz="z"} (foo:bar or foo:baz) error _time:5m`) -} - -func TestParseExtraStreamFilters_Failure(t *testing.T) { - f := func(s string) { - t.Helper() - - _, err := parseExtraStreamFilters(s) - if err == nil { - t.Fatalf("expecting non-nil error") - } - } - - // Invalid JSON - f(`{"foo"}`) - f(`[1,2]`) - f(`{"foo":[1]}`) - - // Invalid LogsQL filter - f(`foo:(bar`) - - // excess pipe - f(`foo | count()`) -} diff --git a/app/vtselect/traces/query/query.go b/app/vtselect/traces/query/query.go index 792a3080d..a278be408 100644 --- a/app/vtselect/traces/query/query.go +++ b/app/vtselect/traces/query/query.go @@ -45,7 +45,7 @@ func GetServiceNameList(ctx context.Context, cp *tracecommon.CommonParams) ([]st } q.AddTimeFilter(currentTime.Add(-*tracecommon.TraceServiceAndSpanNameLookbehind).UnixNano(), currentTime.UnixNano()) - cp.Query = q + cp.Query = cp.ApplyExtraFilters(q) qctx := cp.NewQueryContext(ctx) defer cp.UpdatePerQueryStatsMetrics() @@ -74,7 +74,7 @@ func GetSpanNameList(ctx context.Context, cp *tracecommon.CommonParams, serviceN } q.AddTimeFilter(currentTime.Add(-*tracecommon.TraceServiceAndSpanNameLookbehind).UnixNano(), currentTime.UnixNano()) - cp.Query = q + cp.Query = cp.ApplyExtraFilters(q) qctx := cp.NewQueryContext(ctx) defer cp.UpdatePerQueryStatsMetrics() @@ -161,7 +161,7 @@ func GetTraceList(ctx context.Context, cp *tracecommon.CommonParams, param *Trac ctxWithCancel, cancel := context.WithCancel(ctx) defer cancel() - cp.Query = q + cp.Query = cp.ApplyExtraFilters(q) qctx := cp.NewQueryContext(ctxWithCancel) defer cp.UpdatePerQueryStatsMetrics() @@ -280,7 +280,7 @@ func findTraceIDsSplitTimeRange(ctx context.Context, q *logstorage.Query, cp *tr traceIDList := make([]string, 0, limit) maxStartTimeStr := endTime.Format(time.RFC3339) - cp.Query = q + cp.Query = cp.ApplyExtraFilters(q) qctx := cp.NewQueryContext(ctx) defer cp.UpdatePerQueryStatsMetrics() @@ -461,7 +461,7 @@ func findSpansByTraceIDAndTime(ctx context.Context, cp *tracecommon.CommonParams ctxWithCancel, cancel := context.WithCancel(ctx) defer cancel() - cp.Query = q + cp.Query = cp.ApplyExtraFilters(q) qctx := cp.NewQueryContext(ctxWithCancel) defer cp.UpdatePerQueryStatsMetrics() @@ -560,7 +560,7 @@ func GetServiceGraphList(ctx context.Context, cp *tracecommon.CommonParams, para } q.AddTimeFilter(startTime, endTime) - cp.Query = q + cp.Query = cp.ApplyExtraFilters(q) qctx := cp.NewQueryContext(ctx) var rowsLock sync.Mutex @@ -660,7 +660,7 @@ func GetServiceGraphTimeRange(ctx context.Context, tenantID logstorage.TenantID, q.AddTimeFilter(startTime.UnixNano(), endTime.UnixNano()) q.AddPipeOffsetLimit(0, limit) - cp.Query = q + cp.Query = cp.ApplyExtraFilters(q) qctx := cp.NewQueryContext(ctx) defer cp.UpdatePerQueryStatsMetrics() @@ -737,7 +737,7 @@ func GetServiceDBGraphTimeRange(ctx context.Context, tenantID logstorage.TenantI q.AddPipeOffsetLimit(0, limit) } - cp.Query = q + cp.Query = cp.ApplyExtraFilters(q) qctx := cp.NewQueryContext(ctx) defer cp.UpdatePerQueryStatsMetrics() diff --git a/app/vtselect/traces/tempo/metrics_handler.go b/app/vtselect/traces/tempo/metrics_handler.go index 03365f811..a593df7b8 100644 --- a/app/vtselect/traces/tempo/metrics_handler.go +++ b/app/vtselect/traces/tempo/metrics_handler.go @@ -359,7 +359,7 @@ func runFacetsQuery(ctx context.Context, cp *tracecommon.CommonParams, filterStr } cpCopy := *cp - cpCopy.Query = q + cpCopy.Query = cpCopy.ApplyExtraFilters(q) qctx := cpCopy.NewQueryContext(ctx) defer cpCopy.UpdatePerQueryStatsMetrics() if err := vtstorage.RunQuery(qctx, writeBlock); err != nil { @@ -395,7 +395,7 @@ func runTotalCount(ctx context.Context, cp *tracecommon.CommonParams, filterStr } } cpCopy := *cp - cpCopy.Query = q + cpCopy.Query = cpCopy.ApplyExtraFilters(q) qctx := cpCopy.NewQueryContext(ctx) defer cpCopy.UpdatePerQueryStatsMetrics() if err := vtstorage.RunQuery(qctx, writeBlock); err != nil { @@ -660,7 +660,7 @@ func executeStatsQuery(ctx context.Context, cp *tracecommon.CommonParams, logsQL } cpCopy := *cp - cpCopy.Query = q + cpCopy.Query = cpCopy.ApplyExtraFilters(q) qctx := cpCopy.NewQueryContext(ctx) defer cpCopy.UpdatePerQueryStatsMetrics() @@ -873,7 +873,7 @@ func collectExemplars(ctx context.Context, cp *tracecommon.CommonParams, filterS } cpCopy := *cp - cpCopy.Query = q + cpCopy.Query = cpCopy.ApplyExtraFilters(q) qctx := cpCopy.NewQueryContext(ctx) defer cpCopy.UpdatePerQueryStatsMetrics() diff --git a/app/vtselect/traces/tempo/query.go b/app/vtselect/traces/tempo/query.go index 9edd7665d..6697e6749 100644 --- a/app/vtselect/traces/tempo/query.go +++ b/app/vtselect/traces/tempo/query.go @@ -66,7 +66,7 @@ func GetTraceList(ctx context.Context, cp *tracecommon.CommonParams, filterQuery ctxWithCancel, cancel := context.WithCancel(ctx) defer cancel() - cp.Query = q + cp.Query = cp.ApplyExtraFilters(q) qctx := cp.NewQueryContext(ctxWithCancel) defer cp.UpdatePerQueryStatsMetrics() @@ -194,7 +194,7 @@ func findTraceIDsSplitTimeRange(ctx context.Context, q *logstorage.Query, cp *tr traceIDList := make([]string, 0, limit) maxStartTimeStr := endTime.Format(time.RFC3339) - cp.Query = q + cp.Query = cp.ApplyExtraFilters(q) qctx := cp.NewQueryContext(ctx) defer cp.UpdatePerQueryStatsMetrics() @@ -284,7 +284,7 @@ func findTraceIDTimeSplitTimeRange(ctx context.Context, q *logstorage.Query, cp ctxWithCancel, cancel := context.WithCancel(ctx) defer cancel() - cp.Query = q + cp.Query = cp.ApplyExtraFilters(q) qctx := cp.NewQueryContext(ctxWithCancel) defer cp.UpdatePerQueryStatsMetrics() @@ -386,7 +386,7 @@ func findSpansByTraceIDAndTime(ctx context.Context, cp *tracecommon.CommonParams return nil, fmt.Errorf("cannot parse query [%s]: %s", qStr, err) } ctxWithCancel, cancel := context.WithCancel(ctx) - cp.Query = q + cp.Query = cp.ApplyExtraFilters(q) qctx := cp.NewQueryContext(ctxWithCancel) defer cp.UpdatePerQueryStatsMetrics() diff --git a/app/vtselect/traces/tempo/tempo.go b/app/vtselect/traces/tempo/tempo.go index 3de51b838..eb2124dce 100644 --- a/app/vtselect/traces/tempo/tempo.go +++ b/app/vtselect/traces/tempo/tempo.go @@ -485,7 +485,7 @@ func singleFieldQueryHelper(ctx context.Context, q *logstorage.Query, cp *tracec } } - cp.Query = q + cp.Query = cp.ApplyExtraFilters(q) qctx := cp.NewQueryContext(ctx) defer cp.UpdatePerQueryStatsMetrics() diff --git a/app/vtselect/traces/tracecommon/tracecommon.go b/app/vtselect/traces/tracecommon/tracecommon.go index 1630ff952..ebaf1f7c2 100644 --- a/app/vtselect/traces/tracecommon/tracecommon.go +++ b/app/vtselect/traces/tracecommon/tracecommon.go @@ -12,6 +12,7 @@ import ( "github.com/VictoriaMetrics/VictoriaLogs/lib/logstorage" + "github.com/VictoriaMetrics/VictoriaTraces/app/vtselect/common" "github.com/VictoriaMetrics/VictoriaTraces/app/vtstorage" ) @@ -38,8 +39,9 @@ var ( // CommonParams common query params that shared by all requests. type CommonParams struct { - TenantIDs []logstorage.TenantID - Query *logstorage.Query + TenantIDs []logstorage.TenantID + Query *logstorage.Query + ExtraFilters *logstorage.Filter // Whether to disable compression of the response sent to the vtselect. DisableCompression bool @@ -58,6 +60,14 @@ func (cp *CommonParams) NewQueryContext(ctx context.Context) *logstorage.QueryCo return logstorage.NewQueryContext(ctx, &cp.qs, cp.TenantIDs, cp.Query, cp.AllowPartialResponse, cp.HiddenFieldsFilters) } +func (cp *CommonParams) ApplyExtraFilters(q *logstorage.Query) *logstorage.Query { + if q == nil || cp.ExtraFilters == nil { + return q + } + q.AddExtraFilters(cp.ExtraFilters) + return q +} + func (cp *CommonParams) UpdatePerQueryStatsMetrics() { vtstorage.UpdatePerQueryStatsMetrics(&cp.qs) } @@ -75,14 +85,45 @@ func GetCommonParams(r *http.Request) (*CommonParams, error) { return nil, err } + extraFilters, err := parseExtraFiltersFromRequest(r) + if err != nil { + return nil, err + } + cp := &CommonParams{ TenantIDs: tenantIDs, + ExtraFilters: extraFilters, HiddenFieldsFilters: hiddenFieldsFilters, } return cp, nil } +func parseExtraFiltersFromRequest(r *http.Request) (*logstorage.Filter, error) { + var extraFilters *logstorage.Filter + for _, extraFiltersStr := range r.Form["extra_filters"] { + f, err := common.ParseExtraFilters(extraFiltersStr) + if err != nil { + return nil, fmt.Errorf("cannot parse extra_filters=%q: %w", extraFiltersStr, err) + } + if extraFilters == nil { + extraFilters = f + continue + } + q, err := logstorage.ParseQuery("*") + if err != nil { + return nil, fmt.Errorf("cannot initialize extra_filters query: %w", err) + } + q.AddExtraFilters(extraFilters) + q.AddExtraFilters(f) + extraFilters, err = logstorage.ParseFilter(q.String()) + if err != nil { + return nil, fmt.Errorf("cannot merge extra_filters=%q: %w", extraFiltersStr, err) + } + } + return extraFilters, nil +} + func getStringSliceFromRequest(r *http.Request, argName string) ([]string, error) { s := r.FormValue(argName) if s == "" { diff --git a/app/vtselect/traces/tracecommon/tracecommon_test.go b/app/vtselect/traces/tracecommon/tracecommon_test.go new file mode 100644 index 000000000..5aafa7773 --- /dev/null +++ b/app/vtselect/traces/tracecommon/tracecommon_test.go @@ -0,0 +1,60 @@ +package tracecommon + +import ( + "net/http/httptest" + "strings" + "testing" + + "github.com/VictoriaMetrics/VictoriaLogs/lib/logstorage" +) + +func TestGetCommonParamsAppliesExtraFilters(t *testing.T) { + req := httptest.NewRequest("GET", "/select/tempo/api/search?extra_filters=%7B%22env%22%3A%22prod%22%7D", nil) + + cp, err := GetCommonParams(req) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + q, err := logstorage.ParseQuery(`status_code:=2`) + if err != nil { + t.Fatalf("cannot parse query: %s", err) + } + + got := cp.ApplyExtraFilters(q).String() + if !strings.Contains(got, "env:=prod") { + t.Fatalf("missing extra filter in query %q", got) + } + if !strings.Contains(got, "status_code:=2") { + t.Fatalf("missing original filter in query %q", got) + } +} + +func TestGetCommonParamsMergesMultipleExtraFilters(t *testing.T) { + req := httptest.NewRequest("GET", "/select/tempo/api/search?extra_filters=env:%3Dprod&extra_filters=cluster:%3Deast", nil) + + cp, err := GetCommonParams(req) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + q, err := logstorage.ParseQuery(`status_code:=2`) + if err != nil { + t.Fatalf("cannot parse query: %s", err) + } + + got := cp.ApplyExtraFilters(q).String() + for _, want := range []string{"env:=prod", "cluster:=east", "status_code:=2"} { + if !strings.Contains(got, want) { + t.Fatalf("missing %q in query %q", want, got) + } + } +} + +func TestGetCommonParamsRejectsInvalidExtraFilters(t *testing.T) { + req := httptest.NewRequest("GET", "/select/tempo/api/search?extra_filters=foo:(bar", nil) + + if _, err := GetCommonParams(req); err == nil { + t.Fatal("expected error for invalid extra_filters") + } +}