diff --git a/README.md b/README.md index cdceeba..5629a46 100644 --- a/README.md +++ b/README.md @@ -92,5 +92,6 @@ Improvement: - [] Implement leveled logging (maybe glog or logrus) - [] Consider switching from YAML to TOML - [] Consider value of templating vs injecting values into Env variables + - [] Consider dropping `alert_up` and `alert_down` in favor of using Go templates that offer more control of messaging - [] Async checking - [] Use durations rather than seconds checked in event loop \ No newline at end of file diff --git a/alert.go b/alert.go index 2a27ed1..fd5c861 100644 --- a/alert.go +++ b/alert.go @@ -8,6 +8,7 @@ import ( "time" ) +// Alert is a config driven mechanism for sending a notice type Alert struct { Name string Command []string @@ -16,12 +17,25 @@ type Alert struct { commandShellTemplate *template.Template } +// AlertNotice captures the context for an alert to be sent +type AlertNotice struct { + MonitorName string + AlertCount int16 + FailureCount int16 + LastCheckOutput string + LastSuccess time.Time + IsUp bool +} + +// IsValid returns a boolean indicating if the Alert has been correctly +// configured func (alert Alert) IsValid() bool { atLeastOneCommand := (alert.CommandShell != "" || alert.Command != nil) atMostOneCommand := (alert.CommandShell == "" || alert.Command == nil) return atLeastOneCommand && atMostOneCommand } +// BuildTemplates compiles command templates for the Alert func (alert *Alert) BuildTemplates() { if alert.commandTemplate == nil && alert.Command != nil { // build template @@ -40,17 +54,12 @@ func (alert *Alert) BuildTemplates() { ) log.Printf("Template built: %v", alert.commandShellTemplate) } else { - panic("No template provided?") + panic("No template provided for alert %s", alert.Name) } } -func (alert *Alert) Send(notice AlertNotice) { - // TODO: Validate and build templates in a better place and make this immutable - if !alert.IsValid() { - log.Fatalf("Alert is invalid: %v", alert) - } - alert.BuildTemplates() - +// Send will send an alert notice by executing the command template +func (alert Alert) Send(notice AlertNotice) { var cmd *exec.Cmd if alert.commandTemplate != nil { @@ -86,12 +95,3 @@ func (alert *Alert) Send(notice AlertNotice) { panic(err) } } - -type AlertNotice struct { - MonitorName string - AlertCount int16 - FailureCount int16 - LastCheckOutput string - LastSuccess time.Time - IsUp bool -} diff --git a/config.go b/config.go index 9726d7a..770619f 100644 --- a/config.go +++ b/config.go @@ -7,12 +7,42 @@ import ( "os" ) +// Config type is contains all provided user configuration type Config struct { CheckInterval int64 `yaml:"check_interval"` Monitors []*Monitor Alerts map[string]*Alert } +// IsValid checks config validity and returns true if valid +func (config Config) IsValid() (isValid bool) { + isValid = true + for _, monitor := range config.Monitors { + if !monitor.IsValid() { + log.Printf("ERROR: Invalid monitor configuration: %s", monitor.Name) + isValid = false + } + } + + for _, alert := range config.Alerts { + if !alert.IsValid() { + log.Printf("ERROR: Invalid alert configuration: %s", alert.Name) + isValid = false + } + } + + return +} + +// Init performs extra initialization on top of loading the config from file +func (config *Config) Init() { + for name, alert := range config.Alerts { + alert.Name = name + alert.BuildTemplates() + } +} + +// LoadConfig will read config from the given path and parse it func LoadConfig(filePath string) (config Config) { data, err := ioutil.ReadFile(filePath) if err != nil { @@ -20,9 +50,8 @@ func LoadConfig(filePath string) (config Config) { } // TODO: Decide if this is better expanded here, or only when executing - env_expanded := os.ExpandEnv(string(data)) - - err = yaml.Unmarshal([]byte(env_expanded), &config) + envExpanded := os.ExpandEnv(string(data)) + err = yaml.Unmarshal([]byte(envExpanded), &config) if err != nil { log.Fatalf("ERROR: %v", err) panic(err) @@ -30,5 +59,12 @@ func LoadConfig(filePath string) (config Config) { log.Printf("config:\n%v\n", config) + if !config.IsValid() { + panic("Cannot continue with invalid configuration") + } + + // Finish initializing configuration + config.Init() + return config } diff --git a/main.go b/main.go index dca25a5..befdcf2 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,8 @@ func main() { for _, monitor := range config.Monitors { if monitor.ShouldCheck() { _, alertNotice := monitor.Check() + + // Should probably consider refactoring everything below here if alertNotice != nil { //log.Printf("Recieved an alert notice: %v", alertNotice) var alerts []string diff --git a/util.go b/util.go index 36eef38..b54c42f 100644 --- a/util.go +++ b/util.go @@ -5,7 +5,7 @@ import ( "strings" ) -/// escapeCommandShell accepts a command to be executed by a shell and escapes it +// escapeCommandShell accepts a command to be executed by a shell and escapes it func escapeCommandShell(command string) string { // Remove extra spaces and newlines from ends command = strings.TrimSpace(command) @@ -15,7 +15,7 @@ func escapeCommandShell(command string) string { return command } -/// ShellCommand takes a string and executes it as a command using `sh` +// ShellCommand takes a string and executes it as a command using `sh` func ShellCommand(command string) *exec.Cmd { shellCommand := []string{"sh", "-c", escapeCommandShell(command)} //log.Printf("Shell command: %v", shellCommand)