Files
grafana/pkg/services/ngalert/models/testing.go
T
Yuri Tseretyan 92d6762a3a Alerting: Store information about user that created\updated alert rule (#99395)
* introduce new fields created_by in rule tables
* update domain model and compat layer to support UpdatedBy
* add alert rule generator mutators for UpdatedBy
* ignore UpdatedBy in diff and hash calculation
* Add user context to alert rule insert/update operations
  Updated InsertAlertRules and UpdateAlertRules methods to accept a user context parameter. This change ensures auditability and better tracking of user actions when creating or updating alert rules. Adjusted all relevant calls and interfaces to pass the user context accordingly.

* set UpdatedBy in PreSave because this is where Updated is set
* Use nil userID for system-initiated updates
This ensures differentiation between system and user-initiated changes for better traceability and clarity in update origins.

---------

Signed-off-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
2025-01-24 12:09:17 -05:00

1366 lines
36 KiB
Go

package models
import (
"encoding/base64"
"encoding/json"
"fmt"
"math/rand"
"slices"
"sync"
"testing"
"time"
"github.com/go-openapi/strfmt"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/grafana-plugin-sdk-go/data"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
alertingModels "github.com/grafana/alerting/models"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/util"
)
var (
RuleMuts = AlertRuleMutators{}
NSMuts = NotificationSettingsMutators{}
RuleGen = &AlertRuleGenerator{
mutators: []AlertRuleMutator{
RuleMuts.WithUniqueUID(), RuleMuts.WithUniqueTitle(),
},
}
)
type AlertRuleMutator func(r *AlertRule)
type AlertRuleGenerator struct {
AlertRuleMutators
mutators []AlertRuleMutator
}
func (g *AlertRuleGenerator) With(mutators ...AlertRuleMutator) *AlertRuleGenerator {
return &AlertRuleGenerator{
AlertRuleMutators: g.AlertRuleMutators,
mutators: append(g.mutators, mutators...),
}
}
func (g *AlertRuleGenerator) Generate() AlertRule {
randNoDataState := func() NoDataState {
s := [...]NoDataState{
Alerting,
NoData,
OK,
}
return s[rand.Intn(len(s))]
}
randErrState := func() ExecutionErrorState {
s := [...]ExecutionErrorState{
AlertingErrState,
ErrorErrState,
OkErrState,
}
return s[rand.Intn(len(s))]
}
interval := (rand.Int63n(6) + 1) * 10
forInterval := time.Duration(interval*rand.Int63n(6)) * time.Second
var annotations map[string]string = nil
if rand.Int63()%2 == 0 {
annotations = GenerateAlertLabels(rand.Intn(5), "ann-")
}
var labels map[string]string = nil
if rand.Int63()%2 == 0 {
labels = GenerateAlertLabels(rand.Intn(5), "lbl-")
}
var dashUID *string = nil
var panelID *int64 = nil
if rand.Int63()%2 == 0 {
d := util.GenerateShortUID()
dashUID = &d
p := rand.Int63n(1500)
panelID = &p
}
var ns []NotificationSettings
if rand.Int63()%2 == 0 {
ns = append(ns, NotificationSettingsGen()())
}
var updatedBy *UserUID
if rand.Int63()%2 == 0 {
updatedBy = util.Pointer(UserUID(util.GenerateShortUID()))
}
rule := AlertRule{
ID: 0,
OrgID: rand.Int63n(1500) + 1, // Prevent OrgID=0 as this does not pass alert rule validation.
Title: fmt.Sprintf("title-%s", util.GenerateShortUID()),
Condition: "A",
Data: []AlertQuery{g.GenerateQuery()},
Updated: time.Now().Add(-time.Duration(rand.Intn(100) + 1)),
UpdatedBy: updatedBy,
IntervalSeconds: rand.Int63n(60) + 1,
Version: rand.Int63n(1500), // Don't generate a rule ID too big for postgres
UID: util.GenerateShortUID(),
NamespaceUID: util.GenerateShortUID(),
DashboardUID: dashUID,
PanelID: panelID,
RuleGroup: fmt.Sprintf("group-%s,", util.GenerateShortUID()),
RuleGroupIndex: rand.Intn(1500),
NoDataState: randNoDataState(),
ExecErrState: randErrState(),
For: forInterval,
Annotations: annotations,
Labels: labels,
NotificationSettings: ns,
}
for _, mutator := range g.mutators {
mutator(&rule)
}
return rule
}
func (g *AlertRuleGenerator) GenerateRef() *AlertRule {
r := g.Generate()
return &r
}
func (g *AlertRuleGenerator) getCount(bounds ...int) int {
count := 0
if len(bounds) == 0 {
count = rand.Intn(5) + 1
}
if len(bounds) == 1 {
count = bounds[0]
}
if len(bounds) == 2 {
if bounds[0] > bounds[1] {
panic("min should not be greater than max")
} else if bounds[0] < bounds[1] {
count = rand.Intn(bounds[1]-bounds[0]) + bounds[0]
} else {
count = bounds[0]
}
}
if len(bounds) > 2 {
panic("invalid number of parameter must be up to 2")
}
return count
}
func (g *AlertRuleGenerator) GenerateMany(bounds ...int) []AlertRule {
count := g.getCount(bounds...)
result := make([]AlertRule, 0, count)
for i := 0; i < count; i++ {
result = append(result, g.Generate())
}
return result
}
func (g *AlertRuleGenerator) GenerateManyRef(bounds ...int) []*AlertRule {
count := g.getCount(bounds...)
result := make([]*AlertRule, 0)
for i := 0; i < count; i++ {
r := g.Generate()
result = append(result, &r)
}
return result
}
type AlertRuleMutators struct {
}
func (a *AlertRuleMutators) WithNotEmptyLabels(count int, prefix string) AlertRuleMutator {
return func(rule *AlertRule) {
rule.Labels = GenerateAlertLabels(count, prefix)
}
}
func (a *AlertRuleMutators) WithUniqueID() AlertRuleMutator {
ids := sync.Map{}
return func(rule *AlertRule) {
id := rule.ID
for {
_, exists := ids.LoadOrStore(id, struct{}{})
if !exists {
rule.ID = id
return
}
id = rand.Int63n(1500) + 1
}
}
}
func (a *AlertRuleMutators) WithEditorSettingsSimplifiedQueryAndExpressionsSection(enabled bool) AlertRuleMutator {
return func(rule *AlertRule) {
rule.Metadata.EditorSettings.SimplifiedQueryAndExpressionsSection = enabled
}
}
func (a *AlertRuleMutators) WithEditorSettingsSimplifiedNotificationsSection(enabled bool) AlertRuleMutator {
return func(rule *AlertRule) {
rule.Metadata.EditorSettings.SimplifiedNotificationsSection = enabled
}
}
func (a *AlertRuleMutators) WithGroupIndex(groupIndex int) AlertRuleMutator {
return func(rule *AlertRule) {
rule.RuleGroupIndex = groupIndex
}
}
func (a *AlertRuleMutators) WithUniqueGroupIndex() AlertRuleMutator {
usedIdx := sync.Map{}
return func(rule *AlertRule) {
idx := rule.RuleGroupIndex
for {
if _, exists := usedIdx.LoadOrStore(idx, struct{}{}); !exists {
rule.RuleGroupIndex = idx
return
}
idx = rand.Int()
}
}
}
func (a *AlertRuleMutators) WithSequentialGroupIndex() AlertRuleMutator {
idx := 1
m := sync.Mutex{}
return func(rule *AlertRule) {
m.Lock()
defer m.Unlock()
rule.RuleGroupIndex = idx
idx++
}
}
func (a *AlertRuleMutators) WithOrgID(orgId int64) AlertRuleMutator {
return func(rule *AlertRule) {
rule.OrgID = orgId
}
}
func (a *AlertRuleMutators) WithUniqueOrgID() AlertRuleMutator {
orgs := sync.Map{}
return func(rule *AlertRule) {
orgID := rule.OrgID
for {
if _, exist := orgs.LoadOrStore(orgID, struct{}{}); !exist {
rule.OrgID = orgID
return
}
orgID = rand.Int63()
}
}
}
// WithNamespaceUIDNotIn generates a random namespace UID if it is among excluded
func (a *AlertRuleMutators) WithNamespaceUIDNotIn(exclude ...string) AlertRuleMutator {
return func(rule *AlertRule) {
for {
if !slices.Contains(exclude, rule.NamespaceUID) {
return
}
rule.NamespaceUID = util.GenerateShortUID()
}
}
}
func (a *AlertRuleMutators) WithNamespaceUID(namespaceUID string) AlertRuleMutator {
return func(rule *AlertRule) {
rule.NamespaceUID = namespaceUID
}
}
func (a *AlertRuleMutators) WithNamespace(namespace *folder.Folder) AlertRuleMutator {
return a.WithNamespaceUID(namespace.UID)
}
func (a *AlertRuleMutators) WithInterval(interval time.Duration) AlertRuleMutator {
return func(rule *AlertRule) {
rule.IntervalSeconds = int64(interval.Seconds())
}
}
func (a *AlertRuleMutators) WithIntervalSeconds(seconds int64) AlertRuleMutator {
return func(rule *AlertRule) {
rule.IntervalSeconds = seconds
}
}
// WithIntervalMatching mutator that generates random interval and `for` duration that are times of the provided base interval.
func (a *AlertRuleMutators) WithIntervalMatching(baseInterval time.Duration) AlertRuleMutator {
return func(rule *AlertRule) {
rule.IntervalSeconds = int64(baseInterval.Seconds()) * (rand.Int63n(10) + 1)
rule.For = time.Duration(rule.IntervalSeconds*rand.Int63n(9)+1) * time.Second
}
}
func (a *AlertRuleMutators) WithIntervalBetween(min, max int64) AlertRuleMutator {
return func(rule *AlertRule) {
rule.IntervalSeconds = rand.Int63n(max-min) + min
}
}
func (a *AlertRuleMutators) WithTitle(title string) AlertRuleMutator {
return func(rule *AlertRule) {
rule.Title = title
}
}
func (a *AlertRuleMutators) WithFor(duration time.Duration) AlertRuleMutator {
return func(rule *AlertRule) {
rule.For = duration
}
}
func (a *AlertRuleMutators) WithForNTimes(timesOfInterval int64) AlertRuleMutator {
return func(rule *AlertRule) {
rule.For = time.Duration(rule.IntervalSeconds*timesOfInterval) * time.Second
}
}
func (a *AlertRuleMutators) WithNoDataExecAs(nodata NoDataState) AlertRuleMutator {
return func(rule *AlertRule) {
rule.NoDataState = nodata
}
}
func (a *AlertRuleMutators) WithErrorExecAs(err ExecutionErrorState) AlertRuleMutator {
return func(rule *AlertRule) {
rule.ExecErrState = err
}
}
func (a *AlertRuleMutators) WithAnnotations(lbls data.Labels) AlertRuleMutator {
return func(rule *AlertRule) {
rule.Annotations = lbls
}
}
func (a *AlertRuleMutators) WithAnnotation(key, value string) AlertRuleMutator {
return func(rule *AlertRule) {
if rule.Annotations == nil {
rule.Annotations = data.Labels{}
}
rule.Annotations[key] = value
}
}
func (a *AlertRuleMutators) WithLabels(lbls data.Labels) AlertRuleMutator {
return func(rule *AlertRule) {
rule.Labels = lbls
}
}
func (a *AlertRuleMutators) WithLabel(key, value string) AlertRuleMutator {
return func(rule *AlertRule) {
if rule.Labels == nil {
rule.Labels = data.Labels{}
}
rule.Labels[key] = value
}
}
func (a *AlertRuleMutators) WithDashboardAndPanel(dashboardUID *string, panelID *int64) AlertRuleMutator {
return func(rule *AlertRule) {
rule.DashboardUID = dashboardUID
rule.PanelID = panelID
}
}
// WithUniqueUID returns AlertRuleMutator that generates a random UID if it is among UIDs known by the instance of mutator.
// NOTE: two instances of the mutator do not share known UID.
// Example #1 reuse mutator instance:
//
// mut := WithUniqueUID()
// rule1 := RuleGen.With(mut).Generate()
// rule2 := RuleGen.With(mut).Generate()
//
// Example #2 reuse generator:
//
// gen := RuleGen.With(WithUniqueUID())
// rule1 := gen.Generate()
// rule2 := gen.Generate()
//
// Example #3 non-unique:
//
// rule1 := RuleGen.With(WithUniqueUID()).Generate
// rule2 := RuleGen.With(WithUniqueUID()).Generate
func (a *AlertRuleMutators) WithUniqueUID() AlertRuleMutator {
uids := sync.Map{}
return func(rule *AlertRule) {
uid := rule.UID
for {
_, exist := uids.LoadOrStore(uid, struct{}{})
if !exist {
rule.UID = uid
return
}
uid = util.GenerateShortUID()
}
}
}
// WithUniqueTitle returns AlertRuleMutator that generates a random title if the rule's title is among titles known by the instance of mutator.
// Two instances of the mutator do not share known titles.
// Example #1 reuse mutator instance:
//
// mut := WithUniqueTitle()
// rule1 := RuleGen.With(mut).Generate()
// rule2 := RuleGen.With(mut).Generate()
//
// Example #2 reuse generator:
//
// gen := RuleGen.With(WithUniqueTitle())
// rule1 := gen.Generate()
// rule2 := gen.Generate()
//
// Example #3 non-unique:
//
// rule1 := RuleGen.With(WithUniqueTitle()).Generate
// rule2 := RuleGen.With(WithUniqueTitle()).Generate
func (a *AlertRuleMutators) WithUniqueTitle() AlertRuleMutator {
titles := sync.Map{}
return func(rule *AlertRule) {
title := rule.Title
for {
_, exist := titles.LoadOrStore(title, struct{}{})
if !exist {
rule.Title = title
return
}
title = fmt.Sprintf("title-%s", util.GenerateShortUID())
}
}
}
func (a *AlertRuleMutators) WithQuery(query ...AlertQuery) AlertRuleMutator {
return func(rule *AlertRule) {
rule.Data = query
if len(query) > 1 {
rule.Condition = query[0].RefID
}
}
}
func (a *AlertRuleMutators) WithGroupName(groupName string) AlertRuleMutator {
return func(rule *AlertRule) {
rule.RuleGroup = groupName
}
}
func (a *AlertRuleMutators) WithGroupPrefix(prefix string) AlertRuleMutator {
return func(rule *AlertRule) {
rule.RuleGroup = fmt.Sprintf("%s%s", prefix, util.GenerateShortUID())
}
}
func (a *AlertRuleMutators) WithGroupKey(groupKey AlertRuleGroupKey) AlertRuleMutator {
return func(rule *AlertRule) {
rule.RuleGroup = groupKey.RuleGroup
rule.OrgID = groupKey.OrgID
rule.NamespaceUID = groupKey.NamespaceUID
}
}
// WithSameGroup generates a random group name and assigns it to all rules passed to it
func (a *AlertRuleMutators) WithSameGroup() AlertRuleMutator {
once := sync.Once{}
name := ""
return func(rule *AlertRule) {
once.Do(func() {
name = util.GenerateShortUID()
})
rule.RuleGroup = name
}
}
func (a *AlertRuleMutators) WithNotificationSettingsGen(ns func() NotificationSettings) AlertRuleMutator {
return func(rule *AlertRule) {
rule.NotificationSettings = []NotificationSettings{ns()}
}
}
func (a *AlertRuleMutators) WithNotificationSettings(ns NotificationSettings) AlertRuleMutator {
return func(rule *AlertRule) {
rule.NotificationSettings = []NotificationSettings{ns}
}
}
func (a *AlertRuleMutators) WithNoNotificationSettings() AlertRuleMutator {
return func(rule *AlertRule) {
rule.NotificationSettings = nil
}
}
func (a *AlertRuleMutators) WithIsPaused(paused bool) AlertRuleMutator {
return func(rule *AlertRule) {
rule.IsPaused = paused
}
}
func (a *AlertRuleMutators) WithRandomRecordingRules() AlertRuleMutator {
return func(rule *AlertRule) {
if rand.Int63()%2 == 0 {
return
}
ConvertToRecordingRule(rule)
}
}
func (a *AlertRuleMutators) WithAllRecordingRules() AlertRuleMutator {
return func(rule *AlertRule) {
ConvertToRecordingRule(rule)
}
}
func (a *AlertRuleMutators) WithMetric(metric string) AlertRuleMutator {
return func(rule *AlertRule) {
if rule.Record == nil {
rule.Record = &Record{}
}
rule.Record.Metric = metric
}
}
func (a *AlertRuleMutators) WithRecordFrom(from string) AlertRuleMutator {
return func(rule *AlertRule) {
if rule.Record == nil {
rule.Record = &Record{}
}
rule.Record.From = from
}
}
func (a *AlertRuleMutators) WithUpdatedBy(uid *UserUID) AlertRuleMutator {
return func(r *AlertRule) {
r.UpdatedBy = uid
}
}
func (g *AlertRuleGenerator) GenerateLabels(min, max int, prefix string) data.Labels {
count := max
if min > max {
panic("min should not be greater than max")
} else if min < max {
count = rand.Intn(max-min) + min
}
labels := make(data.Labels, count)
for i := 0; i < count; i++ {
labels[prefix+"key-"+util.GenerateShortUID()] = prefix + "value-" + util.GenerateShortUID()
}
return labels
}
func GenerateAlertLabels(count int, prefix string) data.Labels {
return RuleGen.GenerateLabels(count, count, prefix)
}
func GenerateAlertQuery() AlertQuery {
return RuleGen.GenerateQuery()
}
func (g *AlertRuleGenerator) GenerateQuery() AlertQuery {
f := rand.Intn(10) + 5
t := rand.Intn(f)
return AlertQuery{
DatasourceUID: util.GenerateShortUID(),
Model: json.RawMessage(fmt.Sprintf(`{
"%s": "%s",
"%s":"%d"
}`, util.GenerateShortUID(), util.GenerateShortUID(), util.GenerateShortUID(), rand.Int())),
RelativeTimeRange: RelativeTimeRange{
From: Duration(time.Duration(f) * time.Minute),
To: Duration(time.Duration(t) * time.Minute),
},
RefID: util.GenerateShortUID(),
QueryType: util.GenerateShortUID(),
}
}
func (g *AlertRuleGenerator) WithCondition(condition string) AlertRuleMutator {
return func(r *AlertRule) {
r.Condition = condition
}
}
// GenerateRuleKey generates a random alert rule key
func GenerateRuleKey(orgID int64) AlertRuleKey {
return AlertRuleKey{
OrgID: orgID,
UID: util.GenerateShortUID(),
}
}
// GenerateRuleKeyWithGroup generates a random alert rule key with group
func GenerateRuleKeyWithGroup(orgID int64) AlertRuleKeyWithGroup {
return AlertRuleKeyWithGroup{
AlertRuleKey: GenerateRuleKey(orgID),
RuleGroup: util.GenerateShortUID(),
}
}
// GenerateGroupKey generates a random group key
func GenerateGroupKey(orgID int64) AlertRuleGroupKey {
return AlertRuleGroupKey{
OrgID: orgID,
NamespaceUID: util.GenerateShortUID(),
RuleGroup: util.GenerateShortUID(),
}
}
// CopyRule creates a deep copy of AlertRule
func CopyRule(r *AlertRule, mutators ...AlertRuleMutator) *AlertRule {
result := AlertRule{
ID: r.ID,
OrgID: r.OrgID,
Title: r.Title,
Condition: r.Condition,
Updated: r.Updated,
UpdatedBy: r.UpdatedBy,
IntervalSeconds: r.IntervalSeconds,
Version: r.Version,
UID: r.UID,
NamespaceUID: r.NamespaceUID,
RuleGroup: r.RuleGroup,
RuleGroupIndex: r.RuleGroupIndex,
NoDataState: r.NoDataState,
ExecErrState: r.ExecErrState,
For: r.For,
Record: r.Record,
IsPaused: r.IsPaused,
}
if r.DashboardUID != nil {
dash := *r.DashboardUID
result.DashboardUID = &dash
}
if r.PanelID != nil {
p := *r.PanelID
result.PanelID = &p
}
for _, d := range r.Data {
q := AlertQuery{
RefID: d.RefID,
QueryType: d.QueryType,
RelativeTimeRange: d.RelativeTimeRange,
DatasourceUID: d.DatasourceUID,
}
q.Model = make([]byte, 0, cap(d.Model))
q.Model = append(q.Model, d.Model...)
result.Data = append(result.Data, q)
}
if r.Annotations != nil {
result.Annotations = make(map[string]string, len(r.Annotations))
for s, s2 := range r.Annotations {
result.Annotations[s] = s2
}
}
if r.Labels != nil {
result.Labels = make(map[string]string, len(r.Labels))
for s, s2 := range r.Labels {
result.Labels[s] = s2
}
}
if r.Record != nil {
result.Record = &Record{
From: r.Record.From,
Metric: r.Record.Metric,
}
}
for _, s := range r.NotificationSettings {
result.NotificationSettings = append(result.NotificationSettings, CopyNotificationSettings(s))
}
if len(mutators) > 0 {
for _, mutator := range mutators {
mutator(&result)
}
}
return &result
}
func CreateClassicConditionExpression(refID string, inputRefID string, reducer string, operation string, threshold int) AlertQuery {
return AlertQuery{
RefID: refID,
QueryType: expr.DatasourceType,
DatasourceUID: expr.DatasourceUID,
// the format corresponds to model `ClassicConditionJSON` in /pkg/expr/classic/classic.go
Model: json.RawMessage(fmt.Sprintf(`
{
"refId": "%[1]s",
"hide": false,
"type": "classic_conditions",
"datasource": {
"uid": "%[6]s",
"type": "%[7]s"
},
"conditions": [
{
"type": "query",
"evaluator": {
"params": [
%[4]d
],
"type": "%[3]s"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"%[2]s"
]
},
"reducer": {
"params": [],
"type": "%[5]s"
}
}
]
}`, refID, inputRefID, operation, threshold, reducer, expr.DatasourceUID, expr.DatasourceType)),
}
}
func CreateReduceExpression(refID string, inputRefID string, reducer string) AlertQuery {
return AlertQuery{
RefID: refID,
QueryType: expr.DatasourceType,
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(fmt.Sprintf(`
{
"refId": "%[1]s",
"hide": false,
"type": "reduce",
"expression": "%[2]s",
"reducer": "%[3]s",
"datasource": {
"uid": "%[4]s",
"type": "%[5]s"
}
}`, refID, inputRefID, reducer, expr.DatasourceUID, expr.DatasourceType)),
}
}
func CreatePrometheusQuery(refID string, expr string, intervalMs int64, maxDataPoints int64, isInstant bool, datasourceUID string) AlertQuery {
return AlertQuery{
RefID: refID,
QueryType: "",
DatasourceUID: datasourceUID,
Model: json.RawMessage(fmt.Sprintf(`
{
"refId": "%[1]s",
"expr": "%[2]s",
"intervalMs": %[3]d,
"maxDataPoints": %[4]d,
"exemplar": false,
"instant": %[5]t,
"range": %[6]t,
"datasource": {
"uid": "%[7]s",
"type": "%[8]s"
}
}`, refID, expr, intervalMs, maxDataPoints, isInstant, !isInstant, datasourceUID, datasources.DS_PROMETHEUS)),
}
}
func CreateLokiQuery(refID string, expr string, intervalMs int64, maxDataPoints int64, queryType string, datasourceUID string) AlertQuery {
return AlertQuery{
RefID: refID,
QueryType: queryType,
DatasourceUID: datasourceUID,
Model: json.RawMessage(fmt.Sprintf(`
{
"refId": "%[1]s",
"expr": "%[2]s",
"intervalMs": %[3]d,
"maxDataPoints": %[4]d,
"queryType": "%[5]s",
"datasource": {
"uid": "%[6]s",
"type": "%[7]s"
}
}`, refID, expr, intervalMs, maxDataPoints, queryType, datasourceUID, datasources.DS_LOKI)),
}
}
func CreateHysteresisExpression(t *testing.T, refID string, inputRefID string, threshold int, recoveryThreshold int) AlertQuery {
t.Helper()
q := AlertQuery{
RefID: refID,
QueryType: expr.DatasourceType,
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(fmt.Sprintf(`
{
"refId": "%[1]s",
"type": "threshold",
"datasource": {
"uid": "%[5]s",
"type": "%[6]s"
},
"expression": "%[2]s",
"conditions": [
{
"type": "query",
"evaluator": {
"params": [
%[3]d
],
"type": "gt"
},
"unloadEvaluator": {
"params": [
%[4]d
],
"type": "lt"
}
}
]
}`, refID, inputRefID, threshold, recoveryThreshold, expr.DatasourceUID, expr.DatasourceType)),
}
h, err := q.IsHysteresisExpression()
require.NoError(t, err)
require.Truef(t, h, "test model is expected to be a hysteresis expression")
return q
}
type AlertInstanceMutator func(*AlertInstance)
// AlertInstanceGen provides a factory function that generates a random AlertInstance.
// The mutators arguments allows changing fields of the resulting structure.
func AlertInstanceGen(mutators ...AlertInstanceMutator) *AlertInstance {
var labels map[string]string = nil
if rand.Int63()%2 == 0 {
labels = GenerateAlertLabels(rand.Intn(5), "lbl-")
}
randState := func() InstanceStateType {
s := [...]InstanceStateType{
InstanceStateFiring,
InstanceStateNormal,
InstanceStatePending,
InstanceStateNoData,
InstanceStateError,
}
return s[rand.Intn(len(s))]
}
currentStateSince := time.Now().Add(-time.Duration(rand.Intn(100) + 1))
instance := &AlertInstance{
AlertInstanceKey: AlertInstanceKey{
RuleOrgID: rand.Int63n(1500),
RuleUID: util.GenerateShortUID(),
LabelsHash: util.GenerateShortUID(),
},
Labels: labels,
CurrentState: randState(),
CurrentReason: "TEST-REASON-" + util.GenerateShortUID(),
CurrentStateSince: currentStateSince,
CurrentStateEnd: currentStateSince.Add(time.Duration(rand.Intn(100) + 200)),
LastEvalTime: time.Now().Add(-time.Duration(rand.Intn(100) + 50)),
LastSentAt: util.Pointer(time.Now().Add(-time.Duration(rand.Intn(100) + 50))),
}
if instance.CurrentState == InstanceStateNormal && rand.Intn(2) == 1 {
instance.ResolvedAt = util.Pointer(time.Now().Add(-time.Duration(rand.Intn(100) + 50)))
}
for _, mutator := range mutators {
mutator(instance)
}
return instance
}
type Mutator[T any] func(*T)
// CopyNotificationSettings creates a deep copy of NotificationSettings.
func CopyNotificationSettings(ns NotificationSettings, mutators ...Mutator[NotificationSettings]) NotificationSettings {
c := NotificationSettings{
Receiver: ns.Receiver,
}
if ns.GroupWait != nil {
c.GroupWait = util.Pointer(*ns.GroupWait)
}
if ns.GroupInterval != nil {
c.GroupInterval = util.Pointer(*ns.GroupInterval)
}
if ns.RepeatInterval != nil {
c.RepeatInterval = util.Pointer(*ns.RepeatInterval)
}
if ns.GroupBy != nil {
c.GroupBy = make([]string, len(ns.GroupBy))
copy(c.GroupBy, ns.GroupBy)
}
if ns.MuteTimeIntervals != nil {
c.MuteTimeIntervals = make([]string, len(ns.MuteTimeIntervals))
copy(c.MuteTimeIntervals, ns.MuteTimeIntervals)
}
for _, mutator := range mutators {
mutator(&c)
}
return c
}
// NotificationSettingsGen generates NotificationSettings using a base and mutators.
func NotificationSettingsGen(mutators ...Mutator[NotificationSettings]) func() NotificationSettings {
return func() NotificationSettings {
c := NotificationSettings{
Receiver: util.GenerateShortUID(),
GroupBy: []string{model.AlertNameLabel, FolderTitleLabel, util.GenerateShortUID()},
GroupWait: util.Pointer(model.Duration(time.Duration(rand.Intn(100)+1) * time.Second)),
GroupInterval: util.Pointer(model.Duration(time.Duration(rand.Intn(100)+1) * time.Second)),
RepeatInterval: util.Pointer(model.Duration(time.Duration(rand.Intn(100)+1) * time.Second)),
MuteTimeIntervals: []string{util.GenerateShortUID(), util.GenerateShortUID()},
}
for _, mutator := range mutators {
mutator(&c)
}
return c
}
}
type NotificationSettingsMutators struct{}
func (n NotificationSettingsMutators) WithReceiver(receiver string) Mutator[NotificationSettings] {
return func(ns *NotificationSettings) {
ns.Receiver = receiver
}
}
func (n NotificationSettingsMutators) WithGroupWait(groupWait *time.Duration) Mutator[NotificationSettings] {
return func(ns *NotificationSettings) {
if groupWait == nil {
ns.GroupWait = nil
return
}
dur := model.Duration(*groupWait)
ns.GroupWait = &dur
}
}
func (n NotificationSettingsMutators) WithGroupInterval(groupInterval *time.Duration) Mutator[NotificationSettings] {
return func(ns *NotificationSettings) {
if groupInterval == nil {
ns.GroupInterval = nil
return
}
dur := model.Duration(*groupInterval)
ns.GroupInterval = &dur
}
}
func (n NotificationSettingsMutators) WithRepeatInterval(repeatInterval *time.Duration) Mutator[NotificationSettings] {
return func(ns *NotificationSettings) {
if repeatInterval == nil {
ns.RepeatInterval = nil
return
}
dur := model.Duration(*repeatInterval)
ns.RepeatInterval = &dur
}
}
func (n NotificationSettingsMutators) WithGroupBy(groupBy ...string) Mutator[NotificationSettings] {
return func(ns *NotificationSettings) {
ns.GroupBy = groupBy
}
}
func (n NotificationSettingsMutators) WithMuteTimeIntervals(muteTimeIntervals ...string) Mutator[NotificationSettings] {
return func(ns *NotificationSettings) {
ns.MuteTimeIntervals = muteTimeIntervals
}
}
// Silences
// CopySilenceWith creates a deep copy of Silence and then applies mutators to it.
func CopySilenceWith(s Silence, mutators ...Mutator[Silence]) Silence {
c := CopySilence(s)
for _, mutator := range mutators {
mutator(&c)
}
return c
}
// CopySilence creates a deep copy of Silence.
func CopySilence(s Silence) Silence {
c := Silence{
Silence: amv2.Silence{},
}
if s.ID != nil {
c.ID = util.Pointer(*s.ID)
}
if s.Status != nil {
c.Status = util.Pointer(*s.Status)
}
if s.UpdatedAt != nil {
c.UpdatedAt = util.Pointer(*s.UpdatedAt)
}
if s.Silence.Comment != nil {
c.Silence.Comment = util.Pointer(*s.Silence.Comment)
}
if s.Silence.CreatedBy != nil {
c.Silence.CreatedBy = util.Pointer(*s.Silence.CreatedBy)
}
if s.Silence.EndsAt != nil {
c.Silence.EndsAt = util.Pointer(*s.Silence.EndsAt)
}
if s.Silence.StartsAt != nil {
c.Silence.StartsAt = util.Pointer(*s.Silence.StartsAt)
}
if s.Silence.Matchers != nil {
c.Silence.Matchers = CopyMatchers(s.Silence.Matchers)
}
return c
}
// CopyMatchers creates a deep copy of Matchers.
func CopyMatchers(matchers []*amv2.Matcher) []*amv2.Matcher {
copies := make([]*amv2.Matcher, len(matchers))
for i, m := range matchers {
c := amv2.Matcher{}
if m.IsEqual != nil {
c.IsEqual = util.Pointer(*m.IsEqual)
}
if m.IsRegex != nil {
c.IsRegex = util.Pointer(*m.IsRegex)
}
if m.Name != nil {
c.Name = util.Pointer(*m.Name)
}
if m.Value != nil {
c.Value = util.Pointer(*m.Value)
}
copies[i] = &c
}
return copies
}
// SilenceGen generates Silence using a base and mutators.
func SilenceGen(mutators ...Mutator[Silence]) func() Silence {
return func() Silence {
now := time.Now()
c := Silence{
ID: util.Pointer(util.GenerateShortUID()),
Status: util.Pointer(amv2.SilenceStatus{State: util.Pointer(amv2.SilenceStatusStateActive)}),
UpdatedAt: util.Pointer(strfmt.DateTime(now.Add(time.Minute))),
Silence: amv2.Silence{
Comment: util.Pointer(util.GenerateShortUID()),
CreatedBy: util.Pointer(util.GenerateShortUID()),
StartsAt: util.Pointer(strfmt.DateTime(now.Add(-time.Minute))),
EndsAt: util.Pointer(strfmt.DateTime(now.Add(time.Minute))),
Matchers: []*amv2.Matcher{{Name: util.Pointer(util.GenerateShortUID()), Value: util.Pointer(util.GenerateShortUID()), IsRegex: util.Pointer(false), IsEqual: util.Pointer(true)}},
},
}
for _, mutator := range mutators {
mutator(&c)
}
return c
}
}
var (
SilenceMuts = SilenceMutators{}
)
type SilenceMutators struct{}
func (n SilenceMutators) WithMatcher(name, value string, matchType labels.MatchType) Mutator[Silence] {
return func(s *Silence) {
m := amv2.Matcher{
Name: &name,
Value: &value,
IsRegex: util.Pointer(matchType == labels.MatchRegexp || matchType == labels.MatchNotRegexp),
IsEqual: util.Pointer(matchType == labels.MatchRegexp || matchType == labels.MatchEqual),
}
s.Silence.Matchers = append(s.Silence.Matchers, &m)
}
}
func (n SilenceMutators) WithRuleUID(value string) Mutator[Silence] {
return func(s *Silence) {
name := alertingModels.RuleUIDLabel
m := amv2.Matcher{
Name: &name,
Value: &value,
IsRegex: util.Pointer(false),
IsEqual: util.Pointer(true),
}
for _, matcher := range s.Silence.Matchers {
if isRuleUIDMatcher(*matcher) {
*matcher = m
return
}
}
s.Silence.Matchers = append(s.Silence.Matchers, &m)
}
}
func (n SilenceMutators) Expired() Mutator[Silence] {
return func(s *Silence) {
s.EndsAt = util.Pointer(strfmt.DateTime(time.Now().Add(-time.Minute)))
}
}
func (n SilenceMutators) WithEmptyId() Mutator[Silence] {
return func(s *Silence) {
s.ID = util.Pointer("")
}
}
// Receivers
// CopyReceiverWith creates a deep copy of Receiver and then applies mutators to it.
func CopyReceiverWith(r Receiver, mutators ...Mutator[Receiver]) Receiver {
c := r.Clone()
for _, mutator := range mutators {
mutator(&c)
}
c.Version = c.Fingerprint()
return c
}
// ReceiverGen generates Receiver using a base and mutators.
func ReceiverGen(mutators ...Mutator[Receiver]) func() Receiver {
return func() Receiver {
name := util.GenerateShortUID()
integration := IntegrationGen(IntegrationMuts.WithName(name))()
c := Receiver{
UID: nameToUid(name),
Name: name,
Integrations: []*Integration{&integration},
Provenance: ProvenanceNone,
}
for _, mutator := range mutators {
mutator(&c)
}
c.Version = c.Fingerprint()
return c
}
}
var (
ReceiverMuts = ReceiverMutators{}
)
type ReceiverMutators struct{}
func (n ReceiverMutators) WithName(name string) Mutator[Receiver] {
return func(r *Receiver) {
r.Name = name
r.UID = nameToUid(name)
}
}
func (n ReceiverMutators) WithProvenance(provenance Provenance) Mutator[Receiver] {
return func(r *Receiver) {
r.Provenance = provenance
}
}
func (n ReceiverMutators) WithValidIntegration(integrationType string) Mutator[Receiver] {
return func(r *Receiver) {
integration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))()
r.Integrations = []*Integration{&integration}
}
}
func (n ReceiverMutators) WithInvalidIntegration(integrationType string) Mutator[Receiver] {
return func(r *Receiver) {
integration := IntegrationGen(IntegrationMuts.WithInvalidConfig(integrationType))()
r.Integrations = []*Integration{&integration}
}
}
func (n ReceiverMutators) WithIntegrations(integration ...Integration) Mutator[Receiver] {
return func(r *Receiver) {
integrations := make([]*Integration, len(integration))
for i, v := range integration {
clone := v.Clone()
integrations[i] = &clone
}
r.Integrations = integrations
}
}
func (n ReceiverMutators) Encrypted(fn EncryptFn) Mutator[Receiver] {
return func(r *Receiver) {
_ = r.Encrypt(fn)
}
}
func (n ReceiverMutators) Decrypted(fn DecryptFn) Mutator[Receiver] {
return func(r *Receiver) {
_ = r.Decrypt(fn)
}
}
// Integrations
// CopyIntegrationWith creates a deep copy of Integration and then applies mutators to it.
func CopyIntegrationWith(r Integration, mutators ...Mutator[Integration]) Integration {
c := r.Clone()
for _, mutator := range mutators {
mutator(&c)
}
return c
}
// IntegrationGen generates Integration using a base and mutators.
func IntegrationGen(mutators ...Mutator[Integration]) func() Integration {
return func() Integration {
name := util.GenerateShortUID()
randomIntegrationType, _ := randomMapKey(alertingNotify.AllKnownConfigsForTesting)
c := Integration{
UID: util.GenerateShortUID(),
Name: name,
DisableResolveMessage: rand.Intn(2) == 1,
Settings: make(map[string]any),
SecureSettings: make(map[string]string),
}
IntegrationMuts.WithValidConfig(randomIntegrationType)(&c)
for _, mutator := range mutators {
mutator(&c)
}
return c
}
}
var (
IntegrationMuts = IntegrationMutators{}
Base64Enrypt = func(s string) (string, error) {
return base64.StdEncoding.EncodeToString([]byte(s)), nil
}
Base64Decrypt = func(s string) (string, error) {
b, err := base64.StdEncoding.DecodeString(s)
return string(b), err
}
)
type IntegrationMutators struct{}
func (n IntegrationMutators) WithUID(uid string) Mutator[Integration] {
return func(s *Integration) {
s.UID = uid
}
}
func (n IntegrationMutators) WithName(name string) Mutator[Integration] {
return func(s *Integration) {
s.Name = name
}
}
func (n IntegrationMutators) WithValidConfig(integrationType string) Mutator[Integration] {
return func(c *Integration) {
config := alertingNotify.AllKnownConfigsForTesting[integrationType].GetRawNotifierConfig(c.Name)
integrationConfig, _ := IntegrationConfigFromType(integrationType)
c.Config = integrationConfig
var settings map[string]any
_ = json.Unmarshal(config.Settings, &settings)
c.Settings = settings
// Decrypt secure settings over to normal settings.
for k, v := range c.SecureSettings {
decodeValue, _ := base64.StdEncoding.DecodeString(v)
settings[k] = string(decodeValue)
}
}
}
func (n IntegrationMutators) WithInvalidConfig(integrationType string) Mutator[Integration] {
return func(c *Integration) {
integrationConfig, _ := IntegrationConfigFromType(integrationType)
c.Config = integrationConfig
c.Settings = map[string]interface{}{}
c.SecureSettings = map[string]string{}
if integrationType == "webex" {
// Webex passes validation without any settings but should fail with an unparsable URL.
c.Settings["api_url"] = "(*^$*^%!@#$*()"
}
}
}
func (n IntegrationMutators) WithSettings(settings map[string]any) Mutator[Integration] {
return func(c *Integration) {
c.Settings = maps.Clone(settings)
}
}
func (n IntegrationMutators) AddSetting(key string, val any) Mutator[Integration] {
return func(c *Integration) {
c.Settings[key] = val
}
}
func (n IntegrationMutators) WithSecureSettings(secureSettings map[string]string) Mutator[Integration] {
return func(r *Integration) {
r.SecureSettings = maps.Clone(secureSettings)
}
}
func (n IntegrationMutators) AddSecureSetting(key, val string) Mutator[Integration] {
return func(r *Integration) {
r.SecureSettings[key] = val
}
}
func randomMapKey[K comparable, V any](m map[K]V) (K, V) {
randIdx := rand.Intn(len(m))
i := 0
for key, val := range m {
if i == randIdx {
return key, val
}
i++
}
return *new(K), *new(V)
}
func ConvertToRecordingRule(rule *AlertRule) {
if rule.Record == nil {
rule.Record = &Record{}
}
if rule.Record.From == "" {
rule.Record.From = rule.Condition
}
if rule.Record.Metric == "" {
rule.Record.Metric = fmt.Sprintf("some_metric_%s", util.GenerateShortUID())
}
rule.Condition = ""
rule.NoDataState = ""
rule.ExecErrState = ""
rule.For = 0
rule.NotificationSettings = nil
}
func nameToUid(name string) string { // Avoid legacy_storage.NameToUid import cycle.
return base64.RawURLEncoding.EncodeToString([]byte(name))
}