diff --git a/docs/querying/api.md b/docs/querying/api.md index 28ee1b2b4b..f71bfc1586 100644 --- a/docs/querying/api.md +++ b/docs/querying/api.md @@ -693,7 +693,8 @@ URL query parameters: - `rule_name[]=`: only return rules with the given rule name. If the parameter is repeated, rules with any of the provided names are returned. If we've filtered out all the rules of a group, the group is not returned. When the parameter is absent or empty, no filtering is done. - `rule_group[]=`: only return rules with the given rule group name. If the parameter is repeated, rules with any of the provided rule group names are returned. When the parameter is absent or empty, no filtering is done. - `file[]=`: only return rules with the given filepath. If the parameter is repeated, rules with any of the provided filepaths are returned. When the parameter is absent or empty, no filtering is done. -- `exclude_alerts=`: only return rules, do not return active alerts. +- `exclude_alerts=`: only return rules, do not return active alerts. +- `match[]=`: only return rules that have configured labels that satisfy the label selectors. If the parameter is repeated, rules that match any of the sets of label selectors are returned. Note that matching is on the labels in the definition of each rule, not on the values after template expansion (for alerting rules). Optional. ```json $ curl http://localhost:9090/api/v1/rules diff --git a/rules/group.go b/rules/group.go index c0ad18c187..0bc219a11b 100644 --- a/rules/group.go +++ b/rules/group.go @@ -151,7 +151,42 @@ func (g *Group) Name() string { return g.name } func (g *Group) File() string { return g.file } // Rules returns the group's rules. -func (g *Group) Rules() []Rule { return g.rules } +func (g *Group) Rules(matcherSets ...[]*labels.Matcher) []Rule { + if len(matcherSets) == 0 { + return g.rules + } + var rules []Rule + for _, rule := range g.rules { + if matchesMatcherSets(matcherSets, rule.Labels()) { + rules = append(rules, rule) + } + } + return rules +} + +func matches(lbls labels.Labels, matchers ...*labels.Matcher) bool { + for _, m := range matchers { + if v := lbls.Get(m.Name); !m.Matches(v) { + return false + } + } + return true +} + +// matchesMatcherSets ensures all matches in each matcher set are ANDed and the set of those is ORed. +func matchesMatcherSets(matcherSets [][]*labels.Matcher, lbls labels.Labels) bool { + if len(matcherSets) == 0 { + return true + } + + var ok bool + for _, matchers := range matcherSets { + if matches(lbls, matchers...) { + ok = true + } + } + return ok +} // Queryable returns the group's querable. func (g *Group) Queryable() storage.Queryable { return g.opts.Queryable } diff --git a/rules/manager.go b/rules/manager.go index acc637e718..ab33c3c7d8 100644 --- a/rules/manager.go +++ b/rules/manager.go @@ -380,13 +380,13 @@ func (m *Manager) RuleGroups() []*Group { } // Rules returns the list of the manager's rules. -func (m *Manager) Rules() []Rule { +func (m *Manager) Rules(matcherSets ...[]*labels.Matcher) []Rule { m.mtx.RLock() defer m.mtx.RUnlock() var rules []Rule for _, g := range m.groups { - rules = append(rules, g.rules...) + rules = append(rules, g.Rules(matcherSets...)...) } return rules diff --git a/web/api/v1/api.go b/web/api/v1/api.go index 7e98dac454..dc43350aa2 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -1397,6 +1397,11 @@ func (api *API) rules(r *http.Request) apiFuncResult { rgSet := queryFormToSet(r.Form["rule_group[]"]) fSet := queryFormToSet(r.Form["file[]"]) + matcherSets, err := parseMatchersParam(r.Form["match[]"]) + if err != nil { + return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil} + } + ruleGroups := api.rulesRetriever(r.Context()).RuleGroups() res := &RuleDiscovery{RuleGroups: make([]*RuleGroup, 0, len(ruleGroups))} typ := strings.ToLower(r.URL.Query().Get("type")) @@ -1436,7 +1441,8 @@ func (api *API) rules(r *http.Request) apiFuncResult { EvaluationTime: grp.GetEvaluationTime().Seconds(), LastEvaluation: grp.GetLastEvaluation(), } - for _, rr := range grp.Rules() { + + for _, rr := range grp.Rules(matcherSets...) { var enrichedRule Rule if len(rnSet) > 0 { diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index 9eb7d08c35..d76446f089 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -261,11 +261,36 @@ func (m *rulesRetrieverMock) CreateAlertingRules() { false, log.NewNopLogger(), ) - + rule4 := rules.NewAlertingRule( + "test_metric6", + expr2, + time.Second, + 0, + labels.FromStrings("testlabel", "rule"), + labels.Labels{}, + labels.Labels{}, + "", + true, + log.NewNopLogger(), + ) + rule5 := rules.NewAlertingRule( + "test_metric7", + expr2, + time.Second, + 0, + labels.FromStrings("templatedlabel", "{{ $externalURL }}"), + labels.Labels{}, + labels.Labels{}, + "", + true, + log.NewNopLogger(), + ) var r []*rules.AlertingRule r = append(r, rule1) r = append(r, rule2) r = append(r, rule3) + r = append(r, rule4) + r = append(r, rule5) m.alertingRules = r } @@ -300,7 +325,9 @@ func (m *rulesRetrieverMock) CreateRuleGroups() { recordingExpr, err := parser.ParseExpr(`vector(1)`) require.NoError(m.testing, err, "unable to parse alert expression") recordingRule := rules.NewRecordingRule("recording-rule-1", recordingExpr, labels.Labels{}) + recordingRule2 := rules.NewRecordingRule("recording-rule-2", recordingExpr, labels.FromStrings("testlabel", "rule")) r = append(r, recordingRule) + r = append(r, recordingRule2) group := rules.NewGroup(rules.GroupOptions{ Name: "grp", @@ -2151,6 +2178,28 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E Health: "ok", Type: "alerting", }, + AlertingRule{ + State: "inactive", + Name: "test_metric6", + Query: "up == 1", + Duration: 1, + Labels: labels.FromStrings("testlabel", "rule"), + Annotations: labels.Labels{}, + Alerts: []*Alert{}, + Health: "ok", + Type: "alerting", + }, + AlertingRule{ + State: "inactive", + Name: "test_metric7", + Query: "up == 1", + Duration: 1, + Labels: labels.FromStrings("templatedlabel", "{{ $externalURL }}"), + Annotations: labels.Labels{}, + Alerts: []*Alert{}, + Health: "ok", + Type: "alerting", + }, RecordingRule{ Name: "recording-rule-1", Query: "vector(1)", @@ -2158,6 +2207,13 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E Health: "ok", Type: "recording", }, + RecordingRule{ + Name: "recording-rule-2", + Query: "vector(1)", + Labels: labels.FromStrings("testlabel", "rule"), + Health: "ok", + Type: "recording", + }, }, }, }, @@ -2210,6 +2266,28 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E Health: "ok", Type: "alerting", }, + AlertingRule{ + State: "inactive", + Name: "test_metric6", + Query: "up == 1", + Duration: 1, + Labels: labels.FromStrings("testlabel", "rule"), + Annotations: labels.Labels{}, + Alerts: nil, + Health: "ok", + Type: "alerting", + }, + AlertingRule{ + State: "inactive", + Name: "test_metric7", + Query: "up == 1", + Duration: 1, + Labels: labels.FromStrings("templatedlabel", "{{ $externalURL }}"), + Annotations: labels.Labels{}, + Alerts: nil, + Health: "ok", + Type: "alerting", + }, RecordingRule{ Name: "recording-rule-1", Query: "vector(1)", @@ -2217,6 +2295,13 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E Health: "ok", Type: "recording", }, + RecordingRule{ + Name: "recording-rule-2", + Query: "vector(1)", + Labels: labels.FromStrings("testlabel", "rule"), + Health: "ok", + Type: "recording", + }, }, }, }, @@ -2276,6 +2361,28 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E Health: "ok", Type: "alerting", }, + AlertingRule{ + State: "inactive", + Name: "test_metric6", + Query: "up == 1", + Duration: 1, + Labels: labels.FromStrings("testlabel", "rule"), + Annotations: labels.Labels{}, + Alerts: []*Alert{}, + Health: "ok", + Type: "alerting", + }, + AlertingRule{ + State: "inactive", + Name: "test_metric7", + Query: "up == 1", + Duration: 1, + Labels: labels.FromStrings("templatedlabel", "{{ $externalURL }}"), + Annotations: labels.Labels{}, + Alerts: []*Alert{}, + Health: "ok", + Type: "alerting", + }, }, }, }, @@ -2302,6 +2409,13 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E Health: "ok", Type: "recording", }, + RecordingRule{ + Name: "recording-rule-2", + Query: "vector(1)", + Labels: labels.FromStrings("testlabel", "rule"), + Health: "ok", + Type: "recording", + }, }, }, }, @@ -2369,6 +2483,179 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E }, zeroFunc: rulesZeroFunc, }, + { + endpoint: api.rules, + query: url.Values{ + "match[]": []string{`{testlabel="rule"}`}, + }, + response: &RuleDiscovery{ + RuleGroups: []*RuleGroup{ + { + Name: "grp", + File: "/path/to/file", + Interval: 1, + Limit: 0, + Rules: []Rule{ + AlertingRule{ + State: "inactive", + Name: "test_metric6", + Query: "up == 1", + Duration: 1, + Labels: labels.FromStrings("testlabel", "rule"), + Annotations: labels.Labels{}, + Alerts: []*Alert{}, + Health: "ok", + Type: "alerting", + }, + RecordingRule{ + Name: "recording-rule-2", + Query: "vector(1)", + Labels: labels.FromStrings("testlabel", "rule"), + Health: "ok", + Type: "recording", + }, + }, + }, + }, + }, + zeroFunc: rulesZeroFunc, + }, + { + endpoint: api.rules, + query: url.Values{ + "type": []string{"alert"}, + "match[]": []string{`{templatedlabel="{{ $externalURL }}"}`}, + }, + response: &RuleDiscovery{ + RuleGroups: []*RuleGroup{ + { + Name: "grp", + File: "/path/to/file", + Interval: 1, + Limit: 0, + Rules: []Rule{ + AlertingRule{ + State: "inactive", + Name: "test_metric7", + Query: "up == 1", + Duration: 1, + Labels: labels.FromStrings("templatedlabel", "{{ $externalURL }}"), + Annotations: labels.Labels{}, + Alerts: []*Alert{}, + Health: "ok", + Type: "alerting", + }, + }, + }, + }, + }, + zeroFunc: rulesZeroFunc, + }, + { + endpoint: api.rules, + query: url.Values{ + "match[]": []string{`{testlabel="abc"}`}, + }, + response: &RuleDiscovery{ + RuleGroups: []*RuleGroup{}, + }, + }, + // This is testing OR condition, the api response should return rule if it matches one of the label selector + { + endpoint: api.rules, + query: url.Values{ + "match[]": []string{`{testlabel="abc"}`, `{testlabel="rule"}`}, + }, + response: &RuleDiscovery{ + RuleGroups: []*RuleGroup{ + { + Name: "grp", + File: "/path/to/file", + Interval: 1, + Limit: 0, + Rules: []Rule{ + AlertingRule{ + State: "inactive", + Name: "test_metric6", + Query: "up == 1", + Duration: 1, + Labels: labels.FromStrings("testlabel", "rule"), + Annotations: labels.Labels{}, + Alerts: []*Alert{}, + Health: "ok", + Type: "alerting", + }, + RecordingRule{ + Name: "recording-rule-2", + Query: "vector(1)", + Labels: labels.FromStrings("testlabel", "rule"), + Health: "ok", + Type: "recording", + }, + }, + }, + }, + }, + zeroFunc: rulesZeroFunc, + }, + { + endpoint: api.rules, + query: url.Values{ + "type": []string{"record"}, + "match[]": []string{`{testlabel="rule"}`}, + }, + response: &RuleDiscovery{ + RuleGroups: []*RuleGroup{ + { + Name: "grp", + File: "/path/to/file", + Interval: 1, + Limit: 0, + Rules: []Rule{ + RecordingRule{ + Name: "recording-rule-2", + Query: "vector(1)", + Labels: labels.FromStrings("testlabel", "rule"), + Health: "ok", + Type: "recording", + }, + }, + }, + }, + }, + zeroFunc: rulesZeroFunc, + }, + { + endpoint: api.rules, + query: url.Values{ + "type": []string{"alert"}, + "match[]": []string{`{testlabel="rule"}`}, + }, + response: &RuleDiscovery{ + RuleGroups: []*RuleGroup{ + { + Name: "grp", + File: "/path/to/file", + Interval: 1, + Limit: 0, + Rules: []Rule{ + AlertingRule{ + State: "inactive", + Name: "test_metric6", + Query: "up == 1", + Duration: 1, + Labels: labels.FromStrings("testlabel", "rule"), + Annotations: labels.Labels{}, + Alerts: []*Alert{}, + Health: "ok", + Type: "alerting", + }, + }, + }, + }, + }, + zeroFunc: rulesZeroFunc, + }, { endpoint: api.queryExemplars, query: url.Values{