mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
6f38ac6615
We want to avoid too much change of alert state based on change on alert's fields. For that we ignore some fields from the diff.
316 lines
7.9 KiB
Go
316 lines
7.9 KiB
Go
package schedule
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"math"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
)
|
|
|
|
var errRuleDeleted = errors.New("rule deleted")
|
|
|
|
type ruleFactory interface {
|
|
new(context.Context) Rule
|
|
}
|
|
|
|
type ruleRegistry struct {
|
|
mu sync.Mutex
|
|
rules map[models.AlertRuleKey]Rule
|
|
}
|
|
|
|
func newRuleRegistry() ruleRegistry {
|
|
return ruleRegistry{rules: make(map[models.AlertRuleKey]Rule)}
|
|
}
|
|
|
|
// getOrCreate gets rule routine from registry by the key. If it does not exist, it creates a new one.
|
|
// Returns a pointer to the rule routine and a flag that indicates whether it is a new struct or not.
|
|
func (r *ruleRegistry) getOrCreate(context context.Context, key models.AlertRuleKey, factory ruleFactory) (Rule, bool) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
rule, ok := r.rules[key]
|
|
if !ok {
|
|
rule = factory.new(context)
|
|
r.rules[key] = rule
|
|
}
|
|
return rule, !ok
|
|
}
|
|
|
|
func (r *ruleRegistry) exists(key models.AlertRuleKey) bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
_, ok := r.rules[key]
|
|
return ok
|
|
}
|
|
|
|
// del removes pair that has specific key from the registry.
|
|
// Returns 2-tuple where the first element is value of the removed pair
|
|
// and the second element indicates whether element with the specified key existed.
|
|
func (r *ruleRegistry) del(key models.AlertRuleKey) (Rule, bool) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
rule, ok := r.rules[key]
|
|
if ok {
|
|
delete(r.rules, key)
|
|
}
|
|
return rule, ok
|
|
}
|
|
|
|
func (r *ruleRegistry) keyMap() map[models.AlertRuleKey]struct{} {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
definitionsIDs := make(map[models.AlertRuleKey]struct{}, len(r.rules))
|
|
for k := range r.rules {
|
|
definitionsIDs[k] = struct{}{}
|
|
}
|
|
return definitionsIDs
|
|
}
|
|
|
|
type RuleVersionAndPauseStatus struct {
|
|
Fingerprint fingerprint
|
|
IsPaused bool
|
|
}
|
|
|
|
type Evaluation struct {
|
|
scheduledAt time.Time
|
|
rule *models.AlertRule
|
|
folderTitle string
|
|
}
|
|
|
|
type alertRulesRegistry struct {
|
|
rules map[models.AlertRuleKey]*models.AlertRule
|
|
folderTitles map[models.FolderKey]string
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// all returns all rules in the registry.
|
|
func (r *alertRulesRegistry) all() ([]*models.AlertRule, map[models.FolderKey]string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
result := make([]*models.AlertRule, 0, len(r.rules))
|
|
for _, rule := range r.rules {
|
|
result = append(result, rule)
|
|
}
|
|
return result, r.folderTitles
|
|
}
|
|
|
|
func (r *alertRulesRegistry) get(k models.AlertRuleKey) *models.AlertRule {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.rules[k]
|
|
}
|
|
|
|
// set replaces all rules in the registry. Returns difference between previous and the new current version of the registry
|
|
func (r *alertRulesRegistry) set(rules []*models.AlertRule, folders map[models.FolderKey]string) diff {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
rulesMap := make(map[models.AlertRuleKey]*models.AlertRule)
|
|
for _, rule := range rules {
|
|
rulesMap[rule.GetKey()] = rule
|
|
}
|
|
d := r.getDiff(rulesMap)
|
|
r.rules = rulesMap
|
|
// return the map as is without copying because it is not mutated
|
|
r.folderTitles = folders
|
|
return d
|
|
}
|
|
|
|
// update inserts or replaces a rule in the registry.
|
|
func (r *alertRulesRegistry) update(rule *models.AlertRule) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.rules[rule.GetKey()] = rule
|
|
}
|
|
|
|
// del removes pair that has specific key from alertRulesRegistry.
|
|
// Returns 2-tuple where the first element is value of the removed pair
|
|
// and the second element indicates whether element with the specified key existed.
|
|
func (r *alertRulesRegistry) del(k models.AlertRuleKey) (*models.AlertRule, bool) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
rule, ok := r.rules[k]
|
|
if ok {
|
|
delete(r.rules, k)
|
|
}
|
|
return rule, ok
|
|
}
|
|
|
|
func (r *alertRulesRegistry) isEmpty() bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return len(r.rules) == 0
|
|
}
|
|
|
|
func (r *alertRulesRegistry) needsUpdate(keys []models.AlertRuleKeyWithVersion) bool {
|
|
if len(r.rules) != len(keys) {
|
|
return true
|
|
}
|
|
for _, key := range keys {
|
|
rule, ok := r.rules[key.AlertRuleKey]
|
|
if !ok || rule.Version != key.Version {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type diff struct {
|
|
updated map[models.AlertRuleKey]struct{}
|
|
}
|
|
|
|
func (d diff) IsEmpty() bool {
|
|
return len(d.updated) == 0
|
|
}
|
|
|
|
// getDiff calculates difference between the list of rules fetched previously and provided keys. Returns diff where
|
|
// updated - a list of keys that exist in the registry but with different version,
|
|
func (r *alertRulesRegistry) getDiff(rules map[models.AlertRuleKey]*models.AlertRule) diff {
|
|
result := diff{
|
|
updated: map[models.AlertRuleKey]struct{}{},
|
|
}
|
|
for key, newRule := range rules {
|
|
oldRule, ok := r.rules[key]
|
|
if !ok || newRule.Version == oldRule.Version {
|
|
// a new rule or not updated
|
|
continue
|
|
}
|
|
result.updated[key] = struct{}{}
|
|
}
|
|
return result
|
|
}
|
|
|
|
type fingerprint uint64
|
|
|
|
func (f fingerprint) String() string {
|
|
return fmt.Sprintf("%016x", uint64(f))
|
|
}
|
|
|
|
// fingerprintSeparator used during calculation of fingerprint to separate different fields. Contains a byte sequence that cannot happen in UTF-8 strings.
|
|
var fingerprintSeparator = []byte{255}
|
|
|
|
type ruleWithFolder struct {
|
|
rule *models.AlertRule
|
|
folderTitle string
|
|
}
|
|
|
|
// fingerprint calculates a fingerprint that includes all fields except rule's Version and Update timestamp.
|
|
func (r ruleWithFolder) Fingerprint() fingerprint {
|
|
rule := r.rule
|
|
|
|
sum := fnv.New64()
|
|
|
|
writeBytes := func(b []byte) {
|
|
_, _ = sum.Write(b)
|
|
_, _ = sum.Write(fingerprintSeparator)
|
|
}
|
|
writeString := func(s string) {
|
|
if len(s) == 0 {
|
|
writeBytes(nil)
|
|
return
|
|
}
|
|
// #nosec G103
|
|
// avoid allocation when converting string to byte slice
|
|
writeBytes(unsafe.Slice(unsafe.StringData(s), len(s)))
|
|
}
|
|
// this temp slice is used to convert ints to bytes.
|
|
tmp := make([]byte, 8)
|
|
writeInt := func(u int64) {
|
|
binary.LittleEndian.PutUint64(tmp, uint64(u))
|
|
writeBytes(tmp)
|
|
}
|
|
|
|
// allocate a slice that will be used for sorting keys, so we allocate it only once
|
|
var keys []string
|
|
maxLen := int(math.Max(math.Max(float64(len(rule.Annotations)), float64(len(rule.Labels))), float64(len(rule.Data))))
|
|
if maxLen > 0 {
|
|
keys = make([]string, maxLen)
|
|
}
|
|
|
|
writeLabels := func(lbls map[string]string) {
|
|
// maps do not guarantee predictable sequence of keys.
|
|
// Therefore, to make hash stable, we need to sort keys
|
|
if len(lbls) == 0 {
|
|
return
|
|
}
|
|
idx := 0
|
|
for labelName := range lbls {
|
|
keys[idx] = labelName
|
|
idx++
|
|
}
|
|
sub := keys[:idx]
|
|
sort.Strings(sub)
|
|
for _, name := range sub {
|
|
writeString(name)
|
|
writeString(lbls[name])
|
|
}
|
|
}
|
|
writeQuery := func() {
|
|
// The order of queries is not important as they represent an expression tree.
|
|
// Therefore, the order of elements should not change the hash. Sort by RefID because it is the unique key.
|
|
for i, q := range rule.Data {
|
|
keys[i] = q.RefID
|
|
}
|
|
sub := keys[:len(rule.Data)]
|
|
sort.Strings(sub)
|
|
for _, id := range sub {
|
|
for _, q := range rule.Data {
|
|
if q.RefID == id {
|
|
writeString(q.RefID)
|
|
writeString(q.DatasourceUID)
|
|
writeString(q.QueryType)
|
|
writeInt(int64(q.RelativeTimeRange.From))
|
|
writeInt(int64(q.RelativeTimeRange.To))
|
|
writeBytes(q.Model)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// fields that determine the rule state
|
|
writeString(rule.UID)
|
|
writeString(rule.Title)
|
|
writeString(rule.NamespaceUID)
|
|
writeString(r.folderTitle)
|
|
writeLabels(rule.Labels)
|
|
writeString(rule.Condition)
|
|
writeQuery()
|
|
|
|
if rule.IsPaused {
|
|
writeInt(1)
|
|
} else {
|
|
writeInt(0)
|
|
}
|
|
|
|
for _, setting := range rule.NotificationSettings {
|
|
binary.LittleEndian.PutUint64(tmp, uint64(setting.Fingerprint()))
|
|
writeBytes(tmp)
|
|
}
|
|
|
|
// fields that do not affect the state.
|
|
// TODO consider removing fields below from the fingerprint
|
|
writeInt(rule.ID)
|
|
writeInt(rule.OrgID)
|
|
writeInt(int64(rule.For))
|
|
if rule.DashboardUID != nil {
|
|
writeString(*rule.DashboardUID)
|
|
}
|
|
if rule.PanelID != nil {
|
|
writeInt(*rule.PanelID)
|
|
}
|
|
writeString(rule.RuleGroup)
|
|
writeInt(int64(rule.RuleGroupIndex))
|
|
writeString(string(rule.NoDataState))
|
|
writeString(string(rule.ExecErrState))
|
|
return fingerprint(sum.Sum64())
|
|
}
|