From f3394bf7a1f362b88d89cb0784be47b75d8fa152 Mon Sep 17 00:00:00 2001 From: gotjosh Date: Tue, 18 Apr 2023 10:07:32 +0100 Subject: [PATCH] Rules API: Allow filtering by rule name Introduces support for a new query parameter in the `/rules` API endpoint that allows filtering by rule names. If all the rules of a group are filtered, we skip the group entirely. Signed-off-by: gotjosh --- docs/querying/api.md | 2 ++ web/api/v1/api.go | 33 +++++++++++++++++++++++++++------ web/api/v1/api_test.go | 27 +++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/docs/querying/api.md b/docs/querying/api.md index f2182a205..0cc549b65 100644 --- a/docs/querying/api.md +++ b/docs/querying/api.md @@ -673,7 +673,9 @@ GET /api/v1/rules ``` URL query parameters: + - `type=alert|record`: return only the alerting rules (e.g. `type=alert`) or the recording rules (e.g. `type=record`). When the parameter is absent or empty, no filtering is done. +- `rules=alertName,RuleName`: return only the alerting and recording rules with the specified names. 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. ```json $ curl http://localhost:9090/api/v1/rules diff --git a/web/api/v1/api.go b/web/api/v1/api.go index aeea87ca7..d95595804 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -1296,6 +1296,16 @@ func (api *API) rules(r *http.Request) apiFuncResult { res := &RuleDiscovery{RuleGroups: make([]*RuleGroup, len(ruleGroups))} typ := strings.ToLower(r.URL.Query().Get("type")) + // Parse the rule names into a comma separated list of rule names, then create a set. + rulesQuery := r.URL.Query().Get("rules") + ruleNamesSet := map[string]struct{}{} + if rulesQuery != "" { + names := strings.Split(rulesQuery, ",") + for _, rn := range names { + ruleNamesSet[strings.TrimSpace(rn)] = struct{}{} + } + } + if typ != "" && typ != "alert" && typ != "record" { return invalidParamError(errors.Errorf("not supported value %q", typ), "type") } @@ -1313,14 +1323,20 @@ func (api *API) rules(r *http.Request) apiFuncResult { EvaluationTime: grp.GetEvaluationTime().Seconds(), LastEvaluation: grp.GetLastEvaluation(), } - for _, r := range grp.Rules() { + for _, rr := range grp.Rules() { var enrichedRule Rule - lastError := "" - if r.LastError() != nil { - lastError = r.LastError().Error() + if len(ruleNamesSet) > 0 { + if _, ok := ruleNamesSet[rr.Name()]; !ok { + continue + } } - switch rule := r.(type) { + + lastError := "" + if rr.LastError() != nil { + lastError = rr.LastError().Error() + } + switch rule := rr.(type) { case *rules.AlertingRule: if !returnAlerts { break @@ -1358,11 +1374,16 @@ func (api *API) rules(r *http.Request) apiFuncResult { err := errors.Errorf("failed to assert type of rule '%v'", rule.Name()) return apiFuncResult{nil, &apiError{errorInternal, err}, nil, nil} } + if enrichedRule != nil { apiRuleGroup.Rules = append(apiRuleGroup.Rules, enrichedRule) } } - res.RuleGroups[i] = apiRuleGroup + + // If the rule group response has no rules, skip it - this means we filtered all the rules of this group. + if len(apiRuleGroup.Rules) > 0 { + res.RuleGroups[i] = apiRuleGroup + } } return apiFuncResult{res, nil, nil, nil} } diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index efce04221..e354bf298 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -1973,6 +1973,33 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E }, }, }, + { + endpoint: api.rules, + query: url.Values{"rules": []string{"test_metric4"}}, + response: &RuleDiscovery{ + RuleGroups: []*RuleGroup{ + { + Name: "grp", + File: "/path/to/file", + Interval: 1, + Limit: 0, + Rules: []Rule{ + AlertingRule{ + State: "inactive", + Name: "test_metric4", + Query: "up == 1", + Duration: 1, + Labels: labels.Labels{}, + Annotations: labels.Labels{}, + Alerts: []*Alert{}, + Health: "unknown", + Type: "alerting", + }, + }, + }, + }, + }, + }, { endpoint: api.queryExemplars, query: url.Values{