Alerting: Filter alerts by data source on the back end (search) (#114717)

This commit is contained in:
Santiago
2025-12-02 18:41:51 +01:00
committed by GitHub
parent ac62070ac7
commit f4b8f2d038
5 changed files with 123 additions and 0 deletions
@@ -444,6 +444,7 @@ type paginationContext struct {
panelID int64
ruleGroups []string
receiverName string
dataSourceUIDs []string
title string
searchRuleGroup string
ruleType ngmodels.RuleTypeFilter
@@ -483,6 +484,7 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert
PanelID: ctx.panelID,
RuleGroups: ctx.ruleGroups,
ReceiverName: ctx.receiverName,
DataSourceUIDs: ctx.dataSourceUIDs,
SearchTitle: ctx.title,
SearchRuleGroup: ctx.searchRuleGroup,
},
@@ -737,6 +739,9 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
searchRuleGroup := opts.Query.Get("search.rule_group")
span.SetAttributes(attribute.Bool("search_rule_group_set", searchRuleGroup != ""))
dataSourceUIDs := opts.Query["datasource_uid"]
span.SetAttributes(attribute.Bool("datasource_uid_set", len(dataSourceUIDs) > 0))
var ruleType ngmodels.RuleTypeFilter
switch ngmodels.RuleType(opts.Query.Get("rule_type")) {
case ngmodels.RuleTypeAlerting:
@@ -792,6 +797,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
ruleGroups: ruleGroups,
receiverName: receiverName,
title: title,
dataSourceUIDs: dataSourceUIDs,
searchRuleGroup: searchRuleGroup,
ruleType: ruleType,
ruleNamesSet: ruleNamesSet,
@@ -899,6 +905,7 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
receiverName := opts.Query.Get("receiver_name")
title := opts.Query.Get("search.rule_name")
dataSourceUIDs := opts.Query["datasource_uid"]
searchRuleGroup := opts.Query.Get("search.rule_group")
alertRuleQuery := ngmodels.ListAlertRulesQuery{
@@ -911,6 +918,7 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
ReceiverName: receiverName,
SearchTitle: title,
SearchRuleGroup: searchRuleGroup,
DataSourceUIDs: dataSourceUIDs,
}
ruleList, err := store.ListAlertRules(opts.Ctx, &alertRuleQuery)
if err != nil {
@@ -985,6 +985,10 @@ type ListAlertRulesQuery struct {
ReceiverName string
TimeIntervalName string
// DataSourceUIDs allows searching for alert rules using data sources
// that match any of the given UIDs exactly (case sensitive).
DataSourceUIDs []string
// SearchTitle allows searching for alert rules that contain
// the given string in their title (case insensitive)
SearchTitle string
+16
View File
@@ -410,6 +410,22 @@ func (a *AlertRuleMutators) WithDashboardAndPanel(dashboardUID *string, panelID
}
}
// WithDataSourceUID takes a list of UIDs. It adds the nth UID to the nth query in the alert (same index).
// If there are not enough queries, it adds an empty one with the given data source UID.
func (a *AlertRuleMutators) WithDataSourceUID(dsUIDs ...string) AlertRuleMutator {
return func(rule *AlertRule) {
for i, uid := range dsUIDs {
if i >= len(rule.Data) {
rule.Data = append(rule.Data, AlertQuery{
DatasourceUID: uid,
})
continue
}
rule.Data[i].DatasourceUID = uid
}
}
}
// WithUniqueUID returns AlertRuleMutator that generates a random UID if it is among UIDs known by the instance of mutator.
// NOTE: two instances of the mutator do not share known UID.
// Example #1 reuse mutator instance:
+17
View File
@@ -796,6 +796,7 @@ func (st DBstore) ListAlertRulesPaginated(ctx context.Context, query *ngmodels.L
return result, nextToken, err
}
// nolint:gocyclo
func (st DBstore) buildListAlertRulesQuery(sess *db.Session, query *ngmodels.ListAlertRulesExtendedQuery) (q *xorm.Session, groupsSet map[string]struct{}, err error) {
q = sess.Table("alert_rule")
if query.OrgID >= 0 {
@@ -868,6 +869,22 @@ func (st DBstore) buildListAlertRulesQuery(sess *db.Session, query *ngmodels.Lis
}
}
if len(query.DataSourceUIDs) > 0 {
orConditions := make([]string, 0, len(query.DataSourceUIDs))
orParams := make([]interface{}, 0, len(query.DataSourceUIDs))
for _, dsUID := range query.DataSourceUIDs {
// The 'data' column holds the alert definition as JSON. The data source's UID is in the 'datasourceUid' field.
// Instead of trying to parse that JSON, we can do a simple text search.
// All alert rules go through PreSave(), which normalizes the JSON data using json.Marshal().
pattern := fmt.Sprintf(`"datasourceUid":"%s"`, dsUID)
sql, param := st.SQLStore.GetDialect().LikeOperator("data", true, pattern, true)
orConditions = append(orConditions, sql)
orParams = append(orParams, param)
}
q = q.And("("+strings.Join(orConditions, " OR ")+")", orParams...)
}
if query.SearchTitle != "" {
words := strings.Fields(query.SearchTitle)
if len(words) > 0 {
@@ -2170,6 +2170,84 @@ func TestIntegration_ListAlertRules(t *testing.T) {
}
})
t.Run("filter by DataSourceUIDs", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b)
// Create rules with different data sources.
const (
uid1 = "uid-1"
uid2 = "uid-2"
uid3 = "uid-3"
rule1UID = "rule-1"
rule2UID = "rule-2"
rule3UID = "rule-3"
rule4UID = "rule-4"
rule5UID = "rule-5"
rule6UID = "rule-6"
)
createRule(t, store, ruleGen.With(models.RuleGen.WithUID(rule1UID), models.RuleGen.WithDataSourceUID(uid1)))
createRule(t, store, ruleGen.With(models.RuleGen.WithUID(rule2UID), models.RuleMuts.WithDataSourceUID(uid2)))
createRule(t, store, ruleGen.With(models.RuleGen.WithUID(rule3UID), models.RuleMuts.WithDataSourceUID(uid3)))
createRule(t, store, ruleGen.With(models.RuleGen.WithUID(rule4UID), models.RuleGen.WithDataSourceUID(uid1, uid2)))
createRule(t, store, ruleGen.With(models.RuleGen.WithUID(rule5UID), models.RuleMuts.WithDataSourceUID(uid2, uid3)))
createRule(t, store, ruleGen.With(models.RuleGen.WithUID(rule6UID), models.RuleMuts.WithDataSourceUID(uid1, uid2, uid3)))
tc := []struct {
name string
dsUIDs []string
expectedUIDs []string
}{
{
name: "searching for uid-1 returns rules using it",
dsUIDs: []string{uid1},
expectedUIDs: []string{rule1UID, rule4UID, rule6UID},
},
{
name: "searching for uid-1 and uid-2 returns rules using them",
dsUIDs: []string{uid1, uid2},
expectedUIDs: []string{rule1UID, rule2UID, rule4UID, rule5UID, rule6UID},
},
{
name: "searching for uid-1, uid-2, and uid-3 returns all rules",
dsUIDs: []string{uid1, uid2, uid3},
expectedUIDs: []string{rule1UID, rule2UID, rule3UID, rule4UID, rule5UID, rule6UID},
},
{
name: "searching for a non-existing UID returns no rules",
dsUIDs: []string{"non-existing"},
},
{
name: "searching for uid-1 and a non-existing UID returns the rules using uid-1",
dsUIDs: []string{"non-existing", uid1},
expectedUIDs: []string{rule1UID, rule4UID, rule6UID},
},
{
name: "no data source filter should return all rules",
expectedUIDs: []string{rule1UID, rule2UID, rule3UID, rule4UID, rule5UID, rule6UID},
},
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
query := &models.ListAlertRulesQuery{
OrgID: orgID,
DataSourceUIDs: tt.dsUIDs,
}
result, err := store.ListAlertRules(context.Background(), query)
require.NoError(t, err)
got := make([]string, 0, len(result))
for _, r := range result {
got = append(got, r.UID)
}
require.ElementsMatch(t, tt.expectedUIDs, got)
})
}
})
t.Run("filter by SearchTitle", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())