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
+65 -4
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"sort"
"testing"
"time"
@@ -364,8 +365,6 @@ func TestIntegrationPrometheusRules(t *testing.T) {
func TestIntegrationPrometheusRulesPagination(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
@@ -388,23 +387,30 @@ func TestIntegrationPrometheusRulesPagination(t *testing.T) {
require.NoError(t, err)
// Create 3 rule groups with different numbers of rules
// Group 1: 5 rules, Group 2: 3 rules, Group 3: 2 rules (total: 10 rules)
// Group 1: 5 rules with team=backend
// Group 2: 3 rules with team=frontend
// Group 3: 2 rules with team=platform
for groupIdx := 1; groupIdx <= 3; groupIdx++ {
var rulesCount int
var team string
switch groupIdx {
case 1:
rulesCount = 5
team = "backend"
case 2:
rulesCount = 3
team = "frontend"
case 3:
rulesCount = 2
team = "platform"
}
rules := make([]apimodels.PostableExtendedRuleNode, rulesCount)
for i := 0; i < rulesCount; i++ {
rules[i] = apimodels.PostableExtendedRuleNode{
ApiRuleNode: &apimodels.ApiRuleNode{
For: &interval,
For: &interval,
Labels: map[string]string{"team": team},
},
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: fmt.Sprintf("rule-%d-%d", groupIdx, i+1),
@@ -514,6 +520,61 @@ func TestIntegrationPrometheusRulesPagination(t *testing.T) {
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Len(t, result.Data.RuleGroups, 0, "should return no groups")
})
t.Run("with rule_matcher filter returns only matching rules", func(t *testing.T) {
matcher := url.QueryEscape(`{"name":"team","value":"frontend","isRegex":false,"isEqual":true}`)
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?rule_matcher=%s", grafanaListedAddr, matcher)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
var result apimodels.RuleResponse
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Should only return group-2 (team=frontend, 3 rules)
foundGroups := []string{}
total := 0
for _, group := range result.Data.RuleGroups {
foundGroups = append(foundGroups, group.Name)
total += len(group.Rules)
}
require.Equal(t, []string{"group-2"}, foundGroups)
require.Equal(t, 3, total)
})
t.Run("with rule_matcher regex filter", func(t *testing.T) {
// Filter with regex team=~plat.* (should match group-3 with team=platform)
matcher := url.QueryEscape(`{"name":"team","value":"plat.*","isRegex":true,"isEqual":true}`)
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?rule_matcher=%s", grafanaListedAddr, matcher)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
var result apimodels.RuleResponse
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Should only return group-3 (team=platform matches plat.*)
foundGroups := []string{}
total := 0
for _, group := range result.Data.RuleGroups {
foundGroups = append(foundGroups, group.Name)
total += len(group.Rules)
}
require.Equal(t, []string{"group-3"}, foundGroups)
require.Equal(t, 2, total)
})
}
func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) {