/
rule.go
212 lines (175 loc) · 6.23 KB
/
rule.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
package alerting
import (
"errors"
"fmt"
"regexp"
"strconv"
"time"
"github.com/famarks/grafarg/pkg/bus"
"github.com/famarks/grafarg/pkg/components/simplejson"
"github.com/famarks/grafarg/pkg/models"
)
var (
// ErrFrequencyCannotBeZeroOrLess frequency cannot be below zero
ErrFrequencyCannotBeZeroOrLess = errors.New(`"evaluate every" cannot be zero or below`)
// ErrFrequencyCouldNotBeParsed frequency cannot be parsed
ErrFrequencyCouldNotBeParsed = errors.New(`"evaluate every" field could not be parsed`)
)
// Rule is the in-memory version of an alert rule.
type Rule struct {
ID int64
OrgID int64
DashboardID int64
PanelID int64
Frequency int64
Name string
Message string
LastStateChange time.Time
For time.Duration
NoDataState models.NoDataOption
ExecutionErrorState models.ExecutionErrorOption
State models.AlertStateType
Conditions []Condition
Notifications []string
AlertRuleTags []*models.Tag
StateChanges int64
}
// ValidationError is a typed error with meta data
// about the validation error.
type ValidationError struct {
Reason string
Err error
AlertID int64
DashboardID int64
PanelID int64
}
func (e ValidationError) Error() string {
extraInfo := e.Reason
if e.AlertID != 0 {
extraInfo = fmt.Sprintf("%s AlertId: %v", extraInfo, e.AlertID)
}
if e.PanelID != 0 {
extraInfo = fmt.Sprintf("%s PanelId: %v", extraInfo, e.PanelID)
}
if e.DashboardID != 0 {
extraInfo = fmt.Sprintf("%s DashboardId: %v", extraInfo, e.DashboardID)
}
if e.Err != nil {
return fmt.Sprintf("alert validation error: %s%s", e.Err.Error(), extraInfo)
}
return fmt.Sprintf("alert validation error: %s", extraInfo)
}
var (
valueFormatRegex = regexp.MustCompile(`^\d+`)
unitFormatRegex = regexp.MustCompile(`\w{1}$`)
)
var unitMultiplier = map[string]int{
"s": 1,
"m": 60,
"h": 3600,
"d": 86400,
}
func getTimeDurationStringToSeconds(str string) (int64, error) {
multiplier := 1
matches := valueFormatRegex.FindAllString(str, 1)
if len(matches) == 0 {
return 0, ErrFrequencyCouldNotBeParsed
}
value, err := strconv.Atoi(matches[0])
if err != nil {
return 0, err
}
if value == 0 {
return 0, ErrFrequencyCannotBeZeroOrLess
}
unit := unitFormatRegex.FindAllString(str, 1)[0]
if val, ok := unitMultiplier[unit]; ok {
multiplier = val
}
return int64(value * multiplier), nil
}
// NewRuleFromDBAlert maps a db version of
// alert to an in-memory version.
func NewRuleFromDBAlert(ruleDef *models.Alert, logTranslationFailures bool) (*Rule, error) {
model := &Rule{}
model.ID = ruleDef.Id
model.OrgID = ruleDef.OrgId
model.DashboardID = ruleDef.DashboardId
model.PanelID = ruleDef.PanelId
model.Name = ruleDef.Name
model.Message = ruleDef.Message
model.State = ruleDef.State
model.LastStateChange = ruleDef.NewStateDate
model.For = ruleDef.For
model.NoDataState = models.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data"))
model.ExecutionErrorState = models.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
model.StateChanges = ruleDef.StateChanges
model.Frequency = ruleDef.Frequency
// frequency cannot be zero since that would not execute the alert rule.
// so we fallback to 60 seconds if `Frequency` is missing
if model.Frequency == 0 {
model.Frequency = 60
}
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
jsonModel := simplejson.NewFromAny(v)
if id, err := jsonModel.Get("id").Int64(); err == nil {
uid, err := translateNotificationIDToUID(id, ruleDef.OrgId)
if err != nil {
if !errors.Is(err, models.ErrAlertNotificationFailedTranslateUniqueID) {
logger.Error("Failed to translate notification id to uid", "error", err.Error(), "dashboardId", model.DashboardID, "alert", model.Name, "panelId", model.PanelID, "notificationId", id)
}
if logTranslationFailures {
logger.Warn("Unable to translate notification id to uid", "dashboardId", model.DashboardID, "alert", model.Name, "panelId", model.PanelID, "notificationId", id)
}
} else {
model.Notifications = append(model.Notifications, uid)
}
} else if uid, err := jsonModel.Get("uid").String(); err == nil {
model.Notifications = append(model.Notifications, uid)
} else {
return nil, ValidationError{Reason: "Neither id nor uid is specified in 'notifications' block, " + err.Error(), DashboardID: model.DashboardID, AlertID: model.ID, PanelID: model.PanelID}
}
}
model.AlertRuleTags = ruleDef.GetTagsFromSettings()
for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {
conditionModel := simplejson.NewFromAny(condition)
conditionType := conditionModel.Get("type").MustString()
factory, exist := conditionFactories[conditionType]
if !exist {
return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType, DashboardID: model.DashboardID, AlertID: model.ID, PanelID: model.PanelID}
}
queryCondition, err := factory(conditionModel, index)
if err != nil {
return nil, ValidationError{Err: err, DashboardID: model.DashboardID, AlertID: model.ID, PanelID: model.PanelID}
}
model.Conditions = append(model.Conditions, queryCondition)
}
if len(model.Conditions) == 0 {
return nil, ValidationError{Reason: "Alert is missing conditions"}
}
return model, nil
}
func translateNotificationIDToUID(id int64, orgID int64) (string, error) {
notificationUID, err := getAlertNotificationUIDByIDAndOrgID(id, orgID)
if err != nil {
return "", err
}
return notificationUID, nil
}
func getAlertNotificationUIDByIDAndOrgID(notificationID int64, orgID int64) (string, error) {
query := &models.GetAlertNotificationUidQuery{
OrgId: orgID,
Id: notificationID,
}
if err := bus.Dispatch(query); err != nil {
return "", err
}
return query.Result, nil
}
// ConditionFactory is the function signature for creating `Conditions`.
type ConditionFactory func(model *simplejson.Json, index int) (Condition, error)
var conditionFactories = make(map[string]ConditionFactory)
// RegisterCondition adds support for alerting conditions.
func RegisterCondition(typeName string, factory ConditionFactory) {
conditionFactories[typeName] = factory
}