Refactor test package and some field types

Fairly big test refactor and changing some of the fields from pointers
This commit is contained in:
IamTheFij 2024-11-15 11:30:34 -08:00
parent a0a6b8199a
commit e0af17a599
18 changed files with 237 additions and 344 deletions

View File

@ -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_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| |`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| |`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| |`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 ### Alerts

View File

@ -162,14 +162,3 @@ func (alert Alert) Send(notice AlertNotice) (outputStr string, err error) {
return outputStr, err 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}}",
},
}
}

View File

@ -1,18 +1,20 @@
package main package main_test
import ( import (
"testing" "testing"
m "git.iamthefij.com/iamthefij/minitor-go"
) )
func TestAlertIsValid(t *testing.T) { func TestAlertIsValid(t *testing.T) {
cases := []struct { cases := []struct {
alert Alert alert m.Alert
expected bool expected bool
name string name string
}{ }{
{Alert{Command: []string{"echo", "test"}}, true, "Command only"}, {m.Alert{Command: []string{"echo", "test"}}, true, "Command only"},
{Alert{ShellCommand: "echo test"}, true, "CommandShell only"}, {m.Alert{ShellCommand: "echo test"}, true, "CommandShell only"},
{Alert{}, false, "No commands"}, {m.Alert{}, false, "No commands"},
} }
for _, c := range cases { for _, c := range cases {
@ -31,56 +33,40 @@ func TestAlertIsValid(t *testing.T) {
func TestAlertSend(t *testing.T) { func TestAlertSend(t *testing.T) {
cases := []struct { cases := []struct {
alert Alert alert m.Alert
notice AlertNotice notice m.AlertNotice
expectedOutput string expectedOutput string
expectErr bool expectErr bool
name string name string
}{ }{
{ {
Alert{Command: []string{"echo", "{{.MonitorName}}"}}, m.Alert{Command: []string{"echo", "{{.MonitorName}}"}},
AlertNotice{MonitorName: "test"}, m.AlertNotice{MonitorName: "test"},
"test\n", "test\n",
false, false,
"Command with template", "Command with template",
}, },
{ {
Alert{ShellCommand: "echo {{.MonitorName}}"}, m.Alert{ShellCommand: "echo {{.MonitorName}}"},
AlertNotice{MonitorName: "test"}, m.AlertNotice{MonitorName: "test"},
"test\n", "test\n",
false, false,
"Command shell with template", "Command shell with template",
}, },
{ {
Alert{Command: []string{"echo", "{{.Bad}}"}}, m.Alert{Command: []string{"echo", "{{.Bad}}"}},
AlertNotice{MonitorName: "test"}, m.AlertNotice{MonitorName: "test"},
"", "",
true, true,
"Command with bad template", "Command with bad template",
}, },
{ {
Alert{ShellCommand: "echo {{.Bad}}"}, m.Alert{ShellCommand: "echo {{.Bad}}"},
AlertNotice{MonitorName: "test"}, m.AlertNotice{MonitorName: "test"},
"", "",
true, true,
"Command shell with bad template", "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 { for _, c := range cases {
@ -109,8 +95,8 @@ func TestAlertSend(t *testing.T) {
} }
func TestAlertSendNoTemplates(t *testing.T) { func TestAlertSendNoTemplates(t *testing.T) {
alert := Alert{} alert := m.Alert{}
notice := AlertNotice{} notice := m.AlertNotice{}
output, err := alert.Send(notice) output, err := alert.Send(notice)
if err == nil { if err == nil {
@ -120,13 +106,13 @@ func TestAlertSendNoTemplates(t *testing.T) {
func TestAlertBuildTemplate(t *testing.T) { func TestAlertBuildTemplate(t *testing.T) {
cases := []struct { cases := []struct {
alert Alert alert m.Alert
expectErr bool expectErr bool
name string name string
}{ }{
{Alert{Command: []string{"echo", "test"}}, false, "Command only"}, {m.Alert{Command: []string{"echo", "test"}}, false, "Command only"},
{Alert{ShellCommand: "echo test"}, false, "CommandShell only"}, {m.Alert{ShellCommand: "echo test"}, false, "CommandShell only"},
{Alert{}, true, "No commands"}, {m.Alert{}, true, "No commands"},
} }
for _, c := range cases { for _, c := range cases {

View File

@ -113,6 +113,8 @@ func (config *Config) Init() (err error) {
} }
for _, monitor := range config.Monitors { for _, monitor := range config.Monitors {
// TODO: Move this to a Monitor.Init() method
// Parse the check_interval string into a time.Duration // Parse the check_interval string into a time.Duration
if monitor.CheckIntervalStr != nil { if monitor.CheckIntervalStr != nil {
monitor.CheckInterval, err = time.ParseDuration(*monitor.CheckIntervalStr) monitor.CheckInterval, err = time.ParseDuration(*monitor.CheckIntervalStr)
@ -122,8 +124,10 @@ func (config *Config) Init() (err error) {
} }
// Set default values for monitor alerts // Set default values for monitor alerts
if monitor.AlertAfter == nil { if monitor.AlertAfter == 0 && config.DefaultAlertAfter != nil {
monitor.AlertAfter = config.DefaultAlertAfter monitor.AlertAfter = *config.DefaultAlertAfter
} else if monitor.AlertAfter == 0 {
monitor.AlertAfter = 1
} }
if monitor.AlertEvery == nil { if monitor.AlertEvery == nil {

View File

@ -1,7 +1,9 @@
package main package main_test
import ( import (
"testing" "testing"
m "git.iamthefij.com/iamthefij/minitor-go"
) )
func TestLoadConfig(t *testing.T) { func TestLoadConfig(t *testing.T) {
@ -11,12 +13,11 @@ func TestLoadConfig(t *testing.T) {
name string name string
}{ }{
{"./test/does-not-exist", true, "Invalid config path"}, {"./test/does-not-exist", true, "Invalid config path"},
// {"./test/invalid-config-missing-alerts.yml", true, "Invalid config missing alerts"}, {"./test/invalid-config-missing-alerts.hcl", true, "Invalid config missing alerts"},
// {"./test/invalid-config-type.yml", true, "Invalid config type for key"}, {"./test/invalid-config-type.hcl", true, "Invalid config type for key"},
// {"./test/invalid-config-unknown-alert.yml", true, "Invalid config unknown alert"}, {"./test/invalid-config-unknown-alert.hcl", true, "Invalid config unknown alert"},
// {"./test/valid-config-default-values.yml", false, "Valid config file with default values"}, {"./test/valid-config-default-values.hcl", false, "Valid config file with default values"},
{"./test/valid-config.hcl", false, "Valid config file"}, {"./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 { for _, c := range cases {
c := c c := c
@ -24,7 +25,7 @@ func TestLoadConfig(t *testing.T) {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
t.Parallel() t.Parallel()
_, err := LoadConfig(c.configPath) _, err := m.LoadConfig(c.configPath)
hasErr := (err != nil) hasErr := (err != nil)
if hasErr != c.expectErr { if hasErr != c.expectErr {
@ -39,7 +40,7 @@ func TestLoadConfig(t *testing.T) {
func TestMultiLineConfig(t *testing.T) { func TestMultiLineConfig(t *testing.T) {
t.Parallel() t.Parallel()
config, err := LoadConfig("./test/valid-verify-multi-line.hcl") config, err := m.LoadConfig("./test/valid-verify-multi-line.hcl")
if err != nil { if err != nil {
t.Fatalf("TestMultiLineConfig(load), expected=no_error actual=%v", err) 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)) t.Logf("bytes actual =%v", []byte(actual))
} }
actual, err = alert.Send(AlertNotice{}) actual, err = alert.Send(m.AlertNotice{})
if err != nil { if err != nil {
t.Errorf("Execution of alert failed") t.Errorf("Execution of alert failed")
} }

12
main.go
View File

@ -24,7 +24,7 @@ var (
errUnknownAlert = errors.New("unknown alert") 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) slog.Debugf("Received an alert notice from %s", alertNotice.MonitorName)
alertNames := monitor.GetAlertNames(alertNotice.IsUp) alertNames := monitor.GetAlertNames(alertNotice.IsUp)
@ -65,7 +65,7 @@ func sendAlerts(config *Config, monitor *Monitor, alertNotice *AlertNotice) erro
return nil return nil
} }
func checkMonitors(config *Config) error { func CheckMonitors(config *Config) error {
// TODO: Run this in goroutines and capture exceptions // TODO: Run this in goroutines and capture exceptions
for _, monitor := range config.Monitors { for _, monitor := range config.Monitors {
if monitor.ShouldCheck() { if monitor.ShouldCheck() {
@ -77,7 +77,7 @@ func checkMonitors(config *Config) error {
Metrics.CountCheck(monitor.Name, success, monitor.LastCheckMilliseconds(), hasAlert) Metrics.CountCheck(monitor.Name, success, monitor.LastCheckMilliseconds(), hasAlert)
if alertNotice != nil { 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 there was an error in sending an alert, exit early and bubble it up
if err != nil { if err != nil {
return err return err
@ -89,7 +89,7 @@ func checkMonitors(config *Config) error {
return nil return nil
} }
func sendStartupAlerts(config *Config, alertNames []string) error { func SendStartupAlerts(config *Config, alertNames []string) error {
for _, alertName := range alertNames { for _, alertName := range alertNames {
var err error var err error
@ -148,14 +148,14 @@ func main() {
if *startupAlerts != "" { if *startupAlerts != "" {
alertNames := strings.Split(*startupAlerts, ",") alertNames := strings.Split(*startupAlerts, ",")
err = sendStartupAlerts(&config, alertNames) err = SendStartupAlerts(&config, alertNames)
slog.OnErrPanicf(err, "Error running startup alerts") slog.OnErrPanicf(err, "Error running startup alerts")
} }
// Start main loop // Start main loop
for { for {
err = checkMonitors(&config) err = CheckMonitors(&config)
slog.OnErrPanicf(err, "Error checking monitors") slog.OnErrPanicf(err, "Error checking monitors")
time.Sleep(config.CheckInterval) time.Sleep(config.CheckInterval)

View File

@ -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 { func Ptr[T any](v T) *T {
return &v return &v
} }
// TestCheckConfig tests the checkConfig function
// It also tests results for potentially invalid configuration. For example, no alerts
func TestCheckMonitors(t *testing.T) { func TestCheckMonitors(t *testing.T) {
cases := []struct { cases := []struct {
config Config config m.Config
expectErr bool expectFailureError bool
name string expectRecoverError bool
name string
}{ }{
{ {
config: Config{ config: m.Config{
CheckIntervalStr: "1s", CheckIntervalStr: "1s",
Monitors: []*Monitor{ Monitors: []*m.Monitor{
{ {
Name: "Success", Name: "Success",
Command: []string{"true"},
}, },
}, },
}, },
expectErr: false, expectFailureError: false,
name: "Monitor success, no alerts", expectRecoverError: false,
name: "No alerts",
}, },
{ {
config: Config{ config: m.Config{
CheckIntervalStr: "1s", CheckIntervalStr: "1s",
Monitors: []*Monitor{ Monitors: []*m.Monitor{
{ {
Name: "Failure", 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"}, 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"}, AlertUp: []string{"unknown"},
alertCount: 1, AlertAfter: 1,
}, },
}, },
}, },
expectErr: true, expectFailureError: true,
name: "Monitor recovery, unknown alerts", expectRecoverError: true,
name: "Unknown alerts",
}, },
{ {
config: Config{ config: m.Config{
CheckIntervalStr: "1s", CheckIntervalStr: "1s",
Monitors: []*Monitor{ Monitors: []*m.Monitor{
{ {
Name: "Failure", Name: "Failure",
Command: []string{"false"},
AlertDown: []string{"good"}, AlertDown: []string{"good"},
AlertAfter: Ptr(1), AlertUp: []string{"good"},
AlertAfter: 1,
}, },
}, },
Alerts: []*Alert{{ Alerts: []*m.Alert{{
Name: "good", Name: "good",
Command: []string{"true"}, Command: []string{"true"},
}}, }},
}, },
expectErr: false, expectFailureError: false,
name: "Monitor failure, successful alert", expectRecoverError: false,
name: "Successful alert",
}, },
{ {
config: Config{ config: m.Config{
CheckIntervalStr: "1s", CheckIntervalStr: "1s",
Monitors: []*Monitor{ Monitors: []*m.Monitor{
{ {
Name: "Failure", Name: "Failure",
Command: []string{"false"},
AlertDown: []string{"bad"}, AlertDown: []string{"bad"},
AlertAfter: Ptr(1), AlertUp: []string{"bad"},
AlertAfter: 1,
}, },
}, },
Alerts: []*Alert{{ Alerts: []*m.Alert{{
Name: "bad", Name: "bad",
Command: []string{"false"}, Command: []string{"false"},
}}, }},
}, },
expectErr: true, expectFailureError: true,
name: "Monitor failure, bad alert", 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) t.Errorf("checkMonitors(%s): unexpected error reading config: %v", c.name, err)
} }
err = checkMonitors(&c.config) for _, check := range []struct {
if err == nil && c.expectErr { shellCmd string
t.Errorf("checkMonitors(%s): Expected panic, the code did not panic", c.name) name string
} else if err != nil && !c.expectErr { expectErr bool
t.Errorf("checkMonitors(%s): Did not expect an error, but we got one anyway: %v", c.name, err) }{
{"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) { func TestFirstRunAlerts(t *testing.T) {
cases := []struct { cases := []struct {
config Config config m.Config
expectErr bool expectErr bool
startupAlerts []string startupAlerts []string
name string name string
}{ }{
{ {
config: Config{}, config: m.Config{
expectErr: false, CheckIntervalStr: "1s",
startupAlerts: []string{}, },
name: "Empty",
},
{
config: Config{},
expectErr: true, expectErr: true,
startupAlerts: []string{"missing"}, startupAlerts: []string{"missing"},
name: "Unknown", name: "Unknown",
}, },
{ {
config: Config{ config: m.Config{
Alerts: []*Alert{ CheckIntervalStr: "1s",
Alerts: []*m.Alert{
{ {
Name: "good", Name: "good",
Command: []string{"true"}, Command: []string{"true"},
@ -177,8 +155,9 @@ func TestFirstRunAlerts(t *testing.T) {
name: "Successful alert", name: "Successful alert",
}, },
{ {
config: Config{ config: m.Config{
Alerts: []*Alert{ CheckIntervalStr: "1s",
Alerts: []*m.Alert{
{ {
Name: "bad", Name: "bad",
Command: []string{"false"}, Command: []string{"false"},
@ -202,7 +181,7 @@ func TestFirstRunAlerts(t *testing.T) {
t.Errorf("sendFirstRunAlerts(%s): unexpected error reading config: %v", c.name, err) 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 { if err == nil && c.expectErr {
t.Errorf("sendFirstRunAlerts(%s): Expected error, the code did not error", c.name) t.Errorf("sendFirstRunAlerts(%s): Expected error, the code did not error", c.name)
} else if err != nil && !c.expectErr { } else if err != nil && !c.expectErr {

View File

@ -15,7 +15,7 @@ type Monitor struct { //nolint:maligned
CheckInterval time.Duration CheckInterval time.Duration
Name string `hcl:"name,label"` Name string `hcl:"name,label"`
AlertAfter *int `hcl:"alert_after,optional"` AlertAfter int `hcl:"alert_after,optional"`
AlertEvery *int `hcl:"alert_every,optional"` AlertEvery *int `hcl:"alert_every,optional"`
AlertDown []string `hcl:"alert_down,optional"` AlertDown []string `hcl:"alert_down,optional"`
AlertUp []string `hcl:"alert_up,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 // IsValid returns a boolean indicating if the Monitor has been correctly
// configured // configured
func (monitor Monitor) IsValid() bool { func (monitor Monitor) IsValid() bool {
// TODO: Refactor and return an error containing more information on what was invalid
hasCommand := len(monitor.Command) > 0 hasCommand := len(monitor.Command) > 0
hasShellCommand := monitor.ShellCommand != "" hasShellCommand := monitor.ShellCommand != ""
hasValidAlertAfter := monitor.GetAlertAfter() > 0 hasValidAlertAfter := monitor.AlertAfter > 0
hasAlertDown := len(monitor.AlertDown) > 0 hasAlertDown := len(monitor.AlertDown) > 0
hasAtLeastOneCommand := hasCommand || hasShellCommand hasAtLeastOneCommand := hasCommand || hasShellCommand
@ -48,6 +49,10 @@ func (monitor Monitor) IsValid() bool {
hasAlertDown hasAlertDown
} }
func (monitor Monitor) LastOutput() string {
return monitor.lastOutput
}
// ShouldCheck returns a boolean indicating if the Monitor is ready to be // ShouldCheck returns a boolean indicating if the Monitor is ready to be
// be checked again // be checked again
func (monitor Monitor) ShouldCheck() bool { func (monitor Monitor) ShouldCheck() bool {
@ -126,20 +131,20 @@ func (monitor *Monitor) success() (notice *AlertNotice) {
func (monitor *Monitor) failure() (notice *AlertNotice) { func (monitor *Monitor) failure() (notice *AlertNotice) {
monitor.failureCount++ monitor.failureCount++
// If we haven't hit the minimum failures, we can exit // If we haven't hit the minimum failures, we can exit
if monitor.failureCount < monitor.GetAlertAfter() { if monitor.failureCount < monitor.AlertAfter {
slog.Debugf( slog.Debugf(
"%s failed but did not hit minimum failures. "+ "%s failed but did not hit minimum failures. "+
"Count: %v alert after: %v", "Count: %v alert after: %v",
monitor.Name, monitor.Name,
monitor.failureCount, monitor.failureCount,
monitor.GetAlertAfter(), monitor.AlertAfter,
) )
return return
} }
// Take number of failures after minimum // 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 // Use alert cadence to determine if we should alert
switch { switch {
@ -168,15 +173,6 @@ func (monitor *Monitor) failure() (notice *AlertNotice) {
return notice 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 // GetAlertNames gives a list of alert names for a given monitor status
func (monitor Monitor) GetAlertNames(up bool) []string { func (monitor Monitor) GetAlertNames(up bool) []string {
if up { if up {

View File

@ -1,22 +1,25 @@
package main package main_test
import ( import (
"reflect"
"testing" "testing"
"time" "time"
m "git.iamthefij.com/iamthefij/minitor-go"
) )
// TestMonitorIsValid tests the Monitor.IsValid() // TestMonitorIsValid tests the Monitor.IsValid()
func TestMonitorIsValid(t *testing.T) { func TestMonitorIsValid(t *testing.T) {
cases := []struct { cases := []struct {
monitor Monitor monitor m.Monitor
expected bool expected bool
name string name string
}{ }{
{Monitor{Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, true, "Command only"}, {m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, true, "Command only"},
{Monitor{ShellCommand: "echo test", AlertDown: []string{"log"}}, true, "CommandShell only"}, {m.Monitor{AlertAfter: 1, ShellCommand: "echo test", AlertDown: []string{"log"}}, true, "CommandShell only"},
{Monitor{Command: []string{"echo", "test"}}, false, "No AlertDown"}, {m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}}, false, "No AlertDown"},
{Monitor{AlertDown: []string{"log"}}, false, "No commands"}, {m.Monitor{AlertAfter: 1, 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"}}, false, "Invalid alert threshold, -1"},
} }
for _, c := range cases { for _, c := range cases {
@ -35,75 +38,63 @@ func TestMonitorIsValid(t *testing.T) {
// TestMonitorShouldCheck tests the Monitor.ShouldCheck() // TestMonitorShouldCheck tests the Monitor.ShouldCheck()
func TestMonitorShouldCheck(t *testing.T) { func TestMonitorShouldCheck(t *testing.T) {
timeNow := time.Now() t.Parallel()
timeTenSecAgo := time.Now().Add(time.Second * -10)
timeTwentySecAgo := time.Now().Add(time.Second * -20)
fifteenSeconds := time.Second * 15
cases := []struct { // Create a monitor that should check every second and then verify it checks with some sleeps
monitor Monitor monitor := m.Monitor{ShellCommand: "true", CheckInterval: time.Second}
expected bool
name string if !monitor.ShouldCheck() {
}{ t.Errorf("New monitor should be ready to check")
{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"},
} }
for _, c := range cases { monitor.Check()
c := c
t.Run(c.name, func(t *testing.T) { if monitor.ShouldCheck() {
t.Parallel() t.Errorf("Monitor should not be ready to check after a check")
}
actual := c.monitor.ShouldCheck() time.Sleep(time.Second)
if actual != c.expected {
t.Errorf("ShouldCheck(%v), expected=%t actual=%t", c.name, c.expected, actual) if !monitor.ShouldCheck() {
} t.Errorf("Monitor should be ready to check after a second")
})
} }
} }
// TestMonitorIsUp tests the Monitor.IsUp() // TestMonitorIsUp tests the Monitor.IsUp()
func TestMonitorIsUp(t *testing.T) { func TestMonitorIsUp(t *testing.T) {
cases := []struct { t.Parallel()
monitor Monitor
expected bool // Creating a monitor that should alert after 2 failures. The monitor should be considered up until we reach two failed checks
name string monitor := m.Monitor{ShellCommand: "false", AlertAfter: 2}
}{ if !monitor.IsUp() {
{Monitor{}, true, "Empty"}, t.Errorf("New monitor should be considered up")
{Monitor{alertCount: 1}, false, "Has alert"},
{Monitor{alertCount: -1}, false, "Negative alerts"},
{Monitor{alertCount: 0}, true, "No alerts"},
} }
for _, c := range cases { monitor.Check()
c := c
t.Run(c.name, func(t *testing.T) { if !monitor.IsUp() {
t.Parallel() t.Errorf("Monitor should be considered up with one failure and no alerts")
}
actual := c.monitor.IsUp() monitor.Check()
if actual != c.expected {
t.Errorf("IsUp(%v), expected=%t actual=%t", c.name, c.expected, actual) if monitor.IsUp() {
} t.Errorf("Monitor should be considered down with one alert")
})
} }
} }
// TestMonitorGetAlertNames tests that proper alert names are returned // TestMonitorGetAlertNames tests that proper alert names are returned
func TestMonitorGetAlertNames(t *testing.T) { func TestMonitorGetAlertNames(t *testing.T) {
cases := []struct { cases := []struct {
monitor Monitor monitor m.Monitor
up bool up bool
expected []string expected []string
name string name string
}{ }{
{Monitor{}, true, nil, "Empty up"}, {m.Monitor{}, true, nil, "Empty up"},
{Monitor{}, false, nil, "Empty down"}, {m.Monitor{}, false, nil, "Empty down"},
{Monitor{AlertUp: []string{"alert"}}, true, []string{"alert"}, "Return up"}, {m.Monitor{AlertUp: []string{"alert"}}, true, []string{"alert"}, "Return up"},
{Monitor{AlertDown: []string{"alert"}}, false, []string{"alert"}, "Return down"}, {m.Monitor{AlertDown: []string{"alert"}}, false, []string{"alert"}, "Return down"},
} }
for _, c := range cases { for _, c := range cases {
@ -113,57 +104,30 @@ func TestMonitorGetAlertNames(t *testing.T) {
t.Parallel() t.Parallel()
actual := c.monitor.GetAlertNames(c.up) 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) 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 // TestMonitorFailureAlertAfter tests that alerts will not trigger until
// hitting the threshold provided by AlertAfter // hitting the threshold provided by AlertAfter
func TestMonitorFailureAlertAfter(t *testing.T) { func TestMonitorFailureAlertAfter(t *testing.T) {
var alertEvery int = 1 var alertEveryOne int = 1
cases := []struct { cases := []struct {
monitor Monitor monitor m.Monitor
numChecks int
expectNotice bool expectNotice bool
name string name string
}{ }{
{Monitor{AlertAfter: Ptr(1)}, true, "Empty"}, // Defaults to true because and AlertEvery default to 0 {m.Monitor{ShellCommand: "false", AlertAfter: 1}, 1, true, "Empty After 1"}, // Defaults to true because and AlertEvery default to 0
{Monitor{failureCount: 0, AlertAfter: Ptr(1), AlertEvery: &alertEvery}, true, "Alert after 1: first failure"}, {m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: &alertEveryOne}, 1, true, "Alert after 1: first failure"},
{Monitor{failureCount: 1, AlertAfter: Ptr(1), AlertEvery: &alertEvery}, true, "Alert after 1: second failure"}, {m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: &alertEveryOne}, 2, true, "Alert after 1: second failure"},
{Monitor{failureCount: 0, AlertAfter: Ptr(20), AlertEvery: &alertEvery}, false, "Alert after 20: first failure"}, {m.Monitor{ShellCommand: "false", AlertAfter: 20, AlertEvery: &alertEveryOne}, 1, false, "Alert after 20: first failure"},
{Monitor{failureCount: 19, AlertAfter: Ptr(20), AlertEvery: &alertEvery}, true, "Alert after 20: 20th failure"}, {m.Monitor{ShellCommand: "false", AlertAfter: 20, AlertEvery: &alertEveryOne}, 20, 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: 20, AlertEvery: &alertEveryOne}, 21, true, "Alert after 20: 21st failure"},
} }
for _, c := range cases { for _, c := range cases {
@ -172,8 +136,12 @@ func TestMonitorFailureAlertAfter(t *testing.T) {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
t.Parallel() t.Parallel()
notice := c.monitor.failure() hasNotice := false
hasNotice := (notice != nil)
for i := 0; i < c.numChecks; i++ {
_, notice := c.monitor.Check()
hasNotice = (notice != nil)
}
if hasNotice != c.expectNotice { if hasNotice != c.expectNotice {
t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice) 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 // TestMonitorFailureAlertEvery tests that alerts will trigger
// on the expected intervals // on the expected intervals
func TestMonitorFailureAlertEvery(t *testing.T) { func TestMonitorFailureAlertEvery(t *testing.T) {
var alertEvery0, alertEvery1, alertEvery2 int
alertEvery0 = 0
alertEvery1 = 1
alertEvery2 = 2
cases := []struct { cases := []struct {
monitor Monitor monitor m.Monitor
expectNotice bool expectedNotice []bool
name string name string
}{ }{
/* {m.Monitor{ShellCommand: "false", AlertAfter: 1}, []bool{true}, "No AlertEvery set"}, // Defaults to true because AlertAfter and AlertEvery default to nil
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
// Alert first time only, after 1 // Alert first time only, after 1
{Monitor{failureCount: 0, AlertAfter: Ptr(1), AlertEvery: &alertEvery0}, true, "Alert first time only after 1: first failure"}, {m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(0)}, []bool{true, false, false}, "Alert first time only after 1"},
{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"},
// Alert every time, after 1 // Alert every time, after 1
{Monitor{failureCount: 0, AlertAfter: Ptr(1), AlertEvery: &alertEvery1}, true, "Alert every time after 1: first failure"}, {m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(1)}, []bool{true, true, true}, "Alert every time after 1"},
{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"},
// Alert every other 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"}, {m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(2)}, []bool{true, false, true, false}, "Alert every other time after 1"},
{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"},
} }
for _, c := range cases { for _, c := range cases {
@ -226,11 +173,13 @@ func TestMonitorFailureAlertEvery(t *testing.T) {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
t.Parallel() t.Parallel()
notice := c.monitor.failure() for i, expectNotice := range c.expectedNotice {
hasNotice := (notice != nil) _, notice := c.monitor.Check()
hasNotice := (notice != nil)
if hasNotice != c.expectNotice { if hasNotice != expectNotice {
t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice) 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 // Unlike previous tests, this one requires a static Monitor with repeated
// calls to the failure method // 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 { for _, c := range cases {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
// NOTE: These tests are not parallel because they rely on the state of the Monitor // NOTE: These tests are not parallel because they rely on the state of the Monitor
notice := monitor.failure() _, notice := monitor.Check()
hasNotice := (notice != nil) hasNotice := (notice != nil)
if hasNotice != c.expectNotice { if hasNotice != c.expectNotice {
@ -281,27 +230,27 @@ func TestMonitorCheck(t *testing.T) {
} }
cases := []struct { cases := []struct {
monitor Monitor monitor m.Monitor
expect expected expect expected
name string name string
}{ }{
{ {
Monitor{Command: []string{"echo", "success"}}, m.Monitor{AlertAfter: 1, Command: []string{"echo", "success"}},
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"}, expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
"Test successful command", "Test successful command",
}, },
{ {
Monitor{ShellCommand: "echo success"}, m.Monitor{AlertAfter: 1, ShellCommand: "echo success"},
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"}, expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
"Test successful command shell", "Test successful command shell",
}, },
{ {
Monitor{Command: []string{"total", "failure"}}, m.Monitor{AlertAfter: 1, Command: []string{"total", "failure"}},
expected{isSuccess: false, hasNotice: true, lastOutput: ""}, expected{isSuccess: false, hasNotice: true, lastOutput: ""},
"Test failed command", "Test failed command",
}, },
{ {
Monitor{ShellCommand: "false"}, m.Monitor{AlertAfter: 1, ShellCommand: "false"},
expected{isSuccess: false, hasNotice: true, lastOutput: ""}, expected{isSuccess: false, hasNotice: true, lastOutput: ""},
"Test failed command shell", "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) 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 { if lastOutput != c.expect.lastOutput {
t.Errorf("Check(%v) (output), expected=%v actual=%v", c.name, c.expect.lastOutput, lastOutput) t.Errorf("Check(%v) (output), expected=%v actual=%v", c.name, c.expect.lastOutput, lastOutput)
} }

View File

@ -0,0 +1,7 @@
check_interval = "1s"
monitor "Command" {
command = ["echo", "$PATH"]
alert_down = [ "alert_down", "log_shell", "log_command" ]
alert_every = 0
}

View File

@ -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

View File

@ -0,0 +1 @@
check_interval = "woops, I'm not an int!"

View File

@ -1 +0,0 @@
check_interval: woops, I'm not an int!

View File

@ -0,0 +1,12 @@
check_interval = "1s"
monitor "Command" {
command = ["echo", "$PATH"]
alert_down = ["not_log"]
alert_every = 0
}
alert "log" {
command = ["true"]
}

View File

@ -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']

View 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}}"]
}

View File

@ -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}}"]

View File

@ -1,8 +0,0 @@
---
check_interval: 1
monitors:
- name: Command
command: ['echo', '$PATH']
alert_down: ['log']
alert_every: 0