mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-27 05:40:40 +01:00
#### Description - New `GET /api/v3/rules` list API for alert rules: filter query DSL, `states` filter, sort, and offset pagination (design discussion: SigNoz/pulse-pod#324). - Based on #12806, which extracts the shared list filter SQL compiler; this PR adds only the rules key-policy resolver (`sqlrulestore/filterquery_resolver.go`) on top of it. - Rule state lives only in the rule manager's memory, so state filtering, total, sort and pagination run in code after the SQL fetch; total always equals what is pageable. - Sorting is deterministic on ties: equal rows break on name then id, always ascending, so pages never overlap or drop rows between requests. - Response rows carry only list-page fields, deliberately excluding `condition`, `annotations` and `notificationSettings`. The envelope also returns the org's distinct label pairs and the reserved filter keys for suggestions. - Also guards previously unlocked reads of the rules map (`ListRuleStates`, `GetRule`, `TriggeredAlerts`). **Filter keys and operators** | Key | Operators | Notes | |---|---|---| | `name`, `created_by`, `updated_by` | `=`, `!=`, `CONTAINS`, `LIKE`, `ILIKE`, `IN` and negations | string search | | `labels.<key>` | string operators plus `EXISTS`, `NOT EXISTS` | missing label evaluates as empty string; keys are case-sensitive | | `severity` | same as `labels.<key>` | alias for `labels.severity` | | `created_at`, `updated_at` | `=`, `!=`, `<`, `<=`, `>`, `>=`, `BETWEEN`, `NOT BETWEEN` | quoted RFC3339 values | | `alert_type` | `=`, `!=`, `IN`, `NOT IN` | enum: `METRIC_BASED_ALERT`, `TRACES_BASED_ALERT`, `LOGS_BASED_ALERT`, `EXCEPTIONS_BASED_ALERT` | | `rule_type` | `=`, `!=`, `IN`, `NOT IN` | enum: `threshold_rule`, `promql_rule`, `anomaly_rule` | - A bare word is free text: a case-insensitive substring match over name, description and labels. - `state` is not a DSL key. It is the repeated `states=` query param: `firing`, `pending`, `recovering`, `inactive`, `nodata`, `disabled`. - An unknown key or `REGEXP` returns a 400. #### Issues closed by this PR Closes SigNoz/pulse-pod#226 #### Additional Information - A missing label evaluates as the empty string for every value operator, one uniform rule instead of the querier's per-operator split ([`AddDefaultExistsFilter`](https://github.com/SigNoz/signoz/blob/e0da06f76d/pkg/types/querybuildertypes/querybuildertypesv5/builder_elements.go#L160)); presence is asked with `EXISTS` / `NOT EXISTS`. - Integration tests (`tests/integration/tests/alerts/06_list_rules_v3.py`) cover filters, states, sorting, pagination, totals and the error contract, run against both sqlite and postgres. - Found while testing: the stock `create_notification_channel` fixture teardown silently fails and leaks channels; follow-up fix needed. --------- Co-authored-by: Naman Verma <naman.verma@signoz.io>
120 lines
3.0 KiB
Go
120 lines
3.0 KiB
Go
package sqlstoretest
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
|
"github.com/uptrace/bun/schema"
|
|
)
|
|
|
|
type formatter struct {
|
|
bunf schema.Formatter
|
|
}
|
|
|
|
func newFormatter(dialect schema.Dialect) sqlstore.SQLFormatter {
|
|
return &formatter{bunf: schema.NewFormatter(dialect)}
|
|
}
|
|
|
|
func (f *formatter) JSONExtractString(column, path string) []byte {
|
|
var sql []byte
|
|
sql = append(sql, "json_extract("...)
|
|
sql = f.bunf.AppendIdent(sql, column)
|
|
sql = append(sql, ", "...)
|
|
sql = schema.Append(f.bunf, sql, path)
|
|
sql = append(sql, ")"...)
|
|
return sql
|
|
}
|
|
|
|
func (f *formatter) JSONExtractMapValue(column, mapField, key string) []byte {
|
|
escapedKey := strings.NewReplacer(`\`, `\\`).Replace(key)
|
|
return f.JSONExtractString(column, fmt.Sprintf(`$.%s."%s"`, mapField, escapedKey))
|
|
}
|
|
|
|
func (f *formatter) JSONType(column, path string) []byte {
|
|
var sql []byte
|
|
sql = append(sql, "json_type("...)
|
|
sql = f.bunf.AppendIdent(sql, column)
|
|
sql = append(sql, ", "...)
|
|
sql = schema.Append(f.bunf, sql, path)
|
|
sql = append(sql, ")"...)
|
|
return sql
|
|
}
|
|
|
|
func (f *formatter) JSONIsArray(column, path string) []byte {
|
|
var sql []byte
|
|
sql = append(sql, f.JSONType(column, path)...)
|
|
sql = append(sql, " = "...)
|
|
sql = schema.Append(f.bunf, sql, "array")
|
|
return sql
|
|
}
|
|
|
|
func (f *formatter) JSONArrayElements(column, path, alias string) ([]byte, []byte) {
|
|
var sql []byte
|
|
sql = append(sql, "json_each("...)
|
|
sql = f.bunf.AppendIdent(sql, column)
|
|
if path != "$" && path != "" {
|
|
sql = append(sql, ", "...)
|
|
sql = schema.Append(f.bunf, sql, path)
|
|
}
|
|
sql = append(sql, ") AS "...)
|
|
sql = f.bunf.AppendIdent(sql, alias)
|
|
|
|
return sql, append([]byte(alias), ".value"...)
|
|
}
|
|
|
|
func (f *formatter) JSONArrayOfStrings(column, path, alias string) ([]byte, []byte) {
|
|
return f.JSONArrayElements(column, path, alias)
|
|
}
|
|
|
|
func (f *formatter) JSONKeys(column, path, alias string) ([]byte, []byte) {
|
|
var sql []byte
|
|
sql = append(sql, "json_each("...)
|
|
sql = f.bunf.AppendIdent(sql, column)
|
|
if path != "$" && path != "" {
|
|
sql = append(sql, ", "...)
|
|
sql = schema.Append(f.bunf, sql, path)
|
|
}
|
|
sql = append(sql, ") AS "...)
|
|
sql = f.bunf.AppendIdent(sql, alias)
|
|
|
|
return sql, append([]byte(alias), ".key"...)
|
|
}
|
|
|
|
func (f *formatter) JSONArrayAgg(expression string) []byte {
|
|
var sql []byte
|
|
sql = append(sql, "json_group_array("...)
|
|
sql = append(sql, expression...)
|
|
sql = append(sql, ')')
|
|
return sql
|
|
}
|
|
|
|
func (f *formatter) JSONArrayLiteral(values ...string) []byte {
|
|
var sql []byte
|
|
sql = append(sql, "json_array("...)
|
|
for idx, value := range values {
|
|
if idx > 0 {
|
|
sql = append(sql, ", "...)
|
|
}
|
|
sql = schema.Append(f.bunf, sql, value)
|
|
}
|
|
sql = append(sql, ')')
|
|
return sql
|
|
}
|
|
|
|
func (f *formatter) TextToJsonColumn(column string) []byte {
|
|
return f.bunf.AppendIdent([]byte{}, column)
|
|
}
|
|
|
|
func (f *formatter) LowerExpression(expression string) []byte {
|
|
var sql []byte
|
|
sql = append(sql, "lower("...)
|
|
sql = append(sql, expression...)
|
|
sql = append(sql, ')')
|
|
return sql
|
|
}
|
|
|
|
func (f *formatter) EscapeLikePattern(value string) string {
|
|
return strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(value)
|
|
}
|