From 342b12432e561743d8f870e1cf11e65f46ade3e4 Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Sat, 21 Sep 2019 15:03:26 -0700 Subject: [PATCH] WIP --- .gitignore | 1 + alert.go | 59 ++++++++++++++++++++++++++++++++++++ config.go | 32 ++++++++++++++++++++ main.go | 20 ++++++++++++ monitor.go | 77 +++++++++++++++++++++++++++++++++++++++++++++++ sample-config.yml | 29 ++++++++++++++++++ util.go | 3 ++ 7 files changed, 221 insertions(+) create mode 100644 alert.go create mode 100644 config.go create mode 100644 main.go create mode 100644 monitor.go create mode 100644 sample-config.yml create mode 100644 util.go diff --git a/.gitignore b/.gitignore index 9a3a8d8..5302387 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +config.yml diff --git a/alert.go b/alert.go new file mode 100644 index 0000000..3f998c6 --- /dev/null +++ b/alert.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + "os/exec" + "text/template" + "time" +) + +type Alert struct { + Name string + Command []string + CommandShell string `yaml:"command_shell"` + commandTemplate []template.Template + commandShellTemplate template.Template +} + +func (alert Alert) IsValid() bool { + atLeastOneCommand := (alert.CommandShell != "" || alert.Command != nil) + atMostOneCommand := (alert.CommandShell == "" || alert.Command == nil) + return atLeastOneCommand && atMostOneCommand +} + +func (alert *Alert) BuildTemplates() { + if alert.commandTemplate == nil && alert.Command != nil { + // build template + fmt.Println("Building template for command...") + } else if alert.commandShellTemplate == nil && alert.CommandShell != "" { + alert.commandShellTemplate = template.Must( + template.New(alert.Name).Parse(alert.CommandShell), + ) + } else { + panic("No template?") + } +} + +func (alert Alert) Send(notice AlertNotice) { + var cmd *exec.Cmd + + if alert.commandTemplate != nil { + // build template + fmt.Println("Send command thing...") + } else if alert.commandShellTemplate != nil { + var commandBuffer bytes.Buffer + err := alert.commandShellTemplate.Execute(&commandBuffer, notice) + // TODO handle error + cmd = exec.Command(commandBuffer.String()) + } else { + panic("No template?") + } +} + +type AlertNotice struct { + MonitorName string + AlertCount int64 + FailureCount int64 + LastCheckOutput string + LastSuccess time.Time +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..b22bf10 --- /dev/null +++ b/config.go @@ -0,0 +1,32 @@ +package main + +import ( + "gopkg.in/yaml.v2" + "io/ioutil" + "log" + "os" +) + +type Config struct { + CheckInterval int64 `yaml:"check_interval"` + Monitors []Monitor + Alerts map[string]Alert +} + +func LoadConfig(filePath string) (config Config) { + data, err := ioutil.ReadFile(filePath) + if err != nil { + panic(err) + } + env_expanded := os.ExpandEnv(string(data)) + + err = yaml.Unmarshal([]byte(env_expanded), &config) + if err != nil { + log.Fatalf("error: %v", err) + panic(err) + } + + log.Printf("config:\n%v\n", config) + + return config +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9770a44 --- /dev/null +++ b/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "time" +) + +func main() { + config := LoadConfig("config.yml") + + for { + for _, monitor := range config.Monitors { + if monitor.ShouldCheck() { + monitor.Check() + } + } + + sleepTime := time.Duration(config.CheckInterval) * time.Second + time.Sleep(sleepTime) + } +} diff --git a/monitor.go b/monitor.go new file mode 100644 index 0000000..9874994 --- /dev/null +++ b/monitor.go @@ -0,0 +1,77 @@ +package main + +import ( + "log" + "os/exec" + "time" +) + +type Monitor struct { + // Config values + Name string + Command []string + CommandShell string `yaml:"command_shell` + AlertDown []string `yaml:"alert_down"` + AlertUp []string `yaml:"alert_up"` + CheckInterval float64 `yaml:"check_interval"` + AlertAfter int16 `yaml:"alert_after"` + AlertEvey int16 `yaml:"alert_every"` + // Other values + LastCheck time.Time + LastOutput string +} + +func (monitor Monitor) IsValid() bool { + atLeastOneCommand := (monitor.CommandShell != "" || monitor.Command != nil) + atMostOneCommand := (monitor.CommandShell == "" || monitor.Command == nil) + return atLeastOneCommand && atMostOneCommand +} + +func (monitor Monitor) ShouldCheck() bool { + if monitor.LastCheck.IsZero() { + return true + } + + sinceLastCheck := time.Now().Sub(monitor.LastCheck).Seconds() + return sinceLastCheck >= monitor.CheckInterval +} + +func (monitor *Monitor) Check() bool { + // TODO: This should probably return a list of alerts since the `raise` + // pattern doesn't carry over from Python + var cmd *exec.Cmd + + if monitor.Command != nil { + cmd = exec.Command(monitor.Command[0], monitor.Command[1:]...) + } else { + // TODO: Handle a command shell as well. This is untested + cmd = exec.Command(monitor.CommandShell) + } + + output, err := cmd.CombinedOutput() + log.Printf("Check %s\n---\n%s\n---", monitor.Name, string(output)) + + is_success := (err == nil) + if err != nil { + log.Printf("error: %v", err) + } + + monitor.LastCheck = time.Now() + monitor.LastOutput = string(output) + + if is_success { + monitor.success() + } else { + monitor.failure() + } + + return is_success +} + +func (monitor Monitor) success() { + log.Printf("Great success!") +} + +func (monitor *Monitor) failure() { + log.Printf("Devastating failure. :(") +} diff --git a/sample-config.yml b/sample-config.yml new file mode 100644 index 0000000..2582c2f --- /dev/null +++ b/sample-config.yml @@ -0,0 +1,29 @@ +check_interval: 30 + +monitors: + - name: My Website + command: [ 'curl', '-s', '-o', '/dev/null', 'https://minitor.mon' ] + alert_down: [ log, mailgun_down, sms_down ] + alert_up: [ log, email_up ] + check_interval: 30 # Must be at minimum the global `check_interval` + alert_after: 3 + alert_every: -1 # Defaults to -1 for exponential backoff. 0 to disable repeating + +alerts: + email_up: + command: [ sendmail, "me@minitor.mon", "Recovered: {monitor_name}", "We're back!" ] + mailgun_down: + command: > + curl -s -X POST + -F subject="Alert! {monitor_name} failed" + -F from="Minitor " + -F to=me@minitor.mon + -F text="Our monitor failed" + https://api.mailgun.net/v3/minitor.mon/messages + -u "api:${MAILGUN_API_KEY}" + sms_down: + command: > + curl -s -X POST -F "Body=Failure! {monitor_name} has failed" + -F "From=${AVAILABLE_NUMBER}" -F "To=${MY_PHONE}" + "https://api.twilio.com/2010-04-01/Accounts/${ACCOUNT_SID}/Messages" + -u "${ACCOUNT_SID}:${AUTH_TOKEN}" diff --git a/util.go b/util.go new file mode 100644 index 0000000..1110061 --- /dev/null +++ b/util.go @@ -0,0 +1,3 @@ +package main + +