2021-04-23 14:32:25 -05:00
|
|
|
package state
|
|
|
|
|
|
|
|
import (
|
2021-11-25 04:46:47 -06:00
|
|
|
"errors"
|
2022-04-13 13:45:29 -05:00
|
|
|
"fmt"
|
2022-04-27 13:59:13 -05:00
|
|
|
"math"
|
2022-04-05 13:36:42 -05:00
|
|
|
"strings"
|
2021-04-23 14:32:25 -05:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
2021-11-04 15:42:34 -05:00
|
|
|
|
2021-11-25 04:46:47 -06:00
|
|
|
"github.com/grafana/grafana/pkg/expr"
|
2021-04-23 14:32:25 -05:00
|
|
|
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
2022-05-23 03:49:49 -05:00
|
|
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
2021-04-23 14:32:25 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
type State struct {
|
2022-05-23 03:49:49 -05:00
|
|
|
AlertRuleUID string
|
|
|
|
OrgID int64
|
|
|
|
CacheId string
|
|
|
|
|
|
|
|
StartsAt time.Time
|
|
|
|
EndsAt time.Time
|
|
|
|
LastSentAt time.Time
|
|
|
|
|
2022-02-02 12:18:20 -06:00
|
|
|
State eval.State
|
2022-05-23 03:49:49 -05:00
|
|
|
StateReason string
|
2022-02-02 12:18:20 -06:00
|
|
|
LastEvaluationString string
|
|
|
|
LastEvaluationTime time.Time
|
|
|
|
EvaluationDuration time.Duration
|
2022-05-23 03:49:49 -05:00
|
|
|
Results []Evaluation
|
|
|
|
Resolved bool
|
2022-02-02 12:18:20 -06:00
|
|
|
Annotations map[string]string
|
|
|
|
Labels data.Labels
|
2022-05-26 00:29:56 -05:00
|
|
|
Image *models.Image
|
2022-02-02 12:18:20 -06:00
|
|
|
Error error
|
2021-04-23 14:32:25 -05:00
|
|
|
}
|
|
|
|
|
2022-08-18 08:40:33 -05:00
|
|
|
func (a *State) GetRuleKey() models.AlertRuleKey {
|
|
|
|
return models.AlertRuleKey{
|
|
|
|
OrgID: a.OrgID,
|
|
|
|
UID: a.AlertRuleUID,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-23 14:32:25 -05:00
|
|
|
type Evaluation struct {
|
2022-02-02 12:18:20 -06:00
|
|
|
EvaluationTime time.Time
|
|
|
|
EvaluationState eval.State
|
2021-07-15 07:10:56 -05:00
|
|
|
// Values contains the RefID and value of reduce and math expressions.
|
2022-03-29 14:33:03 -05:00
|
|
|
// Classic conditions can have different values for the same RefID as they can include multiple conditions.
|
|
|
|
// For these, we use the index of the condition in addition RefID as the key e.g. "A0, A1, A2, etc.".
|
2022-01-31 10:56:43 -06:00
|
|
|
Values map[string]*float64
|
2022-04-05 13:36:42 -05:00
|
|
|
// Condition is the refID specified as the condition in the alerting rule at the time of the evaluation.
|
|
|
|
Condition string
|
2021-07-15 07:10:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewEvaluationValues returns the labels and values for each RefID in the capture.
|
2022-01-31 10:56:43 -06:00
|
|
|
func NewEvaluationValues(m map[string]eval.NumberValueCapture) map[string]*float64 {
|
|
|
|
result := make(map[string]*float64, len(m))
|
2021-07-15 07:10:56 -05:00
|
|
|
for k, v := range m {
|
2022-01-31 10:56:43 -06:00
|
|
|
result[k] = v.Value
|
2021-07-15 07:10:56 -05:00
|
|
|
}
|
|
|
|
return result
|
2021-04-23 14:32:25 -05:00
|
|
|
}
|
|
|
|
|
2022-05-23 03:49:49 -05:00
|
|
|
func (a *State) resultNormal(_ *models.AlertRule, result eval.Result) {
|
2022-04-13 13:45:29 -05:00
|
|
|
a.Error = nil // should be nil since state is not error
|
2021-05-26 13:37:42 -05:00
|
|
|
if a.State != eval.Normal {
|
|
|
|
a.EndsAt = result.EvaluatedAt
|
|
|
|
a.StartsAt = result.EvaluatedAt
|
2021-04-23 14:32:25 -05:00
|
|
|
}
|
2021-05-26 13:37:42 -05:00
|
|
|
a.State = eval.Normal
|
2021-04-23 14:32:25 -05:00
|
|
|
}
|
|
|
|
|
2022-05-23 03:49:49 -05:00
|
|
|
func (a *State) resultAlerting(alertRule *models.AlertRule, result eval.Result) {
|
2021-11-26 11:58:19 -06:00
|
|
|
a.Error = result.Error // should be nil since the state is not an error
|
|
|
|
|
2021-04-23 14:32:25 -05:00
|
|
|
switch a.State {
|
|
|
|
case eval.Alerting:
|
2021-06-17 12:01:46 -05:00
|
|
|
a.setEndsAt(alertRule, result)
|
2021-04-23 14:32:25 -05:00
|
|
|
case eval.Pending:
|
2022-03-01 11:06:42 -06:00
|
|
|
if result.EvaluatedAt.Sub(a.StartsAt) >= alertRule.For {
|
2021-04-23 14:32:25 -05:00
|
|
|
a.State = eval.Alerting
|
|
|
|
a.StartsAt = result.EvaluatedAt
|
2021-06-17 12:01:46 -05:00
|
|
|
a.setEndsAt(alertRule, result)
|
2021-04-23 14:32:25 -05:00
|
|
|
}
|
|
|
|
default:
|
|
|
|
a.StartsAt = result.EvaluatedAt
|
2021-06-17 12:01:46 -05:00
|
|
|
a.setEndsAt(alertRule, result)
|
2021-04-23 14:32:25 -05:00
|
|
|
if !(alertRule.For > 0) {
|
2021-06-17 12:01:46 -05:00
|
|
|
// If For is 0, immediately set Alerting
|
2021-04-23 14:32:25 -05:00
|
|
|
a.State = eval.Alerting
|
|
|
|
} else {
|
2021-06-17 12:01:46 -05:00
|
|
|
a.State = eval.Pending
|
2021-04-23 14:32:25 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-23 03:49:49 -05:00
|
|
|
func (a *State) resultError(alertRule *models.AlertRule, result eval.Result) {
|
2021-05-04 12:08:12 -05:00
|
|
|
a.Error = result.Error
|
2021-11-25 04:46:47 -06:00
|
|
|
|
2022-03-31 03:57:58 -05:00
|
|
|
execErrState := eval.Error
|
2022-04-13 13:45:29 -05:00
|
|
|
switch alertRule.ExecErrState {
|
2022-05-23 03:49:49 -05:00
|
|
|
case models.AlertingErrState:
|
2022-03-31 03:57:58 -05:00
|
|
|
execErrState = eval.Alerting
|
2022-05-23 03:49:49 -05:00
|
|
|
case models.ErrorErrState:
|
2021-11-25 04:46:47 -06:00
|
|
|
// If the evaluation failed because a query returned an error then
|
|
|
|
// update the state with the Datasource UID as a label and the error
|
|
|
|
// message as an annotation so other code can use this metadata to
|
|
|
|
// add context to alerts
|
|
|
|
var queryError expr.QueryError
|
|
|
|
if errors.As(a.Error, &queryError) {
|
|
|
|
for _, next := range alertRule.Data {
|
|
|
|
if next.RefID == queryError.RefID {
|
2021-12-03 03:55:16 -06:00
|
|
|
a.Labels["ref_id"] = next.RefID
|
2021-11-25 04:46:47 -06:00
|
|
|
a.Labels["datasource_uid"] = next.DatasourceUID
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
a.Annotations["Error"] = queryError.Error()
|
|
|
|
}
|
2022-03-31 03:57:58 -05:00
|
|
|
execErrState = eval.Error
|
2022-05-23 03:49:49 -05:00
|
|
|
case models.OkErrState:
|
2022-04-13 13:45:29 -05:00
|
|
|
a.resultNormal(alertRule, result)
|
|
|
|
return
|
|
|
|
default:
|
|
|
|
a.Error = fmt.Errorf("cannot map error to a state because option [%s] is not supported. evaluation error: %w", alertRule.ExecErrState, a.Error)
|
2022-03-31 03:57:58 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
switch a.State {
|
|
|
|
case eval.Alerting, eval.Error:
|
2022-07-14 04:53:39 -05:00
|
|
|
// We must set the state here as the state can change both from Alerting
|
|
|
|
// to Error and from Error to Alerting. This can happen when the datasource
|
|
|
|
// is unavailable or queries against the datasource returns errors, and is
|
|
|
|
// then resolved as soon as the datasource is available and queries return
|
|
|
|
// without error
|
|
|
|
a.State = execErrState
|
2022-03-31 03:57:58 -05:00
|
|
|
a.setEndsAt(alertRule, result)
|
|
|
|
case eval.Pending:
|
|
|
|
if result.EvaluatedAt.Sub(a.StartsAt) >= alertRule.For {
|
|
|
|
a.State = execErrState
|
|
|
|
a.StartsAt = result.EvaluatedAt
|
|
|
|
a.setEndsAt(alertRule, result)
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
// For is observed when Alerting is chosen for the alert state
|
|
|
|
// if execution error or timeout.
|
|
|
|
if execErrState == eval.Alerting && alertRule.For > 0 {
|
|
|
|
a.State = eval.Pending
|
|
|
|
} else {
|
|
|
|
a.State = execErrState
|
|
|
|
}
|
|
|
|
a.StartsAt = result.EvaluatedAt
|
|
|
|
a.setEndsAt(alertRule, result)
|
2021-04-23 14:32:25 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-23 03:49:49 -05:00
|
|
|
func (a *State) resultNoData(alertRule *models.AlertRule, result eval.Result) {
|
2021-11-26 11:58:19 -06:00
|
|
|
a.Error = result.Error
|
|
|
|
|
2021-04-23 14:32:25 -05:00
|
|
|
if a.StartsAt.IsZero() {
|
|
|
|
a.StartsAt = result.EvaluatedAt
|
|
|
|
}
|
2021-06-17 12:01:46 -05:00
|
|
|
a.setEndsAt(alertRule, result)
|
2021-04-23 14:32:25 -05:00
|
|
|
|
|
|
|
switch alertRule.NoDataState {
|
2022-05-23 03:49:49 -05:00
|
|
|
case models.Alerting:
|
2021-04-23 14:32:25 -05:00
|
|
|
a.State = eval.Alerting
|
2022-05-23 03:49:49 -05:00
|
|
|
case models.NoData:
|
2021-04-23 14:32:25 -05:00
|
|
|
a.State = eval.NoData
|
2022-05-23 03:49:49 -05:00
|
|
|
case models.OK:
|
2021-04-23 14:32:25 -05:00
|
|
|
a.State = eval.Normal
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-19 15:15:09 -05:00
|
|
|
func (a *State) NeedsSending(resendDelay time.Duration) bool {
|
2021-11-25 04:46:47 -06:00
|
|
|
if a.State == eval.Pending || a.State == eval.Normal && !a.Resolved {
|
2021-07-29 13:29:17 -05:00
|
|
|
return false
|
|
|
|
}
|
2021-05-19 15:15:09 -05:00
|
|
|
// if LastSentAt is before or equal to LastEvaluationTime + resendDelay, send again
|
2021-11-04 15:42:34 -05:00
|
|
|
nextSent := a.LastSentAt.Add(resendDelay)
|
|
|
|
return nextSent.Before(a.LastEvaluationTime) || nextSent.Equal(a.LastEvaluationTime)
|
2021-05-19 15:15:09 -05:00
|
|
|
}
|
|
|
|
|
2021-04-23 14:32:25 -05:00
|
|
|
func (a *State) Equals(b *State) bool {
|
|
|
|
return a.AlertRuleUID == b.AlertRuleUID &&
|
|
|
|
a.OrgID == b.OrgID &&
|
|
|
|
a.CacheId == b.CacheId &&
|
|
|
|
a.Labels.String() == b.Labels.String() &&
|
|
|
|
a.State.String() == b.State.String() &&
|
|
|
|
a.StartsAt == b.StartsAt &&
|
|
|
|
a.EndsAt == b.EndsAt &&
|
2021-04-30 13:23:12 -05:00
|
|
|
a.LastEvaluationTime == b.LastEvaluationTime &&
|
|
|
|
data.Labels(a.Annotations).String() == data.Labels(b.Annotations).String()
|
2021-04-23 14:32:25 -05:00
|
|
|
}
|
2021-05-18 12:56:14 -05:00
|
|
|
|
2022-05-23 03:49:49 -05:00
|
|
|
func (a *State) TrimResults(alertRule *models.AlertRule) {
|
2022-02-02 12:18:20 -06:00
|
|
|
numBuckets := int64(alertRule.For.Seconds()) / alertRule.IntervalSeconds
|
2021-05-18 12:56:14 -05:00
|
|
|
if numBuckets == 0 {
|
|
|
|
numBuckets = 10 // keep at least 10 evaluations in the event For is set to 0
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(a.Results) < int(numBuckets) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
newResults := make([]Evaluation, numBuckets)
|
|
|
|
copy(newResults, a.Results[len(a.Results)-int(numBuckets):])
|
|
|
|
a.Results = newResults
|
|
|
|
}
|
2021-06-17 12:01:46 -05:00
|
|
|
|
2021-09-02 10:22:59 -05:00
|
|
|
// setEndsAt sets the ending timestamp of the alert.
|
|
|
|
// The internal Alertmanager will use this time to know when it should automatically resolve the alert
|
|
|
|
// in case it hasn't received additional alerts. Under regular operations the scheduler will continue to send the
|
|
|
|
// alert with an updated EndsAt, if the alert is resolved then a last alert is sent with EndsAt = last evaluation time.
|
2022-05-23 03:49:49 -05:00
|
|
|
func (a *State) setEndsAt(alertRule *models.AlertRule, result eval.Result) {
|
2021-09-02 10:22:59 -05:00
|
|
|
ends := ResendDelay
|
|
|
|
if alertRule.IntervalSeconds > int64(ResendDelay.Seconds()) {
|
2021-09-22 08:55:46 -05:00
|
|
|
ends = time.Second * time.Duration(alertRule.IntervalSeconds)
|
2021-06-17 12:01:46 -05:00
|
|
|
}
|
2021-09-02 10:22:59 -05:00
|
|
|
|
|
|
|
a.EndsAt = result.EvaluatedAt.Add(ends * 3)
|
2021-06-17 12:01:46 -05:00
|
|
|
}
|
2022-03-16 11:04:19 -05:00
|
|
|
|
2022-05-23 03:49:49 -05:00
|
|
|
func (a *State) GetLabels(opts ...models.LabelOption) map[string]string {
|
2022-03-16 11:04:19 -05:00
|
|
|
labels := a.Labels.Copy()
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
opt(labels)
|
|
|
|
}
|
|
|
|
|
|
|
|
return labels
|
|
|
|
}
|
2022-04-05 13:36:42 -05:00
|
|
|
|
|
|
|
func (a *State) GetLastEvaluationValuesForCondition() map[string]float64 {
|
|
|
|
if len(a.Results) <= 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
lastResult := a.Results[len(a.Results)-1]
|
|
|
|
r := make(map[string]float64, len(lastResult.Values))
|
|
|
|
|
|
|
|
for refID, value := range lastResult.Values {
|
|
|
|
if strings.Contains(refID, lastResult.Condition) {
|
2022-04-27 13:59:13 -05:00
|
|
|
if value != nil {
|
|
|
|
r[refID] = *value
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
r[refID] = math.NaN()
|
2022-04-05 13:36:42 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return r
|
|
|
|
}
|