mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
f6a46744a6
Backend: * Update the Grafana Alerting engine to provide feedback to HysteresisCommand. The feedback information is stored in state.Manager as a fingerprint of each state. The fingerprint is persisted to the database. Only fingerprints that belong to Pending and Alerting states are considered as "loaded" and provided back to the command. - add ResultFingerprint to state.State. It's different from other fingerprints we store in the state because it is calculated from the result labels. - add rule_fingerprint column to alert_instance - update alerting evaluator to accept AlertingResultsReader via context, and update scheduler to provide it. - add AlertingResultsFromRuleState that implements the new interface in eval package - update getExprRequest to patch the hysteresis command. * Only one "Recovery Threshold" query is allowed to be used in the alert rule and it must be the Condition. Frontend: * Add hysteresis option to Threshold in UI. It's called "Recovery Threshold" * Add test for getUnloadEvaluatorTypeFromCondition * Hide hysteresis in panel expressions * Refactor isInvalid and add test for it * Remove unnecesary React.memo * Add tests for updateEvaluatorConditions --------- Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com>
310 lines
7.8 KiB
Go
310 lines
7.8 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
|
|
"github.com/grafana/grafana/pkg/expr"
|
|
)
|
|
|
|
const defaultMaxDataPoints float64 = 43200 // 12 hours at 1sec interval
|
|
const defaultIntervalMS float64 = 1000
|
|
|
|
var ErrNoQuery = errors.New("no `expr` property in the query model")
|
|
|
|
// Duration is a type used for marshalling durations.
|
|
type Duration time.Duration
|
|
|
|
func (d Duration) String() string {
|
|
return time.Duration(d).String()
|
|
}
|
|
|
|
func (d Duration) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(time.Duration(d).Seconds())
|
|
}
|
|
|
|
func (d *Duration) UnmarshalJSON(b []byte) error {
|
|
var v any
|
|
if err := json.Unmarshal(b, &v); err != nil {
|
|
return err
|
|
}
|
|
switch value := v.(type) {
|
|
case float64:
|
|
*d = Duration(time.Duration(value) * time.Second)
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("invalid duration %v", v)
|
|
}
|
|
}
|
|
|
|
func (d Duration) MarshalYAML() (any, error) {
|
|
return time.Duration(d).Seconds(), nil
|
|
}
|
|
|
|
func (d *Duration) UnmarshalYAML(unmarshal func(any) error) error {
|
|
var v any
|
|
if err := unmarshal(&v); err != nil {
|
|
return err
|
|
}
|
|
switch value := v.(type) {
|
|
case int:
|
|
*d = Duration(time.Duration(value) * time.Second)
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("invalid duration %v", v)
|
|
}
|
|
}
|
|
|
|
// RelativeTimeRange is the per query start and end time
|
|
// for requests.
|
|
type RelativeTimeRange struct {
|
|
From Duration `json:"from" yaml:"from"`
|
|
To Duration `json:"to" yaml:"to"`
|
|
}
|
|
|
|
// isValid checks that From duration is greater than To duration.
|
|
func (rtr *RelativeTimeRange) isValid() bool {
|
|
return rtr.From > rtr.To
|
|
}
|
|
|
|
func (rtr *RelativeTimeRange) ToTimeRange() expr.TimeRange {
|
|
return expr.RelativeTimeRange{
|
|
From: -time.Duration(rtr.From),
|
|
To: -time.Duration(rtr.To),
|
|
}
|
|
}
|
|
|
|
// AlertQuery represents a single query associated with an alert definition.
|
|
type AlertQuery struct {
|
|
// RefID is the unique identifier of the query, set by the frontend call.
|
|
RefID string `json:"refId"`
|
|
|
|
// QueryType is an optional identifier for the type of query.
|
|
// It can be used to distinguish different types of queries.
|
|
QueryType string `json:"queryType"`
|
|
|
|
// RelativeTimeRange is the relative Start and End of the query as sent by the frontend.
|
|
RelativeTimeRange RelativeTimeRange `json:"relativeTimeRange"`
|
|
|
|
// Grafana data source unique identifier; it should be '__expr__' for a Server Side Expression operation.
|
|
DatasourceUID string `json:"datasourceUid"`
|
|
|
|
// JSON is the raw JSON query and includes the above properties as well as custom properties.
|
|
Model json.RawMessage `json:"model"`
|
|
|
|
modelProps map[string]any
|
|
}
|
|
|
|
func (aq *AlertQuery) String() string {
|
|
return fmt.Sprintf("refID: %s, queryType: %s, datasourceUID: %s", aq.RefID, aq.QueryType, aq.DatasourceUID)
|
|
}
|
|
|
|
func (aq *AlertQuery) setModelProps() error {
|
|
aq.modelProps = make(map[string]any)
|
|
err := json.Unmarshal(aq.Model, &aq.modelProps)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unmarshal query model: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsExpression returns true if the alert query is an expression.
|
|
func (aq *AlertQuery) IsExpression() (bool, error) {
|
|
return expr.NodeTypeFromDatasourceUID(aq.DatasourceUID) == expr.TypeCMDNode, nil
|
|
}
|
|
|
|
// IsHysteresisExpression returns true if the model describes a hysteresis command expression. Returns error if the Model is not a valid JSON
|
|
func (aq *AlertQuery) IsHysteresisExpression() (bool, error) {
|
|
if aq.modelProps == nil {
|
|
err := aq.setModelProps()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
return expr.IsHysteresisExpression(aq.modelProps), nil
|
|
}
|
|
|
|
// PatchHysteresisExpression updates the AlertQuery to include loaded metrics into hysteresis
|
|
func (aq *AlertQuery) PatchHysteresisExpression(loadedMetrics map[data.Fingerprint]struct{}) error {
|
|
if aq.modelProps == nil {
|
|
err := aq.setModelProps()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return expr.SetLoadedDimensionsToHysteresisCommand(aq.modelProps, loadedMetrics)
|
|
}
|
|
|
|
// setMaxDatapoints sets the model maxDataPoints if it's missing or invalid
|
|
func (aq *AlertQuery) setMaxDatapoints() error {
|
|
if aq.modelProps == nil {
|
|
err := aq.setModelProps()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
i, ok := aq.modelProps["maxDataPoints"] // GEL requires maxDataPoints inside the query JSON
|
|
if !ok {
|
|
aq.modelProps["maxDataPoints"] = defaultMaxDataPoints
|
|
}
|
|
maxDataPoints, ok := i.(float64)
|
|
if !ok || maxDataPoints == 0 {
|
|
aq.modelProps["maxDataPoints"] = defaultMaxDataPoints
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (aq *AlertQuery) GetMaxDatapoints() (int64, error) {
|
|
err := aq.setMaxDatapoints()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
maxDataPoints, ok := aq.modelProps["maxDataPoints"].(float64)
|
|
if !ok {
|
|
return 0, fmt.Errorf("failed to cast maxDataPoints to float64: %v", aq.modelProps["maxDataPoints"])
|
|
}
|
|
return int64(maxDataPoints), nil
|
|
}
|
|
|
|
// setIntervalMS sets the model IntervalMs if it's missing or invalid
|
|
func (aq *AlertQuery) setIntervalMS() error {
|
|
if aq.modelProps == nil {
|
|
err := aq.setModelProps()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
i, ok := aq.modelProps["intervalMs"] // GEL requires intervalMs inside the query JSON
|
|
if !ok {
|
|
aq.modelProps["intervalMs"] = defaultIntervalMS
|
|
}
|
|
intervalMs, ok := i.(float64)
|
|
if !ok || intervalMs == 0 {
|
|
aq.modelProps["intervalMs"] = defaultIntervalMS
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (aq *AlertQuery) getIntervalMS() (int64, error) {
|
|
err := aq.setIntervalMS()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
intervalMs, ok := aq.modelProps["intervalMs"].(float64)
|
|
if !ok {
|
|
return 0, fmt.Errorf("failed to cast intervalMs to float64: %v", aq.modelProps["intervalMs"])
|
|
}
|
|
return int64(intervalMs), nil
|
|
}
|
|
|
|
func (aq *AlertQuery) GetIntervalDuration() (time.Duration, error) {
|
|
err := aq.setIntervalMS()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
intervalMs, ok := aq.modelProps["intervalMs"].(float64)
|
|
if !ok {
|
|
return 0, fmt.Errorf("failed to cast intervalMs to float64: %v", aq.modelProps["intervalMs"])
|
|
}
|
|
return time.Duration(intervalMs) * time.Millisecond, nil
|
|
}
|
|
|
|
// GetDatasource returns the query datasource identifier.
|
|
func (aq *AlertQuery) GetDatasource() (string, error) {
|
|
return aq.DatasourceUID, nil
|
|
}
|
|
|
|
// GetQuery returns the query defined by `expr` within the model.
|
|
// Returns an ErrNoQuery if it is unable to find the query.
|
|
// Returns an error if it is not able to cast the query to a string.
|
|
func (aq *AlertQuery) GetQuery() (string, error) {
|
|
if aq.modelProps == nil {
|
|
err := aq.setModelProps()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
query, ok := aq.modelProps["expr"]
|
|
if !ok {
|
|
return "", ErrNoQuery
|
|
}
|
|
|
|
q, ok := query.(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("failed to cast query to string: %v", aq.modelProps["expr"])
|
|
}
|
|
return q, nil
|
|
}
|
|
|
|
func (aq *AlertQuery) GetModel() ([]byte, error) {
|
|
err := aq.setMaxDatapoints()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = aq.setIntervalMS()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
model, err := json.Marshal(aq.modelProps)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to marshal query model: %w", err)
|
|
}
|
|
return model, nil
|
|
}
|
|
|
|
func (aq *AlertQuery) setQueryType() error {
|
|
if aq.modelProps == nil {
|
|
err := aq.setModelProps()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
i, ok := aq.modelProps["queryType"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
queryType, ok := i.(string)
|
|
if !ok {
|
|
return fmt.Errorf("failed to get queryType from query model: %v", i)
|
|
}
|
|
|
|
aq.QueryType = queryType
|
|
return nil
|
|
}
|
|
|
|
// PreSave sets query's properties.
|
|
// It should be called before being saved.
|
|
func (aq *AlertQuery) PreSave() error {
|
|
if err := aq.setQueryType(); err != nil {
|
|
return fmt.Errorf("failed to set query type to query model: %w", err)
|
|
}
|
|
|
|
// override model
|
|
model, err := aq.GetModel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
aq.Model = model
|
|
|
|
isExpression, err := aq.IsExpression()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ok := isExpression || aq.RelativeTimeRange.isValid(); !ok {
|
|
return fmt.Errorf("invalid relative time range: %+v", aq.RelativeTimeRange)
|
|
}
|
|
return nil
|
|
}
|