Add label-matcher support to Rules API (#10194)

* Add label-matcher support to Rules API

Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>
Signed-off-by: Yijie Qin <qinyijie@amazon.com>

* Implement suggestions

Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>
Signed-off-by: Yijie Qin <qinyijie@amazon.com>

* Match any matcherSet instead of all

Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>
Signed-off-by: Yijie Qin <qinyijie@amazon.com>

* Don't treat labels.Labels as slice

Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>
Signed-off-by: Yijie Qin <qinyijie@amazon.com>

* Remove non-templated check and fix tests

Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>
Signed-off-by: Yijie Qin <qinyijie@amazon.com>

* Update docs

Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>
Signed-off-by: Yijie Qin <qinyijie@amazon.com>

* fix comments

Signed-off-by: Yijie Qin <qinyijie@amazon.com>

* fix comment

Signed-off-by: Yijie Qin <qinyijie@amazon.com>

* Add comment for matching logic, fix tests after rebase

Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>

---------

Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>
Signed-off-by: Yijie Qin <qinyijie@amazon.com>
Co-authored-by: Yijie Qin <qinyijie@amazon.com>
This commit is contained in:
Saswata Mukherjee 2024-07-10 13:18:29 +01:00 committed by GitHub
parent b1c106a9ce
commit 398f42de5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 335 additions and 6 deletions

View file

@ -694,6 +694,7 @@ URL query parameters:
- `rule_group[]=<string>`: 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. - `rule_group[]=<string>`: 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[]=<string>`: 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. - `file[]=<string>`: 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=<bool>`: only return rules, do not return active alerts. - `exclude_alerts=<bool>`: only return rules, do not return active alerts.
- `match[]=<label_selector>`: 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 ```json
$ curl http://localhost:9090/api/v1/rules $ curl http://localhost:9090/api/v1/rules

View file

@ -151,7 +151,42 @@ func (g *Group) Name() string { return g.name }
func (g *Group) File() string { return g.file } func (g *Group) File() string { return g.file }
// Rules returns the group's rules. // 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. // Queryable returns the group's querable.
func (g *Group) Queryable() storage.Queryable { return g.opts.Queryable } func (g *Group) Queryable() storage.Queryable { return g.opts.Queryable }

View file

@ -380,13 +380,13 @@ func (m *Manager) RuleGroups() []*Group {
} }
// Rules returns the list of the manager's rules. // 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() m.mtx.RLock()
defer m.mtx.RUnlock() defer m.mtx.RUnlock()
var rules []Rule var rules []Rule
for _, g := range m.groups { for _, g := range m.groups {
rules = append(rules, g.rules...) rules = append(rules, g.Rules(matcherSets...)...)
} }
return rules return rules

View file

@ -1397,6 +1397,11 @@ func (api *API) rules(r *http.Request) apiFuncResult {
rgSet := queryFormToSet(r.Form["rule_group[]"]) rgSet := queryFormToSet(r.Form["rule_group[]"])
fSet := queryFormToSet(r.Form["file[]"]) 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() ruleGroups := api.rulesRetriever(r.Context()).RuleGroups()
res := &RuleDiscovery{RuleGroups: make([]*RuleGroup, 0, len(ruleGroups))} res := &RuleDiscovery{RuleGroups: make([]*RuleGroup, 0, len(ruleGroups))}
typ := strings.ToLower(r.URL.Query().Get("type")) typ := strings.ToLower(r.URL.Query().Get("type"))
@ -1436,7 +1441,8 @@ func (api *API) rules(r *http.Request) apiFuncResult {
EvaluationTime: grp.GetEvaluationTime().Seconds(), EvaluationTime: grp.GetEvaluationTime().Seconds(),
LastEvaluation: grp.GetLastEvaluation(), LastEvaluation: grp.GetLastEvaluation(),
} }
for _, rr := range grp.Rules() {
for _, rr := range grp.Rules(matcherSets...) {
var enrichedRule Rule var enrichedRule Rule
if len(rnSet) > 0 { if len(rnSet) > 0 {

View file

@ -261,11 +261,36 @@ func (m *rulesRetrieverMock) CreateAlertingRules() {
false, false,
log.NewNopLogger(), 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 var r []*rules.AlertingRule
r = append(r, rule1) r = append(r, rule1)
r = append(r, rule2) r = append(r, rule2)
r = append(r, rule3) r = append(r, rule3)
r = append(r, rule4)
r = append(r, rule5)
m.alertingRules = r m.alertingRules = r
} }
@ -300,7 +325,9 @@ func (m *rulesRetrieverMock) CreateRuleGroups() {
recordingExpr, err := parser.ParseExpr(`vector(1)`) recordingExpr, err := parser.ParseExpr(`vector(1)`)
require.NoError(m.testing, err, "unable to parse alert expression") require.NoError(m.testing, err, "unable to parse alert expression")
recordingRule := rules.NewRecordingRule("recording-rule-1", recordingExpr, labels.Labels{}) 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, recordingRule)
r = append(r, recordingRule2)
group := rules.NewGroup(rules.GroupOptions{ group := rules.NewGroup(rules.GroupOptions{
Name: "grp", Name: "grp",
@ -2151,6 +2178,28 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
Health: "ok", Health: "ok",
Type: "alerting", 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{ RecordingRule{
Name: "recording-rule-1", Name: "recording-rule-1",
Query: "vector(1)", Query: "vector(1)",
@ -2158,6 +2207,13 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
Health: "ok", Health: "ok",
Type: "recording", 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", Health: "ok",
Type: "alerting", 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{ RecordingRule{
Name: "recording-rule-1", Name: "recording-rule-1",
Query: "vector(1)", Query: "vector(1)",
@ -2217,6 +2295,13 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
Health: "ok", Health: "ok",
Type: "recording", 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", Health: "ok",
Type: "alerting", 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", Health: "ok",
Type: "recording", 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, 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, endpoint: api.queryExemplars,
query: url.Values{ query: url.Values{