Compare commits

..

1 Commits

Author SHA1 Message Date
Ian Fijolek
f1451166e6 WIP: Update logging to improve formatting a bit 2019-11-22 15:07:18 -08:00
21 changed files with 240 additions and 600 deletions
+3 -58
View File
@@ -45,70 +45,15 @@ trigger:
- refs/tags/v* - refs/tags/v*
steps: steps:
- name: build all binaries
image: golang:1.12
commands:
- make all
- name: push image - arm # Might consider moving this step into the previous pipeline
- name: push image
image: plugins/docker image: plugins/docker
settings: settings:
repo: iamthefij/minitor-go repo: iamthefij/minitor-go
dockerfile: Dockerfile.multi-stage
auto_tag: true auto_tag: true
auto_tag_suffix: linux-arm
username: username:
from_secret: docker_username from_secret: docker_username
password: password:
from_secret: docker_password from_secret: docker_password
build_args:
- ARCH=arm
- REPO=arm32v7
- name: push image - arm64
image: plugins/docker
settings:
repo: iamthefij/minitor-go
auto_tag: true
auto_tag_suffix: linux-arm64
username:
from_secret: docker_username
password:
from_secret: docker_password
build_args:
- ARCH=arm64
- REPO=arm64v8
- name: push image - amd64
image: plugins/docker
settings:
repo: iamthefij/minitor-go
auto_tag: true
auto_tag_suffix: linux-amd64
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: publish manifest
image: plugins/manifest
settings:
spec: manifest.tmpl
auto_tag: true
ignore_missing: true
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: notify
image: drillster/drone-email
settings:
host:
from_secret: SMTP_HOST
username:
from_secret: SMTP_USER
password:
from_secret: SMTP_PASS
from: drone@iamthefij.com
when:
status: [changed, failure]
Vendored
+1 -3
View File
@@ -16,6 +16,4 @@
config.yml config.yml
# Output binary # Output binary
minitor minitor-go
minitor-linux-*
minitor-darwin-amd64
+4 -9
View File
@@ -1,13 +1,12 @@
ARG REPO=library ARG REPO=library
FROM multiarch/qemu-user-static:4.2.0-2 as qemu-user-static
FROM ${REPO}/alpine:3.10 FROM ${REPO}/alpine:3.10
# Copying all qemu files because amd64 doesn't exist and cannot condional copy
COPY --from=qemu-user-static /usr/bin/qemu-* /usr/bin/
RUN mkdir /app RUN mkdir /app
WORKDIR /app/ WORKDIR /app/
# Copy minitor in
ARG ARCH=amd64
COPY ./minitor-go ./minitor
# Add common checking tools # Add common checking tools
RUN apk --no-cache add bash=~5.0 curl=~7.66 jq=~1.6 RUN apk --no-cache add bash=~5.0 curl=~7.66 jq=~1.6
@@ -18,10 +17,6 @@ RUN addgroup -S minitor && adduser -S minitor -G minitor
COPY ./scripts /app/scripts COPY ./scripts /app/scripts
RUN chmod -R 755 /app/scripts RUN chmod -R 755 /app/scripts
# Copy minitor in
ARG ARCH=amd64
COPY ./minitor-linux-${ARCH} ./minitor
# Drop to non-root user # Drop to non-root user
USER minitor USER minitor
+11 -59
View File
@@ -1,28 +1,23 @@
DOCKER_TAG ?= minitor-go-${USER}
GIT_TAG_NAME := $(shell git tag -l --contains HEAD)
GIT_SHA := $(shell git rev-parse HEAD)
VERSION := $(if $(GIT_TAG_NAME),$(GIT_TAG_NAME),$(GIT_SHA))
.PHONY: all .PHONY: all
all: minitor-linux-amd64 minitor-linux-arm minitor-linux-arm64 DOCKER_TAG ?= minitor-go-${USER}
.PHONY: default .PHONY: default
default: test default: test
.PHONY: build .PHONY: build
build: minitor build:
go build
minitor: minitor-go:
@echo Version: $(VERSION) go build
go build -ldflags '-X "main.version=${VERSION}"' -o minitor
.PHONY: run .PHONY: run
run: minitor build run: minitor-go build
./minitor -debug ./minitor-go -debug
.PHONY: run-metrics .PHONY: run-metrics
run-metrics: minitor build run-metrics: minitor-go build
./minitor -debug -metrics ./minitor-go -debug -metrics
.PHONY: test .PHONY: test
test: test:
@@ -46,56 +41,13 @@ check:
.PHONY: clean .PHONY: clean
clean: clean:
rm -f ./minitor rm -f ./minitor-go
rm -f ./minitor-linux-*
rm -f ./minitor-darwin-amd64
rm -f ./coverage.out rm -f ./coverage.out
.PHONY: docker-build .PHONY: docker-build
docker-build: docker-build:
docker build -f ./Dockerfile.multi-stage -t $(DOCKER_TAG)-linux-amd64 . docker build -f ./Dockerfile.multi-stage -t $(DOCKER_TAG) .
.PHONY: docker-run .PHONY: docker-run
docker-run: docker-build docker-run: docker-build
docker run --rm -v $(shell pwd)/config.yml:/root/config.yml $(DOCKER_TAG) docker run --rm -v $(shell pwd)/config.yml:/root/config.yml $(DOCKER_TAG)
## Multi-arch targets
# Arch specific go build targets
minitor-darwin-amd64:
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 \
go build -ldflags '-X "main.version=${VERSION}"' -a -installsuffix nocgo \
-o minitor-darwin-amd64
minitor-linux-amd64:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
go build -ldflags '-X "main.version=${VERSION}"' -a -installsuffix nocgo \
-o minitor-linux-amd64
minitor-linux-arm:
GOOS=linux GOARCH=arm CGO_ENABLED=0 \
go build -ldflags '-X "main.version=${VERSION}"' -a -installsuffix nocgo \
-o minitor-linux-arm
minitor-linux-arm64:
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 \
go build -ldflags '-X "main.version=${VERSION}"' -a -installsuffix nocgo \
-o minitor-linux-arm64
# Arch specific docker build targets
.PHONY: docker-build-arm
docker-build-arm: minitor-linux-arm
docker build --build-arg REPO=arm32v7 --build-arg ARCH=arm . -t ${DOCKER_TAG}-linux-arm
.PHONY: docker-build-arm
docker-build-arm64: minitor-linux-arm64
docker build --build-arg REPO=arm64v8 --build-arg ARCH=arm64 . -t ${DOCKER_TAG}-linux-arm64
# Cross run on host architechture
.PHONY: docker-run-arm
docker-run-arm: docker-build-arm
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --name $(DOCKER_TAG)-run ${DOCKER_TAG}-linux-arm
.PHONY: docker-run-arm64
docker-run-arm64: docker-build-arm64
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --name $(DOCKER_TAG)-run ${DOCKER_TAG}-linux-arm64
+74 -131
View File
@@ -1,156 +1,99 @@
# [minitor-go](https://git.iamthefij.com/iamthefij/minitor-go) # minitor-go
A minimal monitoring system A reimplementation of [Minitor](https://git.iamthefij/iamthefij/minitor) in Go
## What does it do?
Minitor accepts a YAML configuration file with a set of commands to run and a set of alerts to execute when those commands fail. It is designed to be as simple as possible and relies on other command line tools to do checks and issue alerts.
## But why?
I'm running a few small services and found Sensu, Consul, Nagios, etc. to all be far too complicated for my usecase.
## So how do I use it?
### Running
Install and execute with:
```bash
go get github.com/iamthefij/minitor-go
minitor
```
If locally developing you can use:
```bash
make run
```
It will read the contents of `config.yml` and begin its loop. You could also run it directly and provide a new config file via the `-config` argument.
#### Docker
You can pull this repository directly from Docker:
```bash
docker pull iamthefij/minitor-go:latest
```
The Docker image uses a default `config.yml` that is copied from `sample-config.yml`. This won't really do anything for you, so when you run the Docker image, you should supply your own `config.yml` file:
```bash
docker run -v $PWD/config.yml:/app/config.yml iamthefij/minitor-go:latest
```
Images are provided for `amd64`, `arm`, and `arm64` architechtures.
## Configuring
In this repo, you can explore the `sample-config.yml` file for an example, but the general structure is as follows. It should be noted that environment variable interpolation happens on load of the YAML file.
The global configurations are:
|key|value|
|---|---|
|`check_interval`|Maximum frequency to run checks for each monitor|
|`monitors`|List of all monitors. Detailed description below|
|`alerts`|List of all alerts. Detailed description below|
### Monitors
All monitors should be listed under `monitors`.
Each monitor allows the following configuration:
|key|value|
|---|---|
|`name`|Name of the monitor running. This will show up in messages and logs.|
|`command`|Specifies the command that should be executed, either in exec or shell form. This command's exit value will determine whether the check is successful|
|`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|
|`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_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 exist as objects keyed under `alerts`. Their key should be the name of the Alert. This is used in your monitor setup in `alert_down` and `alert_up`.
Eachy alert allows the following configuration:
|key|value|
|---|---|
|`command`|Specifies the command that should be executed, either in exec or shell form. This is the command that will be run when the alert is executed. This can be templated with environment variables or the variables shown in the table below|
Also, when alerts are executed, they will be passed through Go's format function with arguments for some attributes of the Monitor. The following monitor specific variables can be referenced using Go formatting syntax:
|token|value|
|---|---|
|`{{.AlertCount}}`|Number of times this monitor has alerted|
|`{{.FailureCount}}`|The total number of sequential failed checks for this monitor|
|`{{.LastCheckOutput}}`|The last returned value from the check command to either stderr or stdout|
|`{{.LastSuccess}}`|The ISO datetime of the last successful check|
|`{{.MonitorName}}`|The name of the monitor that failed and triggered the alert|
### Metrics
Minitor supports exporting metrics for [Prometheus](https://prometheus.io/). Prometheus is an open source tool for reading and querying metrics from different sources. Combined with another tool, [Grafana](https://grafana.com/), it allows building of charts and dashboards. You could also opt to just use Minitor to log check results, and instead do your alerting with Grafana.
It is also possible to use the metrics endpoint for monitoring Minitor itself! This allows setting up multiple instances of Minitor on different servers and have them monitor each-other so that you can detect a minitor outage.
To run minitor with metrics, use the `-metrics` flag. The metrics will be served on port `8080` by default, though it can be overriden using `-metrics-port`. They will be accessible on the path `/metrics`. Eg. `localhost:8080/metrics`.
```bash
minitor -metrics
# or
minitor -metrics -metrics-port 3000
```
## Contributing
Whether you're looking to submit a patch or just tell me I broke something, you can contribute through the Github mirror and I can merge PRs back to the source repository.
Primary Repo: https://git.iamthefij.com/iamthefij/minitor.git
Github Mirror: https://github.com/IamTheFij/minitor.git
## Original Minitor
This is a reimplementation of [Minitor](https://git.iamthefij.com/iamthefij/minitor) in Go
Minitor is already a minimal monitoring tool. Python 3 was a quick way to get something live, but Python itself comes with a large footprint. Thus Go feels like a better fit for the project, longer term. Minitor is already a minimal monitoring tool. Python 3 was a quick way to get something live, but Python itself comes with a large footprint. Thus Go feels like a better fit for the project, longer term.
Initial target is meant to be roughly compatible requiring only minor changes to configuration. Future iterations may diverge to take advantage of Go specific features. Initial target is meant to be roughly compatible requiring only minor changes to configuration. Future iterations may diverge to take advantage of Go specific features.
### Differences from Python version ## Differences from Python version
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. 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.
minitor-py: minitor-py:
```yaml ```yaml
alerts: alerts:
log: log_command:
command: 'echo {monitor_name}' command: ['echo', '{monitor_name}']
log_shell:
command_shell: 'echo {monitor_name}'
``` ```
minitor-go: minitor-go:
```yaml ```yaml
alerts: alerts:
log: log_command:
command: 'echo {{.MonitorName}}' command: ['echo', '{{.MonitorName}}']
log_shell:
command_shell: 'echo {{.MonitorName}}'
``` ```
For the time being, legacy configs for the Python version of Minitor should be compatible if you apply the `-py-compat` flag when running Minitor. Eventually, this flag will go away when later breaking changes are introduced. Finally, newlines in a shell command don't terminate a particular command. Semicolons must be used and continuations should not.
## Future minitor-py:
```yaml
alerts:
log_shell:
command_shell: >
echo "line 1"
echo "line 2"
echo "continued" \
"line"
```
Future, potentially breaking changes minitor-go:
```yaml
alerts:
log_shell:
command_shell: >
echo "line 1";
echo "line 2";
echo "continued"
"line"
```
## To do
There are two sets of task lists. The first is to get rough parity on key features with the Python version. The second is to make some improvements to the framework.
Pairity:
- [x] Run monitor commands
- [x] Run monitor commands in a shell
- [x] Run alert commands
- [x] Run alert commands in a shell
- [x] Allow templating of alert commands
- [x] Implement Prometheus client to export metrics
- [x] Test coverage
- [ ] Integration testing (manual or otherwise)
Improvement (potentially breaking):
- [ ] Implement leveled logging (maybe glog or logrus) - [ ] Implement leveled logging (maybe glog or logrus)
- [ ] Consider switching from YAML to TOML
- [ ] Consider value of templating vs injecting values into Env variables - [ ] 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 - [ ] Async checking
- [ ] Use durations rather than seconds checked in event loop
- [ ] Revisit metrics and see if they all make sense - [ ] Revisit metrics and see if they all make sense
- [ ] Consider dropping `alert_up` and `alert_down` in favor of using Go templates that offer more control of messaging (Breaking)
- [ ] Use durations rather than seconds checked in event loop (Potentially breaking)
+14 -44
View File
@@ -3,17 +3,18 @@ package main
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"log"
"os/exec" "os/exec"
"strings"
"text/template" "text/template"
"time" "time"
log "github.com/sirupsen/logrus"
) )
// Alert is a config driven mechanism for sending a notice // Alert is a config driven mechanism for sending a notice
type Alert struct { type Alert struct {
Name string Name string
Command CommandOrShell Command []string
CommandShell string `yaml:"command_shell"`
commandTemplate []*template.Template commandTemplate []*template.Template
commandShellTemplate *template.Template commandShellTemplate *template.Template
} }
@@ -31,40 +32,24 @@ type AlertNotice struct {
// IsValid returns a boolean indicating if the Alert has been correctly // IsValid returns a boolean indicating if the Alert has been correctly
// configured // configured
func (alert Alert) IsValid() bool { func (alert Alert) IsValid() bool {
return !alert.Command.Empty() atLeastOneCommand := (alert.CommandShell != "" || alert.Command != nil)
atMostOneCommand := (alert.CommandShell == "" || alert.Command == nil)
return atLeastOneCommand && atMostOneCommand
} }
// BuildTemplates compiles command templates for the Alert // BuildTemplates compiles command templates for the Alert
func (alert *Alert) BuildTemplates() error { func (alert *Alert) BuildTemplates() error {
// TODO: Remove legacy template support later after 1.0 log.Debugf("Building template for alert %s", alert.Name)
legacy := strings.NewReplacer( if alert.commandTemplate == nil && alert.Command != nil {
"{alert_count}", "{{.AlertCount}}",
"{alert_message}", "{{.MonitorName}} check has failed {{.FailureCount}} times",
"{failure_count}", "{{.FailureCount}}",
"{last_output}", "{{.LastCheckOutput}}",
"{last_success}", "{{.LastSuccess}}",
"{monitor_name}", "{{.MonitorName}}",
)
if LogDebug {
log.Printf("DEBUG: Building template for alert %s", alert.Name)
}
if alert.commandTemplate == nil && alert.Command.Command != nil {
alert.commandTemplate = []*template.Template{} alert.commandTemplate = []*template.Template{}
for i, cmdPart := range alert.Command.Command { for i, cmdPart := range alert.Command {
if PyCompat {
cmdPart = legacy.Replace(cmdPart)
}
alert.commandTemplate = append(alert.commandTemplate, template.Must( alert.commandTemplate = append(alert.commandTemplate, template.Must(
template.New(alert.Name+string(i)).Parse(cmdPart), template.New(alert.Name+string(i)).Parse(cmdPart),
)) ))
} }
} else if alert.commandShellTemplate == nil && alert.Command.ShellCommand != "" { } else if alert.commandShellTemplate == nil && alert.CommandShell != "" {
shellCmd := alert.Command.ShellCommand
if PyCompat {
shellCmd = legacy.Replace(shellCmd)
}
alert.commandShellTemplate = template.Must( alert.commandShellTemplate = template.Must(
template.New(alert.Name).Parse(shellCmd), template.New(alert.Name).Parse(alert.CommandShell),
) )
} else { } else {
return fmt.Errorf("No template provided for alert %s", alert.Name) return fmt.Errorf("No template provided for alert %s", alert.Name)
@@ -75,7 +60,7 @@ func (alert *Alert) BuildTemplates() error {
// Send will send an alert notice by executing the command template // Send will send an alert notice by executing the command template
func (alert Alert) Send(notice AlertNotice) (outputStr string, err error) { func (alert Alert) Send(notice AlertNotice) (outputStr string, err error) {
log.Printf("INFO: Sending alert %s for %s", alert.Name, notice.MonitorName) log.Infof("Sending alert %s for %s", alert.Name, notice.MonitorName)
var cmd *exec.Cmd var cmd *exec.Cmd
if alert.commandTemplate != nil { if alert.commandTemplate != nil {
command := []string{} command := []string{}
@@ -110,22 +95,7 @@ func (alert Alert) Send(notice AlertNotice) (outputStr string, err error) {
var output []byte var output []byte
output, err = cmd.CombinedOutput() output, err = cmd.CombinedOutput()
outputStr = string(output) outputStr = string(output)
if LogDebug { log.Debugf("Alert output for: %s\n---\n%s\n---", alert.Name, outputStr)
log.Printf("DEBUG: Alert output for: %s\n---\n%s\n---", alert.Name, outputStr)
}
return outputStr, err 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}} check has failed {{.FailureCount}} times",
},
},
}
}
+26 -37
View File
@@ -1,8 +1,9 @@
package main package main
import ( import (
"log"
"testing" "testing"
log "github.com/sirupsen/logrus"
) )
func TestAlertIsValid(t *testing.T) { func TestAlertIsValid(t *testing.T) {
@@ -11,19 +12,24 @@ func TestAlertIsValid(t *testing.T) {
expected bool expected bool
name string name string
}{ }{
{Alert{Command: CommandOrShell{Command: []string{"echo", "test"}}}, true, "Command only"}, {Alert{Command: []string{"echo", "test"}}, true, "Command only"},
{Alert{Command: CommandOrShell{ShellCommand: "echo test"}}, true, "CommandShell only"}, {Alert{CommandShell: "echo test"}, true, "CommandShell only"},
{Alert{}, false, "No commands"}, {Alert{}, false, "No commands"},
{
Alert{Command: []string{"echo", "test"}, CommandShell: "echo test"},
false,
"Both commands",
},
} }
for _, c := range cases { for _, c := range cases {
log.Printf("Testing case %s", c.name) log.Debugf("Testing case %s", c.name)
actual := c.alert.IsValid() actual := c.alert.IsValid()
if actual != c.expected { if actual != c.expected {
t.Errorf("IsValid(%v), expected=%t actual=%t", c.name, c.expected, actual) t.Errorf("IsValid(%v), expected=%t actual=%t", c.name, c.expected, actual)
log.Printf("Case failed: %s", c.name) log.Debugf("Case failed: %s", c.name)
} }
log.Println("-----") log.Debugf("-----")
} }
} }
@@ -34,68 +40,51 @@ func TestAlertSend(t *testing.T) {
expectedOutput string expectedOutput string
expectErr bool expectErr bool
name string name string
pyCompat bool
}{ }{
{ {
Alert{Command: CommandOrShell{Command: []string{"echo", "{{.MonitorName}}"}}}, Alert{Command: []string{"echo", "{{.MonitorName}}"}},
AlertNotice{MonitorName: "test"}, AlertNotice{MonitorName: "test"},
"test\n", "test\n",
false, false,
"Command with template", "Command with template",
false,
}, },
{ {
Alert{Command: CommandOrShell{ShellCommand: "echo {{.MonitorName}}"}}, Alert{CommandShell: "echo {{.MonitorName}}"},
AlertNotice{MonitorName: "test"}, AlertNotice{MonitorName: "test"},
"test\n", "test\n",
false, false,
"Command shell with template", "Command shell with template",
false,
}, },
{ {
Alert{Command: CommandOrShell{Command: []string{"echo", "{{.Bad}}"}}}, Alert{Command: []string{"echo", "{{.Bad}}"}},
AlertNotice{MonitorName: "test"}, AlertNotice{MonitorName: "test"},
"", "",
true, true,
"Command with bad template", "Command with bad template",
false,
}, },
{ {
Alert{Command: CommandOrShell{ShellCommand: "echo {{.Bad}}"}}, Alert{CommandShell: "echo {{.Bad}}"},
AlertNotice{MonitorName: "test"}, AlertNotice{MonitorName: "test"},
"", "",
true, true,
"Command shell with bad template", "Command shell with bad template",
false,
},
{
Alert{Command: CommandOrShell{ShellCommand: "echo {alert_message}"}},
AlertNotice{MonitorName: "test", FailureCount: 1},
"test check has failed 1 times\n",
false,
"Command shell with legacy template",
true,
}, },
} }
for _, c := range cases { for _, c := range cases {
log.Printf("Testing case %s", c.name) log.Debugf("Testing case %s", c.name)
// Set PyCompat to value of compat flag
PyCompat = c.pyCompat
c.alert.BuildTemplates() c.alert.BuildTemplates()
output, err := c.alert.Send(c.notice) output, err := c.alert.Send(c.notice)
hasErr := (err != nil) hasErr := (err != nil)
if output != c.expectedOutput { if output != c.expectedOutput {
t.Errorf("Send(%v output), expected=%v actual=%v", c.name, c.expectedOutput, output) t.Errorf("Send(%v output), expected=%v actual=%v", c.name, c.expectedOutput, output)
log.Printf("Case failed: %s", c.name) log.Debugf("Case failed: %s", c.name)
} }
if hasErr != c.expectErr { if hasErr != c.expectErr {
t.Errorf("Send(%v err), expected=%v actual=%v", c.name, "Err", err) t.Errorf("Send(%v err), expected=%v actual=%v", c.name, "Err", err)
log.Printf("Case failed: %s", c.name) log.Debugf("Case failed: %s", c.name)
} }
// Set PyCompat back to default value log.Debugf("-----")
PyCompat = false
log.Println("-----")
} }
} }
@@ -106,7 +95,7 @@ func TestAlertSendNoTemplates(t *testing.T) {
if err == nil { if err == nil {
t.Errorf("Send(no template), expected=%v actual=%v", "Err", output) t.Errorf("Send(no template), expected=%v actual=%v", "Err", output)
} }
log.Println("-----") log.Debugf("-----")
} }
func TestAlertBuildTemplate(t *testing.T) { func TestAlertBuildTemplate(t *testing.T) {
@@ -115,19 +104,19 @@ func TestAlertBuildTemplate(t *testing.T) {
expectErr bool expectErr bool
name string name string
}{ }{
{Alert{Command: CommandOrShell{Command: []string{"echo", "test"}}}, false, "Command only"}, {Alert{Command: []string{"echo", "test"}}, false, "Command only"},
{Alert{Command: CommandOrShell{ShellCommand: "echo test"}}, false, "CommandShell only"}, {Alert{CommandShell: "echo test"}, false, "CommandShell only"},
{Alert{}, true, "No commands"}, {Alert{}, true, "No commands"},
} }
for _, c := range cases { for _, c := range cases {
log.Printf("Testing case %s", c.name) log.Debugf("Testing case %s", c.name)
err := c.alert.BuildTemplates() err := c.alert.BuildTemplates()
hasErr := (err != nil) hasErr := (err != nil)
if hasErr != c.expectErr { if hasErr != c.expectErr {
t.Errorf("IsValid(%v), expected=%t actual=%t", c.name, c.expectErr, err) t.Errorf("IsValid(%v), expected=%t actual=%t", c.name, c.expectErr, err)
log.Printf("Case failed: %s", c.name) log.Debugf("Case failed: %s", c.name)
} }
log.Println("-----") log.Debugf("-----")
} }
} }
+22 -64
View File
@@ -3,8 +3,9 @@ package main
import ( import (
"errors" "errors"
"io/ioutil" "io/ioutil"
"log" "os"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@@ -15,70 +16,26 @@ type Config struct {
Alerts map[string]*Alert 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 // IsValid checks config validity and returns true if valid
func (config Config) IsValid() (isValid bool) { func (config Config) IsValid() (isValid bool) {
isValid = true isValid = true
// Validate alerts
if config.Alerts == nil || len(config.Alerts) == 0 {
// This should never happen because there is a default alert named 'log' for now
log.Printf("ERROR: Invalid alert configuration: Must provide at least one alert")
isValid = false
}
for _, alert := range config.Alerts {
if !alert.IsValid() {
log.Printf("ERROR: Invalid alert configuration: %s", alert.Name)
isValid = false
}
}
// Validate monitors // Validate monitors
if config.Monitors == nil || len(config.Monitors) == 0 { if config.Monitors == nil || len(config.Monitors) == 0 {
log.Printf("ERROR: Invalid monitor configuration: Must provide at least one monitor") log.Errorf("Invalid monitor configuration: Must provide at least one monitor")
isValid = false isValid = false
} }
for _, monitor := range config.Monitors { for _, monitor := range config.Monitors {
if !monitor.IsValid() { if !monitor.IsValid() {
log.Printf("ERROR: Invalid monitor configuration: %s", monitor.Name) log.Errorf("Invalid monitor configuration: %s", monitor.Name)
isValid = false isValid = false
} }
// Check that all Monitor alerts actually exist // Check that all Monitor alerts actually exist
for _, isUp := range []bool{true, false} { for _, isUp := range []bool{true, false} {
for _, alertName := range monitor.GetAlertNames(isUp) { for _, alertName := range monitor.GetAlertNames(isUp) {
if _, ok := config.Alerts[alertName]; !ok { if _, ok := config.Alerts[alertName]; !ok {
log.Printf( log.Errorf(
"ERROR: Invalid monitor configuration: %s. Unknown alert %s", "Invalid monitor configuration: %s. Unknown alert %s",
monitor.Name, alertName, monitor.Name, alertName,
) )
isValid = false isValid = false
@@ -87,6 +44,18 @@ func (config Config) IsValid() (isValid bool) {
} }
} }
// Validate alerts
if config.Alerts == nil || len(config.Alerts) == 0 {
log.Errorf("Invalid alert configuration: Must provide at least one alert")
isValid = false
}
for _, alert := range config.Alerts {
if !alert.IsValid() {
log.Errorf("Invalid alert configuration: %s", alert.Name)
isValid = false
}
}
return return
} }
@@ -109,25 +78,14 @@ func LoadConfig(filePath string) (config Config, err error) {
return return
} }
err = yaml.Unmarshal(data, &config) // TODO: Decide if this is better expanded here, or only when executing
envExpanded := os.ExpandEnv(string(data))
err = yaml.Unmarshal([]byte(envExpanded), &config)
if err != nil { if err != nil {
return return
} }
if LogDebug { log.Debugf("Config values:\n%v\n", config)
log.Printf("DEBUG: Config values:\n%v\n", config)
}
// Add log alert if not present
if PyCompat {
// Intialize alerts list if not present
if config.Alerts == nil {
config.Alerts = map[string]*Alert{}
}
if _, ok := config.Alerts["log"]; !ok {
config.Alerts["log"] = NewLogAlert()
}
}
if !config.IsValid() { if !config.IsValid() {
err = errors.New("Invalid configuration") err = errors.New("Invalid configuration")
+6 -78
View File
@@ -10,94 +10,22 @@ func TestLoadConfig(t *testing.T) {
configPath string configPath string
expectErr bool expectErr bool
name string name string
pyCompat bool
}{ }{
{"./test/valid-config.yml", false, "Valid config file", false}, {"./test/valid-config.yml", false, "Valid config file"},
{"./test/valid-default-log-alert.yml", false, "Valid config file with default log alert PyCompat", true}, {"./test/does-not-exist", true, "Invalid config path"},
{"./test/valid-default-log-alert.yml", true, "Invalid config file no log alert", false}, {"./test/invalid-config-type.yml", true, "Invalid config type for key"},
{"./test/does-not-exist", true, "Invalid config path", false}, {"./test/invalid-config-missing-alerts.yml", true, "Invalid config missing alerts"},
{"./test/invalid-config-type.yml", true, "Invalid config type for key", false}, {"./test/invalid-config-unknown-alert.yml", true, "Invalid config unknown alert"},
{"./test/invalid-config-missing-alerts.yml", true, "Invalid config missing alerts", false},
{"./test/invalid-config-unknown-alert.yml", true, "Invalid config unknown alert", false},
} }
for _, c := range cases { for _, c := range cases {
log.Printf("Testing case %s", c.name) log.Printf("Testing case %s", c.name)
// Set PyCompat based on compatibility mode
PyCompat = c.pyCompat
_, err := LoadConfig(c.configPath) _, err := LoadConfig(c.configPath)
hasErr := (err != nil) hasErr := (err != nil)
if hasErr != c.expectErr { if hasErr != c.expectErr {
t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectErr, err) t.Errorf("LoadConfig(%v), expected=%v actual=%v", c.name, "Err", err)
log.Printf("Case failed: %s", c.name) log.Printf("Case failed: %s", c.name)
} }
// Set PyCompat to default value
PyCompat = false
log.Println("-----") log.Println("-----")
} }
} }
// TestMultiLineConfig is a more complicated test stepping through the parsing
// and execution of mutli-line strings presented in YAML
func TestMultiLineConfig(t *testing.T) {
log.Println("Testing multi-line string config")
config, err := LoadConfig("./test/valid-verify-multi-line.yml")
if err != nil {
t.Fatalf("TestMultiLineConfig(load), expected=no_error actual=%v", err)
}
log.Println("-----")
log.Println("TestMultiLineConfig(parse > string)")
expected := "echo 'Some string with stuff'; echo \"<angle brackets>\"; exit 1\n"
actual := config.Monitors[0].Command.ShellCommand
if expected != actual {
t.Errorf("TestMultiLineConfig(>) failed")
t.Logf("string expected=`%v`", expected)
t.Logf("string actual =`%v`", actual)
t.Logf("bytes expected=%v", []byte(expected))
t.Logf("bytes actual =%v", []byte(actual))
}
log.Println("-----")
log.Println("TestMultiLineConfig(execute > string)")
_, notice := config.Monitors[0].Check()
if notice == nil {
t.Fatalf("Did not receive an alert notice")
}
expected = "Some string with stuff\n<angle brackets>\n"
actual = notice.LastCheckOutput
if expected != actual {
t.Errorf("TestMultiLineConfig(execute > string) check failed")
t.Logf("string expected=`%v`", expected)
t.Logf("string actual =`%v`", actual)
t.Logf("bytes expected=%v", []byte(expected))
t.Logf("bytes actual =%v", []byte(actual))
}
log.Println("-----")
log.Println("TestMultiLineConfig(parse | string)")
expected = "echo 'Some string with stuff'\necho '<angle brackets>'\n"
actual = config.Alerts["log_shell"].Command.ShellCommand
if expected != actual {
t.Errorf("TestMultiLineConfig(|) failed")
t.Logf("string expected=`%v`", expected)
t.Logf("string actual =`%v`", actual)
t.Logf("bytes expected=%v", []byte(expected))
t.Logf("bytes actual =%v", []byte(actual))
}
log.Println("-----")
log.Println("TestMultiLineConfig(execute | string)")
actual, err = config.Alerts["log_shell"].Send(AlertNotice{})
if err != nil {
t.Errorf("Execution of alert failed")
}
expected = "Some string with stuff\n<angle brackets>\n"
if expected != actual {
t.Errorf("TestMultiLineConfig(execute | string) check failed")
t.Logf("string expected=`%v`", expected)
t.Logf("string actual =`%v`", actual)
t.Logf("bytes expected=%v", []byte(expected))
t.Logf("bytes actual =%v", []byte(actual))
}
}
+1
View File
@@ -4,5 +4,6 @@ go 1.12
require ( require (
github.com/prometheus/client_golang v1.2.1 github.com/prometheus/client_golang v1.2.1
github.com/sirupsen/logrus v1.4.2
gopkg.in/yaml.v2 v2.2.4 gopkg.in/yaml.v2 v2.2.4
) )
+2
View File
@@ -53,6 +53,7 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8=
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -68,6 +69,7 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+10 -15
View File
@@ -3,14 +3,12 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"log"
"time" "time"
log "github.com/sirupsen/logrus"
) )
var ( var (
// LogDebug will control whether debug messsages should be logged
LogDebug = false
// ExportMetrics will track whether or not we want to export metrics to prometheus // ExportMetrics will track whether or not we want to export metrics to prometheus
ExportMetrics = false ExportMetrics = false
// MetricsPort is the port to expose metrics on // MetricsPort is the port to expose metrics on
@@ -18,9 +16,6 @@ var (
// Metrics contains all active metrics // Metrics contains all active metrics
Metrics = NewMetrics() Metrics = NewMetrics()
// PyCompat enables support for legacy Python templates
PyCompat = false
// version of minitor being run // version of minitor being run
version = "dev" version = "dev"
) )
@@ -38,9 +33,7 @@ func checkMonitors(config *Config) error {
// Should probably consider refactoring everything below here // Should probably consider refactoring everything below here
if alertNotice != nil { if alertNotice != nil {
if LogDebug { log.Debugf("Recieved an alert notice from %s", alertNotice.MonitorName)
log.Printf("DEBUG: Recieved an alert notice from %s", alertNotice.MonitorName)
}
alertNames := monitor.GetAlertNames(alertNotice.IsUp) alertNames := monitor.GetAlertNames(alertNotice.IsUp)
if alertNames == nil { if alertNames == nil {
// This should only happen for a recovery alert. AlertDown is validated not empty // This should only happen for a recovery alert. AlertDown is validated not empty
@@ -84,14 +77,16 @@ func checkMonitors(config *Config) error {
func main() { func main() {
// Get debug flag // Get debug flag
flag.BoolVar(&LogDebug, "debug", false, "Enables debug logs (default: false)") var debug = flag.Bool("debug", false, "Enables debug logs (default: false)")
flag.BoolVar(&ExportMetrics, "metrics", false, "Enables prometheus metrics exporting (default: false)") flag.BoolVar(&ExportMetrics, "metrics", false, "Enables prometheus metrics exporting (default: false)")
flag.BoolVar(&PyCompat, "py-compat", false, "Enables support for legacy Python Minitor config. Will eventually be removed. (default: false)")
flag.IntVar(&MetricsPort, "metrics-port", 8080, "The port that Prometheus metrics should be exported on, if enabled. (default: 8080)")
var showVersion = flag.Bool("version", false, "Display the version of minitor and exit") var showVersion = flag.Bool("version", false, "Display the version of minitor and exit")
var configPath = flag.String("config", "config.yml", "Alternate configuration path (default: config.yml)")
flag.Parse() flag.Parse()
// Set debug if flag is set
if *debug {
log.SetLevel(log.DebugLevel)
}
// Print version if flag is provided // Print version if flag is provided
if *showVersion { if *showVersion {
log.Println("Minitor version:", version) log.Println("Minitor version:", version)
@@ -99,7 +94,7 @@ func main() {
} }
// Load configuration // Load configuration
config, err := LoadConfig(*configPath) config, err := LoadConfig("config.yml")
if err != nil { if err != nil {
log.Fatalf("Error loading config: %v", err) log.Fatalf("Error loading config: %v", err)
} }
+9 -9
View File
@@ -18,7 +18,7 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{ Monitors: []*Monitor{
&Monitor{ &Monitor{
Name: "Success", Name: "Success",
Command: CommandOrShell{Command: []string{"true"}}, Command: []string{"true"},
}, },
}, },
}, },
@@ -30,12 +30,12 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{ Monitors: []*Monitor{
&Monitor{ &Monitor{
Name: "Failure", Name: "Failure",
Command: CommandOrShell{Command: []string{"false"}}, Command: []string{"false"},
AlertAfter: 1, AlertAfter: 1,
}, },
&Monitor{ &Monitor{
Name: "Failure", Name: "Failure",
Command: CommandOrShell{Command: []string{"false"}}, Command: []string{"false"},
AlertDown: []string{"unknown"}, AlertDown: []string{"unknown"},
AlertAfter: 1, AlertAfter: 1,
}, },
@@ -49,12 +49,12 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{ Monitors: []*Monitor{
&Monitor{ &Monitor{
Name: "Success", Name: "Success",
Command: CommandOrShell{Command: []string{"ls"}}, Command: []string{"ls"},
alertCount: 1, alertCount: 1,
}, },
&Monitor{ &Monitor{
Name: "Success", Name: "Success",
Command: CommandOrShell{Command: []string{"true"}}, Command: []string{"true"},
AlertUp: []string{"unknown"}, AlertUp: []string{"unknown"},
alertCount: 1, alertCount: 1,
}, },
@@ -68,14 +68,14 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{ Monitors: []*Monitor{
&Monitor{ &Monitor{
Name: "Failure", Name: "Failure",
Command: CommandOrShell{Command: []string{"false"}}, Command: []string{"false"},
AlertDown: []string{"good"}, AlertDown: []string{"good"},
AlertAfter: 1, AlertAfter: 1,
}, },
}, },
Alerts: map[string]*Alert{ Alerts: map[string]*Alert{
"good": &Alert{ "good": &Alert{
Command: CommandOrShell{Command: []string{"true"}}, Command: []string{"true"},
}, },
}, },
}, },
@@ -87,7 +87,7 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{ Monitors: []*Monitor{
&Monitor{ &Monitor{
Name: "Failure", Name: "Failure",
Command: CommandOrShell{Command: []string{"false"}}, Command: []string{"false"},
AlertDown: []string{"bad"}, AlertDown: []string{"bad"},
AlertAfter: 1, AlertAfter: 1,
}, },
@@ -95,7 +95,7 @@ func TestCheckMonitors(t *testing.T) {
Alerts: map[string]*Alert{ Alerts: map[string]*Alert{
"bad": &Alert{ "bad": &Alert{
Name: "bad", Name: "bad",
Command: CommandOrShell{Command: []string{"false"}}, Command: []string{"false"},
}, },
}, },
}, },
-25
View File
@@ -1,25 +0,0 @@
image: iamthefij/minitor-go:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
-
image: iamthefij/minitor-go:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
-
image: iamthefij/minitor-go:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
-
image: iamthefij/minitor-go:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7
+26 -25
View File
@@ -1,17 +1,19 @@
package main package main
import ( import (
"log"
"math" "math"
"os/exec" "os/exec"
"time" "time"
log "github.com/sirupsen/logrus"
) )
// Monitor represents a particular periodic check of a command // Monitor represents a particular periodic check of a command
type Monitor struct { type Monitor struct {
// Config values // Config values
Name string Name string
Command CommandOrShell Command []string
CommandShell string `yaml:"command_shell"`
AlertDown []string `yaml:"alert_down"` AlertDown []string `yaml:"alert_down"`
AlertUp []string `yaml:"alert_up"` AlertUp []string `yaml:"alert_up"`
CheckInterval float64 `yaml:"check_interval"` CheckInterval float64 `yaml:"check_interval"`
@@ -28,7 +30,10 @@ type Monitor struct {
// 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 {
return (!monitor.Command.Empty() && atLeastOneCommand := (monitor.CommandShell != "" || monitor.Command != nil)
atMostOneCommand := (monitor.CommandShell == "" || monitor.Command == nil)
return (atLeastOneCommand &&
atMostOneCommand &&
monitor.getAlertAfter() > 0 && monitor.getAlertAfter() > 0 &&
monitor.AlertDown != nil) monitor.AlertDown != nil)
} }
@@ -48,10 +53,10 @@ func (monitor Monitor) ShouldCheck() bool {
// and a possible AlertNotice // and a possible AlertNotice
func (monitor *Monitor) Check() (bool, *AlertNotice) { func (monitor *Monitor) Check() (bool, *AlertNotice) {
var cmd *exec.Cmd var cmd *exec.Cmd
if monitor.Command.Command != nil { if monitor.Command != nil {
cmd = exec.Command(monitor.Command.Command[0], monitor.Command.Command[1:]...) cmd = exec.Command(monitor.Command[0], monitor.Command[1:]...)
} else { } else {
cmd = ShellCommand(monitor.Command.ShellCommand) cmd = ShellCommand(monitor.CommandShell)
} }
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
@@ -66,20 +71,18 @@ func (monitor *Monitor) Check() (bool, *AlertNotice) {
alertNotice = monitor.failure() alertNotice = monitor.failure()
} }
if LogDebug { log.Debugf("Command output: %s", monitor.lastOutput)
log.Printf("DEBUG: Command output: %s", monitor.lastOutput)
}
if err != nil { if err != nil {
if LogDebug { log.Debugf("Command result: %v", err)
log.Printf("DEBUG: Command result: %v", err)
}
} }
log.Printf( log.WithFields(log.Fields{
"INFO: %s success=%t, alert=%t", "monitor": monitor.Name,
"success": isSuccess,
"alert": alertNotice != nil,
}).Infof(
"%s checked",
monitor.Name, monitor.Name,
isSuccess,
alertNotice != nil,
) )
return isSuccess, alertNotice return isSuccess, alertNotice
@@ -105,15 +108,13 @@ 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.getAlertAfter() {
if LogDebug { log.Debugf(
log.Printf( "%s failed but did not hit minimum failures. "+
"DEBUG: %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.getAlertAfter(), )
)
}
return return
} }
+13 -8
View File
@@ -13,11 +13,16 @@ func TestMonitorIsValid(t *testing.T) {
expected bool expected bool
name string name string
}{ }{
{Monitor{Command: CommandOrShell{Command: []string{"echo", "test"}}, AlertDown: []string{"log"}}, true, "Command only"}, {Monitor{Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, true, "Command only"},
{Monitor{Command: CommandOrShell{ShellCommand: "echo test"}, AlertDown: []string{"log"}}, true, "CommandShell only"}, {Monitor{CommandShell: "echo test", AlertDown: []string{"log"}}, true, "CommandShell only"},
{Monitor{Command: CommandOrShell{Command: []string{"echo", "test"}}}, false, "No AlertDown"}, {Monitor{Command: []string{"echo", "test"}}, false, "No AlertDown"},
{Monitor{AlertDown: []string{"log"}}, false, "No commands"}, {Monitor{AlertDown: []string{"log"}}, false, "No commands"},
{Monitor{Command: CommandOrShell{Command: []string{"echo", "test"}}, AlertDown: []string{"log"}, AlertAfter: -1}, false, "Invalid alert threshold, -1"}, {
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"},
} }
for _, c := range cases { for _, c := range cases {
@@ -249,22 +254,22 @@ func TestMonitorCheck(t *testing.T) {
name string name string
}{ }{
{ {
Monitor{Command: CommandOrShell{Command: []string{"echo", "success"}}}, Monitor{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{Command: CommandOrShell{ShellCommand: "echo success"}}, Monitor{CommandShell: "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: CommandOrShell{Command: []string{"total", "failure"}}}, Monitor{Command: []string{"total", "failure"}},
expected{isSuccess: false, hasNotice: true, lastOutput: ""}, expected{isSuccess: false, hasNotice: true, lastOutput: ""},
"Test failed command", "Test failed command",
}, },
{ {
Monitor{Command: CommandOrShell{ShellCommand: "false"}}, Monitor{CommandShell: "false"},
expected{isSuccess: false, hasNotice: true, lastOutput: ""}, expected{isSuccess: false, hasNotice: true, lastOutput: ""},
"Test failed command shell", "Test failed command shell",
}, },
+2 -2
View File
@@ -25,7 +25,7 @@ alerts:
email_up: email_up:
command: [sendmail, "me@minitor.mon", "Recovered: {monitor_name}", "We're back!"] command: [sendmail, "me@minitor.mon", "Recovered: {monitor_name}", "We're back!"]
mailgun_down: mailgun_down:
command: > command_shell: >
curl -s -X POST curl -s -X POST
-F subject="Alert! {{.MonitorName}} failed" -F subject="Alert! {{.MonitorName}} failed"
-F from="Minitor <minitor@minitor.mon>" -F from="Minitor <minitor@minitor.mon>"
@@ -34,7 +34,7 @@ alerts:
https://api.mailgun.net/v3/minitor.mon/messages https://api.mailgun.net/v3/minitor.mon/messages
-u "api:${MAILGUN_API_KEY}" -u "api:${MAILGUN_API_KEY}"
sms_down: sms_down:
command: > command_shell: >
curl -s -X POST -F "Body=Failure! {{.MonitorName}} has failed" curl -s -X POST -F "Body=Failure! {{.MonitorName}} has failed"
-F "From=${AVAILABLE_NUMBER}" -F "To=${MY_PHONE}" -F "From=${AVAILABLE_NUMBER}" -F "To=${MY_PHONE}"
"https://api.twilio.com/2010-04-01/Accounts/${ACCOUNT_SID}/Messages" "https://api.twilio.com/2010-04-01/Accounts/${ACCOUNT_SID}/Messages"
+5 -6
View File
@@ -1,23 +1,22 @@
---
check_interval: 1 check_interval: 1
monitors: monitors:
- name: Command - name: Command
command: ['echo', '$PATH'] command: ['echo', '$PATH']
alert_down: ['log_command', 'log_shell'] alert_down: [ 'log_command', 'log_shell' ]
alert_every: 0 alert_every: 0
- name: Shell - name: Shell
command: > command_shell: >
echo 'Some string with stuff'; echo 'Some string with stuff';
echo 'another line'; echo 'another line';
echo $PATH; echo $PATH;
exit 1 exit 1
alert_down: ['log_command', 'log_shell'] alert_down: [ 'log_command', 'log_shell' ]
alert_after: 5 alert_after: 5
alert_every: 0 alert_every: 0
alerts: alerts:
log_command: log_command:
command: ['echo', 'regular', '"command!!!"', "{{.MonitorName}}"] command: [ 'echo', 'regular', '"command!!!"', "{{.MonitorName}}" ]
log_shell: log_shell:
command: echo "Failure on {{.MonitorName}} User is $USER" command_shell: echo "Failure on {{.MonitorName}} User is $USER"
-8
View File
@@ -1,8 +0,0 @@
---
check_interval: 1
monitors:
- name: Command
command: ['echo', '$PATH']
alert_down: ['log']
alert_every: 0
-18
View File
@@ -1,18 +0,0 @@
---
check_interval: 1
monitors:
- name: Shell
command: >
echo 'Some string with stuff';
echo "<angle brackets>";
exit 1
alert_down: ['log_shell']
alert_after: 1
alert_every: 0
alerts:
log_shell:
command: |
echo 'Some string with stuff'
echo '<angle brackets>'
+11 -1
View File
@@ -5,9 +5,19 @@ import (
"strings" "strings"
) )
// 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)
// TODO: Not sure if this part is actually needed. Should verify
// Escape double quotes since this will be passed in as an argument
command = strings.Replace(command, `"`, `\"`, -1)
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 { func ShellCommand(command string) *exec.Cmd {
shellCommand := []string{"sh", "-c", strings.TrimSpace(command)} shellCommand := []string{"sh", "-c", escapeCommandShell(command)}
//log.Printf("Shell command: %v", shellCommand) //log.Printf("Shell command: %v", shellCommand)
return exec.Command(shellCommand[0], shellCommand[1:]...) return exec.Command(shellCommand[0], shellCommand[1:]...)
} }