Alerting: Filter alerts by data source on the back end (search) (#114717)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user