From 398f42de5fd9f32a8a8977cd88b88b1890763e9a Mon Sep 17 00:00:00 2001 From: Saswata Mukherjee Date: Wed, 10 Jul 2024 13:18:29 +0100 Subject: [PATCH] Add label-matcher support to Rules API (#10194) * Add label-matcher support to Rules API Signed-off-by: Saswata Mukherjee Signed-off-by: Yijie Qin * Implement suggestions Signed-off-by: Saswata Mukherjee Signed-off-by: Yijie Qin * Match any matcherSet instead of all Signed-off-by: Saswata Mukherjee Signed-off-by: Yijie Qin * Don't treat labels.Labels as slice Signed-off-by: Saswata Mukherjee Signed-off-by: Yijie Qin * Remove non-templated check and fix tests Signed-off-by: Saswata Mukherjee Signed-off-by: Yijie Qin * Update docs Signed-off-by: Saswata Mukherjee Signed-off-by: Yijie Qin * fix comments Signed-off-by: Yijie Qin * fix comment Signed-off-by: Yijie Qin * Add comment for matching logic, fix tests after rebase Signed-off-by: Saswata Mukherjee --------- Signed-off-by: Saswata Mukherjee Signed-off-by: Yijie Qin Co-authored-by: Yijie Qin --- docs/querying/api.md | 3 +- rules/group.go | 37 +++++- rules/manager.go | 4 +- web/api/v1/api.go | 8 +- web/api/v1/api_test.go | 289 ++++++++++++++++++++++++++++++++++++++++- 5 files changed, 335 insertions(+), 6 deletions(-) diff --git a/docs/querying/api.md b/docs/querying/api.md index 28ee1b2b4..f71bfc158 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 c0ad18c18..0bc219a11 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 acc637e71..ab33c3c7d 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 7e98dac45..dc43350aa 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 9eb7d08c3..d76446f08 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{