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
20 changed files with 181 additions and 384 deletions
+3 -58
View File
@@ -45,70 +45,15 @@ trigger:
- refs/tags/v*
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
settings:
repo: iamthefij/minitor-go
dockerfile: Dockerfile.multi-stage
auto_tag: true
auto_tag_suffix: linux-arm
username:
from_secret: docker_username
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
# Output binary
minitor
minitor-linux-*
minitor-darwin-amd64
minitor-go
+4 -9
View File
@@ -1,13 +1,12 @@
ARG REPO=library
FROM multiarch/qemu-user-static:4.2.0-2 as qemu-user-static
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
WORKDIR /app/
# Copy minitor in
ARG ARCH=amd64
COPY ./minitor-go ./minitor
# Add common checking tools
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
RUN chmod -R 755 /app/scripts
# Copy minitor in
ARG ARCH=amd64
COPY ./minitor-linux-${ARCH} ./minitor
# Drop to non-root user
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
all: minitor-linux-amd64 minitor-linux-arm minitor-linux-arm64
DOCKER_TAG ?= minitor-go-${USER}
.PHONY: default
default: test
.PHONY: build
build: minitor
build:
go build
minitor:
@echo Version: $(VERSION)
go build -ldflags '-X "main.version=${VERSION}"' -o minitor
minitor-go:
go build
.PHONY: run
run: minitor build
./minitor -debug
run: minitor-go build
./minitor-go -debug
.PHONY: run-metrics
run-metrics: minitor build
./minitor -debug -metrics
run-metrics: minitor-go build
./minitor-go -debug -metrics
.PHONY: test
test:
@@ -46,56 +41,13 @@ check:
.PHONY: clean
clean:
rm -f ./minitor
rm -f ./minitor-linux-*
rm -f ./minitor-darwin-amd64
rm -f ./minitor-go
rm -f ./coverage.out
.PHONY: 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
docker-run: docker-build
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
+28 -8
View File
@@ -1,6 +1,6 @@
# minitor-go
A reimplementation of [Minitor](https://git.iamthefij.com/iamthefij/minitor) in Go
A reimplementation of [Minitor](https://git.iamthefij/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.
@@ -8,8 +8,29 @@ 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.
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.
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:
```yaml
@@ -17,7 +38,7 @@ alerts:
log_command:
command: ['echo', '{monitor_name}']
log_shell:
command: 'echo {monitor_name}'
command_shell: 'echo {monitor_name}'
```
minitor-go:
@@ -26,7 +47,7 @@ alerts:
log_command:
command: ['echo', '{{.MonitorName}}']
log_shell:
command: 'echo {{.MonitorName}}'
command_shell: 'echo {{.MonitorName}}'
```
Finally, newlines in a shell command don't terminate a particular command. Semicolons must be used and continuations should not.
@@ -35,7 +56,7 @@ minitor-py:
```yaml
alerts:
log_shell:
command: >
command_shell: >
echo "line 1"
echo "line 2"
echo "continued" \
@@ -46,7 +67,7 @@ minitor-go:
```yaml
alerts:
log_shell:
command: >
command_shell: >
echo "line 1";
echo "line 2";
echo "continued"
@@ -65,8 +86,7 @@ Pairity:
- [x] Allow templating of alert commands
- [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
- [ ] Integration testing (manual or otherwise)
Improvement (potentially breaking):
+17 -17
View File
@@ -3,16 +3,18 @@ package main
import (
"bytes"
"fmt"
"log"
"os/exec"
"text/template"
"time"
log "github.com/sirupsen/logrus"
)
// Alert is a config driven mechanism for sending a notice
type Alert struct {
Name string
Command CommandOrShell
Command []string
CommandShell string `yaml:"command_shell"`
commandTemplate []*template.Template
commandShellTemplate *template.Template
}
@@ -30,24 +32,24 @@ type AlertNotice struct {
// IsValid returns a boolean indicating if the Alert has been correctly
// configured
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
func (alert *Alert) BuildTemplates() error {
if LogDebug {
log.Printf("DEBUG: Building template for alert %s", alert.Name)
}
if alert.commandTemplate == nil && alert.Command.Command != nil {
log.Debugf("Building template for alert %s", alert.Name)
if alert.commandTemplate == nil && alert.Command != nil {
alert.commandTemplate = []*template.Template{}
for i, cmdPart := range alert.Command.Command {
for i, cmdPart := range alert.Command {
alert.commandTemplate = append(alert.commandTemplate, template.Must(
template.New(alert.Name+string(i)).Parse(cmdPart),
))
}
} else if alert.commandShellTemplate == nil && alert.Command.ShellCommand != "" {
} else if alert.commandShellTemplate == nil && alert.CommandShell != "" {
alert.commandShellTemplate = template.Must(
template.New(alert.Name).Parse(alert.Command.ShellCommand),
template.New(alert.Name).Parse(alert.CommandShell),
)
} else {
return fmt.Errorf("No template provided for alert %s", alert.Name)
@@ -57,8 +59,8 @@ func (alert *Alert) BuildTemplates() error {
}
// Send will send an alert notice by executing the command template
func (alert Alert) Send(notice AlertNotice) (output_str string, err error) {
log.Printf("INFO: Sending alert %s for %s", alert.Name, notice.MonitorName)
func (alert Alert) Send(notice AlertNotice) (outputStr string, err error) {
log.Infof("Sending alert %s for %s", alert.Name, notice.MonitorName)
var cmd *exec.Cmd
if alert.commandTemplate != nil {
command := []string{}
@@ -92,10 +94,8 @@ func (alert Alert) Send(notice AlertNotice) (output_str string, err error) {
var output []byte
output, err = cmd.CombinedOutput()
output_str = string(output)
if LogDebug {
log.Printf("DEBUG: Alert output for: %s\n---\n%s\n---", alert.Name, output_str)
}
outputStr = string(output)
log.Debugf("Alert output for: %s\n---\n%s\n---", alert.Name, outputStr)
return output_str, err
return outputStr, err
}
+26 -20
View File
@@ -1,8 +1,9 @@
package main
import (
"log"
"testing"
log "github.com/sirupsen/logrus"
)
func TestAlertIsValid(t *testing.T) {
@@ -11,19 +12,24 @@ func TestAlertIsValid(t *testing.T) {
expected bool
name string
}{
{Alert{Command: CommandOrShell{Command: []string{"echo", "test"}}}, true, "Command only"},
{Alert{Command: CommandOrShell{ShellCommand: "echo test"}}, true, "CommandShell only"},
{Alert{Command: []string{"echo", "test"}}, true, "Command only"},
{Alert{CommandShell: "echo test"}, true, "CommandShell only"},
{Alert{}, false, "No commands"},
{
Alert{Command: []string{"echo", "test"}, CommandShell: "echo test"},
false,
"Both commands",
},
}
for _, c := range cases {
log.Printf("Testing case %s", c.name)
log.Debugf("Testing case %s", c.name)
actual := c.alert.IsValid()
if actual != c.expected {
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("-----")
}
}
@@ -36,28 +42,28 @@ func TestAlertSend(t *testing.T) {
name string
}{
{
Alert{Command: CommandOrShell{Command: []string{"echo", "{{.MonitorName}}"}}},
Alert{Command: []string{"echo", "{{.MonitorName}}"}},
AlertNotice{MonitorName: "test"},
"test\n",
false,
"Command with template",
},
{
Alert{Command: CommandOrShell{ShellCommand: "echo {{.MonitorName}}"}},
Alert{CommandShell: "echo {{.MonitorName}}"},
AlertNotice{MonitorName: "test"},
"test\n",
false,
"Command shell with template",
},
{
Alert{Command: CommandOrShell{Command: []string{"echo", "{{.Bad}}"}}},
Alert{Command: []string{"echo", "{{.Bad}}"}},
AlertNotice{MonitorName: "test"},
"",
true,
"Command with bad template",
},
{
Alert{Command: CommandOrShell{ShellCommand: "echo {{.Bad}}"}},
Alert{CommandShell: "echo {{.Bad}}"},
AlertNotice{MonitorName: "test"},
"",
true,
@@ -66,19 +72,19 @@ func TestAlertSend(t *testing.T) {
}
for _, c := range cases {
log.Printf("Testing case %s", c.name)
log.Debugf("Testing case %s", c.name)
c.alert.BuildTemplates()
output, err := c.alert.Send(c.notice)
hasErr := (err != nil)
if output != c.expectedOutput {
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 {
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)
}
log.Println("-----")
log.Debugf("-----")
}
}
@@ -89,7 +95,7 @@ func TestAlertSendNoTemplates(t *testing.T) {
if err == nil {
t.Errorf("Send(no template), expected=%v actual=%v", "Err", output)
}
log.Println("-----")
log.Debugf("-----")
}
func TestAlertBuildTemplate(t *testing.T) {
@@ -98,19 +104,19 @@ func TestAlertBuildTemplate(t *testing.T) {
expectErr bool
name string
}{
{Alert{Command: CommandOrShell{Command: []string{"echo", "test"}}}, false, "Command only"},
{Alert{Command: CommandOrShell{ShellCommand: "echo test"}}, false, "CommandShell only"},
{Alert{Command: []string{"echo", "test"}}, false, "Command only"},
{Alert{CommandShell: "echo test"}, false, "CommandShell only"},
{Alert{}, true, "No commands"},
}
for _, c := range cases {
log.Printf("Testing case %s", c.name)
log.Debugf("Testing case %s", c.name)
err := c.alert.BuildTemplates()
hasErr := (err != nil)
if hasErr != c.expectErr {
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("-----")
}
}
+12 -42
View File
@@ -3,8 +3,9 @@ package main
import (
"errors"
"io/ioutil"
"log"
"os"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
@@ -15,57 +16,26 @@ 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
// Validate monitors
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
}
for _, monitor := range config.Monitors {
if !monitor.IsValid() {
log.Printf("ERROR: Invalid monitor configuration: %s", monitor.Name)
log.Errorf("Invalid monitor configuration: %s", monitor.Name)
isValid = false
}
// Check that all Monitor alerts actually exist
for _, isUp := range []bool{true, false} {
for _, alertName := range monitor.GetAlertNames(isUp) {
if _, ok := config.Alerts[alertName]; !ok {
log.Printf(
"ERROR: Invalid monitor configuration: %s. Unknown alert %s",
log.Errorf(
"Invalid monitor configuration: %s. Unknown alert %s",
monitor.Name, alertName,
)
isValid = false
@@ -76,12 +46,12 @@ func (config Config) IsValid() (isValid bool) {
// Validate alerts
if config.Alerts == nil || len(config.Alerts) == 0 {
log.Printf("ERROR: Invalid alert configuration: Must provide at least one alert")
log.Errorf("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)
log.Errorf("Invalid alert configuration: %s", alert.Name)
isValid = false
}
}
@@ -108,14 +78,14 @@ func LoadConfig(filePath string) (config Config, err error) {
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 {
return
}
if LogDebug {
log.Printf("DEBUG: Config values:\n%v\n", config)
}
log.Debugf("Config values:\n%v\n", config)
if !config.IsValid() {
err = errors.New("Invalid configuration")
+1 -66
View File
@@ -23,74 +23,9 @@ func TestLoadConfig(t *testing.T) {
_, err := LoadConfig(c.configPath)
hasErr := (err != nil)
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.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 (
github.com/prometheus/client_golang v1.2.1
github.com/sirupsen/logrus v1.4.2
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/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
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/stretchr/objx v0.1.0/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-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-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
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=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+9 -8
View File
@@ -3,14 +3,12 @@ package main
import (
"flag"
"fmt"
"log"
"time"
log "github.com/sirupsen/logrus"
)
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 = false
// MetricsPort is the port to expose metrics on
@@ -35,9 +33,7 @@ func checkMonitors(config *Config) error {
// Should probably consider refactoring everything below here
if alertNotice != nil {
if LogDebug {
log.Printf("DEBUG: Recieved an alert notice from %s", alertNotice.MonitorName)
}
log.Debugf("Recieved an alert notice from %s", alertNotice.MonitorName)
alertNames := monitor.GetAlertNames(alertNotice.IsUp)
if alertNames == nil {
// This should only happen for a recovery alert. AlertDown is validated not empty
@@ -81,11 +77,16 @@ func checkMonitors(config *Config) error {
func main() {
// 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)")
var showVersion = flag.Bool("version", false, "Display the version of minitor and exit")
flag.Parse()
// Set debug if flag is set
if *debug {
log.SetLevel(log.DebugLevel)
}
// Print version if flag is provided
if *showVersion {
log.Println("Minitor version:", version)
+9 -9
View File
@@ -18,7 +18,7 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{
&Monitor{
Name: "Success",
Command: CommandOrShell{Command: []string{"true"}},
Command: []string{"true"},
},
},
},
@@ -30,12 +30,12 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{
&Monitor{
Name: "Failure",
Command: CommandOrShell{Command: []string{"false"}},
Command: []string{"false"},
AlertAfter: 1,
},
&Monitor{
Name: "Failure",
Command: CommandOrShell{Command: []string{"false"}},
Command: []string{"false"},
AlertDown: []string{"unknown"},
AlertAfter: 1,
},
@@ -49,12 +49,12 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{
&Monitor{
Name: "Success",
Command: CommandOrShell{Command: []string{"ls"}},
Command: []string{"ls"},
alertCount: 1,
},
&Monitor{
Name: "Success",
Command: CommandOrShell{Command: []string{"true"}},
Command: []string{"true"},
AlertUp: []string{"unknown"},
alertCount: 1,
},
@@ -68,14 +68,14 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{
&Monitor{
Name: "Failure",
Command: CommandOrShell{Command: []string{"false"}},
Command: []string{"false"},
AlertDown: []string{"good"},
AlertAfter: 1,
},
},
Alerts: map[string]*Alert{
"good": &Alert{
Command: CommandOrShell{Command: []string{"true"}},
Command: []string{"true"},
},
},
},
@@ -87,7 +87,7 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{
&Monitor{
Name: "Failure",
Command: CommandOrShell{Command: []string{"false"}},
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: 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
+21 -20
View File
@@ -1,17 +1,19 @@
package main
import (
"log"
"math"
"os/exec"
"time"
log "github.com/sirupsen/logrus"
)
// Monitor represents a particular periodic check of a command
type Monitor struct {
// Config values
Name string
Command CommandOrShell
Command []string
CommandShell string `yaml:"command_shell"`
AlertDown []string `yaml:"alert_down"`
AlertUp []string `yaml:"alert_up"`
CheckInterval float64 `yaml:"check_interval"`
@@ -28,7 +30,10 @@ type Monitor struct {
// IsValid returns a boolean indicating if the Monitor has been correctly
// configured
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.AlertDown != nil)
}
@@ -48,10 +53,10 @@ func (monitor Monitor) ShouldCheck() bool {
// and a possible AlertNotice
func (monitor *Monitor) Check() (bool, *AlertNotice) {
var cmd *exec.Cmd
if monitor.Command.Command != nil {
cmd = exec.Command(monitor.Command.Command[0], monitor.Command.Command[1:]...)
if monitor.Command != nil {
cmd = exec.Command(monitor.Command[0], monitor.Command[1:]...)
} else {
cmd = ShellCommand(monitor.Command.ShellCommand)
cmd = ShellCommand(monitor.CommandShell)
}
output, err := cmd.CombinedOutput()
@@ -66,20 +71,18 @@ func (monitor *Monitor) Check() (bool, *AlertNotice) {
alertNotice = monitor.failure()
}
if LogDebug {
log.Printf("DEBUG: Command output: %s", monitor.lastOutput)
}
log.Debugf("Command output: %s", monitor.lastOutput)
if err != nil {
if LogDebug {
log.Printf("DEBUG: Command result: %v", err)
}
log.Debugf("Command result: %v", err)
}
log.Printf(
"INFO: %s success=%t, alert=%t",
log.WithFields(log.Fields{
"monitor": monitor.Name,
"success": isSuccess,
"alert": alertNotice != nil,
}).Infof(
"%s checked",
monitor.Name,
isSuccess,
alertNotice != nil,
)
return isSuccess, alertNotice
@@ -105,15 +108,13 @@ func (monitor *Monitor) failure() (notice *AlertNotice) {
monitor.failureCount++
// If we haven't hit the minimum failures, we can exit
if monitor.failureCount < monitor.getAlertAfter() {
if LogDebug {
log.Printf(
"DEBUG: %s failed but did not hit minimum failures. "+
log.Debugf(
"%s failed but did not hit minimum failures. "+
"Count: %v alert after: %v",
monitor.Name,
monitor.failureCount,
monitor.getAlertAfter(),
)
}
return
}
+13 -8
View File
@@ -13,11 +13,16 @@ func TestMonitorIsValid(t *testing.T) {
expected bool
name string
}{
{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{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{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 {
@@ -249,22 +254,22 @@ func TestMonitorCheck(t *testing.T) {
name string
}{
{
Monitor{Command: CommandOrShell{Command: []string{"echo", "success"}}},
Monitor{Command: []string{"echo", "success"}},
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
"Test successful command",
},
{
Monitor{Command: CommandOrShell{ShellCommand: "echo success"}},
Monitor{CommandShell: "echo success"},
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
"Test successful command shell",
},
{
Monitor{Command: CommandOrShell{Command: []string{"total", "failure"}}},
Monitor{Command: []string{"total", "failure"}},
expected{isSuccess: false, hasNotice: true, lastOutput: ""},
"Test failed command",
},
{
Monitor{Command: CommandOrShell{ShellCommand: "false"}},
Monitor{CommandShell: "false"},
expected{isSuccess: false, hasNotice: true, lastOutput: ""},
"Test failed command shell",
},
+2 -2
View File
@@ -25,7 +25,7 @@ alerts:
email_up:
command: [sendmail, "me@minitor.mon", "Recovered: {monitor_name}", "We're back!"]
mailgun_down:
command: >
command_shell: >
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: >
command_shell: >
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"
+5 -6
View File
@@ -1,23 +1,22 @@
---
check_interval: 1
monitors:
- name: Command
command: ['echo', '$PATH']
alert_down: ['log_command', 'log_shell']
alert_down: [ 'log_command', 'log_shell' ]
alert_every: 0
- name: Shell
command: >
command_shell: >
echo 'Some string with stuff';
echo 'another line';
echo $PATH;
exit 1
alert_down: ['log_command', 'log_shell']
alert_down: [ 'log_command', 'log_shell' ]
alert_after: 5
alert_every: 0
alerts:
log_command:
command: ['echo', 'regular', '"command!!!"', "{{.MonitorName}}"]
command: [ 'echo', 'regular', '"command!!!"', "{{.MonitorName}}" ]
log_shell:
command: echo "Failure on {{.MonitorName}} User is $USER"
command_shell: echo "Failure on {{.MonitorName}} User is $USER"
-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"
)
// 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`
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)
return exec.Command(shellCommand[0], shellCommand[1:]...)
}