diff --git a/pkg/querybuilder/where_clause_visitor.go b/pkg/querybuilder/where_clause_visitor.go index ba4ad3e36824..3c89a8854e66 100644 --- a/pkg/querybuilder/where_clause_visitor.go +++ b/pkg/querybuilder/where_clause_visitor.go @@ -18,6 +18,8 @@ import ( var searchTroubleshootingGuideURL = "https://signoz.io/docs/userguide/search-troubleshooting/" +const stringMatchingOperatorDocURL = "https://signoz.io/docs/userguide/operators-reference/#string-matching-operators" + // filterExpressionVisitor implements the FilterQueryVisitor interface // to convert the parsed filter expressions into ClickHouse WHERE clause type filterExpressionVisitor struct { @@ -533,11 +535,13 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext if ctx.NOT() != nil { op = qbtypes.FilterOperatorNotLike } + v.warnIfLikeWithoutWildcards("LIKE", value) } else if ctx.ILIKE() != nil { op = qbtypes.FilterOperatorILike if ctx.NOT() != nil { op = qbtypes.FilterOperatorNotILike } + v.warnIfLikeWithoutWildcards("ILIKE", value) } else if ctx.REGEXP() != nil { op = qbtypes.FilterOperatorRegexp if ctx.NOT() != nil { @@ -571,6 +575,19 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext return "" // Should not happen with valid input } +// warnIfLikeWithoutWildcards adds a guidance warning when LIKE/ILIKE is used without wildcards +func (v *filterExpressionVisitor) warnIfLikeWithoutWildcards(op string, value any) { + if hasLikeWildcards(value) { + return + } + + msg := op + " operator used without wildcards (% or _). Consider using = operator for exact matches or add wildcards for pattern matching." + v.warnings = append(v.warnings, msg) + if v.mainWarnURL == "" { + v.mainWarnURL = stringMatchingOperatorDocURL + } +} + // VisitInClause handles IN expressions func (v *filterExpressionVisitor) VisitInClause(ctx *grammar.InClauseContext) any { if ctx.ValueList() != nil { @@ -871,6 +888,15 @@ func (v *filterExpressionVisitor) VisitKey(ctx *grammar.KeyContext) any { return fieldKeysForName } +// hasLikeWildcards checks if a value contains LIKE wildcards (% or _) +func hasLikeWildcards(value any) bool { + str, ok := value.(string) + if !ok { + return false + } + return strings.Contains(str, "%") || strings.Contains(str, "_") +} + func trimQuotes(txt string) string { if len(txt) >= 2 { if (txt[0] == '"' && txt[len(txt)-1] == '"') || diff --git a/pkg/telemetrylogs/filter_expr_like_warning_test.go b/pkg/telemetrylogs/filter_expr_like_warning_test.go new file mode 100644 index 000000000000..a107b060c10b --- /dev/null +++ b/pkg/telemetrylogs/filter_expr_like_warning_test.go @@ -0,0 +1,82 @@ +package telemetrylogs + +import ( + "testing" + + "github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest" + "github.com/SigNoz/signoz/pkg/querybuilder" + "github.com/stretchr/testify/require" +) + +// TestLikeAndILikeWithoutWildcards_Warns Tests that LIKE/ILIKE without wildcards add warnings and include docs URL +func TestLikeAndILikeWithoutWildcards_Warns(t *testing.T) { + fm := NewFieldMapper() + cb := NewConditionBuilder(fm) + + keys := buildCompleteFieldKeyMap() + + opts := querybuilder.FilterExprVisitorOpts{ + Logger: instrumentationtest.New().Logger(), + FieldMapper: fm, + ConditionBuilder: cb, + FieldKeys: keys, + FullTextColumn: DefaultFullTextColumn, + JsonBodyPrefix: BodyJSONStringSearchPrefix, + JsonKeyToKey: GetBodyJSONKey, + } + + tests := []string{ + "service.name LIKE 'demo-backend'", + "service.name ILIKE 'demo-backend'", + "service.name NOT LIKE 'demo-backend'", + "service.name NOT ILIKE 'demo-backend'", + } + + for _, expr := range tests { + t.Run(expr, func(t *testing.T) { + clause, err := querybuilder.PrepareWhereClause(expr, opts) + require.NoError(t, err) + require.NotNil(t, clause) + + require.NotEmpty(t, clause.Warnings, "expected warning for: %s", expr) + require.Contains(t, clause.Warnings[0], "operator used without wildcards") + require.Contains(t, clause.WarningsDocURL, "operators-reference/#string-matching-operators") + }) + } +} + +// TestLikeAndILikeWithWildcards_NoWarn Tests that LIKE/ILIKE with wildcards do not add warnings +func TestLikeAndILikeWithWildcards_NoWarn(t *testing.T) { + fm := NewFieldMapper() + cb := NewConditionBuilder(fm) + + keys := buildCompleteFieldKeyMap() + + opts := querybuilder.FilterExprVisitorOpts{ + Logger: instrumentationtest.New().Logger(), + FieldMapper: fm, + ConditionBuilder: cb, + FieldKeys: keys, + FullTextColumn: DefaultFullTextColumn, + JsonBodyPrefix: BodyJSONStringSearchPrefix, + JsonKeyToKey: GetBodyJSONKey, + } + + tests := []string{ + "service.name LIKE 'demo-%'", + "service.name LIKE '%demo'", + "service.name ILIKE '_demo'", + "service.name ILIKE '%demo%'", + } + + for _, expr := range tests { + t.Run(expr, func(t *testing.T) { + clause, err := querybuilder.PrepareWhereClause(expr, opts) + require.NoError(t, err) + require.NotNil(t, clause) + + require.Empty(t, clause.Warnings, "did not expect warnings for: %s", expr) + require.Empty(t, clause.WarningsDocURL) + }) + } +}