mirror of
https://github.com/grafana/grafana.git
synced 2024-12-01 21:19:28 -06:00
a20197229e
Prevent zero duration GroupInterval and RepeatInterval
186 lines
6.4 KiB
Go
186 lines
6.4 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"hash/fnv"
|
|
"slices"
|
|
"unsafe"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
"github.com/prometheus/common/model"
|
|
)
|
|
|
|
// GroupByAll is a special value defined by alertmanager that can be used in a Route's GroupBy field to aggregate by all possible labels.
|
|
const GroupByAll = "..."
|
|
|
|
// DefaultNotificationSettingsGroupBy are the default required GroupBy fields for notification settings.
|
|
var DefaultNotificationSettingsGroupBy = []string{FolderTitleLabel, model.AlertNameLabel}
|
|
|
|
type ListNotificationSettingsQuery struct {
|
|
OrgID int64
|
|
ReceiverName string
|
|
}
|
|
|
|
// NotificationSettings represents the settings for sending notifications for a single AlertRule. It is used to
|
|
// automatically generate labels and an associated matching route containing the given settings.
|
|
type NotificationSettings struct {
|
|
Receiver string `json:"receiver"`
|
|
|
|
GroupBy []string `json:"group_by,omitempty"`
|
|
GroupWait *model.Duration `json:"group_wait,omitempty"`
|
|
GroupInterval *model.Duration `json:"group_interval,omitempty"`
|
|
RepeatInterval *model.Duration `json:"repeat_interval,omitempty"`
|
|
MuteTimeIntervals []string `json:"mute_time_intervals,omitempty"`
|
|
}
|
|
|
|
// NormalizedGroupBy returns a consistent and ordered GroupBy.
|
|
// - If the GroupBy is empty, it returns nil so that the parent group can be inherited.
|
|
// - If the GroupBy contains the special label '...', it returns only '...'.
|
|
// - Otherwise, it returns the default GroupBy labels followed by any custom labels in sorted order.
|
|
//
|
|
// To ensure consistent and valid generated routes, this should be used instead of GroupBy when generating fingerprints
|
|
// or fingerprint-level routes.
|
|
func (s *NotificationSettings) NormalizedGroupBy() []string {
|
|
if len(s.GroupBy) == 0 {
|
|
// Inherit group from parent.
|
|
return nil
|
|
}
|
|
|
|
defaultGroupBySet := make(map[string]struct{}, len(DefaultNotificationSettingsGroupBy))
|
|
for _, lbl := range DefaultNotificationSettingsGroupBy {
|
|
defaultGroupBySet[lbl] = struct{}{}
|
|
}
|
|
|
|
var customLabels []string
|
|
for _, lbl := range s.GroupBy {
|
|
if lbl == GroupByAll {
|
|
return []string{GroupByAll}
|
|
}
|
|
if _, ok := defaultGroupBySet[lbl]; !ok {
|
|
customLabels = append(customLabels, lbl)
|
|
}
|
|
}
|
|
|
|
// Sort the custom labels to ensure consistent ordering while keeping the required labels in the front.
|
|
slices.Sort(customLabels)
|
|
|
|
normalized := make([]string, 0, len(DefaultNotificationSettingsGroupBy)+len(customLabels))
|
|
normalized = append(normalized, DefaultNotificationSettingsGroupBy...)
|
|
return append(normalized, customLabels...)
|
|
}
|
|
|
|
// Validate checks if the NotificationSettings object is valid.
|
|
// It returns an error if any of the validation checks fail.
|
|
// The receiver must be specified.
|
|
// GroupWait, GroupInterval, RepeatInterval must be positive durations.
|
|
func (s *NotificationSettings) Validate() error {
|
|
if s.Receiver == "" {
|
|
return errors.New("receiver must be specified")
|
|
}
|
|
if s.GroupWait != nil && *s.GroupWait < 0 {
|
|
return errors.New("group wait must be a positive duration")
|
|
}
|
|
if s.GroupInterval != nil && *s.GroupInterval <= 0 {
|
|
return errors.New("group interval must be greater than zero")
|
|
}
|
|
if s.RepeatInterval != nil && *s.RepeatInterval <= 0 {
|
|
return errors.New("repeat interval must be greater than zero")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ToLabels converts the NotificationSettings into data.Labels. When added to an AlertRule these labels ensure it will
|
|
// match an autogenerated route with the correct settings.
|
|
// Labels returned:
|
|
// - AutogeneratedRouteLabel: "true"
|
|
// - AutogeneratedRouteReceiverNameLabel: Receiver
|
|
// - AutogeneratedRouteSettingsHashLabel: Fingerprint (if the NotificationSettings are not all default)
|
|
func (s *NotificationSettings) ToLabels() data.Labels {
|
|
result := make(data.Labels, 3)
|
|
result[AutogeneratedRouteLabel] = "true"
|
|
result[AutogeneratedRouteReceiverNameLabel] = s.Receiver
|
|
if !s.IsAllDefault() {
|
|
result[AutogeneratedRouteSettingsHashLabel] = s.Fingerprint().String()
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (s *NotificationSettings) Equals(other *NotificationSettings) bool {
|
|
durationEqual := func(d1, d2 *model.Duration) bool {
|
|
if d1 == nil || d2 == nil {
|
|
return d1 == d2
|
|
}
|
|
return *d1 == *d2
|
|
}
|
|
if s == nil || other == nil {
|
|
return s == nil && other == nil
|
|
}
|
|
if s.Receiver != other.Receiver {
|
|
return false
|
|
}
|
|
if !durationEqual(s.GroupWait, other.GroupWait) {
|
|
return false
|
|
}
|
|
if !durationEqual(s.GroupInterval, other.GroupInterval) {
|
|
return false
|
|
}
|
|
if !durationEqual(s.RepeatInterval, other.RepeatInterval) {
|
|
return false
|
|
}
|
|
if !slices.Equal(s.MuteTimeIntervals, other.MuteTimeIntervals) {
|
|
return false
|
|
}
|
|
sGr := s.GroupBy
|
|
oGr := other.GroupBy
|
|
return slices.Equal(sGr, oGr)
|
|
}
|
|
|
|
// IsAllDefault checks if the NotificationSettings object has all default values for optional fields (all except Receiver) .
|
|
func (s *NotificationSettings) IsAllDefault() bool {
|
|
return len(s.GroupBy) == 0 && s.GroupWait == nil && s.GroupInterval == nil && s.RepeatInterval == nil && len(s.MuteTimeIntervals) == 0
|
|
}
|
|
|
|
// NewDefaultNotificationSettings creates a new default NotificationSettings with the specified receiver.
|
|
func NewDefaultNotificationSettings(receiver string) NotificationSettings {
|
|
return NotificationSettings{
|
|
Receiver: receiver,
|
|
}
|
|
}
|
|
|
|
// Fingerprint calculates a hash value to uniquely identify a NotificationSettings by its attributes.
|
|
// The hash is calculated by concatenating the strings and durations of the NotificationSettings attributes
|
|
// and using an invalid UTF-8 sequence as a separator.
|
|
func (s *NotificationSettings) Fingerprint() data.Fingerprint {
|
|
h := fnv.New64()
|
|
tmp := make([]byte, 8)
|
|
|
|
writeString := func(s string) {
|
|
// save on extra slice allocation when string is converted to bytes.
|
|
_, _ = h.Write(unsafe.Slice(unsafe.StringData(s), len(s))) //nolint:gosec
|
|
// ignore errors returned by Write method because fnv never returns them.
|
|
_, _ = h.Write([]byte{255}) // use an invalid utf-8 sequence as separator
|
|
}
|
|
writeDuration := func(d *model.Duration) {
|
|
if d == nil {
|
|
_, _ = h.Write([]byte{255})
|
|
} else {
|
|
binary.LittleEndian.PutUint64(tmp, uint64(*d))
|
|
_, _ = h.Write(tmp)
|
|
_, _ = h.Write([]byte{255})
|
|
}
|
|
}
|
|
|
|
writeString(s.Receiver)
|
|
for _, gb := range s.NormalizedGroupBy() {
|
|
writeString(gb)
|
|
}
|
|
writeDuration(s.GroupWait)
|
|
writeDuration(s.GroupInterval)
|
|
writeDuration(s.RepeatInterval)
|
|
for _, interval := range s.MuteTimeIntervals {
|
|
writeString(interval)
|
|
}
|
|
return data.Fingerprint(h.Sum64())
|
|
}
|