193 lines
4.8 KiB
Go
193 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"git.iamthefij.com/iamthefij/slog"
|
|
)
|
|
|
|
var (
|
|
errNoTemplate = errors.New("no template")
|
|
|
|
// ErrAlertFailed indicates that an alert failed to send
|
|
ErrAlertFailed = errors.New("alert failed")
|
|
)
|
|
|
|
// Alert is a config driven mechanism for sending a notice
|
|
type Alert struct {
|
|
Name string
|
|
Command CommandOrShell
|
|
commandTemplate []*template.Template
|
|
commandShellTemplate *template.Template
|
|
}
|
|
|
|
// AlertNotice captures the context for an alert to be sent
|
|
type AlertNotice struct {
|
|
AlertCount int16
|
|
FailureCount int16
|
|
IsUp bool
|
|
LastSuccess time.Time
|
|
MonitorName string
|
|
LastCheckOutput string
|
|
}
|
|
|
|
// IsValid returns a boolean indicating if the Alert has been correctly
|
|
// configured
|
|
func (alert Alert) IsValid() bool {
|
|
return !alert.Command.Empty()
|
|
}
|
|
|
|
// BuildTemplates compiles command templates for the Alert
|
|
func (alert *Alert) BuildTemplates() error {
|
|
// TODO: Remove legacy template support later after 1.0
|
|
legacy := strings.NewReplacer(
|
|
"{alert_count}", "{{.AlertCount}}",
|
|
"{alert_message}", "{{.MonitorName}} check has failed {{.FailureCount}} times",
|
|
"{failure_count}", "{{.FailureCount}}",
|
|
"{last_output}", "{{.LastCheckOutput}}",
|
|
"{last_success}", "{{.LastSuccess}}",
|
|
"{monitor_name}", "{{.MonitorName}}",
|
|
)
|
|
|
|
slog.Debugf("Building template for alert %s", alert.Name)
|
|
|
|
// Time format func factory
|
|
tff := func(formatString string) func(time.Time) string {
|
|
return func(t time.Time) string {
|
|
return t.Format(formatString)
|
|
}
|
|
}
|
|
|
|
// Create some functions for formatting datetimes in popular formats
|
|
timeFormatFuncs := template.FuncMap{
|
|
"ANSIC": tff(time.ANSIC),
|
|
"UnixDate": tff(time.UnixDate),
|
|
"RubyDate": tff(time.RubyDate),
|
|
"RFC822Z": tff(time.RFC822Z),
|
|
"RFC850": tff(time.RFC850),
|
|
"RFC1123": tff(time.RFC1123),
|
|
"RFC1123Z": tff(time.RFC1123Z),
|
|
"RFC3339": tff(time.RFC3339),
|
|
"RFC3339Nano": tff(time.RFC3339Nano),
|
|
"FormatTime": func(t time.Time, timeFormat string) string {
|
|
return t.Format(timeFormat)
|
|
},
|
|
"InTZ": func(t time.Time, tzName string) (time.Time, error) {
|
|
tz, err := time.LoadLocation(tzName)
|
|
if err != nil {
|
|
return t, fmt.Errorf("failed to convert time to specified tz: %w", err)
|
|
}
|
|
|
|
return t.In(tz), nil
|
|
},
|
|
}
|
|
|
|
switch {
|
|
case alert.commandTemplate == nil && alert.Command.Command != nil:
|
|
alert.commandTemplate = []*template.Template{}
|
|
for i, cmdPart := range alert.Command.Command {
|
|
if PyCompat {
|
|
cmdPart = legacy.Replace(cmdPart)
|
|
}
|
|
|
|
alert.commandTemplate = append(alert.commandTemplate, template.Must(
|
|
template.New(alert.Name+fmt.Sprint(i)).Funcs(timeFormatFuncs).Parse(cmdPart),
|
|
))
|
|
}
|
|
case alert.commandShellTemplate == nil && alert.Command.ShellCommand != "":
|
|
shellCmd := alert.Command.ShellCommand
|
|
|
|
if PyCompat {
|
|
shellCmd = legacy.Replace(shellCmd)
|
|
}
|
|
|
|
alert.commandShellTemplate = template.Must(
|
|
template.New(alert.Name).Funcs(timeFormatFuncs).Parse(shellCmd),
|
|
)
|
|
default:
|
|
return fmt.Errorf("No template provided for alert %s: %w", alert.Name, errNoTemplate)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Send will send an alert notice by executing the command template
|
|
func (alert Alert) Send(notice AlertNotice) (outputStr string, err error) {
|
|
slog.Infof("Sending alert %s for %s", alert.Name, notice.MonitorName)
|
|
|
|
var cmd *exec.Cmd
|
|
|
|
switch {
|
|
case alert.commandTemplate != nil:
|
|
command := []string{}
|
|
|
|
for _, cmdTmp := range alert.commandTemplate {
|
|
var commandBuffer bytes.Buffer
|
|
|
|
err = cmdTmp.Execute(&commandBuffer, notice)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
command = append(command, commandBuffer.String())
|
|
}
|
|
|
|
cmd = exec.Command(command[0], command[1:]...)
|
|
case alert.commandShellTemplate != nil:
|
|
var commandBuffer bytes.Buffer
|
|
|
|
err = alert.commandShellTemplate.Execute(&commandBuffer, notice)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
shellCommand := commandBuffer.String()
|
|
|
|
cmd = ShellCommand(shellCommand)
|
|
default:
|
|
err = fmt.Errorf("No templates compiled for alert %s: %w", alert.Name, errNoTemplate)
|
|
|
|
return
|
|
}
|
|
|
|
// Exit if we're not ready to run the command
|
|
if cmd == nil || err != nil {
|
|
return
|
|
}
|
|
|
|
var output []byte
|
|
output, err = cmd.CombinedOutput()
|
|
outputStr = string(output)
|
|
slog.Debugf("Alert output for: %s\n---\n%s\n---", alert.Name, outputStr)
|
|
|
|
if err != nil {
|
|
err = fmt.Errorf(
|
|
"Alert %s failed to send. Returned %w: %w",
|
|
alert.Name,
|
|
err,
|
|
ErrAlertFailed,
|
|
)
|
|
}
|
|
|
|
return outputStr, err
|
|
}
|
|
|
|
// NewLogAlert creates an alert that does basic logging using echo
|
|
func NewLogAlert() *Alert {
|
|
return &Alert{
|
|
Name: "log",
|
|
Command: CommandOrShell{
|
|
Command: []string{
|
|
"echo",
|
|
"{{.MonitorName}} {{if .IsUp}}has recovered{{else}}check has failed {{.FailureCount}} times{{end}}",
|
|
},
|
|
},
|
|
}
|
|
}
|