mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-05 21:32:00 +00:00
Compare commits
3 Commits
lp-filter-
...
like-op
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96cebb095b | ||
|
|
a4d24d5055 | ||
|
|
36c5db177f |
@@ -38,7 +38,6 @@ var logOperatorsToExpr = map[qbtypes.FilterOperator]string{
|
||||
qbtypes.FilterOperatorNotIn: "not in",
|
||||
qbtypes.FilterOperatorExists: "in",
|
||||
qbtypes.FilterOperatorNotExists: "not in",
|
||||
// LIKE/NOT LIKE and ILIKE/NOT ILIKE are not supported yet
|
||||
}
|
||||
|
||||
func getName(key *telemetrytypes.TelemetryFieldKey) (string, error) {
|
||||
@@ -249,17 +248,32 @@ func (v *exprVisitor) VisitComparison(ctx *grammar.ComparisonContext) any {
|
||||
op = qbtypes.FilterOperatorIn
|
||||
case ctx.NotInClause() != nil:
|
||||
op = qbtypes.FilterOperatorNotIn
|
||||
case ctx.LIKE() != nil || ctx.ILIKE() != nil:
|
||||
v.errors = append(v.errors, "LIKE/ILIKE operators are not supported in expr expressions")
|
||||
return ""
|
||||
case ctx.LIKE() != nil:
|
||||
if notModifier {
|
||||
op = qbtypes.FilterOperatorNotLike
|
||||
} else {
|
||||
op = qbtypes.FilterOperatorLike
|
||||
}
|
||||
case ctx.ILIKE() != nil:
|
||||
if notModifier {
|
||||
op = qbtypes.FilterOperatorNotILike
|
||||
} else {
|
||||
op = qbtypes.FilterOperatorILike
|
||||
}
|
||||
default:
|
||||
v.errors = append(v.errors, fmt.Sprintf("unsupported comparison operator: %s", ctx.GetText()))
|
||||
return ""
|
||||
}
|
||||
|
||||
if _, ok := logOperatorsToExpr[op]; !ok {
|
||||
v.errors = append(v.errors, fmt.Sprintf("operator not supported: %v", op))
|
||||
return ""
|
||||
switch op {
|
||||
case qbtypes.FilterOperatorLike, qbtypes.FilterOperatorNotLike,
|
||||
qbtypes.FilterOperatorILike, qbtypes.FilterOperatorNotILike:
|
||||
// supported using functions
|
||||
default:
|
||||
if _, ok := logOperatorsToExpr[op]; !ok {
|
||||
v.errors = append(v.errors, fmt.Sprintf("operator not supported: %v", op))
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Build the right-hand side value.
|
||||
@@ -275,8 +289,9 @@ func (v *exprVisitor) VisitComparison(ctx *grammar.ComparisonContext) any {
|
||||
value = v.buildValue(valuesCtx[0])
|
||||
}
|
||||
|
||||
// Validate regex patterns eagerly.
|
||||
if op == qbtypes.FilterOperatorRegexp || op == qbtypes.FilterOperatorNotRegexp {
|
||||
// Validate patterns eagerly.
|
||||
switch op {
|
||||
case qbtypes.FilterOperatorRegexp, qbtypes.FilterOperatorNotRegexp:
|
||||
str, ok := value.(string)
|
||||
if !ok {
|
||||
v.errors = append(v.errors, "value for regex operator must be a string")
|
||||
@@ -286,6 +301,13 @@ func (v *exprVisitor) VisitComparison(ctx *grammar.ComparisonContext) any {
|
||||
v.errors = append(v.errors, "value for regex operator must be a valid regex")
|
||||
return ""
|
||||
}
|
||||
case qbtypes.FilterOperatorLike, qbtypes.FilterOperatorNotLike,
|
||||
qbtypes.FilterOperatorILike, qbtypes.FilterOperatorNotILike:
|
||||
_, ok := value.(string)
|
||||
if !ok {
|
||||
v.errors = append(v.errors, "value for LIKE/NOT LIKE/ILIKE/ NOT ILIKE operator must be a string")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
filter, err := buildFilterExpr(key, op, value)
|
||||
@@ -400,10 +422,19 @@ func buildFilterExpr(key *telemetrytypes.TelemetryFieldKey, op qbtypes.FilterOpe
|
||||
fmtValue := exprFormattedValue(value)
|
||||
var filter string
|
||||
|
||||
if op == qbtypes.FilterOperatorContains || op == qbtypes.FilterOperatorNotContains {
|
||||
switch op {
|
||||
case qbtypes.FilterOperatorContains, qbtypes.FilterOperatorNotContains:
|
||||
// contains / not contains must be case-insensitive to match query-time behaviour.
|
||||
filter = fmt.Sprintf("lower(%s) %s lower(%s)", keyName, logOperatorsToExpr[op], fmtValue)
|
||||
} else {
|
||||
case qbtypes.FilterOperatorLike:
|
||||
filter = fmt.Sprintf(`type(%s) != "map" && like(string(%s), %s)`, keyName, keyName, fmtValue)
|
||||
case qbtypes.FilterOperatorNotLike:
|
||||
filter = fmt.Sprintf(`type(%s) != "map" && not like(string(%s), %s)`, keyName, keyName, fmtValue)
|
||||
case qbtypes.FilterOperatorILike:
|
||||
filter = fmt.Sprintf(`type(%s) != "map" && ilike(string(%s), %s)`, keyName, keyName, fmtValue)
|
||||
case qbtypes.FilterOperatorNotILike:
|
||||
filter = fmt.Sprintf(`type(%s) != "map" && not ilike(string(%s), %s)`, keyName, keyName, fmtValue)
|
||||
default:
|
||||
filter = fmt.Sprintf("%s %s %s", keyName, logOperatorsToExpr[op], fmtValue)
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +200,48 @@ func TestParseExpression(t *testing.T) {
|
||||
},
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "LIKE",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key LIKE 'foo%'",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && type(attributes["key"]) != "map" && like(string(attributes["key"]), "foo%")`,
|
||||
},
|
||||
{
|
||||
Name: "NOT LIKE",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key NOT LIKE 'foo%'",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && type(attributes["key"]) != "map" && not like(string(attributes["key"]), "foo%")`,
|
||||
},
|
||||
{
|
||||
Name: "ILIKE",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key ILIKE 'FOO%'",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && type(attributes["key"]) != "map" && ilike(string(attributes["key"]), "FOO%")`,
|
||||
},
|
||||
{
|
||||
Name: "NOT ILIKE",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key NOT ILIKE 'FOO%'",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && type(attributes["key"]) != "map" && not ilike(string(attributes["key"]), "FOO%")`,
|
||||
},
|
||||
{
|
||||
Name: "body LIKE",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body LIKE 'Server%'",
|
||||
},
|
||||
Expr: `body != nil && type(body) != "map" && like(string(body), "Server%")`,
|
||||
},
|
||||
{
|
||||
Name: "body ILIKE",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body ILIKE 'server%'",
|
||||
},
|
||||
Expr: `body != nil && type(body) != "map" && ilike(string(body), "server%")`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
@@ -512,6 +554,123 @@ func TestExpressionVSEntry(t *testing.T) {
|
||||
},
|
||||
ExpectedMatches: []int{5},
|
||||
},
|
||||
// LIKE / NOT LIKE / ILIKE / NOT ILIKE – pattern and type-safety coverage
|
||||
{
|
||||
Name: "LIKE exact match (attribute)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.level LIKE 'info'",
|
||||
},
|
||||
ExpectedMatches: []int{0, 2, 16, 20},
|
||||
},
|
||||
{
|
||||
Name: "LIKE prefix match (body)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body LIKE 'Server%'",
|
||||
},
|
||||
ExpectedMatches: []int{13},
|
||||
},
|
||||
{
|
||||
Name: "LIKE suffix match (body)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body LIKE '%8080'",
|
||||
},
|
||||
ExpectedMatches: []int{13},
|
||||
},
|
||||
{
|
||||
Name: "LIKE substring with % (body)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body LIKE '%checkbody%'",
|
||||
},
|
||||
// map bodies excluded by type check; entries 18-20 have map body → no match
|
||||
ExpectedMatches: []int{2, 9, 16},
|
||||
},
|
||||
{
|
||||
Name: "LIKE single-char wildcard (attribute)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.level LIKE 'inf_'",
|
||||
},
|
||||
ExpectedMatches: []int{0, 2, 16, 20},
|
||||
},
|
||||
{
|
||||
Name: "LIKE prefix on attribute",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.service LIKE 'au%'",
|
||||
},
|
||||
ExpectedMatches: []int{6, 7, 15},
|
||||
},
|
||||
{
|
||||
Name: "LIKE pattern on resource",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "resource.host LIKE 'node-1%'",
|
||||
},
|
||||
ExpectedMatches: []int{1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19},
|
||||
},
|
||||
{
|
||||
Name: "NOT LIKE exact match (attribute)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.level NOT LIKE 'info'",
|
||||
},
|
||||
ExpectedMatches: []int{1, 3, 14, 15, 17},
|
||||
},
|
||||
{
|
||||
Name: "NOT LIKE suffix pattern (attribute)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.level NOT LIKE '%o'",
|
||||
},
|
||||
// level ending in 'o': info; not ending in 'o': error, debug, warn
|
||||
ExpectedMatches: []int{1, 3, 14, 15, 17},
|
||||
},
|
||||
{
|
||||
Name: "ILIKE case-insensitive exact (attribute)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.level ILIKE 'INFO'",
|
||||
},
|
||||
ExpectedMatches: []int{0, 2, 16, 20},
|
||||
},
|
||||
{
|
||||
Name: "ILIKE case-insensitive prefix (attribute)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.status ILIKE 'ACT%'",
|
||||
},
|
||||
ExpectedMatches: []int{11},
|
||||
},
|
||||
{
|
||||
Name: "ILIKE case-insensitive substring (body)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body ILIKE '%checkbody%'",
|
||||
},
|
||||
// map bodies excluded by type check
|
||||
ExpectedMatches: []int{2, 9, 10, 16},
|
||||
},
|
||||
{
|
||||
Name: "NOT ILIKE case-insensitive exact (attribute)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.level NOT ILIKE 'INFO'",
|
||||
},
|
||||
ExpectedMatches: []int{1, 3, 14, 15, 17},
|
||||
},
|
||||
{
|
||||
Name: "NOT ILIKE case-insensitive (attribute)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.service NOT ILIKE 'API'",
|
||||
},
|
||||
ExpectedMatches: []int{6, 7, 15},
|
||||
},
|
||||
{
|
||||
Name: "LIKE numeric attribute converted via string()",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.count LIKE '%5%'",
|
||||
},
|
||||
// count int64(5) -> string "5" matches; count int64(10) -> "10" does not
|
||||
ExpectedMatches: []int{4},
|
||||
},
|
||||
{
|
||||
Name: "LIKE with multi filter (attribute + resource)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.level LIKE 'info' and resource.env = 'prod'",
|
||||
},
|
||||
ExpectedMatches: []int{0},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
|
||||
Reference in New Issue
Block a user