Alerting: Introduce alert rule models in storage (#93187)

* introduce storage model for alert rule tables
* remove AlertRuleVersion from models because it's not used anywhere other than in storage
* update historian xorm store to use alerting store to fetch rules

* fix folder tests

---------

Co-authored-by: Matthew Jacobson <matthew.jacobson@grafana.com>
This commit is contained in:
Yuri Tseretyan
2024-09-12 13:20:33 -04:00
committed by GitHub
co-authored by Matthew Jacobson
parent 0a976f831c
commit f8fa5286a1
13 changed files with 572 additions and 273 deletions
@@ -39,10 +39,12 @@ const (
var (
ErrLokiStoreInternal = errutil.Internal("annotations.loki.internal")
ErrLokiStoreNotFound = errutil.NotFound("annotations.loki.notFound")
errMissingRule = errors.New("rule not found")
)
type RuleStore interface {
GetRuleByID(ctx context.Context, query ngmodels.GetAlertRuleByIDQuery) (result *ngmodels.AlertRule, err error)
}
type lokiQueryClient interface {
RangeQuery(ctx context.Context, query string, start, end, limit int64) (historian.QueryRes, error)
MaxQuerySize() int
@@ -50,12 +52,13 @@ type lokiQueryClient interface {
// LokiHistorianStore is a read store that queries Loki for alert state history.
type LokiHistorianStore struct {
client lokiQueryClient
db db.DB
log log.Logger
client lokiQueryClient
db db.DB
log log.Logger
ruleStore RuleStore
}
func NewLokiHistorianStore(cfg setting.UnifiedAlertingStateHistorySettings, ft featuremgmt.FeatureToggles, db db.DB, log log.Logger, tracer tracing.Tracer) *LokiHistorianStore {
func NewLokiHistorianStore(cfg setting.UnifiedAlertingStateHistorySettings, ft featuremgmt.FeatureToggles, db db.DB, ruleStore RuleStore, log log.Logger, tracer tracing.Tracer) *LokiHistorianStore {
if !useStore(cfg, ft) {
return nil
}
@@ -66,9 +69,10 @@ func NewLokiHistorianStore(cfg setting.UnifiedAlertingStateHistorySettings, ft f
}
return &LokiHistorianStore{
client: historian.NewLokiClient(lokiCfg, historian.NewRequester(), ngmetrics.NewHistorianMetrics(prometheus.DefaultRegisterer, subsystem), log, tracer),
db: db,
log: log,
client: historian.NewLokiClient(lokiCfg, historian.NewRequester(), ngmetrics.NewHistorianMetrics(prometheus.DefaultRegisterer, subsystem), log, tracer),
db: db,
log: log,
ruleStore: ruleStore,
}
}
@@ -90,9 +94,9 @@ func (r *LokiHistorianStore) Get(ctx context.Context, query *annotations.ItemQue
rule := &ngmodels.AlertRule{}
if query.AlertID != 0 {
var err error
rule, err = getRule(ctx, r.db, query.OrgID, query.AlertID)
rule, err = r.ruleStore.GetRuleByID(ctx, ngmodels.GetAlertRuleByIDQuery{OrgID: query.OrgID, ID: query.AlertID})
if err != nil {
if errors.Is(err, errMissingRule) {
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) {
return make([]*annotations.ItemDTO, 0), ErrLokiStoreNotFound.Errorf("rule with ID %d does not exist", query.AlertID)
}
return make([]*annotations.ItemDTO, 0), ErrLokiStoreInternal.Errorf("failed to query rule: %w", err)
@@ -194,22 +198,6 @@ func (r *LokiHistorianStore) GetTags(ctx context.Context, query *annotations.Tag
// util
func getRule(ctx context.Context, sql db.DB, orgID int64, ruleID int64) (*ngmodels.AlertRule, error) {
rule := &ngmodels.AlertRule{OrgID: orgID, ID: ruleID}
err := sql.WithDbSession(ctx, func(sess *db.Session) error {
exists, err := sess.Get(rule)
if err != nil {
return err
}
if !exists {
return errMissingRule
}
return nil
})
return rule, err
}
func hasAccess(entry historian.LokiEntry, resources accesscontrol.AccessResources) bool {
orgFilter := resources.CanAccessOrgAnnotations && entry.DashboardUID == ""
dashFilter := func() bool {
@@ -3,15 +3,16 @@ package loki
import (
"context"
"encoding/json"
"errors"
"math/rand"
"net/url"
"slices"
"strconv"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
@@ -28,6 +29,8 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/state"
"github.com/grafana/grafana/pkg/services/ngalert/state/historian"
historymodel "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tests/testsuite"
)
@@ -105,6 +108,35 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) {
require.Len(t, res, numTransitions)
})
t.Run("should return ErrLokiStoreNotFound if rule is not found", func(t *testing.T) {
var rules = slices.Concat(maps.Values(dashboardRules)...)
id := rand.Int63n(1000) // in Postgres ID is integer, so limit range
// make sure id is not known
for slices.IndexFunc(rules, func(rule *ngmodels.AlertRule) bool {
return rule.ID == id
}) >= 0 {
id = rand.Int63n(1000)
}
query := annotations.ItemQuery{
OrgID: 1,
AlertID: id,
From: start.UnixMilli(),
To: start.Add(time.Second * time.Duration(numTransitions+1)).UnixMilli(),
}
_, err := store.Get(
context.Background(),
&query,
&annotation_ac.AccessResources{
Dashboards: map[string]int64{
dashboard1.UID: dashboard1.ID,
},
CanAccessDashAnnotations: true,
},
)
require.ErrorIs(t, err, ErrLokiStoreNotFound)
})
t.Run("can query history by dashboard id", func(t *testing.T) {
fakeLokiClient.rangeQueryRes = []historian.Stream{
historian.StatesToStream(ruleMetaFromRule(t, dashboardRules[dashboard1.UID][0]), transitions, map[string]string{}, log.NewNopLogger()),
@@ -572,19 +604,21 @@ func TestBuildTransition(t *testing.T) {
})
}
func createTestLokiStore(t *testing.T, sql db.DB, client lokiQueryClient) *LokiHistorianStore {
func createTestLokiStore(t *testing.T, sql *sqlstore.ReplStore, client lokiQueryClient) *LokiHistorianStore {
t.Helper()
ruleStore := store.SetupStoreForTesting(t, sql)
return &LokiHistorianStore{
client: client,
db: sql,
log: log.NewNopLogger(),
client: client,
db: sql,
log: log.NewNopLogger(),
ruleStore: ruleStore,
}
}
// createAlertRule creates an alert rule in the database and returns it.
// If a generator is not specified, uniqueness of primary key is not guaranteed.
func createAlertRule(t *testing.T, sql db.DB, title string, generator *ngmodels.AlertRuleGenerator) *ngmodels.AlertRule {
func createAlertRule(t *testing.T, sql *sqlstore.ReplStore, title string, generator *ngmodels.AlertRuleGenerator) *ngmodels.AlertRule {
t.Helper()
if generator == nil {
@@ -592,7 +626,7 @@ func createAlertRule(t *testing.T, sql db.DB, title string, generator *ngmodels.
generator = g.With(g.WithTitle(title), g.WithDashboardAndPanel(nil, nil), g.WithOrgID(1))
}
rule := generator.GenerateRef()
rule := generator.Generate()
// ensure rule has correct values
if rule.Title != title {
rule.Title = title
@@ -601,32 +635,17 @@ func createAlertRule(t *testing.T, sql db.DB, title string, generator *ngmodels.
rule.DashboardUID = nil
rule.PanelID = nil
err := sql.WithDbSession(context.Background(), func(sess *db.Session) error {
_, err := sess.Table(ngmodels.AlertRule{}).InsertOne(rule)
if err != nil {
return err
}
dbRule := &ngmodels.AlertRule{}
exist, err := sess.Table(ngmodels.AlertRule{}).ID(rule.ID).Get(dbRule)
if err != nil {
return err
}
if !exist {
return errors.New("cannot read inserted record")
}
rule = dbRule
return nil
})
ruleStore := store.SetupStoreForTesting(t, sql)
ids, err := ruleStore.InsertAlertRules(context.Background(), []ngmodels.AlertRule{rule})
require.NoError(t, err)
return rule
result, err := ruleStore.GetAlertRuleByUID(context.Background(), &ngmodels.GetAlertRuleByUIDQuery{OrgID: rule.OrgID, UID: ids[0].UID})
require.NoError(t, err)
return result
}
// createAlertRuleFromDashboard creates an alert rule with a linked dashboard and panel in the database and returns it.
// If a generator is not specified, uniqueness of primary key is not guaranteed.
func createAlertRuleFromDashboard(t *testing.T, sql db.DB, title string, dashboard dashboards.Dashboard, generator *ngmodels.AlertRuleGenerator) *ngmodels.AlertRule {
func createAlertRuleFromDashboard(t *testing.T, sql *sqlstore.ReplStore, title string, dashboard dashboards.Dashboard, generator *ngmodels.AlertRuleGenerator) *ngmodels.AlertRule {
t.Helper()
panelID := new(int64)
@@ -637,7 +656,7 @@ func createAlertRuleFromDashboard(t *testing.T, sql db.DB, title string, dashboa
generator = g.With(g.WithTitle(title), g.WithDashboardAndPanel(&dashboard.UID, panelID), g.WithOrgID(1))
}
rule := generator.GenerateRef()
rule := generator.Generate()
// ensure rule has correct values
if rule.Title != title {
rule.Title = title
@@ -648,28 +667,12 @@ func createAlertRuleFromDashboard(t *testing.T, sql db.DB, title string, dashboa
if rule.PanelID == nil || (rule.PanelID != nil && *rule.PanelID != *panelID) {
rule.PanelID = panelID
}
err := sql.WithDbSession(context.Background(), func(sess *db.Session) error {
_, err := sess.Table(ngmodels.AlertRule{}).InsertOne(rule)
if err != nil {
return err
}
dbRule := &ngmodels.AlertRule{}
exist, err := sess.Table(ngmodels.AlertRule{}).ID(rule.ID).Get(dbRule)
if err != nil {
return err
}
if !exist {
return errors.New("cannot read inserted record")
}
rule = dbRule
return nil
})
ruleStore := store.SetupStoreForTesting(t, sql)
ids, err := ruleStore.InsertAlertRules(context.Background(), []ngmodels.AlertRule{rule})
require.NoError(t, err)
return rule
result, err := ruleStore.GetAlertRuleByUID(context.Background(), &ngmodels.GetAlertRuleByUIDQuery{OrgID: rule.OrgID, UID: ids[0].UID})
require.NoError(t, err)
return result
}
func ruleMetaFromRule(t *testing.T, rule *ngmodels.AlertRule) historymodel.RuleMeta {