Refactor test package and some field types
Fairly big test refactor and changing some of the fields from pointers
This commit is contained in:
parent
a0a6b8199a
commit
e0af17a599
@ -76,7 +76,7 @@ Each monitor allows the following configuration:
|
||||
|`alert_down`|A list of Alerts to be triggered when the monitor is in a "down" state|
|
||||
|`alert_up`|A list of Alerts to be triggered when the monitor moves to an "up" state|
|
||||
|`check_interval`|The interval at which this monitor should be checked. This must be greater than the global `check_interval` value|
|
||||
|`alert_after`|Allows specifying the number of failed checks before an alert should be triggered|
|
||||
|`alert_after`|Allows specifying the number of failed checks before an alert should be triggered. A value of 1 will start sending alerts after the first failure.|
|
||||
|`alert_every`|Allows specifying how often an alert should be retriggered. There are a few magic numbers here. Defaults to `-1` for an exponential backoff. Setting to `0` disables re-alerting. Positive values will allow retriggering after the specified number of checks|
|
||||
|
||||
### Alerts
|
||||
|
11
alert.go
11
alert.go
@ -162,14 +162,3 @@ func (alert Alert) Send(notice AlertNotice) (outputStr string, err error) {
|
||||
|
||||
return outputStr, err
|
||||
}
|
||||
|
||||
// NewLogAlert creates an alert that does basic logging using echo
|
||||
func NewLogAlert() *Alert {
|
||||
return &Alert{
|
||||
Name: "log",
|
||||
Command: []string{
|
||||
"echo",
|
||||
"{{.MonitorName}} {{if .IsUp}}has recovered{{else}}check has failed {{.FailureCount}} times{{end}}",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,20 @@
|
||||
package main
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
m "git.iamthefij.com/iamthefij/minitor-go"
|
||||
)
|
||||
|
||||
func TestAlertIsValid(t *testing.T) {
|
||||
cases := []struct {
|
||||
alert Alert
|
||||
alert m.Alert
|
||||
expected bool
|
||||
name string
|
||||
}{
|
||||
{Alert{Command: []string{"echo", "test"}}, true, "Command only"},
|
||||
{Alert{ShellCommand: "echo test"}, true, "CommandShell only"},
|
||||
{Alert{}, false, "No commands"},
|
||||
{m.Alert{Command: []string{"echo", "test"}}, true, "Command only"},
|
||||
{m.Alert{ShellCommand: "echo test"}, true, "CommandShell only"},
|
||||
{m.Alert{}, false, "No commands"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@ -31,56 +33,40 @@ func TestAlertIsValid(t *testing.T) {
|
||||
|
||||
func TestAlertSend(t *testing.T) {
|
||||
cases := []struct {
|
||||
alert Alert
|
||||
notice AlertNotice
|
||||
alert m.Alert
|
||||
notice m.AlertNotice
|
||||
expectedOutput string
|
||||
expectErr bool
|
||||
name string
|
||||
}{
|
||||
{
|
||||
Alert{Command: []string{"echo", "{{.MonitorName}}"}},
|
||||
AlertNotice{MonitorName: "test"},
|
||||
m.Alert{Command: []string{"echo", "{{.MonitorName}}"}},
|
||||
m.AlertNotice{MonitorName: "test"},
|
||||
"test\n",
|
||||
false,
|
||||
"Command with template",
|
||||
},
|
||||
{
|
||||
Alert{ShellCommand: "echo {{.MonitorName}}"},
|
||||
AlertNotice{MonitorName: "test"},
|
||||
m.Alert{ShellCommand: "echo {{.MonitorName}}"},
|
||||
m.AlertNotice{MonitorName: "test"},
|
||||
"test\n",
|
||||
false,
|
||||
"Command shell with template",
|
||||
},
|
||||
{
|
||||
Alert{Command: []string{"echo", "{{.Bad}}"}},
|
||||
AlertNotice{MonitorName: "test"},
|
||||
m.Alert{Command: []string{"echo", "{{.Bad}}"}},
|
||||
m.AlertNotice{MonitorName: "test"},
|
||||
"",
|
||||
true,
|
||||
"Command with bad template",
|
||||
},
|
||||
{
|
||||
Alert{ShellCommand: "echo {{.Bad}}"},
|
||||
AlertNotice{MonitorName: "test"},
|
||||
m.Alert{ShellCommand: "echo {{.Bad}}"},
|
||||
m.AlertNotice{MonitorName: "test"},
|
||||
"",
|
||||
true,
|
||||
"Command shell with bad template",
|
||||
},
|
||||
// Test default log alert down
|
||||
{
|
||||
*NewLogAlert(),
|
||||
AlertNotice{MonitorName: "Test", FailureCount: 1, IsUp: false},
|
||||
"Test check has failed 1 times\n",
|
||||
false,
|
||||
"Default log alert down",
|
||||
},
|
||||
// Test default log alert up
|
||||
{
|
||||
*NewLogAlert(),
|
||||
AlertNotice{MonitorName: "Test", IsUp: true},
|
||||
"Test has recovered\n",
|
||||
false,
|
||||
"Default log alert up",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@ -109,8 +95,8 @@ func TestAlertSend(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertSendNoTemplates(t *testing.T) {
|
||||
alert := Alert{}
|
||||
notice := AlertNotice{}
|
||||
alert := m.Alert{}
|
||||
notice := m.AlertNotice{}
|
||||
|
||||
output, err := alert.Send(notice)
|
||||
if err == nil {
|
||||
@ -120,13 +106,13 @@ func TestAlertSendNoTemplates(t *testing.T) {
|
||||
|
||||
func TestAlertBuildTemplate(t *testing.T) {
|
||||
cases := []struct {
|
||||
alert Alert
|
||||
alert m.Alert
|
||||
expectErr bool
|
||||
name string
|
||||
}{
|
||||
{Alert{Command: []string{"echo", "test"}}, false, "Command only"},
|
||||
{Alert{ShellCommand: "echo test"}, false, "CommandShell only"},
|
||||
{Alert{}, true, "No commands"},
|
||||
{m.Alert{Command: []string{"echo", "test"}}, false, "Command only"},
|
||||
{m.Alert{ShellCommand: "echo test"}, false, "CommandShell only"},
|
||||
{m.Alert{}, true, "No commands"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
@ -113,6 +113,8 @@ func (config *Config) Init() (err error) {
|
||||
}
|
||||
|
||||
for _, monitor := range config.Monitors {
|
||||
// TODO: Move this to a Monitor.Init() method
|
||||
|
||||
// Parse the check_interval string into a time.Duration
|
||||
if monitor.CheckIntervalStr != nil {
|
||||
monitor.CheckInterval, err = time.ParseDuration(*monitor.CheckIntervalStr)
|
||||
@ -122,8 +124,10 @@ func (config *Config) Init() (err error) {
|
||||
}
|
||||
|
||||
// Set default values for monitor alerts
|
||||
if monitor.AlertAfter == nil {
|
||||
monitor.AlertAfter = config.DefaultAlertAfter
|
||||
if monitor.AlertAfter == 0 && config.DefaultAlertAfter != nil {
|
||||
monitor.AlertAfter = *config.DefaultAlertAfter
|
||||
} else if monitor.AlertAfter == 0 {
|
||||
monitor.AlertAfter = 1
|
||||
}
|
||||
|
||||
if monitor.AlertEvery == nil {
|
||||
|
@ -1,7 +1,9 @@
|
||||
package main
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
m "git.iamthefij.com/iamthefij/minitor-go"
|
||||
)
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
@ -11,12 +13,11 @@ func TestLoadConfig(t *testing.T) {
|
||||
name string
|
||||
}{
|
||||
{"./test/does-not-exist", true, "Invalid config path"},
|
||||
// {"./test/invalid-config-missing-alerts.yml", true, "Invalid config missing alerts"},
|
||||
// {"./test/invalid-config-type.yml", true, "Invalid config type for key"},
|
||||
// {"./test/invalid-config-unknown-alert.yml", true, "Invalid config unknown alert"},
|
||||
// {"./test/valid-config-default-values.yml", false, "Valid config file with default values"},
|
||||
{"./test/invalid-config-missing-alerts.hcl", true, "Invalid config missing alerts"},
|
||||
{"./test/invalid-config-type.hcl", true, "Invalid config type for key"},
|
||||
{"./test/invalid-config-unknown-alert.hcl", true, "Invalid config unknown alert"},
|
||||
{"./test/valid-config-default-values.hcl", false, "Valid config file with default values"},
|
||||
{"./test/valid-config.hcl", false, "Valid config file"},
|
||||
// {"./test/valid-default-log-alert.yml", true, "Invalid config file no log alert"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
@ -24,7 +25,7 @@ func TestLoadConfig(t *testing.T) {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := LoadConfig(c.configPath)
|
||||
_, err := m.LoadConfig(c.configPath)
|
||||
hasErr := (err != nil)
|
||||
|
||||
if hasErr != c.expectErr {
|
||||
@ -39,7 +40,7 @@ func TestLoadConfig(t *testing.T) {
|
||||
func TestMultiLineConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
config, err := LoadConfig("./test/valid-verify-multi-line.hcl")
|
||||
config, err := m.LoadConfig("./test/valid-verify-multi-line.hcl")
|
||||
if err != nil {
|
||||
t.Fatalf("TestMultiLineConfig(load), expected=no_error actual=%v", err)
|
||||
}
|
||||
@ -87,7 +88,7 @@ func TestMultiLineConfig(t *testing.T) {
|
||||
t.Logf("bytes actual =%v", []byte(actual))
|
||||
}
|
||||
|
||||
actual, err = alert.Send(AlertNotice{})
|
||||
actual, err = alert.Send(m.AlertNotice{})
|
||||
if err != nil {
|
||||
t.Errorf("Execution of alert failed")
|
||||
}
|
||||
|
12
main.go
12
main.go
@ -24,7 +24,7 @@ var (
|
||||
errUnknownAlert = errors.New("unknown alert")
|
||||
)
|
||||
|
||||
func sendAlerts(config *Config, monitor *Monitor, alertNotice *AlertNotice) error {
|
||||
func SendAlerts(config *Config, monitor *Monitor, alertNotice *AlertNotice) error {
|
||||
slog.Debugf("Received an alert notice from %s", alertNotice.MonitorName)
|
||||
alertNames := monitor.GetAlertNames(alertNotice.IsUp)
|
||||
|
||||
@ -65,7 +65,7 @@ func sendAlerts(config *Config, monitor *Monitor, alertNotice *AlertNotice) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMonitors(config *Config) error {
|
||||
func CheckMonitors(config *Config) error {
|
||||
// TODO: Run this in goroutines and capture exceptions
|
||||
for _, monitor := range config.Monitors {
|
||||
if monitor.ShouldCheck() {
|
||||
@ -77,7 +77,7 @@ func checkMonitors(config *Config) error {
|
||||
Metrics.CountCheck(monitor.Name, success, monitor.LastCheckMilliseconds(), hasAlert)
|
||||
|
||||
if alertNotice != nil {
|
||||
err := sendAlerts(config, monitor, alertNotice)
|
||||
err := SendAlerts(config, monitor, alertNotice)
|
||||
// If there was an error in sending an alert, exit early and bubble it up
|
||||
if err != nil {
|
||||
return err
|
||||
@ -89,7 +89,7 @@ func checkMonitors(config *Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendStartupAlerts(config *Config, alertNames []string) error {
|
||||
func SendStartupAlerts(config *Config, alertNames []string) error {
|
||||
for _, alertName := range alertNames {
|
||||
var err error
|
||||
|
||||
@ -148,14 +148,14 @@ func main() {
|
||||
if *startupAlerts != "" {
|
||||
alertNames := strings.Split(*startupAlerts, ",")
|
||||
|
||||
err = sendStartupAlerts(&config, alertNames)
|
||||
err = SendStartupAlerts(&config, alertNames)
|
||||
|
||||
slog.OnErrPanicf(err, "Error running startup alerts")
|
||||
}
|
||||
|
||||
// Start main loop
|
||||
for {
|
||||
err = checkMonitors(&config)
|
||||
err = CheckMonitors(&config)
|
||||
slog.OnErrPanicf(err, "Error checking monitors")
|
||||
|
||||
time.Sleep(config.CheckInterval)
|
||||
|
161
main_test.go
161
main_test.go
@ -1,125 +1,92 @@
|
||||
package main
|
||||
package main_test
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
m "git.iamthefij.com/iamthefij/minitor-go"
|
||||
)
|
||||
|
||||
func Ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
// TestCheckConfig tests the checkConfig function
|
||||
// It also tests results for potentially invalid configuration. For example, no alerts
|
||||
func TestCheckMonitors(t *testing.T) {
|
||||
cases := []struct {
|
||||
config Config
|
||||
expectErr bool
|
||||
name string
|
||||
config m.Config
|
||||
expectFailureError bool
|
||||
expectRecoverError bool
|
||||
name string
|
||||
}{
|
||||
{
|
||||
config: Config{
|
||||
config: m.Config{
|
||||
CheckIntervalStr: "1s",
|
||||
Monitors: []*Monitor{
|
||||
Monitors: []*m.Monitor{
|
||||
{
|
||||
Name: "Success",
|
||||
Command: []string{"true"},
|
||||
Name: "Success",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
name: "Monitor success, no alerts",
|
||||
expectFailureError: false,
|
||||
expectRecoverError: false,
|
||||
name: "No alerts",
|
||||
},
|
||||
{
|
||||
config: Config{
|
||||
config: m.Config{
|
||||
CheckIntervalStr: "1s",
|
||||
Monitors: []*Monitor{
|
||||
Monitors: []*m.Monitor{
|
||||
{
|
||||
Name: "Failure",
|
||||
Command: []string{"false"},
|
||||
AlertAfter: Ptr(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
name: "Monitor failure, no alerts",
|
||||
},
|
||||
{
|
||||
config: Config{
|
||||
CheckIntervalStr: "1s",
|
||||
Monitors: []*Monitor{
|
||||
{
|
||||
Name: "Success",
|
||||
Command: []string{"ls"},
|
||||
alertCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
name: "Monitor recovery, no alerts",
|
||||
},
|
||||
{
|
||||
config: Config{
|
||||
CheckIntervalStr: "1s",
|
||||
Monitors: []*Monitor{
|
||||
{
|
||||
Name: "Failure",
|
||||
Command: []string{"false"},
|
||||
AlertDown: []string{"unknown"},
|
||||
AlertAfter: Ptr(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
name: "Monitor failure, unknown alerts",
|
||||
},
|
||||
{
|
||||
config: Config{
|
||||
CheckIntervalStr: "1s",
|
||||
Monitors: []*Monitor{
|
||||
{
|
||||
Name: "Success",
|
||||
Command: []string{"true"},
|
||||
AlertUp: []string{"unknown"},
|
||||
alertCount: 1,
|
||||
AlertAfter: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
name: "Monitor recovery, unknown alerts",
|
||||
expectFailureError: true,
|
||||
expectRecoverError: true,
|
||||
name: "Unknown alerts",
|
||||
},
|
||||
{
|
||||
config: Config{
|
||||
config: m.Config{
|
||||
CheckIntervalStr: "1s",
|
||||
Monitors: []*Monitor{
|
||||
Monitors: []*m.Monitor{
|
||||
{
|
||||
Name: "Failure",
|
||||
Command: []string{"false"},
|
||||
AlertDown: []string{"good"},
|
||||
AlertAfter: Ptr(1),
|
||||
AlertUp: []string{"good"},
|
||||
AlertAfter: 1,
|
||||
},
|
||||
},
|
||||
Alerts: []*Alert{{
|
||||
Alerts: []*m.Alert{{
|
||||
Name: "good",
|
||||
Command: []string{"true"},
|
||||
}},
|
||||
},
|
||||
expectErr: false,
|
||||
name: "Monitor failure, successful alert",
|
||||
expectFailureError: false,
|
||||
expectRecoverError: false,
|
||||
name: "Successful alert",
|
||||
},
|
||||
{
|
||||
config: Config{
|
||||
config: m.Config{
|
||||
CheckIntervalStr: "1s",
|
||||
Monitors: []*Monitor{
|
||||
Monitors: []*m.Monitor{
|
||||
{
|
||||
Name: "Failure",
|
||||
Command: []string{"false"},
|
||||
AlertDown: []string{"bad"},
|
||||
AlertAfter: Ptr(1),
|
||||
AlertUp: []string{"bad"},
|
||||
AlertAfter: 1,
|
||||
},
|
||||
},
|
||||
Alerts: []*Alert{{
|
||||
Alerts: []*m.Alert{{
|
||||
Name: "bad",
|
||||
Command: []string{"false"},
|
||||
}},
|
||||
},
|
||||
expectErr: true,
|
||||
name: "Monitor failure, bad alert",
|
||||
expectFailureError: true,
|
||||
expectRecoverError: true,
|
||||
name: "Failing alert",
|
||||
},
|
||||
}
|
||||
|
||||
@ -134,11 +101,25 @@ func TestCheckMonitors(t *testing.T) {
|
||||
t.Errorf("checkMonitors(%s): unexpected error reading config: %v", c.name, err)
|
||||
}
|
||||
|
||||
err = checkMonitors(&c.config)
|
||||
if err == nil && c.expectErr {
|
||||
t.Errorf("checkMonitors(%s): Expected panic, the code did not panic", c.name)
|
||||
} else if err != nil && !c.expectErr {
|
||||
t.Errorf("checkMonitors(%s): Did not expect an error, but we got one anyway: %v", c.name, err)
|
||||
for _, check := range []struct {
|
||||
shellCmd string
|
||||
name string
|
||||
expectErr bool
|
||||
}{
|
||||
{"false", "Failure", c.expectFailureError}, {"true", "Success", c.expectRecoverError},
|
||||
} {
|
||||
// Set the shell command for this check
|
||||
c.config.Monitors[0].ShellCommand = check.shellCmd
|
||||
|
||||
// Run the check
|
||||
err = m.CheckMonitors(&c.config)
|
||||
|
||||
// Check the results
|
||||
if err == nil && check.expectErr {
|
||||
t.Errorf("checkMonitors(%s:%s): Expected error, the code did not error", c.name, check.name)
|
||||
} else if err != nil && !check.expectErr {
|
||||
t.Errorf("checkMonitors(%s:%s): Did not expect an error, but we got one anyway: %v", c.name, check.name, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -146,26 +127,23 @@ func TestCheckMonitors(t *testing.T) {
|
||||
|
||||
func TestFirstRunAlerts(t *testing.T) {
|
||||
cases := []struct {
|
||||
config Config
|
||||
config m.Config
|
||||
expectErr bool
|
||||
startupAlerts []string
|
||||
name string
|
||||
}{
|
||||
{
|
||||
config: Config{},
|
||||
expectErr: false,
|
||||
startupAlerts: []string{},
|
||||
name: "Empty",
|
||||
},
|
||||
{
|
||||
config: Config{},
|
||||
config: m.Config{
|
||||
CheckIntervalStr: "1s",
|
||||
},
|
||||
expectErr: true,
|
||||
startupAlerts: []string{"missing"},
|
||||
name: "Unknown",
|
||||
},
|
||||
{
|
||||
config: Config{
|
||||
Alerts: []*Alert{
|
||||
config: m.Config{
|
||||
CheckIntervalStr: "1s",
|
||||
Alerts: []*m.Alert{
|
||||
{
|
||||
Name: "good",
|
||||
Command: []string{"true"},
|
||||
@ -177,8 +155,9 @@ func TestFirstRunAlerts(t *testing.T) {
|
||||
name: "Successful alert",
|
||||
},
|
||||
{
|
||||
config: Config{
|
||||
Alerts: []*Alert{
|
||||
config: m.Config{
|
||||
CheckIntervalStr: "1s",
|
||||
Alerts: []*m.Alert{
|
||||
{
|
||||
Name: "bad",
|
||||
Command: []string{"false"},
|
||||
@ -202,7 +181,7 @@ func TestFirstRunAlerts(t *testing.T) {
|
||||
t.Errorf("sendFirstRunAlerts(%s): unexpected error reading config: %v", c.name, err)
|
||||
}
|
||||
|
||||
err = sendStartupAlerts(&c.config, c.startupAlerts)
|
||||
err = m.SendStartupAlerts(&c.config, c.startupAlerts)
|
||||
if err == nil && c.expectErr {
|
||||
t.Errorf("sendFirstRunAlerts(%s): Expected error, the code did not error", c.name)
|
||||
} else if err != nil && !c.expectErr {
|
||||
|
24
monitor.go
24
monitor.go
@ -15,7 +15,7 @@ type Monitor struct { //nolint:maligned
|
||||
CheckInterval time.Duration
|
||||
|
||||
Name string `hcl:"name,label"`
|
||||
AlertAfter *int `hcl:"alert_after,optional"`
|
||||
AlertAfter int `hcl:"alert_after,optional"`
|
||||
AlertEvery *int `hcl:"alert_every,optional"`
|
||||
AlertDown []string `hcl:"alert_down,optional"`
|
||||
AlertUp []string `hcl:"alert_up,optional"`
|
||||
@ -34,9 +34,10 @@ type Monitor struct { //nolint:maligned
|
||||
// IsValid returns a boolean indicating if the Monitor has been correctly
|
||||
// configured
|
||||
func (monitor Monitor) IsValid() bool {
|
||||
// TODO: Refactor and return an error containing more information on what was invalid
|
||||
hasCommand := len(monitor.Command) > 0
|
||||
hasShellCommand := monitor.ShellCommand != ""
|
||||
hasValidAlertAfter := monitor.GetAlertAfter() > 0
|
||||
hasValidAlertAfter := monitor.AlertAfter > 0
|
||||
hasAlertDown := len(monitor.AlertDown) > 0
|
||||
|
||||
hasAtLeastOneCommand := hasCommand || hasShellCommand
|
||||
@ -48,6 +49,10 @@ func (monitor Monitor) IsValid() bool {
|
||||
hasAlertDown
|
||||
}
|
||||
|
||||
func (monitor Monitor) LastOutput() string {
|
||||
return monitor.lastOutput
|
||||
}
|
||||
|
||||
// ShouldCheck returns a boolean indicating if the Monitor is ready to be
|
||||
// be checked again
|
||||
func (monitor Monitor) ShouldCheck() bool {
|
||||
@ -126,20 +131,20 @@ func (monitor *Monitor) success() (notice *AlertNotice) {
|
||||
func (monitor *Monitor) failure() (notice *AlertNotice) {
|
||||
monitor.failureCount++
|
||||
// If we haven't hit the minimum failures, we can exit
|
||||
if monitor.failureCount < monitor.GetAlertAfter() {
|
||||
if monitor.failureCount < monitor.AlertAfter {
|
||||
slog.Debugf(
|
||||
"%s failed but did not hit minimum failures. "+
|
||||
"Count: %v alert after: %v",
|
||||
monitor.Name,
|
||||
monitor.failureCount,
|
||||
monitor.GetAlertAfter(),
|
||||
monitor.AlertAfter,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Take number of failures after minimum
|
||||
failureCount := (monitor.failureCount - monitor.GetAlertAfter())
|
||||
failureCount := (monitor.failureCount - monitor.AlertAfter)
|
||||
|
||||
// Use alert cadence to determine if we should alert
|
||||
switch {
|
||||
@ -168,15 +173,6 @@ func (monitor *Monitor) failure() (notice *AlertNotice) {
|
||||
return notice
|
||||
}
|
||||
|
||||
// GetAlertAfter will get or return the default alert after value
|
||||
func (monitor Monitor) GetAlertAfter() int {
|
||||
if monitor.AlertAfter == nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
return *monitor.AlertAfter
|
||||
}
|
||||
|
||||
// GetAlertNames gives a list of alert names for a given monitor status
|
||||
func (monitor Monitor) GetAlertNames(up bool) []string {
|
||||
if up {
|
||||
|
211
monitor_test.go
211
monitor_test.go
@ -1,22 +1,25 @@
|
||||
package main
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
m "git.iamthefij.com/iamthefij/minitor-go"
|
||||
)
|
||||
|
||||
// TestMonitorIsValid tests the Monitor.IsValid()
|
||||
func TestMonitorIsValid(t *testing.T) {
|
||||
cases := []struct {
|
||||
monitor Monitor
|
||||
monitor m.Monitor
|
||||
expected bool
|
||||
name string
|
||||
}{
|
||||
{Monitor{Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, true, "Command only"},
|
||||
{Monitor{ShellCommand: "echo test", AlertDown: []string{"log"}}, true, "CommandShell only"},
|
||||
{Monitor{Command: []string{"echo", "test"}}, false, "No AlertDown"},
|
||||
{Monitor{AlertDown: []string{"log"}}, false, "No commands"},
|
||||
{Monitor{Command: []string{"echo", "test"}, AlertDown: []string{"log"}, AlertAfter: Ptr(-1)}, false, "Invalid alert threshold, -1"},
|
||||
{m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, true, "Command only"},
|
||||
{m.Monitor{AlertAfter: 1, ShellCommand: "echo test", AlertDown: []string{"log"}}, true, "CommandShell only"},
|
||||
{m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}}, false, "No AlertDown"},
|
||||
{m.Monitor{AlertAfter: 1, AlertDown: []string{"log"}}, false, "No commands"},
|
||||
{m.Monitor{AlertAfter: -1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, false, "Invalid alert threshold, -1"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@ -35,75 +38,63 @@ func TestMonitorIsValid(t *testing.T) {
|
||||
|
||||
// TestMonitorShouldCheck tests the Monitor.ShouldCheck()
|
||||
func TestMonitorShouldCheck(t *testing.T) {
|
||||
timeNow := time.Now()
|
||||
timeTenSecAgo := time.Now().Add(time.Second * -10)
|
||||
timeTwentySecAgo := time.Now().Add(time.Second * -20)
|
||||
fifteenSeconds := time.Second * 15
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
monitor Monitor
|
||||
expected bool
|
||||
name string
|
||||
}{
|
||||
{Monitor{}, true, "Empty"},
|
||||
{Monitor{lastCheck: timeNow, CheckInterval: fifteenSeconds}, false, "Just checked"},
|
||||
{Monitor{lastCheck: timeTenSecAgo, CheckInterval: fifteenSeconds}, false, "-10s"},
|
||||
{Monitor{lastCheck: timeTwentySecAgo, CheckInterval: fifteenSeconds}, true, "-20s"},
|
||||
// Create a monitor that should check every second and then verify it checks with some sleeps
|
||||
monitor := m.Monitor{ShellCommand: "true", CheckInterval: time.Second}
|
||||
|
||||
if !monitor.ShouldCheck() {
|
||||
t.Errorf("New monitor should be ready to check")
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
monitor.Check()
|
||||
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if monitor.ShouldCheck() {
|
||||
t.Errorf("Monitor should not be ready to check after a check")
|
||||
}
|
||||
|
||||
actual := c.monitor.ShouldCheck()
|
||||
if actual != c.expected {
|
||||
t.Errorf("ShouldCheck(%v), expected=%t actual=%t", c.name, c.expected, actual)
|
||||
}
|
||||
})
|
||||
time.Sleep(time.Second)
|
||||
|
||||
if !monitor.ShouldCheck() {
|
||||
t.Errorf("Monitor should be ready to check after a second")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMonitorIsUp tests the Monitor.IsUp()
|
||||
func TestMonitorIsUp(t *testing.T) {
|
||||
cases := []struct {
|
||||
monitor Monitor
|
||||
expected bool
|
||||
name string
|
||||
}{
|
||||
{Monitor{}, true, "Empty"},
|
||||
{Monitor{alertCount: 1}, false, "Has alert"},
|
||||
{Monitor{alertCount: -1}, false, "Negative alerts"},
|
||||
{Monitor{alertCount: 0}, true, "No alerts"},
|
||||
t.Parallel()
|
||||
|
||||
// Creating a monitor that should alert after 2 failures. The monitor should be considered up until we reach two failed checks
|
||||
monitor := m.Monitor{ShellCommand: "false", AlertAfter: 2}
|
||||
if !monitor.IsUp() {
|
||||
t.Errorf("New monitor should be considered up")
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
monitor.Check()
|
||||
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !monitor.IsUp() {
|
||||
t.Errorf("Monitor should be considered up with one failure and no alerts")
|
||||
}
|
||||
|
||||
actual := c.monitor.IsUp()
|
||||
if actual != c.expected {
|
||||
t.Errorf("IsUp(%v), expected=%t actual=%t", c.name, c.expected, actual)
|
||||
}
|
||||
})
|
||||
monitor.Check()
|
||||
|
||||
if monitor.IsUp() {
|
||||
t.Errorf("Monitor should be considered down with one alert")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMonitorGetAlertNames tests that proper alert names are returned
|
||||
func TestMonitorGetAlertNames(t *testing.T) {
|
||||
cases := []struct {
|
||||
monitor Monitor
|
||||
monitor m.Monitor
|
||||
up bool
|
||||
expected []string
|
||||
name string
|
||||
}{
|
||||
{Monitor{}, true, nil, "Empty up"},
|
||||
{Monitor{}, false, nil, "Empty down"},
|
||||
{Monitor{AlertUp: []string{"alert"}}, true, []string{"alert"}, "Return up"},
|
||||
{Monitor{AlertDown: []string{"alert"}}, false, []string{"alert"}, "Return down"},
|
||||
{m.Monitor{}, true, nil, "Empty up"},
|
||||
{m.Monitor{}, false, nil, "Empty down"},
|
||||
{m.Monitor{AlertUp: []string{"alert"}}, true, []string{"alert"}, "Return up"},
|
||||
{m.Monitor{AlertDown: []string{"alert"}}, false, []string{"alert"}, "Return down"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@ -113,57 +104,30 @@ func TestMonitorGetAlertNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual := c.monitor.GetAlertNames(c.up)
|
||||
if !EqualSliceString(actual, c.expected) {
|
||||
if !reflect.DeepEqual(actual, c.expected) {
|
||||
t.Errorf("GetAlertNames(%v), expected=%v actual=%v", c.name, c.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMonitorSuccess tests the Monitor.success()
|
||||
func TestMonitorSuccess(t *testing.T) {
|
||||
cases := []struct {
|
||||
monitor Monitor
|
||||
expectNotice bool
|
||||
name string
|
||||
}{
|
||||
{Monitor{}, false, "Empty"},
|
||||
{Monitor{alertCount: 0}, false, "No alerts"},
|
||||
{Monitor{alertCount: 1}, true, "Has alert"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
notice := c.monitor.success()
|
||||
hasNotice := (notice != nil)
|
||||
|
||||
if hasNotice != c.expectNotice {
|
||||
t.Errorf("success(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMonitorFailureAlertAfter tests that alerts will not trigger until
|
||||
// hitting the threshold provided by AlertAfter
|
||||
func TestMonitorFailureAlertAfter(t *testing.T) {
|
||||
var alertEvery int = 1
|
||||
var alertEveryOne int = 1
|
||||
|
||||
cases := []struct {
|
||||
monitor Monitor
|
||||
monitor m.Monitor
|
||||
numChecks int
|
||||
expectNotice bool
|
||||
name string
|
||||
}{
|
||||
{Monitor{AlertAfter: Ptr(1)}, true, "Empty"}, // Defaults to true because and AlertEvery default to 0
|
||||
{Monitor{failureCount: 0, AlertAfter: Ptr(1), AlertEvery: &alertEvery}, true, "Alert after 1: first failure"},
|
||||
{Monitor{failureCount: 1, AlertAfter: Ptr(1), AlertEvery: &alertEvery}, true, "Alert after 1: second failure"},
|
||||
{Monitor{failureCount: 0, AlertAfter: Ptr(20), AlertEvery: &alertEvery}, false, "Alert after 20: first failure"},
|
||||
{Monitor{failureCount: 19, AlertAfter: Ptr(20), AlertEvery: &alertEvery}, true, "Alert after 20: 20th failure"},
|
||||
{Monitor{failureCount: 20, AlertAfter: Ptr(20), AlertEvery: &alertEvery}, true, "Alert after 20: 21st failure"},
|
||||
{m.Monitor{ShellCommand: "false", AlertAfter: 1}, 1, true, "Empty After 1"}, // Defaults to true because and AlertEvery default to 0
|
||||
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: &alertEveryOne}, 1, true, "Alert after 1: first failure"},
|
||||
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: &alertEveryOne}, 2, true, "Alert after 1: second failure"},
|
||||
{m.Monitor{ShellCommand: "false", AlertAfter: 20, AlertEvery: &alertEveryOne}, 1, false, "Alert after 20: first failure"},
|
||||
{m.Monitor{ShellCommand: "false", AlertAfter: 20, AlertEvery: &alertEveryOne}, 20, true, "Alert after 20: 20th failure"},
|
||||
{m.Monitor{ShellCommand: "false", AlertAfter: 20, AlertEvery: &alertEveryOne}, 21, true, "Alert after 20: 21st failure"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@ -172,8 +136,12 @@ func TestMonitorFailureAlertAfter(t *testing.T) {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
notice := c.monitor.failure()
|
||||
hasNotice := (notice != nil)
|
||||
hasNotice := false
|
||||
|
||||
for i := 0; i < c.numChecks; i++ {
|
||||
_, notice := c.monitor.Check()
|
||||
hasNotice = (notice != nil)
|
||||
}
|
||||
|
||||
if hasNotice != c.expectNotice {
|
||||
t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice)
|
||||
@ -185,39 +153,18 @@ func TestMonitorFailureAlertAfter(t *testing.T) {
|
||||
// TestMonitorFailureAlertEvery tests that alerts will trigger
|
||||
// on the expected intervals
|
||||
func TestMonitorFailureAlertEvery(t *testing.T) {
|
||||
var alertEvery0, alertEvery1, alertEvery2 int
|
||||
alertEvery0 = 0
|
||||
alertEvery1 = 1
|
||||
alertEvery2 = 2
|
||||
|
||||
cases := []struct {
|
||||
monitor Monitor
|
||||
expectNotice bool
|
||||
name string
|
||||
monitor m.Monitor
|
||||
expectedNotice []bool
|
||||
name string
|
||||
}{
|
||||
/*
|
||||
TODO: Actually found a bug in original implementation. There is an inconsistency in the way AlertAfter is treated.
|
||||
For "First alert only" (ie. AlertEvery=0), it is the number of failures to ignore before alerting, so AlertAfter=1
|
||||
will ignore the first failure and alert on the second failure
|
||||
For other intervals (ie. AlertEvery=1), it is essentially indexed on one. Essentially making AlertAfter=1 trigger
|
||||
on the first failure.
|
||||
|
||||
For usabilty, this should be consistent. Consistent with what though? minitor-py? Or itself? Dun dun duuuunnnnn!
|
||||
*/
|
||||
{Monitor{AlertAfter: Ptr(1)}, true, "Empty"}, // Defaults to true because AlertAfter and AlertEvery default to nil
|
||||
{m.Monitor{ShellCommand: "false", AlertAfter: 1}, []bool{true}, "No AlertEvery set"}, // Defaults to true because AlertAfter and AlertEvery default to nil
|
||||
// Alert first time only, after 1
|
||||
{Monitor{failureCount: 0, AlertAfter: Ptr(1), AlertEvery: &alertEvery0}, true, "Alert first time only after 1: first failure"},
|
||||
{Monitor{failureCount: 1, AlertAfter: Ptr(1), AlertEvery: &alertEvery0}, false, "Alert first time only after 1: second failure"},
|
||||
{Monitor{failureCount: 2, AlertAfter: Ptr(1), AlertEvery: &alertEvery0}, false, "Alert first time only after 1: third failure"},
|
||||
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(0)}, []bool{true, false, false}, "Alert first time only after 1"},
|
||||
// Alert every time, after 1
|
||||
{Monitor{failureCount: 0, AlertAfter: Ptr(1), AlertEvery: &alertEvery1}, true, "Alert every time after 1: first failure"},
|
||||
{Monitor{failureCount: 1, AlertAfter: Ptr(1), AlertEvery: &alertEvery1}, true, "Alert every time after 1: second failure"},
|
||||
{Monitor{failureCount: 2, AlertAfter: Ptr(1), AlertEvery: &alertEvery1}, true, "Alert every time after 1: third failure"},
|
||||
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(1)}, []bool{true, true, true}, "Alert every time after 1"},
|
||||
// Alert every other time, after 1
|
||||
{Monitor{failureCount: 0, AlertAfter: Ptr(1), AlertEvery: &alertEvery2}, true, "Alert every other time after 1: first failure"},
|
||||
{Monitor{failureCount: 1, AlertAfter: Ptr(1), AlertEvery: &alertEvery2}, false, "Alert every other time after 1: second failure"},
|
||||
{Monitor{failureCount: 2, AlertAfter: Ptr(1), AlertEvery: &alertEvery2}, true, "Alert every other time after 1: third failure"},
|
||||
{Monitor{failureCount: 3, AlertAfter: Ptr(1), AlertEvery: &alertEvery2}, false, "Alert every other time after 1: fourth failure"},
|
||||
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(2)}, []bool{true, false, true, false}, "Alert every other time after 1"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@ -226,11 +173,13 @@ func TestMonitorFailureAlertEvery(t *testing.T) {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
notice := c.monitor.failure()
|
||||
hasNotice := (notice != nil)
|
||||
for i, expectNotice := range c.expectedNotice {
|
||||
_, notice := c.monitor.Check()
|
||||
hasNotice := (notice != nil)
|
||||
|
||||
if hasNotice != c.expectNotice {
|
||||
t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice)
|
||||
if hasNotice != expectNotice {
|
||||
t.Errorf("failed %s check %d: expected=%t actual=%t", c.name, i, expectNotice, hasNotice)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -257,12 +206,12 @@ func TestMonitorFailureExponential(t *testing.T) {
|
||||
|
||||
// Unlike previous tests, this one requires a static Monitor with repeated
|
||||
// calls to the failure method
|
||||
monitor := Monitor{failureCount: 0, AlertAfter: Ptr(1), AlertEvery: &alertEveryExp}
|
||||
monitor := m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: &alertEveryExp}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
// NOTE: These tests are not parallel because they rely on the state of the Monitor
|
||||
notice := monitor.failure()
|
||||
_, notice := monitor.Check()
|
||||
hasNotice := (notice != nil)
|
||||
|
||||
if hasNotice != c.expectNotice {
|
||||
@ -281,27 +230,27 @@ func TestMonitorCheck(t *testing.T) {
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
monitor Monitor
|
||||
monitor m.Monitor
|
||||
expect expected
|
||||
name string
|
||||
}{
|
||||
{
|
||||
Monitor{Command: []string{"echo", "success"}},
|
||||
m.Monitor{AlertAfter: 1, Command: []string{"echo", "success"}},
|
||||
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
|
||||
"Test successful command",
|
||||
},
|
||||
{
|
||||
Monitor{ShellCommand: "echo success"},
|
||||
m.Monitor{AlertAfter: 1, ShellCommand: "echo success"},
|
||||
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
|
||||
"Test successful command shell",
|
||||
},
|
||||
{
|
||||
Monitor{Command: []string{"total", "failure"}},
|
||||
m.Monitor{AlertAfter: 1, Command: []string{"total", "failure"}},
|
||||
expected{isSuccess: false, hasNotice: true, lastOutput: ""},
|
||||
"Test failed command",
|
||||
},
|
||||
{
|
||||
Monitor{ShellCommand: "false"},
|
||||
m.Monitor{AlertAfter: 1, ShellCommand: "false"},
|
||||
expected{isSuccess: false, hasNotice: true, lastOutput: ""},
|
||||
"Test failed command shell",
|
||||
},
|
||||
@ -323,7 +272,7 @@ func TestMonitorCheck(t *testing.T) {
|
||||
t.Errorf("Check(%v) (notice), expected=%t actual=%t", c.name, c.expect.hasNotice, hasNotice)
|
||||
}
|
||||
|
||||
lastOutput := c.monitor.lastOutput
|
||||
lastOutput := c.monitor.LastOutput()
|
||||
if lastOutput != c.expect.lastOutput {
|
||||
t.Errorf("Check(%v) (output), expected=%v actual=%v", c.name, c.expect.lastOutput, lastOutput)
|
||||
}
|
||||
|
7
test/invalid-config-missing-alerts.hcl
Normal file
7
test/invalid-config-missing-alerts.hcl
Normal file
@ -0,0 +1,7 @@
|
||||
check_interval = "1s"
|
||||
|
||||
monitor "Command" {
|
||||
command = ["echo", "$PATH"]
|
||||
alert_down = [ "alert_down", "log_shell", "log_command" ]
|
||||
alert_every = 0
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
check_interval: 1
|
||||
|
||||
monitors:
|
||||
- name: Command
|
||||
command: ['echo', '$PATH']
|
||||
alert_down: [ 'alert_down', 'log_shell', 'log_command' ]
|
||||
# alert_every: -1
|
||||
alert_every: 0
|
1
test/invalid-config-type.hcl
Normal file
1
test/invalid-config-type.hcl
Normal file
@ -0,0 +1 @@
|
||||
check_interval = "woops, I'm not an int!"
|
@ -1 +0,0 @@
|
||||
check_interval: woops, I'm not an int!
|
12
test/invalid-config-unknown-alert.hcl
Normal file
12
test/invalid-config-unknown-alert.hcl
Normal file
@ -0,0 +1,12 @@
|
||||
check_interval = "1s"
|
||||
|
||||
monitor "Command" {
|
||||
command = ["echo", "$PATH"]
|
||||
alert_down = ["not_log"]
|
||||
alert_every = 0
|
||||
}
|
||||
|
||||
|
||||
alert "log" {
|
||||
command = ["true"]
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
check_interval: 1
|
||||
|
||||
monitors:
|
||||
- name: Command
|
||||
command: ['echo', '$PATH']
|
||||
alert_down: [ 'not_log']
|
||||
# alert_every: -1
|
||||
alert_every: 0
|
||||
|
||||
|
||||
alerts:
|
||||
log:
|
||||
command: ['true']
|
11
test/valid-config-default-values.hcl
Normal file
11
test/valid-config-default-values.hcl
Normal file
@ -0,0 +1,11 @@
|
||||
check_interval = "1s"
|
||||
default_alert_down = ["log_command"]
|
||||
default_alert_after = 1
|
||||
|
||||
monitor "Command" {
|
||||
command = ["echo", "$PATH"]
|
||||
}
|
||||
|
||||
alert "log_command" {
|
||||
command = ["echo", "default", "'command!!!'", "{{.MonitorName}}"]
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
---
|
||||
check_interval: 1
|
||||
default_alert_down: ["log_command"]
|
||||
default_alert_after: 1
|
||||
|
||||
monitors:
|
||||
- name: Command
|
||||
command: ["echo", "$PATH"]
|
||||
|
||||
alerts:
|
||||
log_command:
|
||||
command: ["echo", "regular", '"command!!!"', "{{.MonitorName}}"]
|
@ -1,8 +0,0 @@
|
||||
---
|
||||
check_interval: 1
|
||||
|
||||
monitors:
|
||||
- name: Command
|
||||
command: ['echo', '$PATH']
|
||||
alert_down: ['log']
|
||||
alert_every: 0
|
Loading…
x
Reference in New Issue
Block a user