Alerting: Add rule_matcher filter to Prometheus rules API (#115297)

**What is this feature?**

Add `rule_matcher` filter to the Prometheus-compatible list rules API: `/api/prometheus/grafana/api/v1/rules`. It allows to filter rules by static labels (not by alert instance labels).

**Special notes:**
  - Equality (`=`) and inequality (`!=`) matchers are pushed down to the database. Regex matchers (`=~`, `!~`) are applied in-memory at the API layer.
  - SQLite: Uses GLOB pattern matching
  - MySQL / PostgreSQL: Use JSON functions to compare label values


---------

Co-authored-by: Konrad Lalik <konradlalik@gmail.com>
This commit is contained in:
Alexander Akhmetov
2025-12-16 14:13:50 +01:00
committed by GitHub
co-authored by Konrad Lalik
parent c7c1dd4ead
commit c0295d06a3
18 changed files with 1137 additions and 71 deletions
@@ -2886,6 +2886,189 @@ func TestRouteGetRuleStatuses(t *testing.T) {
})
}
})
t.Run("with rule_matcher filter", func(t *testing.T) {
fakeStore, fakeAIM, api := setupAPI(t)
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule1"), gen.WithLabels(map[string]string{"team": "alerting", "severity": "critical"}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule2"), gen.WithLabels(map[string]string{"team": "Alerting", "severity": "warning"}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule3"), gen.WithLabels(map[string]string{"team": "platform", "severity": "critical"}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule4"), gen.WithLabels(map[string]string{"env": "production"}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule_special"), gen.WithLabels(map[string]string{"key": `value"with"quotes`}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule_empty"), gen.WithLabels(map[string]string{"empty": ""}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule_nonempty"), gen.WithLabels(map[string]string{"empty": "nonempty"}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule_multiline"), gen.WithLabels(map[string]string{"description": "line1\nline2\\end\"quote"}), gen.WithNoNotificationSettings())
testCases := []struct {
name string
matchers []string
expectedUIDs []string
}{
{
name: "equality matcher filters by team=alerting",
matchers: []string{`{"name":"team","value":"alerting","isRegex":false,"isEqual":true}`},
expectedUIDs: []string{"rule1"},
},
{
name: "inequality matcher filters severity!=warning",
matchers: []string{`{"name":"severity","value":"warning","isRegex":false,"isEqual":false}`},
expectedUIDs: []string{"rule1", "rule3", "rule4", "rule_special", "rule_empty", "rule_nonempty", "rule_multiline"},
},
{
name: "regex matcher filters team=~plat.*",
matchers: []string{`{"name":"team","value":"plat.*","isRegex":true,"isEqual":true}`},
expectedUIDs: []string{"rule3"},
},
{
name: "not-regex matcher filters severity!~warn.*",
matchers: []string{`{"name":"severity","value":"warn.*","isRegex":true,"isEqual":false}`},
expectedUIDs: []string{"rule1", "rule3", "rule4", "rule_special", "rule_empty", "rule_nonempty", "rule_multiline"},
},
{
name: "multiple matchers are ANDed",
matchers: []string{
`{"name":"team","value":"alerting","isRegex":false,"isEqual":true}`,
`{"name":"severity","value":"critical","isRegex":false,"isEqual":true}`,
},
expectedUIDs: []string{"rule1"},
},
{
name: "matcher with non-existent label returns no rules",
matchers: []string{`{"name":"nonexistent","value":"value","isRegex":false,"isEqual":true}`},
expectedUIDs: []string{},
},
{
name: "equality matcher is case-sensitive",
matchers: []string{`{"name":"team","value":"Alerting","isRegex":false,"isEqual":true}`},
expectedUIDs: []string{"rule2"},
},
{
name: "quotes in label value are handled correctly",
matchers: []string{`{"name":"key","value":"value\"with\"quotes","isRegex":false,"isEqual":true}`},
expectedUIDs: []string{"rule_special"},
},
{
name: "no matchers returns all rules",
matchers: []string{},
expectedUIDs: []string{"rule1", "rule2", "rule3", "rule4", "rule_special", "rule_empty", "rule_nonempty", "rule_multiline"},
},
{
name: "empty string value matches correctly",
matchers: []string{`{"name":"empty","value":"","isRegex":false,"isEqual":true}`},
expectedUIDs: []string{"rule1", "rule2", "rule3", "rule4", "rule_special", "rule_empty", "rule_multiline"},
},
{
name: "special characters in label value are handled correctly",
matchers: []string{`{"name":"description","value":"line1\nline2\\end\"quote","isRegex":false,"isEqual":true}`},
expectedUIDs: []string{"rule_multiline"},
},
{
name: "inequality matcher on non-existent label matches all rules",
matchers: []string{`{"name":"nonexistent","value":"value","isRegex":false,"isEqual":false}`},
expectedUIDs: []string{"rule1", "rule2", "rule3", "rule4", "rule_special", "rule_empty", "rule_nonempty", "rule_multiline"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
reqURL := "/api/v1/rules"
for i, matcher := range tc.matchers {
if i == 0 {
reqURL += "?rule_matcher=" + url.QueryEscape(matcher)
} else {
reqURL += "&rule_matcher=" + url.QueryEscape(matcher)
}
}
req, err := http.NewRequest("GET", reqURL, nil)
require.NoError(t, err)
ctx := &contextmodel.ReqContext{
Context: &web.Context{Req: req},
SignedInUser: &user.SignedInUser{OrgID: orgID, Permissions: queryPermissions},
}
resp := api.RouteGetRuleStatuses(ctx)
require.Equal(t, http.StatusOK, resp.Status())
var res apimodels.RuleResponse
require.NoError(t, json.Unmarshal(resp.Body(), &res))
require.Equal(t, "success", res.Status)
actualUIDs := []string{}
for _, group := range res.Data.RuleGroups {
for _, rule := range group.Rules {
actualUIDs = append(actualUIDs, rule.UID)
}
}
require.ElementsMatch(t, tc.expectedUIDs, actualUIDs)
})
}
})
t.Run("pagination with rule_matcher in-memory filtering", func(t *testing.T) {
fakeStore, fakeAIM, api := setupAPI(t)
// Create 3 groups with 2 rules each:
// Group 1 & 2: team=backend (won't match filter)
// Group 3: team=frontend (will match filter)
// This tests that pagination continues fetching when early pages are filtered out
group1Key := ngmodels.AlertRuleGroupKey{OrgID: orgID, NamespaceUID: "namespace1", RuleGroup: "group1"}
group2Key := ngmodels.AlertRuleGroupKey{OrgID: orgID, NamespaceUID: "namespace2", RuleGroup: "group2"}
group3Key := ngmodels.AlertRuleGroupKey{OrgID: orgID, NamespaceUID: "namespace3", RuleGroup: "group3"}
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule1"), gen.WithLabels(map[string]string{"team": "security"}), gen.WithGroupKey(group1Key), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule2"), gen.WithLabels(map[string]string{"team": "security"}), gen.WithGroupKey(group1Key), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule3"), gen.WithLabels(map[string]string{"team": "security"}), gen.WithGroupKey(group2Key), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule4"), gen.WithLabels(map[string]string{"team": "security"}), gen.WithGroupKey(group2Key), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule5"), gen.WithLabels(map[string]string{"team": "alerting"}), gen.WithGroupKey(group3Key), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule6"), gen.WithLabels(map[string]string{"team": "alerting"}), gen.WithGroupKey(group3Key), gen.WithNoNotificationSettings())
// Request with regex rule_matcher filter for team=~"alerting" and group_limit=1 to force pagination
matcher := `{"name":"team","value":"alerting","isRegex":true,"isEqual":true}`
reqURL := "/api/v1/rules?rule_matcher=" + url.QueryEscape(matcher) + "&group_limit=1"
req, err := http.NewRequest("GET", reqURL, nil)
require.NoError(t, err)
ctx := &contextmodel.ReqContext{
Context: &web.Context{Req: req},
SignedInUser: &user.SignedInUser{OrgID: orgID, Permissions: queryPermissions},
}
resp := api.RouteGetRuleStatuses(ctx)
require.Equal(t, http.StatusOK, resp.Status())
var res apimodels.RuleResponse
require.NoError(t, json.Unmarshal(resp.Body(), &res))
require.Equal(t, "success", res.Status)
actualUIDs := []string{}
for _, group := range res.Data.RuleGroups {
for _, rule := range group.Rules {
actualUIDs = append(actualUIDs, rule.UID)
}
}
// Should return group3 rules (rule5, rule6), pagination should continue past filtered groups
require.ElementsMatch(t, []string{"rule5", "rule6"}, actualUIDs)
})
}
func setupAPI(t *testing.T) (*fakes.RuleStore, *fakeAlertInstanceManager, PrometheusSrv) {
@@ -33,6 +33,12 @@ import (
"go.opentelemetry.io/otel/trace"
)
const (
queryIncludeInternalLabels = "includeInternalLabels"
queryRuleMatcher = "rule_matcher"
queryInstanceMatcher = "matcher"
)
type RuleStoreReader interface {
GetUserVisibleNamespaces(context.Context, int64, identity.Requester) (map[string]*folder.Folder, error)
ListAlertRulesStoreV2
@@ -62,6 +68,20 @@ type PrometheusSrv struct {
// Package-level OpenTelemetry tracer per Grafana instrumentation conventions.
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/ngalert/api/prometheus")
// badRequestError returns a Prometheus-compatible error response for bad request data.
func badRequestError(err error) apimodels.RuleResponse {
return apimodels.RuleResponse{
DiscoveryBase: apimodels.DiscoveryBase{
Status: "error",
Error: err.Error(),
ErrorType: apiv1.ErrBadData,
},
Data: apimodels.RuleDiscovery{
RuleGroups: []apimodels.RuleGroup{},
},
}
}
func NewPrometheusSrv(log log.Logger, manager state.AlertInstanceManager, status StatusReader, store RuleStoreReader, authz RuleGroupAccessControlService, provenanceStore ProvenanceStore) *PrometheusSrv {
return &PrometheusSrv{
log,
@@ -73,8 +93,6 @@ func NewPrometheusSrv(log log.Logger, manager state.AlertInstanceManager, status
}
}
const queryIncludeInternalLabels = "includeInternalLabels"
func getBoolWithDefault(vals url.Values, field string, d bool) bool {
f := vals.Get(field)
if f == "" {
@@ -188,15 +206,15 @@ func getPanelIDFromQuery(v url.Values) (int64, error) {
return 0, nil
}
func getMatchersFromQuery(v url.Values) (labels.Matchers, error) {
func getMatchersFromQuery(v url.Values, paramName string) (labels.Matchers, error) {
var matchers labels.Matchers
for _, s := range v["matcher"] {
for _, s := range v[paramName] {
var m labels.Matcher
if err := json.Unmarshal([]byte(s), &m); err != nil {
return nil, err
}
if len(m.Name) == 0 {
return nil, errors.New("bad matcher: the name cannot be blank")
return nil, fmt.Errorf("bad %s: the name cannot be blank", paramName)
}
matchers = append(matchers, &m)
}
@@ -454,6 +472,7 @@ type paginationContext struct {
stateFilterSet map[eval.State]struct{}
healthFilterSet map[string]struct{}
matchers labels.Matchers
ruleLabelMatchers labels.Matchers
labelOptions []ngmodels.LabelOption
limitAlertsPerRule int64
limitRulesPerGroup int64
@@ -476,6 +495,9 @@ func accumulateTotals(dest, source map[string]int64) {
// fetchAndFilterPage fetches one page from the store and applies filters
func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlertRulesStoreV2, span trace.Span, token string, remainingGroups, remainingRules int64) (pageResult, error) {
// Split matchers: only equality/inequality are supported by the store
storeMatchers := filterOutRegexMatchers(ctx.ruleLabelMatchers)
byGroupQuery := ngmodels.ListAlertRulesExtendedQuery{
ListAlertRulesQuery: ngmodels.ListAlertRulesQuery{
OrgID: ctx.opts.OrgID,
@@ -488,6 +510,7 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert
DataSourceUIDs: ctx.dataSourceUIDs,
SearchTitle: ctx.title,
SearchRuleGroup: ctx.searchRuleGroup,
LabelMatchers: storeMatchers,
},
RuleType: ctx.ruleType,
Limit: remainingGroups,
@@ -534,6 +557,8 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert
filterRulesByHealth(ruleGroup, ctx.healthFilterSet)
}
filterRulesByLabelMatchers(ruleGroup, ctx.ruleLabelMatchers)
if ctx.limitRulesPerGroup > -1 && int64(len(ruleGroup.Rules)) > ctx.limitRulesPerGroup {
ruleGroup.Rules = ruleGroup.Rules[0:ctx.limitRulesPerGroup]
}
@@ -546,6 +571,17 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert
return result, nil
}
func filterOutRegexMatchers(matchers labels.Matchers) labels.Matchers {
var result labels.Matchers
for _, m := range matchers {
if m.Type == labels.MatchEqual || m.Type == labels.MatchNotEqual {
result = append(result, m)
}
}
return result
}
// paginateRuleGroups fetches pages until limits are satisfied applying filters at each step
func paginateRuleGroups(log log.Logger, store ListAlertRulesStoreV2, ctx *paginationContext, span trace.Span, maxGroups, maxRules int64, startToken string) ([]apimodels.RuleGroup, map[string]int64, string, error) {
allGroups := []apimodels.RuleGroup{}
@@ -644,21 +680,30 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
attribute.Int64("limit_rules", limitRulesPerGroup),
attribute.Int64("limit_alerts", limitAlertsPerRule),
)
matchers, err := getMatchersFromQuery(opts.Query)
matchers, err := getMatchersFromQuery(opts.Query, queryInstanceMatcher)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(err)
}
span.SetAttributes(attribute.Int("matcher_count", len(matchers)))
ruleLabelMatchers, err := getMatchersFromQuery(opts.Query, queryRuleMatcher)
if err != nil {
return badRequestError(err)
}
regexCount := 0
for _, m := range ruleLabelMatchers {
if m.Type == labels.MatchRegexp || m.Type == labels.MatchNotRegexp {
regexCount++
}
}
span.SetAttributes(
attribute.Int("rule_matcher_count", len(ruleLabelMatchers)),
attribute.Int("rule_matcher_regex_count", regexCount),
)
stateFilterSet, err := GetStatesFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(err)
}
span.SetAttributes(
attribute.Int("state_filter_count", len(stateFilterSet)),
@@ -667,10 +712,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
healthFilterSet, err := GetHealthFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(err)
}
span.SetAttributes(
attribute.Int("health_filter_count", len(healthFilterSet)),
@@ -808,6 +850,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
stateFilterSet: stateFilterSet,
healthFilterSet: healthFilterSet,
matchers: matchers,
ruleLabelMatchers: ruleLabelMatchers,
labelOptions: labelOptions,
limitAlertsPerRule: limitAlertsPerRule,
limitRulesPerGroup: limitRulesPerGroup,
@@ -833,6 +876,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
return ruleResponse
}
// nolint:gocyclo
func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts RuleGroupStatusesOptions, ruleStatusMutator RuleStatusMutator, alertStateMutator RuleAlertStateMutator, provenanceRecords map[string]ngmodels.Provenance) apimodels.RuleResponse {
ruleResponse := apimodels.RuleResponse{
DiscoveryBase: apimodels.DiscoveryBase{
@@ -846,41 +890,30 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
dashboardUID := opts.Query.Get("dashboard_uid")
panelID, err := getPanelIDFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = fmt.Sprintf("invalid panel_id: %s", err.Error())
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(fmt.Errorf("invalid panel_id: %w", err))
}
if dashboardUID == "" && panelID != 0 {
ruleResponse.Status = "error"
ruleResponse.Error = "panel_id must be set with dashboard_uid"
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(errors.New("panel_id must be set with dashboard_uid"))
}
limitRulesPerGroup := getInt64WithDefault(opts.Query, "limit_rules", -1)
limitAlertsPerRule := getInt64WithDefault(opts.Query, "limit_alerts", -1)
matchers, err := getMatchersFromQuery(opts.Query)
matchers, err := getMatchersFromQuery(opts.Query, queryInstanceMatcher)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(err)
}
ruleLabelMatchers, err := getMatchersFromQuery(opts.Query, queryRuleMatcher)
if err != nil {
return badRequestError(err)
}
stateFilterSet, err := GetStatesFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(err)
}
healthFilterSet, err := GetHealthFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(err)
}
var labelOptions []ngmodels.LabelOption
@@ -913,6 +946,9 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
dataSourceUIDs := opts.Query["datasource_uid"]
searchRuleGroup := opts.Query.Get("search.rule_group")
// Split matchers: only equality/inequality are supported by the store
storeMatchers := filterOutRegexMatchers(ruleLabelMatchers)
alertRuleQuery := ngmodels.ListAlertRulesQuery{
OrgID: opts.OrgID,
NamespaceUIDs: namespaceUIDs,
@@ -924,6 +960,7 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
SearchTitle: title,
SearchRuleGroup: searchRuleGroup,
DataSourceUIDs: dataSourceUIDs,
LabelMatchers: storeMatchers,
}
ruleList, err := store.ListAlertRules(opts.Ctx, &alertRuleQuery)
if err != nil {
@@ -978,6 +1015,10 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
filterRulesByHealth(ruleGroup, healthFilterSet)
}
if len(ruleLabelMatchers) > 0 {
filterRulesByLabelMatchers(ruleGroup, ruleLabelMatchers)
}
if limitRulesPerGroup > -1 && int64(len(ruleGroup.Rules)) > limitRulesPerGroup {
ruleGroup.Rules = ruleGroup.Rules[0:limitRulesPerGroup]
}
@@ -1105,6 +1146,30 @@ func filterRulesByHealth(ruleGroup *apimodels.RuleGroup, withHealthFast map[stri
ruleGroup.Rules = filteredRules
}
func filterRulesByLabelMatchers(ruleGroup *apimodels.RuleGroup, matchers labels.Matchers) {
if len(matchers) == 0 {
return
}
filteredRules := make([]apimodels.AlertingRule, 0, len(ruleGroup.Rules))
for _, rule := range ruleGroup.Rules {
ruleLabels := rule.Labels.Map()
matches := true
for _, m := range matchers {
if !m.Matches(ruleLabels[m.Name]) {
matches = false
break
}
}
if matches {
filteredRules = append(filteredRules, rule)
}
}
ruleGroup.Rules = filteredRules
}
// This is the same as matchers.Matches but avoids the need to create a LabelSet
func matchersMatch(matchers []*labels.Matcher, labels map[string]string) bool {
for _, m := range matchers {
@@ -462,4 +462,10 @@ type GetGrafanaRuleStatusesParams struct {
// in: query
// required: false
Matchers []string `json:"matcher"`
// Filter rules by their static labels (not alert instance labels). Each value is a JSON-encoded Prometheus-like matcher (for example, {"type":0,"name":"severity","value":"critical"}).
// For equality matchers with empty string values (e.g., name=""), rules that have the label with an empty value OR rules without the label will match (standard Prometheus behavior).
// in: query
// required: false
RuleLabelMatchers []string `json:"rule_matcher"`
}
@@ -7561,6 +7561,15 @@
},
"name": "matcher",
"type": "array"
},
{
"description": "Filter rules by their static labels (not alert instance labels). Each value is a JSON-encoded Prometheus-like matcher (for example, {\"type\":0,\"name\":\"severity\",\"value\":\"critical\"}).\nFor equality matchers with empty string values (e.g., name=\"\"), rules that have the label with an empty value OR rules without the label will match (standard Prometheus behavior).",
"in": "query",
"items": {
"type": "string"
},
"name": "rule_matcher",
"type": "array"
}
],
"responses": {
@@ -1947,6 +1947,15 @@
"description": "Filter by label matchers encoded as JSON representations of Prometheus matchers (for example, {\"type\":0,\"name\":\"severity\",\"value\":\"critical\"}). Provide one matcher per query string value.",
"name": "matcher",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"description": "Filter rules by their static labels (not alert instance labels). Each value is a JSON-encoded Prometheus-like matcher (for example, {\"type\":0,\"name\":\"severity\",\"value\":\"critical\"}).\nFor equality matchers with empty string values (e.g., name=\"\"), rules that have the label with an empty value OR rules without the label will match (standard Prometheus behavior).",
"name": "rule_matcher",
"in": "query"
}
],
"responses": {