AlertingNG: manage and evaluate alert definitions via the API (#28377)
* Alerting NG: prototype v2 (WIP) * Separate eval package * Modify eval alert definition endpoint * Disable migration if ngalert is not enabled * Remove premature test * Fix lint issues * Delete obsolete struct * Apply suggestions from code review * Update pkg/services/ngalert/ngalert.go Co-authored-by: Kyle Brandt <kyle@grafana.com> * Add API endpoint for listing alert definitions * Introduce index for alert_definition table * make ds object for expression to avoid panic * wrap error * Update pkg/services/ngalert/eval/eval.go * Swith to backend.DataQuery * Export TransformWrapper callback * Fix lint issues * Update pkg/services/ngalert/ngalert.go Co-authored-by: Kyle Brandt <kyle@grafana.com> * Validate alert definitions before storing them * Introduce AlertQuery * Add test * Add QueryType in AlertQuery * Accept only float64 (seconds) durations * Apply suggestions from code review * Get rid of bus * Do not export symbols * Fix failing test * Fix failure due to service initialization order Introduce MediumHigh service priority and assign it to backendplugin service * Fix test * Apply suggestions from code review * Fix renamed reference Co-authored-by: Kyle Brandt <kyle@grafana.com>
This commit is contained in:
co-authored by
Kyle Brandt
parent
a1e80af800
commit
43f580c299
@@ -0,0 +1,294 @@
|
||||
package eval
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
|
||||
)
|
||||
|
||||
const defaultMaxDataPoints float64 = 100
|
||||
const defaultIntervalMS float64 = 1000
|
||||
|
||||
// DefaultExprDatasourceID is the datasource identifier for expressions.:w
|
||||
const DefaultExprDatasourceID = -100
|
||||
|
||||
// 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 interface{}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// RelativeTimeRange is the per query start and end time
|
||||
// for requests.
|
||||
type RelativeTimeRange struct {
|
||||
From Duration
|
||||
To Duration
|
||||
}
|
||||
|
||||
// isValid checks that From duration is greater than To duration.
|
||||
func (rtr *RelativeTimeRange) isValid() bool {
|
||||
return rtr.From > rtr.To
|
||||
}
|
||||
|
||||
func (rtr *RelativeTimeRange) toTimeRange(now time.Time) *pluginv2.TimeRange {
|
||||
return &pluginv2.TimeRange{
|
||||
FromEpochMS: now.Add(-time.Duration(rtr.From)).UnixNano() / 1e6,
|
||||
ToEpochMS: now.Add(-time.Duration(rtr.To)).UnixNano() / 1e6,
|
||||
}
|
||||
}
|
||||
|
||||
// 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"`
|
||||
|
||||
DatasourceID int64 `json:"-"`
|
||||
|
||||
// JSON is the raw JSON query and includes the above properties as well as custom properties.
|
||||
Model json.RawMessage `json:"model"`
|
||||
|
||||
modelProps map[string]interface{} `json:"-"`
|
||||
}
|
||||
|
||||
func (aq *AlertQuery) setModelProps() error {
|
||||
aq.modelProps = make(map[string]interface{})
|
||||
err := json.Unmarshal(aq.Model, &aq.modelProps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal query model: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setDatasource sets DatasourceID.
|
||||
// If it's an expression sets DefaultExprDatasourceID.
|
||||
func (aq *AlertQuery) setDatasource() error {
|
||||
if aq.modelProps == nil {
|
||||
err := aq.setModelProps()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
dsName, ok := aq.modelProps["datasource"]
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to get datasource from query model")
|
||||
}
|
||||
|
||||
if dsName == "__expr__" {
|
||||
aq.DatasourceID = DefaultExprDatasourceID
|
||||
aq.modelProps["datasourceId"] = DefaultExprDatasourceID
|
||||
return nil
|
||||
}
|
||||
|
||||
i, ok := aq.modelProps["datasourceId"]
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to get datasourceId from query model")
|
||||
}
|
||||
dsID, ok := i.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to cast datasourceId to float64: %v", i)
|
||||
}
|
||||
aq.DatasourceID = int64(dsID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsExpression returns true if the alert query is an expression.
|
||||
func (aq *AlertQuery) IsExpression() (bool, error) {
|
||||
err := aq.setDatasource()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return aq.DatasourceID == DefaultExprDatasourceID, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// GetDatasource returns the query datasource identifier.
|
||||
func (aq *AlertQuery) GetDatasource() (int64, error) {
|
||||
err := aq.setDatasource()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return aq.DatasourceID, nil
|
||||
}
|
||||
|
||||
func (aq *AlertQuery) getModel() ([]byte, error) {
|
||||
err := aq.setDatasource()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) setOrgID(orgID int64) error {
|
||||
if aq.modelProps == nil {
|
||||
err := aq.setModelProps()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
aq.modelProps["orgId"] = orgID
|
||||
return 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(orgID int64) error {
|
||||
err := aq.setOrgID(orgID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set orgId to query model: %w", err)
|
||||
}
|
||||
|
||||
if err := aq.setDatasource(); err != nil {
|
||||
return fmt.Errorf("failed to set datasource to query model: %w", err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package eval
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAlertQuery(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
alertQuery AlertQuery
|
||||
expectedIsExpression bool
|
||||
expectedDatasource string
|
||||
expectedDatasourceID int64
|
||||
expectedMaxPoints int64
|
||||
expectedIntervalMS int64
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "given an expression query",
|
||||
alertQuery: AlertQuery{
|
||||
RefID: "A",
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "__expr__",
|
||||
"queryType": "metricQuery",
|
||||
"extraParam": "some text"
|
||||
}`),
|
||||
},
|
||||
expectedIsExpression: true,
|
||||
expectedDatasource: "__expr__",
|
||||
expectedDatasourceID: int64(DefaultExprDatasourceID),
|
||||
expectedMaxPoints: int64(defaultMaxDataPoints),
|
||||
expectedIntervalMS: int64(defaultIntervalMS),
|
||||
},
|
||||
{
|
||||
desc: "given a query",
|
||||
alertQuery: AlertQuery{
|
||||
RefID: "A",
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "my datasource",
|
||||
"datasourceId": 1,
|
||||
"queryType": "metricQuery",
|
||||
"extraParam": "some text"
|
||||
}`),
|
||||
},
|
||||
expectedIsExpression: false,
|
||||
expectedDatasource: "my datasource",
|
||||
expectedDatasourceID: 1,
|
||||
expectedMaxPoints: int64(defaultMaxDataPoints),
|
||||
expectedIntervalMS: int64(defaultIntervalMS),
|
||||
},
|
||||
{
|
||||
desc: "given a query with valid maxDataPoints",
|
||||
alertQuery: AlertQuery{
|
||||
RefID: "A",
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "my datasource",
|
||||
"datasourceId": 1,
|
||||
"queryType": "metricQuery",
|
||||
"maxDataPoints": 200,
|
||||
"extraParam": "some text"
|
||||
}`),
|
||||
},
|
||||
expectedIsExpression: false,
|
||||
expectedDatasource: "my datasource",
|
||||
expectedDatasourceID: 1,
|
||||
expectedMaxPoints: 200,
|
||||
expectedIntervalMS: int64(defaultIntervalMS),
|
||||
},
|
||||
{
|
||||
desc: "given a query with invalid maxDataPoints",
|
||||
alertQuery: AlertQuery{
|
||||
RefID: "A",
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "my datasource",
|
||||
"datasourceId": 1,
|
||||
"queryType": "metricQuery",
|
||||
"maxDataPoints": "invalid",
|
||||
"extraParam": "some text"
|
||||
}`),
|
||||
},
|
||||
expectedIsExpression: false,
|
||||
expectedDatasource: "my datasource",
|
||||
expectedDatasourceID: 1,
|
||||
expectedMaxPoints: int64(defaultMaxDataPoints),
|
||||
expectedIntervalMS: int64(defaultIntervalMS),
|
||||
},
|
||||
{
|
||||
desc: "given a query with zero maxDataPoints",
|
||||
alertQuery: AlertQuery{
|
||||
RefID: "A",
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "my datasource",
|
||||
"datasourceId": 1,
|
||||
"queryType": "metricQuery",
|
||||
"maxDataPoints": 0,
|
||||
"extraParam": "some text"
|
||||
}`),
|
||||
},
|
||||
expectedIsExpression: false,
|
||||
expectedDatasource: "my datasource",
|
||||
expectedDatasourceID: 1,
|
||||
expectedMaxPoints: int64(defaultMaxDataPoints),
|
||||
expectedIntervalMS: int64(defaultIntervalMS),
|
||||
},
|
||||
{
|
||||
desc: "given a query with valid intervalMs",
|
||||
alertQuery: AlertQuery{
|
||||
RefID: "A",
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "my datasource",
|
||||
"datasourceId": 1,
|
||||
"queryType": "metricQuery",
|
||||
"intervalMs": 2000,
|
||||
"extraParam": "some text"
|
||||
}`),
|
||||
},
|
||||
expectedIsExpression: false,
|
||||
expectedDatasource: "my datasource",
|
||||
expectedDatasourceID: 1,
|
||||
expectedMaxPoints: int64(defaultMaxDataPoints),
|
||||
expectedIntervalMS: 2000,
|
||||
},
|
||||
{
|
||||
desc: "given a query with invalid intervalMs",
|
||||
alertQuery: AlertQuery{
|
||||
RefID: "A",
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "my datasource",
|
||||
"datasourceId": 1,
|
||||
"queryType": "metricQuery",
|
||||
"intervalMs": "invalid",
|
||||
"extraParam": "some text"
|
||||
}`),
|
||||
},
|
||||
expectedIsExpression: false,
|
||||
expectedDatasource: "my datasource",
|
||||
expectedDatasourceID: 1,
|
||||
expectedMaxPoints: int64(defaultMaxDataPoints),
|
||||
expectedIntervalMS: int64(defaultIntervalMS),
|
||||
},
|
||||
{
|
||||
desc: "given a query with invalid intervalMs",
|
||||
alertQuery: AlertQuery{
|
||||
RefID: "A",
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "my datasource",
|
||||
"datasourceId": 1,
|
||||
"queryType": "metricQuery",
|
||||
"intervalMs": 0,
|
||||
"extraParam": "some text"
|
||||
}`),
|
||||
},
|
||||
expectedIsExpression: false,
|
||||
expectedDatasource: "my datasource",
|
||||
expectedDatasourceID: 1,
|
||||
expectedMaxPoints: int64(defaultMaxDataPoints),
|
||||
expectedIntervalMS: int64(defaultIntervalMS),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
t.Run("can recognize if it's an expression", func(t *testing.T) {
|
||||
isExpression, err := tc.alertQuery.IsExpression()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedIsExpression, isExpression)
|
||||
})
|
||||
|
||||
t.Run("can set datasource for expression", func(t *testing.T) {
|
||||
err := tc.alertQuery.setDatasource()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedDatasourceID, tc.alertQuery.DatasourceID)
|
||||
})
|
||||
|
||||
t.Run("can set queryType for expression", func(t *testing.T) {
|
||||
err := tc.alertQuery.setQueryType()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "metricQuery", tc.alertQuery.QueryType)
|
||||
})
|
||||
|
||||
t.Run("can update model maxDataPoints (if missing)", func(t *testing.T) {
|
||||
maxDataPoints, err := tc.alertQuery.getMaxDatapoints()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedMaxPoints, maxDataPoints)
|
||||
})
|
||||
|
||||
t.Run("can update model intervalMs (if missing)", func(t *testing.T) {
|
||||
intervalMS, err := tc.alertQuery.getIntervalMS()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, intervalMS, tc.expectedIntervalMS)
|
||||
})
|
||||
|
||||
t.Run("can get the updated model with the default properties (if missing)", func(t *testing.T) {
|
||||
blob, err := tc.alertQuery.getModel()
|
||||
require.NoError(t, err)
|
||||
model := make(map[string]interface{})
|
||||
err = json.Unmarshal(blob, &model)
|
||||
require.NoError(t, err)
|
||||
|
||||
i, ok := model["datasource"]
|
||||
require.True(t, ok)
|
||||
datasource, ok := i.(string)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, tc.expectedDatasource, datasource)
|
||||
|
||||
i, ok = model["datasourceId"]
|
||||
require.True(t, ok)
|
||||
datasourceID, ok := i.(float64)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, tc.expectedDatasourceID, int64(datasourceID))
|
||||
|
||||
i, ok = model["maxDataPoints"]
|
||||
require.True(t, ok)
|
||||
maxDataPoints, ok := i.(float64)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, tc.expectedMaxPoints, int64(maxDataPoints))
|
||||
|
||||
i, ok = model["intervalMs"]
|
||||
require.True(t, ok)
|
||||
intervalMs, ok := i.(float64)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, tc.expectedIntervalMS, int64(intervalMs))
|
||||
|
||||
i, ok = model["extraParam"]
|
||||
require.True(t, ok)
|
||||
extraParam, ok := i.(string)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "some text", extraParam)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertQueryMarshalling(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
blob string
|
||||
err error
|
||||
expectedFrom Duration
|
||||
expectedTo Duration
|
||||
}{
|
||||
{
|
||||
desc: "unmarshalling successfully when input is correct",
|
||||
blob: `{
|
||||
"refId": "B",
|
||||
"relativeTimeRange": {
|
||||
"from": 18000,
|
||||
"to": 10800
|
||||
},
|
||||
"model": {}
|
||||
}`,
|
||||
expectedFrom: Duration(5 * time.Hour),
|
||||
expectedTo: Duration(3 * time.Hour),
|
||||
},
|
||||
{
|
||||
desc: "failing unmarshalling gracefully when from is incorrect",
|
||||
blob: `{
|
||||
"refId": "B",
|
||||
"relativeTimeRange": {
|
||||
"from": "5h10m",
|
||||
"to": 18000
|
||||
},
|
||||
"model": {}
|
||||
}`,
|
||||
err: fmt.Errorf("invalid duration 5h10m"),
|
||||
},
|
||||
{
|
||||
desc: "failing unmarshalling gracefully when to is incorrect",
|
||||
blob: `{
|
||||
"refId": "B",
|
||||
"relativeTimeRange": {
|
||||
"from": 18000,
|
||||
"to": "5h10m"
|
||||
},
|
||||
"model": {}
|
||||
}`,
|
||||
err: fmt.Errorf("invalid duration 5h10m"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
var aq AlertQuery
|
||||
err := json.Unmarshal([]byte(tc.blob), &aq)
|
||||
require.Equal(t, tc.err, err)
|
||||
if tc.err == nil {
|
||||
assert.Equal(t, tc.expectedFrom, aq.RelativeTimeRange.From)
|
||||
assert.Equal(t, tc.expectedTo, aq.RelativeTimeRange.To)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// Package eval executes the condition for an alert definition, evaluates the condition results, and
|
||||
// returns the alert instance states.
|
||||
package eval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
// invalidEvalResultFormatError is an error for invalid format of the alert definition evaluation results.
|
||||
type invalidEvalResultFormatError struct {
|
||||
refID string
|
||||
reason string
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *invalidEvalResultFormatError) Error() string {
|
||||
s := fmt.Sprintf("invalid format of evaluation results for the alert definition %s: %s", e.refID, e.reason)
|
||||
if e.err != nil {
|
||||
s = fmt.Sprintf("%s: %s", s, e.err.Error())
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (e *invalidEvalResultFormatError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
// Condition contains backend expressions and queries and the RefID
|
||||
// of the query or expression that will be evaluated.
|
||||
type Condition struct {
|
||||
RefID string `json:"refId"`
|
||||
|
||||
QueriesAndExpressions []AlertQuery `json:"queriesAndExpressions"`
|
||||
}
|
||||
|
||||
// ExecutionResults contains the unevaluated results from executing
|
||||
// a condition.
|
||||
type ExecutionResults struct {
|
||||
AlertDefinitionID int64
|
||||
|
||||
Error error
|
||||
|
||||
Results data.Frames
|
||||
}
|
||||
|
||||
// Results is a slice of evaluated alert instances states.
|
||||
type Results []result
|
||||
|
||||
// result contains the evaluated state of an alert instance
|
||||
// identified by its labels.
|
||||
type result struct {
|
||||
Instance data.Labels
|
||||
State state // Enum
|
||||
}
|
||||
|
||||
// state is an enum of the evaluation state for an alert instance.
|
||||
type state int
|
||||
|
||||
const (
|
||||
// Normal is the eval state for an alert instance condition
|
||||
// that evaluated to false.
|
||||
Normal state = iota
|
||||
|
||||
// Alerting is the eval state for an alert instance condition
|
||||
// that evaluated to false.
|
||||
Alerting
|
||||
)
|
||||
|
||||
func (s state) String() string {
|
||||
return [...]string{"Normal", "Alerting"}[s]
|
||||
}
|
||||
|
||||
// IsValid checks the condition's validity.
|
||||
func (c Condition) IsValid() bool {
|
||||
// TODO search for refIDs in QueriesAndExpressions
|
||||
return len(c.QueriesAndExpressions) != 0
|
||||
}
|
||||
|
||||
// AlertExecCtx is the context provided for executing an alert condition.
|
||||
type AlertExecCtx struct {
|
||||
AlertDefitionID int64
|
||||
SignedInUser *models.SignedInUser
|
||||
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
// Execute runs the Condition's expressions or queries.
|
||||
func (c *Condition) Execute(ctx AlertExecCtx, fromStr, toStr string) (*ExecutionResults, error) {
|
||||
result := ExecutionResults{}
|
||||
if !c.IsValid() {
|
||||
return nil, fmt.Errorf("invalid conditions")
|
||||
}
|
||||
|
||||
pbQuery := &pluginv2.QueryDataRequest{
|
||||
PluginContext: &pluginv2.PluginContext{
|
||||
// TODO: Things probably
|
||||
},
|
||||
Queries: []*pluginv2.DataQuery{},
|
||||
}
|
||||
|
||||
for i := range c.QueriesAndExpressions {
|
||||
q := c.QueriesAndExpressions[i]
|
||||
model, err := q.getModel()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get query model: %w", err)
|
||||
}
|
||||
intervalMS, err := q.getIntervalMS()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve intervalMs from the model: %w", err)
|
||||
}
|
||||
|
||||
maxDatapoints, err := q.getMaxDatapoints()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve maxDatapoints from the model: %w", err)
|
||||
}
|
||||
|
||||
pbQuery.Queries = append(pbQuery.Queries, &pluginv2.DataQuery{
|
||||
Json: model,
|
||||
IntervalMS: intervalMS,
|
||||
RefId: q.RefID,
|
||||
MaxDataPoints: maxDatapoints,
|
||||
QueryType: q.QueryType,
|
||||
TimeRange: q.RelativeTimeRange.toTimeRange(time.Now()),
|
||||
})
|
||||
}
|
||||
|
||||
tw := plugins.Transform
|
||||
pbRes, err := tw.TransformClient.TransformData(ctx.Ctx, pbQuery, tw.Callback)
|
||||
if err != nil {
|
||||
return &result, err
|
||||
}
|
||||
|
||||
for refID, res := range pbRes.Responses {
|
||||
if refID != c.RefID {
|
||||
continue
|
||||
}
|
||||
df := tsdb.NewEncodedDataFrames(res.Frames)
|
||||
result.Results, err = df.Decoded()
|
||||
if err != nil {
|
||||
result.Error = err
|
||||
return &result, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Results) == 0 {
|
||||
err = fmt.Errorf("no GEL results")
|
||||
result.Error = err
|
||||
return &result, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// EvaluateExecutionResult takes the ExecutionResult, and returns a frame where
|
||||
// each column is a string type that holds a string representing its state.
|
||||
func EvaluateExecutionResult(results *ExecutionResults) (Results, error) {
|
||||
evalResults := make([]result, 0)
|
||||
labels := make(map[string]bool)
|
||||
for _, f := range results.Results {
|
||||
rowLen, err := f.RowLen()
|
||||
if err != nil {
|
||||
return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: "unable to get frame row length", err: err}
|
||||
}
|
||||
if rowLen > 1 {
|
||||
return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: fmt.Sprintf("unexpected row length: %d instead of 1", rowLen)}
|
||||
}
|
||||
|
||||
if len(f.Fields) > 1 {
|
||||
return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: fmt.Sprintf("unexpected field length: %d instead of 1", len(f.Fields))}
|
||||
}
|
||||
|
||||
if f.Fields[0].Type() != data.FieldTypeNullableFloat64 {
|
||||
return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: fmt.Sprintf("invalid field type: %d", f.Fields[0].Type())}
|
||||
}
|
||||
|
||||
labelsStr := f.Fields[0].Labels.String()
|
||||
_, ok := labels[labelsStr]
|
||||
if ok {
|
||||
return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: fmt.Sprintf("frame cannot uniquely be identified by its labels: %s", labelsStr)}
|
||||
}
|
||||
labels[labelsStr] = true
|
||||
|
||||
state := Normal
|
||||
val, err := f.Fields[0].FloatAt(0)
|
||||
if err != nil || val != 0 {
|
||||
state = Alerting
|
||||
}
|
||||
|
||||
evalResults = append(evalResults, result{
|
||||
Instance: f.Fields[0].Labels,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
return evalResults, nil
|
||||
}
|
||||
|
||||
// AsDataFrame forms the EvalResults in Frame suitable for displaying in the table panel of the front end.
|
||||
// This may be temporary, as there might be a fair amount we want to display in the frontend, and it might not make sense to store that in data.Frame.
|
||||
// For the first pass, I would expect a Frame with a single row, and a column for each instance with a boolean value.
|
||||
func (evalResults Results) AsDataFrame() data.Frame {
|
||||
fields := make([]*data.Field, 0)
|
||||
for _, evalResult := range evalResults {
|
||||
fields = append(fields, data.NewField("", evalResult.Instance, []bool{evalResult.State != Normal}))
|
||||
}
|
||||
f := data.NewFrame("", fields...)
|
||||
return *f
|
||||
}
|
||||
Reference in New Issue
Block a user