mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 00:42:03 +08:00

What is this feature? Prometheus conversion: ensures that AlertRule.Data queries always have default parameters set (intervalMs, maxDataPoints). Without this, updates of the same rule can cause version increments. Why do we need this feature? Currently, when converting Prometheus rules to Grafana alerts, some default parameters are not explicitly set in the query model. This creates a problem during rule updates: When a user updates a rule that hasn't changed, we still detect differences in the AlertQuery.Model because the newly converted rules are missing the default fields, such as intervalMs and maxDataPoints. This causes unnecessary version increments of alert rules.
319 lines
8.0 KiB
Go
319 lines
8.0 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)
|
|
}
|
|
|
|
// Initialize defaults, which also overrides the model
|
|
if err := aq.InitDefaults(); err != nil {
|
|
return err
|
|
}
|
|
|
|
isExpression, err := aq.IsExpression()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ok := isExpression || aq.RelativeTimeRange.isValid(); !ok {
|
|
return ErrInvalidRelativeTimeRange(aq.RefID, aq.RelativeTimeRange)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// InitDefaults ensures all default parameters are set in the query model.
|
|
// This helps maintain consistent query models for comparisons.
|
|
func (aq *AlertQuery) InitDefaults() error {
|
|
model, err := aq.GetModel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
aq.Model = model
|
|
return nil
|
|
}
|