Alerting: Add checks for non supported units - disable defaulting to seconds (#32477)

* Add error handling for unknown units

* Fix test cases

* Add case for empty string

* Changed tests from convey to testify

* Fix lints

* Move regex vars

* Add regex as ng-patterns on alert_tab.html

* Update public/app/features/alerting/partials/alert_tab.html

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>

* Update public/app/features/alerting/partials/alert_tab.html

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>

* Make zero and empty string not throw errors

* Updated validation error comments

* Frequency should allow zero or empty strings

* use checkFrequency instead of ng-pattern

checkFrequency is not triggered if ng-pattern is defined.

* Extract getForValue func - add tests

* Fix linting

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
Dimitris Sotirakis 2021-04-12 15:53:51 +03:00 committed by GitHub
parent 1ed73ecef8
commit 258578766b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 342 additions and 345 deletions

View File

@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
@ -109,12 +108,10 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
}
rawFor := jsonAlert.Get("for").MustString()
var forValue time.Duration
if rawFor != "" {
forValue, err = time.ParseDuration(rawFor)
if err != nil {
return nil, ValidationError{Reason: "Could not parse for"}
}
forValue, err := getForValue(rawFor)
if err != nil {
return nil, err
}
alert := &models.Alert{

View File

@ -9,293 +9,239 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestAlertRuleExtraction(t *testing.T) {
Convey("Parsing alert rules from dashboard json", t, func() {
RegisterCondition("query", func(model *simplejson.Json, index int) (Condition, error) {
return &FakeCondition{}, nil
})
RegisterCondition("query", func(model *simplejson.Json, index int) (Condition, error) {
return &FakeCondition{}, nil
})
// mock data
defaultDs := &models.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
graphite2Ds := &models.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
influxDBDs := &models.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"}
prom := &models.DataSource{Id: 17, OrgId: 1, Name: "Prometheus"}
// mock data
defaultDs := &models.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
graphite2Ds := &models.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
influxDBDs := &models.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"}
prom := &models.DataSource{Id: 17, OrgId: 1, Name: "Prometheus"}
bus.AddHandler("test", func(query *models.GetDefaultDataSourceQuery) error {
bus.AddHandler("test", func(query *models.GetDefaultDataSourceQuery) error {
query.Result = defaultDs
return nil
})
bus.AddHandler("test", func(query *models.GetDataSourceQuery) error {
if query.Name == defaultDs.Name {
query.Result = defaultDs
return nil
})
}
if query.Name == graphite2Ds.Name {
query.Result = graphite2Ds
}
if query.Name == influxDBDs.Name {
query.Result = influxDBDs
}
if query.Name == prom.Name {
query.Result = prom
}
bus.AddHandler("test", func(query *models.GetDataSourceQuery) error {
if query.Name == defaultDs.Name {
query.Result = defaultDs
}
if query.Name == graphite2Ds.Name {
query.Result = graphite2Ds
}
if query.Name == influxDBDs.Name {
query.Result = influxDBDs
}
if query.Name == prom.Name {
query.Result = prom
}
return nil
})
return nil
})
json, err := ioutil.ReadFile("./testdata/graphite-alert.json")
require.Nil(t, err)
json, err := ioutil.ReadFile("./testdata/graphite-alert.json")
So(err, ShouldBeNil)
t.Run("Parsing alert rules from dashboard json", func(t *testing.T) {
dashJSON, err := simplejson.NewJson(json)
require.Nil(t, err)
Convey("Extractor should not modify the original json", func() {
dashJSON, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := models.NewDashboardFromJson(dashJSON)
dash := models.NewDashboardFromJson(dashJSON)
getTarget := func(j *simplejson.Json) string {
rowObj := j.Get("rows").MustArray()[0]
row := simplejson.NewFromAny(rowObj)
panelObj := row.Get("panels").MustArray()[0]
panel := simplejson.NewFromAny(panelObj)
conditionObj := panel.Get("alert").Get("conditions").MustArray()[0]
condition := simplejson.NewFromAny(conditionObj)
return condition.Get("query").Get("model").Get("target").MustString()
}
getTarget := func(j *simplejson.Json) string {
rowObj := j.Get("rows").MustArray()[0]
row := simplejson.NewFromAny(rowObj)
panelObj := row.Get("panels").MustArray()[0]
panel := simplejson.NewFromAny(panelObj)
conditionObj := panel.Get("alert").Get("conditions").MustArray()[0]
condition := simplejson.NewFromAny(conditionObj)
return condition.Get("query").Get("model").Get("target").MustString()
}
require.Equal(t, getTarget(dashJSON), "")
Convey("Dashboard json rows.panels.alert.query.model.target should be empty", func() {
So(getTarget(dashJSON), ShouldEqual, "")
})
extractor := NewDashAlertExtractor(dash, 1, nil)
_, _ = extractor.GetAlerts()
extractor := NewDashAlertExtractor(dash, 1, nil)
_, _ = extractor.GetAlerts()
require.Equal(t, getTarget(dashJSON), "")
})
Convey("Dashboard json should not be updated after extracting rules", func() {
So(getTarget(dashJSON), ShouldEqual, "")
})
})
t.Run("Parsing and validating dashboard containing graphite alerts", func(t *testing.T) {
dashJSON, err := simplejson.NewJson(json)
require.Nil(t, err)
Convey("Parsing and validating dashboard containing graphite alerts", func() {
dashJSON, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
alerts, err := extractor.GetAlerts()
alerts, err := extractor.GetAlerts()
require.Nil(t, err)
Convey("Get rules without error", func() {
So(err, ShouldBeNil)
})
require.Len(t, alerts, 2)
Convey("all properties have been set", func() {
So(len(alerts), ShouldEqual, 2)
for _, v := range alerts {
require.EqualValues(t, v.DashboardId, 57)
require.NotEmpty(t, v.Name)
require.NotEmpty(t, v.Message)
for _, v := range alerts {
So(v.DashboardId, ShouldEqual, 57)
So(v.Name, ShouldNotBeEmpty)
So(v.Message, ShouldNotBeEmpty)
settings := simplejson.NewFromAny(v.Settings)
require.Equal(t, settings.Get("interval").MustString(""), "")
}
settings := simplejson.NewFromAny(v.Settings)
So(settings.Get("interval").MustString(""), ShouldEqual, "")
}
require.EqualValues(t, alerts[0].Handler, 1)
require.EqualValues(t, alerts[1].Handler, 0)
Convey("should extract handler property", func() {
So(alerts[0].Handler, ShouldEqual, 1)
So(alerts[1].Handler, ShouldEqual, 0)
})
require.EqualValues(t, alerts[0].Frequency, 60)
require.EqualValues(t, alerts[1].Frequency, 60)
Convey("should extract frequency in seconds", func() {
So(alerts[0].Frequency, ShouldEqual, 60)
So(alerts[1].Frequency, ShouldEqual, 60)
})
require.EqualValues(t, alerts[0].PanelId, 3)
require.EqualValues(t, alerts[1].PanelId, 4)
Convey("should extract panel idc", func() {
So(alerts[0].PanelId, ShouldEqual, 3)
So(alerts[1].PanelId, ShouldEqual, 4)
})
require.Equal(t, alerts[0].For, time.Minute*2)
require.Equal(t, alerts[1].For, time.Duration(0))
Convey("should extract for param", func() {
So(alerts[0].For, ShouldEqual, time.Minute*2)
So(alerts[1].For, ShouldEqual, time.Duration(0))
})
require.Equal(t, alerts[0].Name, "name1")
require.Equal(t, alerts[0].Message, "desc1")
require.Equal(t, alerts[1].Name, "name2")
require.Equal(t, alerts[1].Message, "desc2")
Convey("should extract name and desc", func() {
So(alerts[0].Name, ShouldEqual, "name1")
So(alerts[0].Message, ShouldEqual, "desc1")
So(alerts[1].Name, ShouldEqual, "name2")
So(alerts[1].Message, ShouldEqual, "desc2")
})
condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0])
query := condition.Get("query")
require.EqualValues(t, query.Get("datasourceId").MustInt64(), 12)
Convey("should set datasourceId", func() {
condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0])
query := condition.Get("query")
So(query.Get("datasourceId").MustInt64(), ShouldEqual, 12)
})
condition = simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0])
model := condition.Get("query").Get("model")
require.Equal(t, model.Get("target").MustString(), "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)")
})
Convey("should copy query model to condition", func() {
condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0])
model := condition.Get("query").Get("model")
So(model.Get("target").MustString(), ShouldEqual, "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)")
})
})
})
t.Run("Panels missing id should return error", func(t *testing.T) {
panelWithoutID, err := ioutil.ReadFile("./testdata/panels-missing-id.json")
require.Nil(t, err)
Convey("Panels missing id should return error", func() {
panelWithoutID, err := ioutil.ReadFile("./testdata/panels-missing-id.json")
So(err, ShouldBeNil)
dashJSON, err := simplejson.NewJson(panelWithoutID)
require.Nil(t, err)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
dashJSON, err := simplejson.NewJson(panelWithoutID)
So(err, ShouldBeNil)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
_, err = extractor.GetAlerts()
_, err = extractor.GetAlerts()
require.NotNil(t, err)
})
Convey("panels without Id should return error", func() {
So(err, ShouldNotBeNil)
})
})
t.Run("Panels missing id should return error", func(t *testing.T) {
panelWithIDZero, err := ioutil.ReadFile("./testdata/panel-with-id-0.json")
require.Nil(t, err)
Convey("Panel with id set to zero should return error", func() {
panelWithIDZero, err := ioutil.ReadFile("./testdata/panel-with-id-0.json")
So(err, ShouldBeNil)
dashJSON, err := simplejson.NewJson(panelWithIDZero)
require.Nil(t, err)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
dashJSON, err := simplejson.NewJson(panelWithIDZero)
So(err, ShouldBeNil)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
_, err = extractor.GetAlerts()
_, err = extractor.GetAlerts()
require.NotNil(t, err)
})
Convey("panel with id 0 should return error", func() {
So(err, ShouldNotBeNil)
})
})
t.Run("Panel does not have datasource configured, use the default datasource", func(t *testing.T) {
panelWithoutSpecifiedDatasource, err := ioutil.ReadFile("./testdata/panel-without-specified-datasource.json")
require.Nil(t, err)
Convey("Panel does not have datasource configured, use the default datasource", func() {
panelWithoutSpecifiedDatasource, err := ioutil.ReadFile("./testdata/panel-without-specified-datasource.json")
So(err, ShouldBeNil)
dashJSON, err := simplejson.NewJson(panelWithoutSpecifiedDatasource)
require.Nil(t, err)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
dashJSON, err := simplejson.NewJson(panelWithoutSpecifiedDatasource)
So(err, ShouldBeNil)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
alerts, err := extractor.GetAlerts()
require.Nil(t, err)
alerts, err := extractor.GetAlerts()
condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0])
query := condition.Get("query")
require.EqualValues(t, query.Get("datasourceId").MustInt64(), 12)
})
Convey("Get rules without error", func() {
So(err, ShouldBeNil)
})
t.Run("Parse alerts from dashboard without rows", func(t *testing.T) {
json, err := ioutil.ReadFile("./testdata/v5-dashboard.json")
require.Nil(t, err)
Convey("Use default datasource", func() {
condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0])
query := condition.Get("query")
So(query.Get("datasourceId").MustInt64(), ShouldEqual, 12)
})
})
dashJSON, err := simplejson.NewJson(json)
require.Nil(t, err)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
Convey("Parse alerts from dashboard without rows", func() {
json, err := ioutil.ReadFile("./testdata/v5-dashboard.json")
So(err, ShouldBeNil)
alerts, err := extractor.GetAlerts()
require.Nil(t, err)
dashJSON, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
require.Len(t, alerts, 2)
})
alerts, err := extractor.GetAlerts()
t.Run("Alert notifications are in DB", func(t *testing.T) {
sqlstore.InitTestDB(t)
firstNotification := models.CreateAlertNotificationCommand{Uid: "notifier1", OrgId: 1, Name: "1"}
err = sqlstore.CreateAlertNotificationCommand(&firstNotification)
require.Nil(t, err)
secondNotification := models.CreateAlertNotificationCommand{Uid: "notifier2", OrgId: 1, Name: "2"}
err = sqlstore.CreateAlertNotificationCommand(&secondNotification)
require.Nil(t, err)
Convey("Get rules without error", func() {
So(err, ShouldBeNil)
})
json, err := ioutil.ReadFile("./testdata/influxdb-alert.json")
require.Nil(t, err)
Convey("Should have 2 alert rule", func() {
So(len(alerts), ShouldEqual, 2)
})
})
dashJSON, err := simplejson.NewJson(json)
require.Nil(t, err)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
Convey("Alert notifications are in DB", func() {
sqlstore.InitTestDB(t)
firstNotification := models.CreateAlertNotificationCommand{Uid: "notifier1", OrgId: 1, Name: "1"}
err = sqlstore.CreateAlertNotificationCommand(&firstNotification)
So(err, ShouldBeNil)
secondNotification := models.CreateAlertNotificationCommand{Uid: "notifier2", OrgId: 1, Name: "2"}
err = sqlstore.CreateAlertNotificationCommand(&secondNotification)
So(err, ShouldBeNil)
alerts, err := extractor.GetAlerts()
require.Nil(t, err)
Convey("Parse and validate dashboard containing influxdb alert", func() {
json, err := ioutil.ReadFile("./testdata/influxdb-alert.json")
So(err, ShouldBeNil)
require.Len(t, alerts, 1)
dashJSON, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
for _, alert := range alerts {
require.EqualValues(t, alert.DashboardId, 4)
alerts, err := extractor.GetAlerts()
conditions := alert.Settings.Get("conditions").MustArray()
cond := simplejson.NewFromAny(conditions[0])
Convey("Get rules without error", func() {
So(err, ShouldBeNil)
})
require.Equal(t, cond.Get("query").Get("model").Get("interval").MustString(), ">10s")
}
})
Convey("should be able to read interval", func() {
So(len(alerts), ShouldEqual, 1)
t.Run("Should be able to extract collapsed panels", func(t *testing.T) {
json, err := ioutil.ReadFile("./testdata/collapsed-panels.json")
require.Nil(t, err)
for _, alert := range alerts {
So(alert.DashboardId, ShouldEqual, 4)
dashJSON, err := simplejson.NewJson(json)
require.Nil(t, err)
conditions := alert.Settings.Get("conditions").MustArray()
cond := simplejson.NewFromAny(conditions[0])
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
So(cond.Get("query").Get("model").Get("interval").MustString(), ShouldEqual, ">10s")
}
})
})
alerts, err := extractor.GetAlerts()
require.Nil(t, err)
Convey("Should be able to extract collapsed panels", func() {
json, err := ioutil.ReadFile("./testdata/collapsed-panels.json")
So(err, ShouldBeNil)
require.Len(t, alerts, 4)
})
dashJSON, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
t.Run("Parse and validate dashboard without id and containing an alert", func(t *testing.T) {
json, err := ioutil.ReadFile("./testdata/dash-without-id.json")
require.Nil(t, err)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
dashJSON, err := simplejson.NewJson(json)
require.Nil(t, err)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
alerts, err := extractor.GetAlerts()
err = extractor.ValidateAlerts()
Convey("Get rules without error", func() {
So(err, ShouldBeNil)
})
require.Nil(t, err)
Convey("should be able to extract collapsed alerts", func() {
So(len(alerts), ShouldEqual, 4)
})
})
Convey("Parse and validate dashboard without id and containing an alert", func() {
json, err := ioutil.ReadFile("./testdata/dash-without-id.json")
So(err, ShouldBeNil)
dashJSON, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
err = extractor.ValidateAlerts()
Convey("Should validate without error", func() {
So(err, ShouldBeNil)
})
Convey("Should fail on save", func() {
_, err := extractor.GetAlerts()
So(err.Error(), ShouldEqual, "alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1")
})
})
})
_, err = extractor.GetAlerts()
require.Equal(t, err.Error(), "alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1")
})
}

View File

@ -3,6 +3,7 @@ package alerting
import (
"errors"
"fmt"
"reflect"
"regexp"
"strconv"
"time"
@ -12,12 +13,28 @@ import (
"github.com/grafana/grafana/pkg/models"
)
var unitMultiplier = map[string]int{
"s": 1,
"m": 60,
"h": 3600,
"d": 86400,
}
var (
valueFormatRegex = regexp.MustCompile(`^\d+`)
isDigitRegex = regexp.MustCompile(`^[0-9]+$`)
unitFormatRegex = regexp.MustCompile(`[a-z]+`)
)
var (
// ErrFrequencyCannotBeZeroOrLess frequency cannot be below zero
ErrFrequencyCannotBeZeroOrLess = errors.New(`"evaluate every" cannot be zero or below`)
// ErrFrequencyCouldNotBeParsed frequency cannot be parsed
ErrFrequencyCouldNotBeParsed = errors.New(`"evaluate every" field could not be parsed`)
// ErrWrongUnitFormat wrong unit format
ErrWrongUnitFormat = fmt.Errorf(`time unit not supported. supported units: %s`, reflect.ValueOf(unitMultiplier).MapKeys())
)
// Rule is the in-memory version of an alert rule.
@ -72,20 +89,18 @@ func (e ValidationError) Error() string {
return fmt.Sprintf("alert validation error: %s", extraInfo)
}
var (
valueFormatRegex = regexp.MustCompile(`^\d+`)
unitFormatRegex = regexp.MustCompile(`\w{1}$`)
)
var unitMultiplier = map[string]int{
"s": 1,
"m": 60,
"h": 3600,
"d": 86400,
}
func getTimeDurationStringToSeconds(str string) (int64, error) {
multiplier := 1
// Check if frequency lacks unit
if isDigitRegex.MatchString(str) || str == "" {
return 0, ErrFrequencyCouldNotBeParsed
}
unit := unitFormatRegex.FindAllString(str, 1)[0]
if _, ok := unitMultiplier[unit]; !ok {
return 0, ErrWrongUnitFormat
}
multiplier := unitMultiplier[unit]
matches := valueFormatRegex.FindAllString(str, 1)
@ -102,15 +117,31 @@ func getTimeDurationStringToSeconds(str string) (int64, error) {
return 0, ErrFrequencyCannotBeZeroOrLess
}
unit := unitFormatRegex.FindAllString(str, 1)[0]
if val, ok := unitMultiplier[unit]; ok {
multiplier = val
}
return int64(value * multiplier), nil
}
func getForValue(rawFor string) (time.Duration, error) {
var forValue time.Duration
var err error
if rawFor != "" {
if rawFor != "0" {
strings := unitFormatRegex.FindAllString(rawFor, 1)
if strings == nil {
return 0, ValidationError{Reason: fmt.Sprintf("no specified unit, error: %s", ErrWrongUnitFormat.Error())}
}
if _, ok := unitMultiplier[strings[0]]; !ok {
return 0, ValidationError{Reason: fmt.Sprintf("could not parse for field, error: %s", ErrWrongUnitFormat.Error())}
}
}
forValue, err = time.ParseDuration(rawFor)
if err != nil {
return 0, ValidationError{Reason: "Could not parse for field"}
}
}
return forValue, nil
}
// NewRuleFromDBAlert maps a db version of
// alert to an in-memory version.
func NewRuleFromDBAlert(ruleDef *models.Alert, logTranslationFailures bool) (*Rule, error) {

View File

@ -1,13 +1,14 @@
package alerting
import (
"fmt"
"testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/sqlstore"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -28,11 +29,12 @@ func TestAlertRuleFrequencyParsing(t *testing.T) {
{input: "10m", result: 600},
{input: "1h", result: 3600},
{input: "1d", result: 86400},
{input: "1o", result: 1},
{input: "1o", err: ErrWrongUnitFormat},
{input: "0s", err: ErrFrequencyCannotBeZeroOrLess},
{input: "0m", err: ErrFrequencyCannotBeZeroOrLess},
{input: "0h", err: ErrFrequencyCannotBeZeroOrLess},
{input: "0", err: ErrFrequencyCannotBeZeroOrLess},
{input: "0", err: ErrFrequencyCouldNotBeParsed},
{input: "", err: ErrFrequencyCouldNotBeParsed},
{input: "-1s", err: ErrFrequencyCouldNotBeParsed},
}
@ -49,28 +51,52 @@ func TestAlertRuleFrequencyParsing(t *testing.T) {
}
}
func TestAlertRuleForParsing(t *testing.T) {
tcs := []struct {
input string
err error
result time.Duration
}{
{input: "10s", result: time.Duration(10000000000)},
{input: "10m", result: time.Duration(600000000000)},
{input: "1h", result: time.Duration(3600000000000)},
{input: "1o", err: fmt.Errorf("alert validation error: could not parse for field, error: %s", ErrWrongUnitFormat)},
{input: "1", err: fmt.Errorf("alert validation error: no specified unit, error: %s", ErrWrongUnitFormat)},
{input: "0s", result: time.Duration(0)},
{input: "0m", result: time.Duration(0)},
{input: "0h", result: time.Duration(0)},
{input: "0", result: time.Duration(0)},
{input: "", result: time.Duration(0)},
}
for _, tc := range tcs {
t.Run(tc.input, func(t *testing.T) {
r, err := getForValue(tc.input)
if tc.err == nil {
require.NoError(t, err)
} else {
require.EqualError(t, err, tc.err.Error())
}
assert.Equal(t, tc.result, r)
})
}
}
func TestAlertRuleModel(t *testing.T) {
sqlstore.InitTestDB(t)
Convey("Testing alert rule", t, func() {
RegisterCondition("test", func(model *simplejson.Json, index int) (Condition, error) {
return &FakeCondition{}, nil
})
RegisterCondition("test", func(model *simplejson.Json, index int) (Condition, error) {
return &FakeCondition{}, nil
})
Convey("should return err for empty string", func() {
_, err := getTimeDurationStringToSeconds("")
So(err, ShouldNotBeNil)
})
firstNotification := models.CreateAlertNotificationCommand{Uid: "notifier1", OrgId: 1, Name: "1"}
err := sqlstore.CreateAlertNotificationCommand(&firstNotification)
require.Nil(t, err)
secondNotification := models.CreateAlertNotificationCommand{Uid: "notifier2", OrgId: 1, Name: "2"}
err = sqlstore.CreateAlertNotificationCommand(&secondNotification)
require.Nil(t, err)
Convey("can construct alert rule model", func() {
firstNotification := models.CreateAlertNotificationCommand{Uid: "notifier1", OrgId: 1, Name: "1"}
err := sqlstore.CreateAlertNotificationCommand(&firstNotification)
So(err, ShouldBeNil)
secondNotification := models.CreateAlertNotificationCommand{Uid: "notifier2", OrgId: 1, Name: "2"}
err = sqlstore.CreateAlertNotificationCommand(&secondNotification)
So(err, ShouldBeNil)
Convey("with notification id and uid", func() {
json := `
t.Run("Testing alert rule with notification id and uid", func(t *testing.T) {
json := `
{
"name": "name2",
"description": "desc2",
@ -91,33 +117,30 @@ func TestAlertRuleModel(t *testing.T) {
}
`
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
So(jsonErr, ShouldBeNil)
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
require.Nil(t, jsonErr)
alert := &models.Alert{
Id: 1,
OrgId: 1,
DashboardId: 1,
PanelId: 1,
alert := &models.Alert{
Id: 1,
OrgId: 1,
DashboardId: 1,
PanelId: 1,
Settings: alertJSON,
}
Settings: alertJSON,
}
alertRule, err := NewRuleFromDBAlert(alert, false)
So(err, ShouldBeNil)
alertRule, err := NewRuleFromDBAlert(alert, false)
require.Nil(t, err)
So(len(alertRule.Conditions), ShouldEqual, 1)
So(len(alertRule.Notifications), ShouldEqual, 2)
require.Len(t, alertRule.Conditions, 1)
require.Len(t, alertRule.Notifications, 2)
Convey("Can read Id and Uid notifications (translate Id to Uid)", func() {
So(alertRule.Notifications, ShouldContain, "notifier2")
So(alertRule.Notifications, ShouldContain, "notifier1")
})
})
})
require.Contains(t, alertRule.Notifications, "notifier2")
require.Contains(t, alertRule.Notifications, "notifier1")
})
Convey("with non existing notification id", func() {
json := `
t.Run("Testing alert rule with non existing notification id", func(t *testing.T) {
json := `
{
"name": "name3",
"description": "desc3",
@ -133,28 +156,26 @@ func TestAlertRuleModel(t *testing.T) {
}
`
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
So(jsonErr, ShouldBeNil)
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
require.Nil(t, jsonErr)
alert := &models.Alert{
Id: 1,
OrgId: 1,
DashboardId: 1,
PanelId: 1,
alert := &models.Alert{
Id: 1,
OrgId: 1,
DashboardId: 1,
PanelId: 1,
Settings: alertJSON,
}
Settings: alertJSON,
}
alertRule, err := NewRuleFromDBAlert(alert, false)
Convey("swallows the error", func() {
So(err, ShouldBeNil)
So(alertRule.Notifications, ShouldNotContain, "999")
So(alertRule.Notifications, ShouldContain, "notifier2")
})
})
alertRule, err := NewRuleFromDBAlert(alert, false)
require.Nil(t, err)
require.NotContains(t, alertRule.Notifications, "999")
require.Contains(t, alertRule.Notifications, "notifier2")
})
Convey("can construct alert rule model with invalid frequency", func() {
json := `
t.Run("Testing alert rule which can construct alert rule model with invalid frequency", func(t *testing.T) {
json := `
{
"name": "name2",
"description": "desc2",
@ -165,26 +186,26 @@ func TestAlertRuleModel(t *testing.T) {
"notifications": []
}`
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
So(jsonErr, ShouldBeNil)
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
require.Nil(t, jsonErr)
alert := &models.Alert{
Id: 1,
OrgId: 1,
DashboardId: 1,
PanelId: 1,
Frequency: 0,
alert := &models.Alert{
Id: 1,
OrgId: 1,
DashboardId: 1,
PanelId: 1,
Frequency: 0,
Settings: alertJSON,
}
Settings: alertJSON,
}
alertRule, err := NewRuleFromDBAlert(alert, false)
So(err, ShouldBeNil)
So(alertRule.Frequency, ShouldEqual, 60)
})
alertRule, err := NewRuleFromDBAlert(alert, false)
require.Nil(t, err)
require.EqualValues(t, alertRule.Frequency, 60)
})
Convey("raise error in case of missing notification id and uid", func() {
json := `
t.Run("Testing alert rule which will raise error in case of missing notification id and uid", func(t *testing.T) {
json := `
{
"name": "name2",
"description": "desc2",
@ -203,22 +224,21 @@ func TestAlertRuleModel(t *testing.T) {
}
`
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
So(jsonErr, ShouldBeNil)
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
require.Nil(t, jsonErr)
alert := &models.Alert{
Id: 1,
OrgId: 1,
DashboardId: 1,
PanelId: 1,
Frequency: 0,
alert := &models.Alert{
Id: 1,
OrgId: 1,
DashboardId: 1,
PanelId: 1,
Frequency: 0,
Settings: alertJSON,
}
Settings: alertJSON,
}
_, err := NewRuleFromDBAlert(alert, false)
So(err, ShouldNotBeNil)
So(err.Error(), ShouldEqual, "alert validation error: Neither id nor uid is specified in 'notifications' block, type assertion to string failed AlertId: 1 PanelId: 1 DashboardId: 1")
})
_, err := NewRuleFromDBAlert(alert, false)
require.NotNil(t, err)
require.EqualValues(t, err.Error(), "alert validation error: Neither id nor uid is specified in 'notifications' block, type assertion to string failed AlertId: 1 PanelId: 1 DashboardId: 1")
})
}

View File

@ -264,12 +264,14 @@ export class AlertTabCtrl {
}
checkFrequency() {
if (!this.alert.frequency) {
this.frequencyWarning = '';
if (!(this.alert.frequency || '').match(/^\d+([dhms])$/)) {
this.frequencyWarning =
'Invalid frequency, has to be numeric followed by one of the following units: "d, h, m, s"';
return;
}
this.frequencyWarning = '';
try {
const frequencySecs = rangeUtil.intervalToSeconds(this.alert.frequency);
if (frequencySecs < this.alertingMinIntervalSecs) {

View File

@ -30,6 +30,7 @@
ng-model="ctrl.alert.for"
spellcheck="false"
placeholder="5m"
ng-pattern="/(^\d+([dhms])$)|(0)|(^$)/"
/>
<info-popover mode="right-absolute">
If an alert rule has a configured and the query violates the configured threshold, then it goes from OK