mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
We have received a lot of feedback regarding the ValueString in alert notifications. Perhaps one of the most frequent complaints about ValueString is that it is difficult to read because it contains a lot of information, and the information is shown as a JSON-like string. Users have often asked how it can be templated and the answer is that it can't. Until now users have been able to add custom annotations to their alert rules which contains values via the $values variable added in previous versions of Grafana. However, these custom annotations must be added for each of the user's alert rule, instead of once in a template that all of their alerts can be notified via. This commit adds then the much requested feature to support values in notification templates. Users can then create a single template that prints the annotations, labels and values of their alerts in a format of their choice!
460 lines
14 KiB
Go
460 lines
14 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
|
|
"github.com/grafana/grafana/pkg/util/cmputil"
|
|
)
|
|
|
|
var (
|
|
// ErrAlertRuleNotFound is an error for an unknown alert rule.
|
|
ErrAlertRuleNotFound = fmt.Errorf("could not find alert rule")
|
|
// ErrAlertRuleFailedGenerateUniqueUID is an error for failure to generate alert rule UID
|
|
ErrAlertRuleFailedGenerateUniqueUID = errors.New("failed to generate alert rule UID")
|
|
// ErrCannotEditNamespace is an error returned if the user does not have permissions to edit the namespace
|
|
ErrCannotEditNamespace = errors.New("user does not have permissions to edit the namespace")
|
|
ErrRuleGroupNamespaceNotFound = errors.New("rule group not found under this namespace")
|
|
ErrAlertRuleFailedValidation = errors.New("invalid alert rule")
|
|
ErrAlertRuleUniqueConstraintViolation = errors.New("a conflicting alert rule is found: rule title under the same organisation and folder should be unique")
|
|
ErrQuotaReached = errors.New("quota has been exceeded")
|
|
)
|
|
|
|
// swagger:enum NoDataState
|
|
type NoDataState string
|
|
|
|
func (noDataState NoDataState) String() string {
|
|
return string(noDataState)
|
|
}
|
|
|
|
func NoDataStateFromString(state string) (NoDataState, error) {
|
|
switch state {
|
|
case string(Alerting):
|
|
return Alerting, nil
|
|
case string(NoData):
|
|
return NoData, nil
|
|
case string(OK):
|
|
return OK, nil
|
|
default:
|
|
return "", fmt.Errorf("unknown NoData state option %s", state)
|
|
}
|
|
}
|
|
|
|
const (
|
|
Alerting NoDataState = "Alerting"
|
|
NoData NoDataState = "NoData"
|
|
OK NoDataState = "OK"
|
|
)
|
|
|
|
// swagger:enum ExecutionErrorState
|
|
type ExecutionErrorState string
|
|
|
|
func (executionErrorState ExecutionErrorState) String() string {
|
|
return string(executionErrorState)
|
|
}
|
|
|
|
func ErrStateFromString(opt string) (ExecutionErrorState, error) {
|
|
switch opt {
|
|
case string(Alerting):
|
|
return AlertingErrState, nil
|
|
case string(ErrorErrState):
|
|
return ErrorErrState, nil
|
|
case string(OkErrState):
|
|
return OkErrState, nil
|
|
default:
|
|
return "", fmt.Errorf("unknown Error state option %s", opt)
|
|
}
|
|
}
|
|
|
|
const (
|
|
AlertingErrState ExecutionErrorState = "Alerting"
|
|
ErrorErrState ExecutionErrorState = "Error"
|
|
OkErrState ExecutionErrorState = "OK"
|
|
)
|
|
|
|
const (
|
|
RuleUIDLabel = "__alert_rule_uid__"
|
|
NamespaceUIDLabel = "__alert_rule_namespace_uid__"
|
|
|
|
// Annotations are actually a set of labels, so technically this is the label name of an annotation.
|
|
DashboardUIDAnnotation = "__dashboardUid__"
|
|
PanelIDAnnotation = "__panelId__"
|
|
|
|
// This isn't a hard-coded secret token, hence the nolint.
|
|
//nolint:gosec
|
|
ImageTokenAnnotation = "__alertImageToken__"
|
|
|
|
// GrafanaReservedLabelPrefix contains the prefix for Grafana reserved labels. These differ from "__<label>__" labels
|
|
// in that they are not meant for internal-use only and will be passed-through to AMs and available to users in the same
|
|
// way as manually configured labels.
|
|
GrafanaReservedLabelPrefix = "grafana_"
|
|
|
|
// FolderTitleLabel is the label that will contain the title of an alert's folder/namespace.
|
|
FolderTitleLabel = GrafanaReservedLabelPrefix + "folder"
|
|
|
|
// StateReasonAnnotation is the name of the annotation that explains the difference between evaluation state and alert state (i.e. changing state when NoData or Error).
|
|
StateReasonAnnotation = GrafanaReservedLabelPrefix + "state_reason"
|
|
|
|
ValuesAnnotation = "__values__"
|
|
ValueStringAnnotation = "__value_string__"
|
|
)
|
|
|
|
var (
|
|
StateReasonMissingSeries = "MissingSeries"
|
|
)
|
|
|
|
var (
|
|
// InternalLabelNameSet are labels that grafana automatically include as part of the labelset.
|
|
InternalLabelNameSet = map[string]struct{}{
|
|
RuleUIDLabel: {},
|
|
NamespaceUIDLabel: {},
|
|
}
|
|
InternalAnnotationNameSet = map[string]struct{}{
|
|
DashboardUIDAnnotation: {},
|
|
PanelIDAnnotation: {},
|
|
ImageTokenAnnotation: {},
|
|
}
|
|
)
|
|
|
|
// AlertRuleGroup is the base model for a rule group in unified alerting.
|
|
type AlertRuleGroup struct {
|
|
Title string
|
|
FolderUID string
|
|
Interval int64
|
|
Provenance Provenance
|
|
Rules []AlertRule
|
|
}
|
|
|
|
// AlertRule is the model for alert rules in unified alerting.
|
|
type AlertRule struct {
|
|
ID int64 `xorm:"pk autoincr 'id'"`
|
|
OrgID int64 `xorm:"org_id"`
|
|
Title string
|
|
Condition string
|
|
Data []AlertQuery
|
|
Updated time.Time
|
|
IntervalSeconds int64
|
|
Version int64 `xorm:"version"` // this tag makes xorm add optimistic lock (see https://xorm.io/docs/chapter-06/1.lock/)
|
|
UID string `xorm:"uid"`
|
|
NamespaceUID string `xorm:"namespace_uid"`
|
|
DashboardUID *string `xorm:"dashboard_uid"`
|
|
PanelID *int64 `xorm:"panel_id"`
|
|
RuleGroup string
|
|
RuleGroupIndex int `xorm:"rule_group_idx"`
|
|
NoDataState NoDataState
|
|
ExecErrState ExecutionErrorState
|
|
// ideally this field should have been apimodels.ApiDuration
|
|
// but this is currently not possible because of circular dependencies
|
|
For time.Duration
|
|
Annotations map[string]string
|
|
Labels map[string]string
|
|
}
|
|
|
|
type LabelOption func(map[string]string)
|
|
|
|
func WithoutInternalLabels() LabelOption {
|
|
return func(labels map[string]string) {
|
|
for k := range labels {
|
|
if _, ok := InternalLabelNameSet[k]; ok {
|
|
delete(labels, k)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetLabels returns the labels specified as part of the alert rule.
|
|
func (alertRule *AlertRule) GetLabels(opts ...LabelOption) map[string]string {
|
|
labels := alertRule.Labels
|
|
|
|
for _, opt := range opts {
|
|
opt(labels)
|
|
}
|
|
|
|
return labels
|
|
}
|
|
|
|
func (alertRule *AlertRule) GetEvalCondition() Condition {
|
|
return Condition{
|
|
Condition: alertRule.Condition,
|
|
Data: alertRule.Data,
|
|
}
|
|
}
|
|
|
|
// Diff calculates diff between two alert rules. Returns nil if two rules are equal. Otherwise, returns cmputil.DiffReport
|
|
func (alertRule *AlertRule) Diff(rule *AlertRule, ignore ...string) cmputil.DiffReport {
|
|
var reporter cmputil.DiffReporter
|
|
ops := make([]cmp.Option, 0, 5)
|
|
|
|
// json.RawMessage is a slice of bytes and therefore cmp's default behavior is to compare it by byte, which is not really useful
|
|
var jsonCmp = cmp.Transformer("", func(in json.RawMessage) string {
|
|
return string(in)
|
|
})
|
|
ops = append(ops, cmp.Reporter(&reporter), cmpopts.IgnoreFields(AlertQuery{}, "modelProps"), jsonCmp, cmpopts.EquateEmpty())
|
|
|
|
if len(ignore) > 0 {
|
|
ops = append(ops, cmpopts.IgnoreFields(AlertRule{}, ignore...))
|
|
}
|
|
cmp.Equal(alertRule, rule, ops...)
|
|
return reporter.Diffs
|
|
}
|
|
|
|
// SetDashboardAndPanel will set the DashboardUID and PanlID
|
|
// field be doing a lookup in the annotations. Errors when
|
|
// the found annotations are not valid.
|
|
func (alertRule *AlertRule) SetDashboardAndPanel() error {
|
|
if alertRule.Annotations == nil {
|
|
return nil
|
|
}
|
|
dashUID := alertRule.Annotations[DashboardUIDAnnotation]
|
|
panelID := alertRule.Annotations[PanelIDAnnotation]
|
|
if dashUID != "" && panelID == "" || dashUID == "" && panelID != "" {
|
|
return fmt.Errorf("both annotations %s and %s must be specified",
|
|
DashboardUIDAnnotation, PanelIDAnnotation)
|
|
}
|
|
if dashUID != "" {
|
|
panelIDValue, err := strconv.ParseInt(panelID, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("annotation %s must be a valid integer Panel ID",
|
|
PanelIDAnnotation)
|
|
}
|
|
alertRule.DashboardUID = &dashUID
|
|
alertRule.PanelID = &panelIDValue
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AlertRuleKey is the alert definition identifier
|
|
type AlertRuleKey struct {
|
|
OrgID int64 `xorm:"org_id"`
|
|
UID string `xorm:"uid"`
|
|
}
|
|
|
|
func (k AlertRuleKey) LogContext() []interface{} {
|
|
return []interface{}{"rule_uid", k.UID, "org_id", k.OrgID}
|
|
}
|
|
|
|
type AlertRuleKeyWithVersion struct {
|
|
Version int64
|
|
AlertRuleKey `xorm:"extends"`
|
|
}
|
|
|
|
// AlertRuleGroupKey is the identifier of a group of alerts
|
|
type AlertRuleGroupKey struct {
|
|
OrgID int64
|
|
NamespaceUID string
|
|
RuleGroup string
|
|
}
|
|
|
|
func (k AlertRuleGroupKey) String() string {
|
|
return fmt.Sprintf("{orgID: %d, namespaceUID: %s, groupName: %s}", k.OrgID, k.NamespaceUID, k.RuleGroup)
|
|
}
|
|
|
|
func (k AlertRuleKey) String() string {
|
|
return fmt.Sprintf("{orgID: %d, UID: %s}", k.OrgID, k.UID)
|
|
}
|
|
|
|
// GetKey returns the alert definitions identifier
|
|
func (alertRule *AlertRule) GetKey() AlertRuleKey {
|
|
return AlertRuleKey{OrgID: alertRule.OrgID, UID: alertRule.UID}
|
|
}
|
|
|
|
// GetGroupKey returns the identifier of a group the rule belongs to
|
|
func (alertRule *AlertRule) GetGroupKey() AlertRuleGroupKey {
|
|
return AlertRuleGroupKey{OrgID: alertRule.OrgID, NamespaceUID: alertRule.NamespaceUID, RuleGroup: alertRule.RuleGroup}
|
|
}
|
|
|
|
// PreSave sets default values and loads the updated model for each alert query.
|
|
func (alertRule *AlertRule) PreSave(timeNow func() time.Time) error {
|
|
for i, q := range alertRule.Data {
|
|
err := q.PreSave()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid alert query %s: %w", q.RefID, err)
|
|
}
|
|
alertRule.Data[i] = q
|
|
}
|
|
alertRule.Updated = timeNow()
|
|
return nil
|
|
}
|
|
|
|
func (alertRule *AlertRule) ResourceType() string {
|
|
return "alertRule"
|
|
}
|
|
|
|
func (alertRule *AlertRule) ResourceID() string {
|
|
return alertRule.UID
|
|
}
|
|
|
|
func (alertRule *AlertRule) ResourceOrgID() int64 {
|
|
return alertRule.OrgID
|
|
}
|
|
|
|
// AlertRuleVersion is the model for alert rule versions in unified alerting.
|
|
type AlertRuleVersion struct {
|
|
ID int64 `xorm:"pk autoincr 'id'"`
|
|
RuleOrgID int64 `xorm:"rule_org_id"`
|
|
RuleUID string `xorm:"rule_uid"`
|
|
RuleNamespaceUID string `xorm:"rule_namespace_uid"`
|
|
RuleGroup string
|
|
RuleGroupIndex int `xorm:"rule_group_idx"`
|
|
ParentVersion int64
|
|
RestoredFrom int64
|
|
Version int64
|
|
|
|
Created time.Time
|
|
Title string
|
|
Condition string
|
|
Data []AlertQuery
|
|
IntervalSeconds int64
|
|
NoDataState NoDataState
|
|
ExecErrState ExecutionErrorState
|
|
// ideally this field should have been apimodels.ApiDuration
|
|
// but this is currently not possible because of circular dependencies
|
|
For time.Duration
|
|
Annotations map[string]string
|
|
Labels map[string]string
|
|
}
|
|
|
|
// GetAlertRuleByUIDQuery is the query for retrieving/deleting an alert rule by UID and organisation ID.
|
|
type GetAlertRuleByUIDQuery struct {
|
|
UID string
|
|
OrgID int64
|
|
|
|
Result *AlertRule
|
|
}
|
|
|
|
// GetAlertRulesGroupByRuleUIDQuery is the query for retrieving a group of alerts by UID of a rule that belongs to that group
|
|
type GetAlertRulesGroupByRuleUIDQuery struct {
|
|
UID string
|
|
OrgID int64
|
|
|
|
Result []*AlertRule
|
|
}
|
|
|
|
// ListAlertRulesQuery is the query for listing alert rules
|
|
type ListAlertRulesQuery struct {
|
|
OrgID int64
|
|
NamespaceUIDs []string
|
|
ExcludeOrgs []int64
|
|
RuleGroup string
|
|
|
|
// DashboardUID and PanelID are optional and allow filtering rules
|
|
// to return just those for a dashboard and panel.
|
|
DashboardUID string
|
|
PanelID int64
|
|
|
|
Result RulesGroup
|
|
}
|
|
|
|
type GetAlertRulesForSchedulingQuery struct {
|
|
PopulateFolders bool
|
|
|
|
ResultRules []*AlertRule
|
|
ResultFoldersTitles map[string]string
|
|
}
|
|
|
|
// ListNamespaceAlertRulesQuery is the query for listing namespace alert rules
|
|
type ListNamespaceAlertRulesQuery struct {
|
|
OrgID int64
|
|
// Namespace is the folder slug
|
|
NamespaceUID string
|
|
|
|
Result []*AlertRule
|
|
}
|
|
|
|
// ListOrgRuleGroupsQuery is the query for listing unique rule groups
|
|
// for an organization
|
|
type ListOrgRuleGroupsQuery struct {
|
|
OrgID int64
|
|
NamespaceUIDs []string
|
|
|
|
// DashboardUID and PanelID are optional and allow filtering rules
|
|
// to return just those for a dashboard and panel.
|
|
DashboardUID string
|
|
PanelID int64
|
|
|
|
Result [][]string
|
|
}
|
|
|
|
type UpdateRule struct {
|
|
Existing *AlertRule
|
|
New AlertRule
|
|
}
|
|
|
|
// Condition contains backend expressions and queries and the RefID
|
|
// of the query or expression that will be evaluated.
|
|
type Condition struct {
|
|
// Condition is the RefID of the query or expression from
|
|
// the Data property to get the results for.
|
|
Condition string `json:"condition"`
|
|
|
|
// Data is an array of data source queries and/or server side expressions.
|
|
Data []AlertQuery `json:"data"`
|
|
}
|
|
|
|
// IsValid checks the condition's validity.
|
|
func (c Condition) IsValid() bool {
|
|
// TODO search for refIDs in QueriesAndExpressions
|
|
return len(c.Data) != 0
|
|
}
|
|
|
|
// PatchPartialAlertRule patches `ruleToPatch` by `existingRule` following the rule that if a field of `ruleToPatch` is empty or has the default value, it is populated by the value of the corresponding field from `existingRule`.
|
|
// There are several exceptions:
|
|
// 1. Following fields are not patched and therefore will be ignored: AlertRule.ID, AlertRule.OrgID, AlertRule.Updated, AlertRule.Version, AlertRule.UID, AlertRule.DashboardUID, AlertRule.PanelID, AlertRule.Annotations and AlertRule.Labels
|
|
// 2. There are fields that are patched together:
|
|
// - AlertRule.Condition and AlertRule.Data
|
|
//
|
|
// If either of the pair is specified, neither is patched.
|
|
func PatchPartialAlertRule(existingRule *AlertRule, ruleToPatch *AlertRule) {
|
|
if ruleToPatch.Title == "" {
|
|
ruleToPatch.Title = existingRule.Title
|
|
}
|
|
if ruleToPatch.Condition == "" || len(ruleToPatch.Data) == 0 {
|
|
ruleToPatch.Condition = existingRule.Condition
|
|
ruleToPatch.Data = existingRule.Data
|
|
}
|
|
if ruleToPatch.IntervalSeconds == 0 {
|
|
ruleToPatch.IntervalSeconds = existingRule.IntervalSeconds
|
|
}
|
|
if ruleToPatch.NamespaceUID == "" {
|
|
ruleToPatch.NamespaceUID = existingRule.NamespaceUID
|
|
}
|
|
if ruleToPatch.RuleGroup == "" {
|
|
ruleToPatch.RuleGroup = existingRule.RuleGroup
|
|
}
|
|
if ruleToPatch.ExecErrState == "" {
|
|
ruleToPatch.ExecErrState = existingRule.ExecErrState
|
|
}
|
|
if ruleToPatch.NoDataState == "" {
|
|
ruleToPatch.NoDataState = existingRule.NoDataState
|
|
}
|
|
if ruleToPatch.For == -1 {
|
|
ruleToPatch.For = existingRule.For
|
|
}
|
|
}
|
|
|
|
func ValidateRuleGroupInterval(intervalSeconds, baseIntervalSeconds int64) error {
|
|
if intervalSeconds%baseIntervalSeconds != 0 || intervalSeconds <= 0 {
|
|
return fmt.Errorf("%w: interval (%v) should be non-zero and divided exactly by scheduler interval: %v",
|
|
ErrAlertRuleFailedValidation, time.Duration(intervalSeconds)*time.Second, baseIntervalSeconds)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type RulesGroup []*AlertRule
|
|
|
|
func (g RulesGroup) SortByGroupIndex() {
|
|
sort.Slice(g, func(i, j int) bool {
|
|
if g[i].RuleGroupIndex == g[j].RuleGroupIndex {
|
|
return g[i].ID < g[j].ID
|
|
}
|
|
return g[i].RuleGroupIndex < g[j].RuleGroupIndex
|
|
})
|
|
}
|