Compare commits
2 Commits
master
...
single-com
Author | SHA1 | Date | |
---|---|---|---|
0a0f6fe7c9 | |||
d4e2cb7b9f |
32
README.md
32
README.md
@ -8,29 +8,8 @@ Initial target is meant to be roughly compatible requiring only minor changes to
|
||||
|
||||
## Differences from Python version
|
||||
|
||||
There are a few key differences between the Python version and the v0.x Go version.
|
||||
|
||||
First, configuration keys cannot have multiple types in Go, so a different key must be used when specifying a Shell command as a string rather than a list of args. Instead of `command`, you must use `command_shell`. Eg:
|
||||
|
||||
minitor-py:
|
||||
```yaml
|
||||
monitors:
|
||||
- name: Exec command
|
||||
command: ['echo', 'test']
|
||||
- name: Shell command
|
||||
command: echo 'test'
|
||||
```
|
||||
|
||||
minitor-go:
|
||||
```yaml
|
||||
monitors:
|
||||
- name: Exec command
|
||||
command: ['echo', 'test']
|
||||
- name: Shell command
|
||||
command_shell: echo 'test'
|
||||
```
|
||||
|
||||
Second, templating for Alert messages has been updated. In the Python version, `str.format(...)` was used with certain keys passed in that could be used to format messages. In the Go version, we use a struct, `AlertNotice` defined in `alert.go` and the built in Go templating format. Eg.
|
||||
Templating for Alert messages has been updated. In the Python version, `str.format(...)` was used with certain keys passed in that could be used to format messages. In the Go version, we use a struct, `AlertNotice` defined in `alert.go` and the built in Go templating format. Eg.
|
||||
|
||||
minitor-py:
|
||||
```yaml
|
||||
@ -38,7 +17,7 @@ alerts:
|
||||
log_command:
|
||||
command: ['echo', '{monitor_name}']
|
||||
log_shell:
|
||||
command_shell: 'echo {monitor_name}'
|
||||
command: 'echo {monitor_name}'
|
||||
```
|
||||
|
||||
minitor-go:
|
||||
@ -47,7 +26,7 @@ alerts:
|
||||
log_command:
|
||||
command: ['echo', '{{.MonitorName}}']
|
||||
log_shell:
|
||||
command_shell: 'echo {{.MonitorName}}'
|
||||
command: 'echo {{.MonitorName}}'
|
||||
```
|
||||
|
||||
Finally, newlines in a shell command don't terminate a particular command. Semicolons must be used and continuations should not.
|
||||
@ -56,7 +35,7 @@ minitor-py:
|
||||
```yaml
|
||||
alerts:
|
||||
log_shell:
|
||||
command_shell: >
|
||||
command: >
|
||||
echo "line 1"
|
||||
echo "line 2"
|
||||
echo "continued" \
|
||||
@ -67,7 +46,7 @@ minitor-go:
|
||||
```yaml
|
||||
alerts:
|
||||
log_shell:
|
||||
command_shell: >
|
||||
command: >
|
||||
echo "line 1";
|
||||
echo "line 2";
|
||||
echo "continued"
|
||||
@ -87,6 +66,7 @@ Pairity:
|
||||
- [x] Implement Prometheus client to export metrics
|
||||
- [x] Test coverage
|
||||
- [x] Integration testing (manual or otherwise)
|
||||
- [x] Allow commands and shell commands in the same config key
|
||||
|
||||
Improvement (potentially breaking):
|
||||
|
||||
|
15
alert.go
15
alert.go
@ -12,8 +12,7 @@ import (
|
||||
// Alert is a config driven mechanism for sending a notice
|
||||
type Alert struct {
|
||||
Name string
|
||||
Command []string
|
||||
CommandShell string `yaml:"command_shell"`
|
||||
Command CommandOrShell
|
||||
commandTemplate []*template.Template
|
||||
commandShellTemplate *template.Template
|
||||
}
|
||||
@ -31,9 +30,7 @@ type AlertNotice struct {
|
||||
// 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
|
||||
return !alert.Command.Empty()
|
||||
}
|
||||
|
||||
// BuildTemplates compiles command templates for the Alert
|
||||
@ -41,16 +38,16 @@ func (alert *Alert) BuildTemplates() error {
|
||||
if LogDebug {
|
||||
log.Printf("DEBUG: Building template for alert %s", alert.Name)
|
||||
}
|
||||
if alert.commandTemplate == nil && alert.Command != nil {
|
||||
if alert.commandTemplate == nil && alert.Command.Command != nil {
|
||||
alert.commandTemplate = []*template.Template{}
|
||||
for i, cmdPart := range alert.Command {
|
||||
for i, cmdPart := range alert.Command.Command {
|
||||
alert.commandTemplate = append(alert.commandTemplate, template.Must(
|
||||
template.New(alert.Name+string(i)).Parse(cmdPart),
|
||||
))
|
||||
}
|
||||
} else if alert.commandShellTemplate == nil && alert.CommandShell != "" {
|
||||
} else if alert.commandShellTemplate == nil && alert.Command.ShellCommand != "" {
|
||||
alert.commandShellTemplate = template.Must(
|
||||
template.New(alert.Name).Parse(alert.CommandShell),
|
||||
template.New(alert.Name).Parse(alert.Command.ShellCommand),
|
||||
)
|
||||
} else {
|
||||
return fmt.Errorf("No template provided for alert %s", alert.Name)
|
||||
|
@ -11,14 +11,9 @@ func TestAlertIsValid(t *testing.T) {
|
||||
expected bool
|
||||
name string
|
||||
}{
|
||||
{Alert{Command: []string{"echo", "test"}}, true, "Command only"},
|
||||
{Alert{CommandShell: "echo test"}, true, "CommandShell only"},
|
||||
{Alert{Command: CommandOrShell{Command: []string{"echo", "test"}}}, true, "Command only"},
|
||||
{Alert{Command: CommandOrShell{ShellCommand: "echo test"}}, true, "CommandShell only"},
|
||||
{Alert{}, false, "No commands"},
|
||||
{
|
||||
Alert{Command: []string{"echo", "test"}, CommandShell: "echo test"},
|
||||
false,
|
||||
"Both commands",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@ -41,28 +36,28 @@ func TestAlertSend(t *testing.T) {
|
||||
name string
|
||||
}{
|
||||
{
|
||||
Alert{Command: []string{"echo", "{{.MonitorName}}"}},
|
||||
Alert{Command: CommandOrShell{Command: []string{"echo", "{{.MonitorName}}"}}},
|
||||
AlertNotice{MonitorName: "test"},
|
||||
"test\n",
|
||||
false,
|
||||
"Command with template",
|
||||
},
|
||||
{
|
||||
Alert{CommandShell: "echo {{.MonitorName}}"},
|
||||
Alert{Command: CommandOrShell{ShellCommand: "echo {{.MonitorName}}"}},
|
||||
AlertNotice{MonitorName: "test"},
|
||||
"test\n",
|
||||
false,
|
||||
"Command shell with template",
|
||||
},
|
||||
{
|
||||
Alert{Command: []string{"echo", "{{.Bad}}"}},
|
||||
Alert{Command: CommandOrShell{Command: []string{"echo", "{{.Bad}}"}}},
|
||||
AlertNotice{MonitorName: "test"},
|
||||
"",
|
||||
true,
|
||||
"Command with bad template",
|
||||
},
|
||||
{
|
||||
Alert{CommandShell: "echo {{.Bad}}"},
|
||||
Alert{Command: CommandOrShell{ShellCommand: "echo {{.Bad}}"}},
|
||||
AlertNotice{MonitorName: "test"},
|
||||
"",
|
||||
true,
|
||||
@ -103,8 +98,8 @@ func TestAlertBuildTemplate(t *testing.T) {
|
||||
expectErr bool
|
||||
name string
|
||||
}{
|
||||
{Alert{Command: []string{"echo", "test"}}, false, "Command only"},
|
||||
{Alert{CommandShell: "echo test"}, false, "CommandShell only"},
|
||||
{Alert{Command: CommandOrShell{Command: []string{"echo", "test"}}}, false, "Command only"},
|
||||
{Alert{Command: CommandOrShell{ShellCommand: "echo test"}}, false, "CommandShell only"},
|
||||
{Alert{}, true, "No commands"},
|
||||
}
|
||||
|
||||
|
31
config.go
31
config.go
@ -15,6 +15,37 @@ type Config struct {
|
||||
Alerts map[string]*Alert
|
||||
}
|
||||
|
||||
// CommandOrShell type wraps a string or list of strings
|
||||
// for executing a command directly or in a shell
|
||||
type CommandOrShell struct {
|
||||
ShellCommand string
|
||||
Command []string
|
||||
}
|
||||
|
||||
// Empty checks if the Command has a value
|
||||
func (cos CommandOrShell) Empty() bool {
|
||||
return (cos.ShellCommand == "" && cos.Command == nil)
|
||||
}
|
||||
|
||||
// UnmarshalYAML allows unmarshalling either a string or slice of strings
|
||||
// and parsing them as either a command or a shell command.
|
||||
func (cos *CommandOrShell) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var cmd []string
|
||||
err := unmarshal(&cmd)
|
||||
// Error indicates this is shell command
|
||||
if err != nil {
|
||||
var shellCmd string
|
||||
err := unmarshal(&shellCmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cos.ShellCommand = shellCmd
|
||||
} else {
|
||||
cos.Command = cmd
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValid checks config validity and returns true if valid
|
||||
func (config Config) IsValid() (isValid bool) {
|
||||
isValid = true
|
||||
|
@ -42,7 +42,7 @@ func TestMultiLineConfig(t *testing.T) {
|
||||
log.Println("-----")
|
||||
log.Println("TestMultiLineConfig(parse > string)")
|
||||
expected := "echo 'Some string with stuff'; echo \"<angle brackets>\"; exit 1\n"
|
||||
actual := config.Monitors[0].CommandShell
|
||||
actual := config.Monitors[0].Command.ShellCommand
|
||||
if expected != actual {
|
||||
t.Errorf("TestMultiLineConfig(>) failed")
|
||||
t.Logf("string expected=`%v`", expected)
|
||||
@ -70,7 +70,7 @@ func TestMultiLineConfig(t *testing.T) {
|
||||
log.Println("-----")
|
||||
log.Println("TestMultiLineConfig(parse | string)")
|
||||
expected = "echo 'Some string with stuff'\necho '<angle brackets>'\n"
|
||||
actual = config.Alerts["log_shell"].CommandShell
|
||||
actual = config.Alerts["log_shell"].Command.ShellCommand
|
||||
if expected != actual {
|
||||
t.Errorf("TestMultiLineConfig(|) failed")
|
||||
t.Logf("string expected=`%v`", expected)
|
||||
|
18
main_test.go
18
main_test.go
@ -18,7 +18,7 @@ func TestCheckMonitors(t *testing.T) {
|
||||
Monitors: []*Monitor{
|
||||
&Monitor{
|
||||
Name: "Success",
|
||||
Command: []string{"true"},
|
||||
Command: CommandOrShell{Command: []string{"true"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -30,12 +30,12 @@ func TestCheckMonitors(t *testing.T) {
|
||||
Monitors: []*Monitor{
|
||||
&Monitor{
|
||||
Name: "Failure",
|
||||
Command: []string{"false"},
|
||||
Command: CommandOrShell{Command: []string{"false"}},
|
||||
AlertAfter: 1,
|
||||
},
|
||||
&Monitor{
|
||||
Name: "Failure",
|
||||
Command: []string{"false"},
|
||||
Command: CommandOrShell{Command: []string{"false"}},
|
||||
AlertDown: []string{"unknown"},
|
||||
AlertAfter: 1,
|
||||
},
|
||||
@ -49,12 +49,12 @@ func TestCheckMonitors(t *testing.T) {
|
||||
Monitors: []*Monitor{
|
||||
&Monitor{
|
||||
Name: "Success",
|
||||
Command: []string{"ls"},
|
||||
Command: CommandOrShell{Command: []string{"ls"}},
|
||||
alertCount: 1,
|
||||
},
|
||||
&Monitor{
|
||||
Name: "Success",
|
||||
Command: []string{"true"},
|
||||
Command: CommandOrShell{Command: []string{"true"}},
|
||||
AlertUp: []string{"unknown"},
|
||||
alertCount: 1,
|
||||
},
|
||||
@ -68,14 +68,14 @@ func TestCheckMonitors(t *testing.T) {
|
||||
Monitors: []*Monitor{
|
||||
&Monitor{
|
||||
Name: "Failure",
|
||||
Command: []string{"false"},
|
||||
Command: CommandOrShell{Command: []string{"false"}},
|
||||
AlertDown: []string{"good"},
|
||||
AlertAfter: 1,
|
||||
},
|
||||
},
|
||||
Alerts: map[string]*Alert{
|
||||
"good": &Alert{
|
||||
Command: []string{"true"},
|
||||
Command: CommandOrShell{Command: []string{"true"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -87,7 +87,7 @@ func TestCheckMonitors(t *testing.T) {
|
||||
Monitors: []*Monitor{
|
||||
&Monitor{
|
||||
Name: "Failure",
|
||||
Command: []string{"false"},
|
||||
Command: CommandOrShell{Command: []string{"false"}},
|
||||
AlertDown: []string{"bad"},
|
||||
AlertAfter: 1,
|
||||
},
|
||||
@ -95,7 +95,7 @@ func TestCheckMonitors(t *testing.T) {
|
||||
Alerts: map[string]*Alert{
|
||||
"bad": &Alert{
|
||||
Name: "bad",
|
||||
Command: []string{"false"},
|
||||
Command: CommandOrShell{Command: []string{"false"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
14
monitor.go
14
monitor.go
@ -11,8 +11,7 @@ import (
|
||||
type Monitor struct {
|
||||
// Config values
|
||||
Name string
|
||||
Command []string
|
||||
CommandShell string `yaml:"command_shell"`
|
||||
Command CommandOrShell
|
||||
AlertDown []string `yaml:"alert_down"`
|
||||
AlertUp []string `yaml:"alert_up"`
|
||||
CheckInterval float64 `yaml:"check_interval"`
|
||||
@ -29,10 +28,7 @@ type Monitor struct {
|
||||
// IsValid returns a boolean indicating if the Monitor has been correctly
|
||||
// configured
|
||||
func (monitor Monitor) IsValid() bool {
|
||||
atLeastOneCommand := (monitor.CommandShell != "" || monitor.Command != nil)
|
||||
atMostOneCommand := (monitor.CommandShell == "" || monitor.Command == nil)
|
||||
return (atLeastOneCommand &&
|
||||
atMostOneCommand &&
|
||||
return (!monitor.Command.Empty() &&
|
||||
monitor.getAlertAfter() > 0 &&
|
||||
monitor.AlertDown != nil)
|
||||
}
|
||||
@ -52,10 +48,10 @@ func (monitor Monitor) ShouldCheck() bool {
|
||||
// and a possible AlertNotice
|
||||
func (monitor *Monitor) Check() (bool, *AlertNotice) {
|
||||
var cmd *exec.Cmd
|
||||
if monitor.Command != nil {
|
||||
cmd = exec.Command(monitor.Command[0], monitor.Command[1:]...)
|
||||
if monitor.Command.Command != nil {
|
||||
cmd = exec.Command(monitor.Command.Command[0], monitor.Command.Command[1:]...)
|
||||
} else {
|
||||
cmd = ShellCommand(monitor.CommandShell)
|
||||
cmd = ShellCommand(monitor.Command.ShellCommand)
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
@ -13,16 +13,11 @@ func TestMonitorIsValid(t *testing.T) {
|
||||
expected bool
|
||||
name string
|
||||
}{
|
||||
{Monitor{Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, true, "Command only"},
|
||||
{Monitor{CommandShell: "echo test", AlertDown: []string{"log"}}, true, "CommandShell only"},
|
||||
{Monitor{Command: []string{"echo", "test"}}, false, "No AlertDown"},
|
||||
{Monitor{Command: CommandOrShell{Command: []string{"echo", "test"}}, AlertDown: []string{"log"}}, true, "Command only"},
|
||||
{Monitor{Command: CommandOrShell{ShellCommand: "echo test"}, AlertDown: []string{"log"}}, true, "CommandShell only"},
|
||||
{Monitor{Command: CommandOrShell{Command: []string{"echo", "test"}}}, false, "No AlertDown"},
|
||||
{Monitor{AlertDown: []string{"log"}}, false, "No commands"},
|
||||
{
|
||||
Monitor{Command: []string{"echo", "test"}, CommandShell: "echo test", AlertDown: []string{"log"}},
|
||||
false,
|
||||
"Both commands",
|
||||
},
|
||||
{Monitor{Command: []string{"echo", "test"}, AlertDown: []string{"log"}, AlertAfter: -1}, false, "Invalid alert threshold, -1"},
|
||||
{Monitor{Command: CommandOrShell{Command: []string{"echo", "test"}}, AlertDown: []string{"log"}, AlertAfter: -1}, false, "Invalid alert threshold, -1"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@ -254,22 +249,22 @@ func TestMonitorCheck(t *testing.T) {
|
||||
name string
|
||||
}{
|
||||
{
|
||||
Monitor{Command: []string{"echo", "success"}},
|
||||
Monitor{Command: CommandOrShell{Command: []string{"echo", "success"}}},
|
||||
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
|
||||
"Test successful command",
|
||||
},
|
||||
{
|
||||
Monitor{CommandShell: "echo success"},
|
||||
Monitor{Command: CommandOrShell{ShellCommand: "echo success"}},
|
||||
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
|
||||
"Test successful command shell",
|
||||
},
|
||||
{
|
||||
Monitor{Command: []string{"total", "failure"}},
|
||||
Monitor{Command: CommandOrShell{Command: []string{"total", "failure"}}},
|
||||
expected{isSuccess: false, hasNotice: true, lastOutput: ""},
|
||||
"Test failed command",
|
||||
},
|
||||
{
|
||||
Monitor{CommandShell: "false"},
|
||||
Monitor{Command: CommandOrShell{ShellCommand: "false"}},
|
||||
expected{isSuccess: false, hasNotice: true, lastOutput: ""},
|
||||
"Test failed command shell",
|
||||
},
|
||||
|
@ -25,7 +25,7 @@ alerts:
|
||||
email_up:
|
||||
command: [sendmail, "me@minitor.mon", "Recovered: {monitor_name}", "We're back!"]
|
||||
mailgun_down:
|
||||
command_shell: >
|
||||
command: >
|
||||
curl -s -X POST
|
||||
-F subject="Alert! {{.MonitorName}} failed"
|
||||
-F from="Minitor <minitor@minitor.mon>"
|
||||
@ -34,7 +34,7 @@ alerts:
|
||||
https://api.mailgun.net/v3/minitor.mon/messages
|
||||
-u "api:${MAILGUN_API_KEY}"
|
||||
sms_down:
|
||||
command_shell: >
|
||||
command: >
|
||||
curl -s -X POST -F "Body=Failure! {{.MonitorName}} has failed"
|
||||
-F "From=${AVAILABLE_NUMBER}" -F "To=${MY_PHONE}"
|
||||
"https://api.twilio.com/2010-04-01/Accounts/${ACCOUNT_SID}/Messages"
|
||||
|
@ -7,7 +7,7 @@ monitors:
|
||||
alert_down: ['log_command', 'log_shell']
|
||||
alert_every: 0
|
||||
- name: Shell
|
||||
command_shell: >
|
||||
command: >
|
||||
echo 'Some string with stuff';
|
||||
echo 'another line';
|
||||
echo $PATH;
|
||||
@ -20,4 +20,4 @@ alerts:
|
||||
log_command:
|
||||
command: ['echo', 'regular', '"command!!!"', "{{.MonitorName}}"]
|
||||
log_shell:
|
||||
command_shell: echo "Failure on {{.MonitorName}} User is $USER"
|
||||
command: echo "Failure on {{.MonitorName}} User is $USER"
|
||||
|
@ -3,7 +3,7 @@ check_interval: 1
|
||||
|
||||
monitors:
|
||||
- name: Shell
|
||||
command_shell: >
|
||||
command: >
|
||||
echo 'Some string with stuff';
|
||||
echo "<angle brackets>";
|
||||
exit 1
|
||||
@ -13,6 +13,6 @@ monitors:
|
||||
|
||||
alerts:
|
||||
log_shell:
|
||||
command_shell: |
|
||||
command: |
|
||||
echo 'Some string with stuff'
|
||||
echo '<angle brackets>'
|
||||
|
Loading…
Reference in New Issue
Block a user