| package alerting |
| |
| import ( |
| "fmt" |
| "testing" |
| "time" |
| |
| "github.com/BurntSushi/toml" |
| "github.com/influxdb/influxdb/client" |
| ) |
| |
| type mockClient struct { |
| mockQuery func(string) ([]*client.Series, error) |
| } |
| |
| func (c mockClient) Query(query string, precision ...client.TimePrecision) ([]*client.Series, error) { |
| return c.mockQuery(query) |
| } |
| |
| func getAlert() *Alert { |
| a := &Alert{ |
| Name: "TestAlert", |
| Query: "DummyQuery", |
| Message: "Dummy query meets dummy condition!", |
| Condition: "x > 0", |
| client: &mockClient{func(string) ([]*client.Series, error) { |
| s := client.Series{ |
| Name: "Results", |
| Columns: []string{"time", "value"}, |
| Points: [][]interface{}{[]interface{}{1234567, 1.0}}, |
| } |
| return []*client.Series{&s}, nil |
| }}, |
| autoDismiss: false, |
| actions: nil, |
| lastTriggered: time.Time{}, |
| snoozedUntil: time.Time{}, |
| } |
| a.actions = []Action{NewPrintAction(a)} |
| return a |
| } |
| |
| func TestAlert(t *testing.T) { |
| // TODO(borenet): This test is really racy. Is there a good fix? |
| a := getAlert() |
| if a.Active() { |
| t.Errorf("Alert is active before firing.") |
| } |
| if a.Snoozed() { |
| t.Errorf("Alert is snoozed before firing.") |
| } |
| a.tick() |
| time.Sleep(10 * time.Millisecond) |
| if !a.Active() { |
| t.Errorf("Alert did not fire as expected.") |
| } |
| a.snooze(time.Now().Add(30*time.Millisecond), "Snoozed by default.user@gmail.com") |
| time.Sleep(10 * time.Millisecond) |
| if !a.Snoozed() { |
| t.Errorf("Alert did not snooze as expected.") |
| } |
| // Wait for alert to wake itself up. |
| time.Sleep(50 * time.Millisecond) |
| a.tick() |
| if a.Active() || a.Snoozed() { |
| t.Errorf("Alert did not dismiss itself after snooze period ended.") |
| } |
| } |
| |
| func TestAutoDismiss(t *testing.T) { |
| a := getAlert() |
| a.autoDismiss = true |
| if a.Active() { |
| t.Errorf("Alert is active before firing.") |
| } |
| if a.Snoozed() { |
| t.Errorf("Alert is snoozed before firing.") |
| } |
| a.tick() |
| time.Sleep(10 * time.Millisecond) |
| if !a.Active() { |
| t.Errorf("Alert did not fire as expected.") |
| } |
| // Hack the condition so that it's no longer true with the fake query results. |
| a.Condition = "x > 10" |
| a.tick() |
| time.Sleep(10 * time.Millisecond) |
| if a.Active() { |
| t.Errorf("Alert did not auto-dismiss.") |
| } |
| } |
| |
| func TestExecuteQuery(t *testing.T) { |
| type queryCase struct { |
| Name string |
| QueryFunc func(string) ([]*client.Series, error) |
| ExpectedVal float64 |
| ExpectedErr error |
| } |
| cases := []queryCase{ |
| queryCase{ |
| Name: "QueryFailed", |
| QueryFunc: func(q string) ([]*client.Series, error) { |
| return nil, fmt.Errorf("<dummy error>") |
| }, |
| ExpectedVal: 0.0, |
| ExpectedErr: fmt.Errorf("Failed to query InfluxDB with query \"<dummy query>\": <dummy error>"), |
| }, |
| queryCase{ |
| Name: "EmptyResults", |
| QueryFunc: func(q string) ([]*client.Series, error) { |
| return []*client.Series{}, nil |
| }, |
| ExpectedVal: 0.0, |
| ExpectedErr: fmt.Errorf("Query returned no data: \"<dummy query>\""), |
| }, |
| queryCase{ |
| Name: "EmptySeries", |
| QueryFunc: func(q string) ([]*client.Series, error) { |
| s := client.Series{ |
| Name: "Empty", |
| Columns: []string{}, |
| Points: [][]interface{}{}, |
| } |
| return []*client.Series{&s}, nil |
| }, |
| ExpectedVal: 0.0, |
| ExpectedErr: fmt.Errorf("Query returned no points: \"<dummy query>\""), |
| }, |
| queryCase{ |
| Name: "TooManyPoints", |
| QueryFunc: func(q string) ([]*client.Series, error) { |
| s := client.Series{ |
| Name: "TooMany", |
| Columns: []string{"time", "value"}, |
| Points: [][]interface{}{[]interface{}{}, []interface{}{}}, |
| } |
| return []*client.Series{&s}, nil |
| }, |
| ExpectedVal: 0.0, |
| ExpectedErr: fmt.Errorf("Query returned more than one point: \"<dummy query>\""), |
| }, |
| queryCase{ |
| Name: "NotEnoughCols", |
| QueryFunc: func(q string) ([]*client.Series, error) { |
| s := client.Series{ |
| Name: "NotEnoughCols", |
| Columns: []string{"time"}, |
| Points: [][]interface{}{[]interface{}{}}, |
| } |
| return []*client.Series{&s}, nil |
| }, |
| ExpectedVal: 0.0, |
| ExpectedErr: fmt.Errorf("Query returned an incorrect set of columns: \"<dummy query>\" [time]"), |
| }, |
| queryCase{ |
| Name: "TooManyCols", |
| QueryFunc: func(q string) ([]*client.Series, error) { |
| s := client.Series{ |
| Name: "NotEnoughCols", |
| Columns: []string{"time", "value", "extraCol"}, |
| Points: [][]interface{}{[]interface{}{}}, |
| } |
| return []*client.Series{&s}, nil |
| }, |
| ExpectedVal: 0.0, |
| ExpectedErr: fmt.Errorf("Query returned an incorrect set of columns: \"<dummy query>\" [time value extraCol]"), |
| }, |
| queryCase{ |
| Name: "ColsPointsMismatch", |
| QueryFunc: func(q string) ([]*client.Series, error) { |
| s := client.Series{ |
| Name: "BadData", |
| Columns: []string{"time", "value"}, |
| Points: [][]interface{}{[]interface{}{}}, |
| } |
| return []*client.Series{&s}, nil |
| }, |
| ExpectedVal: 0.0, |
| ExpectedErr: fmt.Errorf("Invalid data from InfluxDB: Point data does not match column spec."), |
| }, |
| queryCase{ |
| Name: "GoodData", |
| QueryFunc: func(q string) ([]*client.Series, error) { |
| s := client.Series{ |
| Name: "GoodData", |
| Columns: []string{"time", "value"}, |
| Points: [][]interface{}{[]interface{}{"mean", 1.5}}, |
| } |
| return []*client.Series{&s}, nil |
| }, |
| ExpectedVal: 1.5, |
| ExpectedErr: nil, |
| }, |
| queryCase{ |
| Name: "GoodWithSequenceNumber", |
| QueryFunc: func(q string) ([]*client.Series, error) { |
| s := client.Series{ |
| Name: "GoodData", |
| Columns: []string{"time", "sequence_number", "value"}, |
| Points: [][]interface{}{[]interface{}{1234567, 10, 1.5}}, |
| } |
| return []*client.Series{&s}, nil |
| }, |
| ExpectedVal: 1.5, |
| ExpectedErr: nil, |
| }, |
| } |
| errorStr := "Case %s:\nExpected:\n%v\nActual:\n%v" |
| for _, c := range cases { |
| client := mockClient{c.QueryFunc} |
| actualErrStr := "nil" |
| expectedErrStr := "nil" |
| if c.ExpectedErr != nil { |
| expectedErrStr = c.ExpectedErr.Error() |
| } |
| val, err := executeQuery(client, "<dummy query>") |
| if err != nil { |
| actualErrStr = err.Error() |
| } |
| if expectedErrStr != actualErrStr { |
| t.Errorf(errorStr, c.Name, expectedErrStr, actualErrStr) |
| } |
| if val != c.ExpectedVal { |
| t.Errorf(errorStr, c.Name, c.ExpectedVal, val) |
| } |
| } |
| } |
| |
| func TestAlertParsing(t *testing.T) { |
| type parseCase struct { |
| Name string |
| Input string |
| ExpectedErr error |
| } |
| cases := []parseCase{ |
| parseCase{ |
| Name: "Good", |
| Input: `[[rule]] |
| name = "randombits" |
| message = "randombits generates more 1's than 0's in last 5 seconds" |
| query = "select mean(value) from random_bits where time > now() - 5s" |
| condition = "x > 0.5" |
| actions = ["Print"] |
| auto-dismiss = false |
| `, |
| ExpectedErr: nil, |
| }, |
| parseCase{ |
| Name: "NoName", |
| Input: `[[rule]] |
| message = "randombits generates more 1's than 0's in last 5 seconds" |
| query = "select mean(value) from random_bits where time > now() - 5s" |
| condition = "x > 0.5" |
| actions = ["Print"] |
| auto-dismiss = false |
| `, |
| ExpectedErr: fmt.Errorf("Alert rule missing field \"name\""), |
| }, |
| parseCase{ |
| Name: "NoQuery", |
| Input: `[[rule]] |
| name = "randombits" |
| message = "randombits generates more 1's than 0's in last 5 seconds" |
| condition = "x > 0.5" |
| actions = ["Print"] |
| auto-dismiss = false |
| `, |
| ExpectedErr: fmt.Errorf("Alert rule missing field \"query\""), |
| }, |
| parseCase{ |
| Name: "NoCondition", |
| Input: `[[rule]] |
| name = "randombits" |
| message = "randombits generates more 1's than 0's in last 5 seconds" |
| query = "select mean(value) from random_bits where time > now() - 5s" |
| actions = ["Print"] |
| auto-dismiss = false |
| `, |
| ExpectedErr: fmt.Errorf("Alert rule missing field \"condition\""), |
| }, |
| parseCase{ |
| Name: "NoActions", |
| Input: `[[rule]] |
| name = "randombits" |
| message = "randombits generates more 1's than 0's in last 5 seconds" |
| query = "select mean(value) from random_bits where time > now() - 5s" |
| condition = "x > 0.5" |
| auto-dismiss = false |
| `, |
| ExpectedErr: fmt.Errorf("Alert rule missing field \"actions\""), |
| }, |
| parseCase{ |
| Name: "UnknownAction", |
| Input: `[[rule]] |
| name = "randombits" |
| message = "randombits generates more 1's than 0's in last 5 seconds" |
| query = "select mean(value) from random_bits where time > now() - 5s" |
| condition = "x > 0.5" |
| actions = ["Print", "UnknownAction"] |
| auto-dismiss = false |
| `, |
| ExpectedErr: fmt.Errorf("Unknown action: \"UnknownAction\""), |
| }, |
| parseCase{ |
| Name: "BadCondition", |
| Input: `[[rule]] |
| name = "randombits" |
| message = "randombits generates more 1's than 0's in last 5 seconds" |
| query = "select mean(value) from random_bits where time > now() - 5s" |
| condition = "x > y" |
| actions = ["Print"] |
| auto-dismiss = false |
| `, |
| ExpectedErr: fmt.Errorf("Failed to evaluate condition \"x > y\": 1:1: undeclared name: y"), |
| }, |
| parseCase{ |
| Name: "NoMessage", |
| Input: `[[rule]] |
| name = "randombits" |
| query = "select mean(value) from random_bits where time > now() - 5s" |
| condition = "x > 0.5" |
| actions = ["Print"] |
| auto-dismiss = false |
| `, |
| ExpectedErr: fmt.Errorf("Alert rule missing field \"message\""), |
| }, |
| parseCase{ |
| Name: "NoAutoDismiss", |
| Input: `[[rule]] |
| name = "randombits" |
| message = "randombits generates more 1's than 0's in last 5 seconds" |
| query = "select mean(value) from random_bits where time > now() - 5s" |
| condition = "x > 0.5" |
| actions = ["Print"] |
| `, |
| ExpectedErr: fmt.Errorf("Alert rule missing field \"auto-dismiss\""), |
| }, |
| parseCase{ |
| Name: "Nag", |
| Input: `[[rule]] |
| name = "randombits" |
| message = "randombits generates more 1's than 0's in last 5 seconds" |
| query = "select mean(value) from random_bits where time > now() - 5s" |
| condition = "x > 0.5" |
| actions = ["Print"] |
| auto-dismiss = false |
| nag = "1h10m" |
| `, |
| ExpectedErr: nil, |
| }, |
| } |
| errorStr := "Case %s:\nExpected:\n%v\nActual:\n%v" |
| for _, c := range cases { |
| expectedErrStr := "nil" |
| if c.ExpectedErr != nil { |
| expectedErrStr = c.ExpectedErr.Error() |
| } |
| var cfg struct { |
| Rule []parsedRule |
| } |
| _, err := toml.Decode(c.Input, &cfg) |
| if err != nil { |
| t.Errorf("Failed to parse:\n%v", c.Input) |
| } |
| _, err = newAlert(cfg.Rule[0], nil, nil, false) |
| actualErrStr := "nil" |
| if err != nil { |
| actualErrStr = err.Error() |
| } |
| if actualErrStr != expectedErrStr { |
| t.Errorf(errorStr, c.Name, expectedErrStr, actualErrStr) |
| } |
| } |
| } |