mirror of
https://github.com/grafana/grafana.git
synced 2024-12-01 13:09:22 -06:00
ce90a1f2be
* Alerting: Apply query optimization to eval endpoints Previously, query optimization was applied to alert queries when scheduled but not when ran through `api/v1/eval` or `/api/v1/rule/test/grafana`. This could lead to discrepancies between preview and scheduled alert results.
368 lines
12 KiB
Go
368 lines
12 KiB
Go
package store
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/grafana/pkg/services/datasources"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
)
|
|
|
|
const (
|
|
promIsInstant = true
|
|
promIsNotInstant = false
|
|
promExternalDS = "some-external-ds"
|
|
)
|
|
|
|
func TestCanBeInstant(t *testing.T) {
|
|
tcs := []struct {
|
|
name string
|
|
expected bool
|
|
expectedOptimizations []Optimization
|
|
rule *models.AlertRule
|
|
}{
|
|
{
|
|
name: "valid loki rule that can be migrated from range to instant",
|
|
expected: true,
|
|
expectedOptimizations: []Optimization{{i: 0, t: datasources.DS_LOKI, RefID: "A"}},
|
|
rule: createMigrateableLokiRule(t),
|
|
},
|
|
{
|
|
name: "valid prom rule that can be migrated from range to instant",
|
|
expected: true,
|
|
expectedOptimizations: []Optimization{{i: 0, t: datasources.DS_PROMETHEUS, RefID: "A"}},
|
|
rule: createMigratablePromRule(t),
|
|
},
|
|
{
|
|
name: "valid loki rule with external loki datasource",
|
|
expected: true,
|
|
expectedOptimizations: []Optimization{{i: 0, t: datasources.DS_LOKI, RefID: "A"}},
|
|
rule: createMigrateableLokiRule(t, func(r *models.AlertRule) {
|
|
r.Data[0].DatasourceUID = "something-external"
|
|
}),
|
|
},
|
|
{
|
|
name: "valid prom rule with external prometheus datasource",
|
|
expected: true,
|
|
expectedOptimizations: []Optimization{{i: 0, t: datasources.DS_PROMETHEUS, RefID: "A"}},
|
|
rule: createMigratablePromRule(t, func(r *models.AlertRule) {
|
|
r.Data[0].DatasourceUID = "something-external"
|
|
}),
|
|
},
|
|
{
|
|
name: "valid prom rule with missing datasource",
|
|
expected: true,
|
|
expectedOptimizations: []Optimization{{i: 0, t: datasources.DS_PROMETHEUS, RefID: "A"}},
|
|
rule: createMigratablePromRuleWithDefaultDS(t),
|
|
},
|
|
{
|
|
name: "valid prom rule with missing datasource and instant query",
|
|
expected: false,
|
|
rule: createMigratablePromRuleWithDefaultDS(t, func(r *models.AlertRule) {
|
|
raw := make(map[string]any)
|
|
err := json.Unmarshal(r.Data[0].Model, &raw)
|
|
require.NoError(t, err)
|
|
raw["range"] = false
|
|
r.Data[0].Model, err = json.Marshal(raw)
|
|
require.NoError(t, err)
|
|
}),
|
|
},
|
|
{
|
|
name: "valid loki multi query rule with loki datasources",
|
|
expected: true,
|
|
expectedOptimizations: []Optimization{
|
|
{i: 0, t: datasources.DS_LOKI, RefID: "TotalRequests"},
|
|
{i: 1, t: datasources.DS_LOKI, RefID: "TotalErrors"},
|
|
},
|
|
rule: createMultiQueryMigratableLokiRule(t),
|
|
},
|
|
{
|
|
name: "valid prom multi query rule with prom datasources",
|
|
expected: true,
|
|
expectedOptimizations: []Optimization{
|
|
{i: 0, t: datasources.DS_PROMETHEUS, RefID: "TotalRequests"},
|
|
{i: 1, t: datasources.DS_PROMETHEUS, RefID: "TotalErrors"},
|
|
},
|
|
rule: createMultiQueryMigratablePromRule(t),
|
|
},
|
|
{
|
|
name: "invalid rule where the data array is too short to be migrateable",
|
|
expected: false,
|
|
rule: createMigrateableLokiRule(t, func(r *models.AlertRule) {
|
|
r.Data = []models.AlertQuery{r.Data[0]}
|
|
}),
|
|
},
|
|
{
|
|
name: "invalid rule that is not a range query",
|
|
expected: false,
|
|
rule: createMigrateableLokiRule(t, func(r *models.AlertRule) {
|
|
r.Data[0].QueryType = "something-else"
|
|
}),
|
|
},
|
|
{
|
|
name: "invalid rule that has not last() as aggregation",
|
|
expected: false,
|
|
rule: createMigrateableLokiRule(t, func(r *models.AlertRule) {
|
|
r.Data[1] = reducer(t, "B", "A", "avg")
|
|
}),
|
|
},
|
|
{
|
|
name: "invalid rule that has not all reducers last()",
|
|
expected: false,
|
|
rule: createMigrateableLokiRule(t, func(r *models.AlertRule) {
|
|
r.Data = append(r.Data, reducer(t, "invalid-reducer", "A", "min"))
|
|
}),
|
|
},
|
|
{
|
|
name: "invalid rule that has no aggregation",
|
|
expected: false,
|
|
rule: createMigrateableLokiRule(t, func(r *models.AlertRule) {
|
|
r.Data[1].DatasourceUID = "something-else"
|
|
}),
|
|
},
|
|
{
|
|
name: "invalid rule that has not last() pointing to range query",
|
|
expected: false,
|
|
rule: createMigrateableLokiRule(t, func(r *models.AlertRule) {
|
|
raw := make(map[string]any)
|
|
err := json.Unmarshal(r.Data[1].Model, &raw)
|
|
require.NoError(t, err)
|
|
raw["expression"] = "C"
|
|
r.Data[1].Model, err = json.Marshal(raw)
|
|
require.NoError(t, err)
|
|
}),
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
optimizations, canBe := canBeInstant(tc.rule.Data)
|
|
require.Equal(t, tc.expected, canBe)
|
|
require.Equal(t, tc.expectedOptimizations, optimizations)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMigrateLokiQueryToInstant(t *testing.T) {
|
|
original := createMigrateableLokiRule(t)
|
|
migrated := createMigrateableLokiRule(t, func(r *models.AlertRule) {
|
|
r.Data[0] = lokiQuery(t, "A", "instant", "grafanacloud-logs")
|
|
})
|
|
|
|
optimizableIndices, canBeOptimized := canBeInstant(original.Data)
|
|
require.True(t, canBeOptimized)
|
|
require.NoError(t, migrateToInstant(original.Data, optimizableIndices))
|
|
|
|
require.Equal(t, migrated.Data[0].QueryType, original.Data[0].QueryType)
|
|
|
|
originalModel := make(map[string]any)
|
|
require.NoError(t, json.Unmarshal(original.Data[0].Model, &originalModel))
|
|
migratedModel := make(map[string]any)
|
|
require.NoError(t, json.Unmarshal(migrated.Data[0].Model, &migratedModel))
|
|
|
|
require.Equal(t, migratedModel, originalModel)
|
|
|
|
_, canBeOptimized = canBeInstant(original.Data)
|
|
require.False(t, canBeOptimized)
|
|
}
|
|
|
|
func TestMigrateMultiLokiQueryToInstant(t *testing.T) {
|
|
original := createMultiQueryMigratableLokiRule(t)
|
|
migrated := createMultiQueryMigratableLokiRule(t, func(r *models.AlertRule) {
|
|
r.Data[0] = lokiQuery(t, "TotalRequests", "instant", "grafanacloud-logs")
|
|
r.Data[1] = lokiQuery(t, "TotalErrors", "instant", "grafanacloud-logs")
|
|
})
|
|
|
|
_, canBeOptimized := canBeInstant(original.Data)
|
|
require.True(t, canBeOptimized)
|
|
|
|
optimizations, err := OptimizeAlertQueries(original.Data)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, optimizations[0].RefID, original.Data[0].RefID)
|
|
require.Equal(t, optimizations[1].RefID, original.Data[1].RefID)
|
|
|
|
require.Equal(t, migrated.Data[0].QueryType, original.Data[0].QueryType)
|
|
require.Equal(t, migrated.Data[1].QueryType, original.Data[1].QueryType)
|
|
|
|
originalModel := make(map[string]any)
|
|
require.NoError(t, json.Unmarshal(original.Data[0].Model, &originalModel))
|
|
migratedModel := make(map[string]any)
|
|
require.NoError(t, json.Unmarshal(migrated.Data[0].Model, &migratedModel))
|
|
|
|
require.Equal(t, migratedModel, originalModel)
|
|
|
|
originalModel = make(map[string]any)
|
|
require.NoError(t, json.Unmarshal(original.Data[1].Model, &originalModel))
|
|
migratedModel = make(map[string]any)
|
|
require.NoError(t, json.Unmarshal(migrated.Data[1].Model, &migratedModel))
|
|
|
|
require.Equal(t, migratedModel, originalModel)
|
|
|
|
_, canBeOptimized = canBeInstant(original.Data)
|
|
require.False(t, canBeOptimized)
|
|
}
|
|
|
|
func TestMigratePromQueryToInstant(t *testing.T) {
|
|
original := createMigratablePromRule(t)
|
|
migrated := createMigratablePromRule(t, func(r *models.AlertRule) {
|
|
r.Data[0] = prometheusQuery(t, "A", promExternalDS, promIsInstant)
|
|
})
|
|
|
|
optimizableIndices, canBeOptimized := canBeInstant(original.Data)
|
|
require.True(t, canBeOptimized)
|
|
require.NoError(t, migrateToInstant(original.Data, optimizableIndices))
|
|
|
|
originalModel := make(map[string]any)
|
|
require.NoError(t, json.Unmarshal(original.Data[0].Model, &originalModel))
|
|
migratedModel := make(map[string]any)
|
|
require.NoError(t, json.Unmarshal(migrated.Data[0].Model, &migratedModel))
|
|
|
|
require.Equal(t, migratedModel, originalModel)
|
|
|
|
_, canBeOptimized = canBeInstant(original.Data)
|
|
require.False(t, canBeOptimized)
|
|
}
|
|
|
|
func TestMigrateMultiPromQueryToInstant(t *testing.T) {
|
|
original := createMultiQueryMigratablePromRule(t)
|
|
migrated := createMultiQueryMigratablePromRule(t, func(r *models.AlertRule) {
|
|
r.Data[0] = prometheusQuery(t, "TotalRequests", promExternalDS, promIsInstant)
|
|
r.Data[1] = prometheusQuery(t, "TotalErrors", promExternalDS, promIsInstant)
|
|
})
|
|
|
|
_, canBeOptimized := canBeInstant(original.Data)
|
|
require.True(t, canBeOptimized)
|
|
|
|
optimizations, err := OptimizeAlertQueries(original.Data)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, optimizations[0].RefID, original.Data[0].RefID)
|
|
require.Equal(t, optimizations[1].RefID, original.Data[1].RefID)
|
|
|
|
originalModel := make(map[string]any)
|
|
require.NoError(t, json.Unmarshal(original.Data[0].Model, &originalModel))
|
|
migratedModel := make(map[string]any)
|
|
require.NoError(t, json.Unmarshal(migrated.Data[0].Model, &migratedModel))
|
|
|
|
require.Equal(t, migratedModel, originalModel)
|
|
|
|
originalModel = make(map[string]any)
|
|
require.NoError(t, json.Unmarshal(original.Data[1].Model, &originalModel))
|
|
migratedModel = make(map[string]any)
|
|
require.NoError(t, json.Unmarshal(migrated.Data[1].Model, &migratedModel))
|
|
|
|
require.Equal(t, migratedModel, originalModel)
|
|
|
|
_, canBeOptimized = canBeInstant(original.Data)
|
|
require.False(t, canBeOptimized)
|
|
}
|
|
|
|
func createMigrateableLokiRule(t *testing.T, muts ...func(*models.AlertRule)) *models.AlertRule {
|
|
t.Helper()
|
|
r := &models.AlertRule{
|
|
Data: []models.AlertQuery{
|
|
lokiQuery(t, "A", "range", "grafanacloud-logs"),
|
|
reducer(t, "B", "A", "last"),
|
|
},
|
|
}
|
|
for _, m := range muts {
|
|
m(r)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func createMultiQueryMigratableLokiRule(t *testing.T, muts ...func(*models.AlertRule)) *models.AlertRule {
|
|
t.Helper()
|
|
r := &models.AlertRule{
|
|
Data: []models.AlertQuery{
|
|
lokiQuery(t, "TotalRequests", "range", "grafanacloud-logs"),
|
|
lokiQuery(t, "TotalErrors", "range", "grafanacloud-logs"),
|
|
reducer(t, "TotalRequests_Last", "TotalRequests", "last"),
|
|
reducer(t, "TotalErrors_Last", "TotalErrors", "last"),
|
|
},
|
|
}
|
|
for _, m := range muts {
|
|
m(r)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func createMigratablePromRule(t *testing.T, muts ...func(*models.AlertRule)) *models.AlertRule {
|
|
t.Helper()
|
|
r := &models.AlertRule{
|
|
Data: []models.AlertQuery{
|
|
prometheusQuery(t, "A", promExternalDS, promIsNotInstant),
|
|
reducer(t, "B", "A", "last"),
|
|
},
|
|
}
|
|
for _, m := range muts {
|
|
m(r)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func createMigratablePromRuleWithDefaultDS(t *testing.T, muts ...func(*models.AlertRule)) *models.AlertRule {
|
|
t.Helper()
|
|
r := &models.AlertRule{
|
|
Data: []models.AlertQuery{
|
|
prometheusQueryWithoutDS(t, "A", grafanaCloudProm, promIsNotInstant),
|
|
reducer(t, "B", "A", "last"),
|
|
},
|
|
}
|
|
for _, m := range muts {
|
|
m(r)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func createMultiQueryMigratablePromRule(t *testing.T, muts ...func(*models.AlertRule)) *models.AlertRule {
|
|
t.Helper()
|
|
r := &models.AlertRule{
|
|
Data: []models.AlertQuery{
|
|
prometheusQuery(t, "TotalRequests", promExternalDS, promIsNotInstant),
|
|
prometheusQuery(t, "TotalErrors", promExternalDS, promIsNotInstant),
|
|
reducer(t, "TotalRequests_Last", "TotalRequests", "last"),
|
|
reducer(t, "TotalErrors_Last", "TotalErrors", "last"),
|
|
},
|
|
}
|
|
for _, m := range muts {
|
|
m(r)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func lokiQuery(t *testing.T, refID, queryType, datasourceUID string) models.AlertQuery {
|
|
t.Helper()
|
|
return models.CreateLokiQuery(refID, "1", 1000, 43200, queryType, datasourceUID)
|
|
}
|
|
|
|
func prometheusQuery(t *testing.T, refID, datasourceUID string, isInstant bool) models.AlertQuery {
|
|
t.Helper()
|
|
return models.CreatePrometheusQuery(refID, "1", 1000, 43200, isInstant, datasourceUID)
|
|
}
|
|
|
|
func prometheusQueryWithoutDS(t *testing.T, refID, datasourceUID string, isInstant bool) models.AlertQuery {
|
|
t.Helper()
|
|
return models.AlertQuery{
|
|
RefID: refID,
|
|
DatasourceUID: datasourceUID,
|
|
Model: []byte(fmt.Sprintf(`{
|
|
"instant": %t,
|
|
"range": %t,
|
|
"editorMode": "code",
|
|
"expr": "1",
|
|
"intervalMs": 1000,
|
|
"maxDataPoints": 43200,
|
|
"refId": "%s"
|
|
}`, isInstant, !isInstant, refID)),
|
|
}
|
|
}
|
|
|
|
func reducer(t *testing.T, refID, exp, op string) models.AlertQuery {
|
|
t.Helper()
|
|
return models.CreateReduceExpression(refID, exp, op)
|
|
}
|