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:
co-authored by
Matthew Jacobson
parent
0a976f831c
commit
f8fa5286a1
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user