Compare commits

..

3 Commits

Author SHA1 Message Date
Ian Fijolek
7cac6c94d7 Update readme and update some test files to be better examples 2024-11-15 12:05:17 -08:00
Ian Fijolek
e0af17a599 Refactor test package and some field types
Fairly big test refactor and changing some of the fields from pointers
2024-11-15 11:37:03 -08:00
Ian Fijolek
a0a6b8199a WIP: Try migration to hcl 2024-11-14 13:42:50 -08:00
26 changed files with 416 additions and 744 deletions
+86 -39
View File
@@ -1,53 +1,100 @@
---
kind: pipeline kind: pipeline
type: kubernetes name: test
name: default
node_selector: steps:
zone: dev - name: test
image: golang:1.20
environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
commands:
- make test
- name: check
image: iamthefij/drone-pre-commit:personal
---
kind: pipeline
name: publish
depends_on:
- test
trigger: trigger:
event: event:
- push - push
- tag - tag
refs:
workspace: - refs/heads/master
path: /drone/src - refs/tags/v*
steps: steps:
- name: pull image to dockerhub - name: build all binaries
image: docker.io/owncloudci/drone-docker-buildx:4 image: golang:1.20
privileged: true environment:
settings: VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
cache-from: [ "safebox/minitor" ] commands:
repo: safebox/minitor - make all
tags: latest
username:
from_secret: dockerhub-username
password:
from_secret: dockerhub-password
platforms:
- linux/amd64
- linux/arm64
when:
event:
- tag
- name: build multiarch from dev - name: compress binaries for release
image: docker.io/owncloudci/drone-docker-buildx:4 image: ubuntu
privileged: true commands:
- find ./dist -type f -executable -execdir tar -czvf {}.tar.gz {} \;
when:
event: tag
- name: upload gitea release
image: plugins/gitea-release
settings: settings:
cache-from: [ "registry.dev.format.hu/minitor" ] title: ${DRONE_TAG}
registry: registry.dev.format.hu files: dist/*.tar.gz
repo: registry.dev.format.hu/minitor checksum:
tags: latest - md5
dockerfile: Dockerfile - sha1
username: - sha256
from_secret: dev-hu-registry-username - sha512
password: base_url:
from_secret: dev-hu-registry-password from_secret: gitea_base_url
api_key:
from_secret: gitea_token
when:
event: tag
- name: Build and publish docker images
image: thegeeklab/drone-docker-buildx
settings:
repo: iamthefij/minitor-go
auto_tag: true
platforms: platforms:
- linux/amd64 - linux/amd64
- linux/arm64 - linux/arm64
when: - linux/arm
event: username:
- push from_secret: docker_username
password:
from_secret: docker_password
---
kind: pipeline
name: notify
depends_on:
- test
- publish
trigger:
status:
- failure
steps:
- name: notify
image: drillster/drone-email
settings:
host:
from_secret: SMTP_HOST # pragma: whitelist secret
username:
from_secret: SMTP_USER # pragma: whitelist secret
password:
from_secret: SMTP_PASS # pragma: whitelist secret
from: drone@iamthefij.com
Vendored
-1
View File
@@ -14,7 +14,6 @@
# User configuration # User configuration
config.yml config.yml
config.hcl
# Output binary # Output binary
minitor minitor
+12 -28
View File
@@ -1,13 +1,16 @@
version: "2" ---
linters: linters:
enable: enable:
- errname - errname
- errorlint - errorlint
- exhaustive - exhaustive
- gofumpt
- goimports
- gomnd
- goprintffuncname - goprintffuncname
- misspell - misspell
- mnd
- tagliatelle - tagliatelle
- tenv
- testpackage - testpackage
- thelper - thelper
- tparallel - tparallel
@@ -16,37 +19,18 @@ linters:
- wsl - wsl
disable: disable:
- gochecknoglobals - gochecknoglobals
settings:
linters-settings:
gosec: gosec:
excludes: excludes:
- G204 - G204
tagliatelle: tagliatelle:
case: case:
rules: rules:
json: snake
yaml: snake yaml: snake
exclusions:
generated: lax issues:
presets: exclude-rules:
- comments - path: _test\.go
- common-false-positives linters:
- legacy
- std-error-handling
rules:
- linters:
- gosec - gosec
path: _test\.go
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
+4 -4
View File
@@ -1,7 +1,7 @@
--- ---
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 rev: v4.4.0
hooks: hooks:
- id: check-added-large-files - id: check-added-large-files
- id: check-yaml - id: check-yaml
@@ -11,10 +11,10 @@ repos:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: check-merge-conflict - id: check-merge-conflict
- repo: https://github.com/golangci/golangci-lint - repo: https://github.com/golangci/golangci-lint
rev: v2.7.2 rev: v1.52.2
hooks: hooks:
- id: golangci-lint - id: golangci-lint
- repo: https://github.com/hadolint/hadolint - repo: https://github.com/hadolint/hadolint
rev: refs/pull/1152/head rev: v2.12.1-beta
hooks: hooks:
- id: hadolint-github - id: hadolint
+7 -113
View File
@@ -1,129 +1,23 @@
FROM golang:1.25 AS gomail-builder FROM alpine:3.18
ARG TARGETARCH=amd64
ARG TARGETOS=linux
WORKDIR /gomail
RUN { \
echo 'package main'; \
echo ''; \
echo 'import ('; \
echo ' "fmt"'; \
echo ' "io"'; \
echo ' "net/mail"'; \
echo ' "net/smtp"'; \
echo ' "os"'; \
echo ' "strings"'; \
echo ')'; \
echo ''; \
echo 'func main() {'; \
echo ' readHeaders := strings.Contains(strings.Join(os.Args[1:], " "), "-t")'; \
echo ' recipients := []string{}'; \
echo ' for _, arg := range os.Args[1:] {'; \
echo ' if !strings.HasPrefix(arg, "-") {'; \
echo ' recipients = append(recipients, arg)'; \
echo ' }'; \
echo ' }'; \
echo ''; \
echo ' body, err := io.ReadAll(os.Stdin)'; \
echo ' if err != nil {'; \
echo ' fmt.Fprintln(os.Stderr, err)'; \
echo ' os.Exit(1)'; \
echo ' }'; \
echo ''; \
echo ' if readHeaders {'; \
echo ' msg, parseErr := mail.ReadMessage(strings.NewReader(string(body)))'; \
echo ' if parseErr != nil {'; \
echo ' fmt.Fprintln(os.Stderr, parseErr)'; \
echo ' os.Exit(1)'; \
echo ' }'; \
echo ' for _, hdr := range []string{"To", "Cc", "Bcc"} {'; \
echo ' if val := msg.Header.Get(hdr); val != "" {'; \
echo ' addrs, addrErr := mail.ParseAddressList(val)'; \
echo ' if addrErr != nil {'; \
echo ' fmt.Fprintln(os.Stderr, addrErr)'; \
echo ' os.Exit(1)'; \
echo ' }'; \
echo ' for _, addr := range addrs {'; \
echo ' recipients = append(recipients, addr.Address)'; \
echo ' }'; \
echo ' }'; \
echo ' }'; \
echo ' }'; \
echo ''; \
echo ' if len(recipients) == 0 {'; \
echo ' fmt.Fprintln(os.Stderr, "usage: sendmail [-t] recipient...")'; \
echo ' os.Exit(1)'; \
echo ' }'; \
echo ''; \
echo ' relay := os.Getenv("SMTP_RELAY")'; \
echo ' if relay == "" {'; \
echo ' relay = "172.17.0.2"'; \
echo ' }'; \
echo ''; \
echo ' port := os.Getenv("SMTP_PORT")'; \
echo ' if port == "" {'; \
echo ' port = "25"'; \
echo ' }'; \
echo ''; \
echo ' sender := os.Getenv("EMAIL_FROM")'; \
echo ' if sender == "" {'; \
echo ' fmt.Fprintln(os.Stderr, "[sendmail] EMAIL_FROM is not set, skipping")'; \
echo ' os.Exit(0)'; \
echo ' }'; \
echo ''; \
echo ' debug := os.Getenv("DEBUG") != ""'; \
echo ' if debug {'; \
echo ' fmt.Fprintln(os.Stderr, fmt.Sprintf("[sendmail] relay=%s port=%s sender=%s recipients=%v", relay, port, sender, recipients))'; \
echo ' fmt.Fprintln(os.Stderr, "[sendmail] body:")'; \
echo ' fmt.Fprintln(os.Stderr, string(body))'; \
echo ' }'; \
echo ''; \
echo ' if err = smtp.SendMail(relay+":"+port, nil, sender, recipients, body); err != nil {'; \
echo ' fmt.Fprintln(os.Stderr, err)'; \
echo ' os.Exit(1)'; \
echo ' }'; \
echo ' if debug {'; \
echo ' fmt.Fprintln(os.Stderr, "[sendmail] sent successfully")'; \
echo ' }'; \
echo '}'; \
} > main.go
RUN go mod init gomail && \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /usr/local/bin/sendmail .
FROM golang:1.25 AS builder
WORKDIR /app
COPY ./go.mod ./go.sum /app/
RUN go mod download
COPY ./*.go /app/
RUN rm -f /app/gomail.go
ARG TARGETARCH=amd64
ARG TARGETOS=linux
ARG VERSION=dev
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags "-X main.version=${VERSION}" -a -installsuffix nocgo -o minitor .
FROM alpine:3.23
RUN mkdir /app RUN mkdir /app
WORKDIR /app/ WORKDIR /app/
# Copy minitor in
COPY --from=builder /app/minitor .
# Copy sendmail (gomail) in
COPY --from=gomail-builder /usr/local/bin/sendmail /usr/local/bin/sendmail
# Add common checking tools # Add common checking tools
# hadolint ignore=DL3018 RUN apk --no-cache add bash=~5 curl=~8 jq=~1 bind-tools=~9 tzdata~=2024a
RUN apk --no-cache add bash=~5 curl=~8 jq=~1 bind-tools=~9 tzdata
# Add minitor user for running as non-root # Add minitor user for running as non-root
RUN addgroup -S minitor && adduser -S minitor -G minitor RUN addgroup -S minitor && adduser -S minitor -G minitor
# Copy scripts # Copy scripts
COPY ./scripts /app/scripts COPY ./scripts /app/scripts
COPY default_config.hcl /app/config.hcl
RUN chmod -R 755 /app/scripts RUN chmod -R 755 /app/scripts
# Copy minitor in
ARG TARGETOS
ARG TARGETARCH
COPY ./dist/minitor-${TARGETOS}-${TARGETARCH} ./minitor
# Drop to non-root user # Drop to non-root user
USER minitor USER minitor
+38
View File
@@ -0,0 +1,38 @@
FROM golang:1.20 AS builder
WORKDIR /app
COPY ./go.mod ./go.sum /app/
RUN go mod download
COPY ./*.go /app/
ARG TARGETOS
ARG TARGETARCH
ARG VERSION=dev
ENV CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=${TARGETARCH}
RUN go build -ldflags "-X main.version=${VERSION}" -a -installsuffix nocgo -o minitor .
FROM alpine:3.18
RUN mkdir /app
WORKDIR /app/
# Copy minitor in
COPY --from=builder /app/minitor .
# Add common checking tools
RUN apk --no-cache add bash=~5 curl=~8 jq=~1 bind-tools=~9 tzdata~=2024a
# Add minitor user for running as non-root
RUN addgroup -S minitor && adduser -S minitor -G minitor
# Copy scripts
COPY ./scripts /app/scripts
RUN chmod -R 755 /app/scripts
# Drop to non-root user
USER minitor
ENTRYPOINT [ "./minitor" ]
# vim: set filetype=dockerfile:
+13 -49
View File
@@ -17,7 +17,7 @@ I'm running a few small services and found Sensu, Consul, Nagios, etc. to all be
Install and execute with: Install and execute with:
```bash ```bash
go install github.com/iamthefij/minitor-go/v2@latest go install github.com/iamthefij/minitor-go@latest
minitor minitor
``` ```
@@ -50,17 +50,15 @@ You can configure the timezone for the container by passing a `TZ` env variable.
## Configuring ## Configuring
In this repo, you can explore the `sample-config.hcl` file for an example, but the general structure is as follows. If you are passing environment variables to your commands or alerts, you should be aware that `${VAR}` syntax is reserved for HCL variable interpolation. To avoid issues, you can use `$${VAR}` syntax to escape the `$` character, simply use `$VAR`. In this repo, you can explore the `sample-config.hcl` file for an example, but the general structure is as follows. It should be noted that environment variable interpolation happens on load of the HCL file.
```hcl
The global configurations are: The global configurations are:
|key|value| |key|value|
|---|---| |---|---|
|`check_interval`|Maximum frequency to run checks for each monitor as duration, eg. 1m2s.| |`check_interval`|Maximum frequency to run checks for each monitor as duration, eg. 1m2s.|
|`default_alert_after`|A default value used as an `alert_after` value for a monitor if not specified. Defaults 1, which will alert immediately.| |`default_alert_after`|A default value used as an `alert_after` value for a monitor if not specified or 0.|
|`default_alert_every`|A default value used as an `alert_every` value for a monitor if not specified. Defaults to -1, which will re-alert exponentially.| |`default_alert_every`|A default value used as an `alert_every` value for a monitor if not specified.|
|`default_alert_down`|Default down alerts to used by a monitor in case none are provided.| |`default_alert_down`|Default down alerts to used by a monitor in case none are provided.|
|`default_alert_up`|Default up alerts to used by a monitor in case none are provided.| |`default_alert_up`|Default up alerts to used by a monitor in case none are provided.|
|`monitor`|block listing monitors. Detailed description below| |`monitor`|block listing monitors. Detailed description below|
@@ -77,7 +75,7 @@ monitor "example" {
alert_up = ["log"] alert_up = ["log"]
check_interval = "1m" check_interval = "1m"
alert_after = 1 alert_after = 1
alert_every = -1 alert_every = 0
} }
``` ```
@@ -169,48 +167,6 @@ minitor -metrics
minitor -metrics -metrics-port 3000 minitor -metrics -metrics-port 3000
``` ```
## Migrating from v1 to v2
Minitor v2 introduces some breaking changes from v1. The most notable changes are:
- The configuration file is now in HCL format instead of YAML.
- The the Python formatting backwards compatability is removed.
- The Command and ShellCommand fields are now mutually exclusive.
- The check_interval is now strictly a duration string value. Eg. "30s" rather than `30`.
- Default alert_every is now -1 (exponential backoff) rather than 0 (no re-alerting).
For the configuration, a confic that looked like this in v1:
```yaml
check_interval: 60
monitors:
- name: example
command: "false"
alert_down: ["log"]
alerts:
log:
command: ["echo", "Minitor up={{.IsUp}} for {{.MonitorName}}"]
```
Would now look like this in v2:
```hcl
check_interval = "1m"
monitor "example" {
# example showing string to shell command migration
shell_command = "false"
alert_down = ["log"]
check_interval = "1m"
}
alert "log" {
# example showing list to exec command migration
command = ["echo", "Minitor up={{.IsUp}} for {{.MonitorName}}"]
}
```
## Contributing ## Contributing
Whether you're looking to submit a patch or tell me I broke something, you can contribute through the Github mirror and I can merge PRs back to the source repository. Whether you're looking to submit a patch or tell me I broke something, you can contribute through the Github mirror and I can merge PRs back to the source repository.
@@ -218,3 +174,11 @@ Whether you're looking to submit a patch or tell me I broke something, you can c
Primary Repo: https://git.iamthefij.com/iamthefij/minitor.git Primary Repo: https://git.iamthefij.com/iamthefij/minitor.git
Github Mirror: https://github.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.
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.
+8 -27
View File
@@ -37,32 +37,13 @@ type AlertNotice struct {
LastCheckOutput string LastCheckOutput string
} }
// Validate checks that the Alert is properly configured and returns errors if not // IsValid returns a boolean indicating if the Alert has been correctly
func (alert Alert) Validate() error { // configured
hasCommand := len(alert.Command) > 0 func (alert Alert) IsValid() bool {
hasShellCommand := alert.ShellCommand != "" hasAtLeastOneCommand := alert.Command != nil || alert.ShellCommand != ""
hasAtMostOneCommand := alert.Command == nil || alert.ShellCommand == ""
var err error return hasAtLeastOneCommand && hasAtMostOneCommand
hasAtLeastOneCommand := hasCommand || hasShellCommand
if !hasAtLeastOneCommand {
err = errors.Join(err, fmt.Errorf(
"%w: alert %s has no command or shell_command configured",
ErrInvalidAlert,
alert.Name,
))
}
hasAtMostOneCommand := !(hasCommand && hasShellCommand)
if !hasAtMostOneCommand {
err = errors.Join(err, fmt.Errorf(
"%w: alert %s has both command and shell_command configured",
ErrInvalidAlert,
alert.Name,
))
}
return err
} }
// BuildTemplates compiles command templates for the Alert // BuildTemplates compiles command templates for the Alert
@@ -101,14 +82,14 @@ func (alert *Alert) BuildTemplates() error {
} }
switch { switch {
case alert.Command != nil: case alert.commandTemplate == nil && alert.Command != nil:
alert.commandTemplate = []*template.Template{} alert.commandTemplate = []*template.Template{}
for i, cmdPart := range alert.Command { for i, cmdPart := range alert.Command {
alert.commandTemplate = append(alert.commandTemplate, template.Must( alert.commandTemplate = append(alert.commandTemplate, template.Must(
template.New(alert.Name+fmt.Sprint(i)).Funcs(timeFormatFuncs).Parse(cmdPart), template.New(alert.Name+fmt.Sprint(i)).Funcs(timeFormatFuncs).Parse(cmdPart),
)) ))
} }
case alert.ShellCommand != "": case alert.commandShellTemplate == nil && alert.ShellCommand != "":
shellCmd := alert.ShellCommand shellCmd := alert.ShellCommand
alert.commandShellTemplate = template.Must( alert.commandShellTemplate = template.Must(
+8 -15
View File
@@ -1,24 +1,20 @@
package main_test package main_test
import ( import (
"errors"
"testing" "testing"
m "git.iamthefij.com/iamthefij/minitor-go/v2" m "git.iamthefij.com/iamthefij/minitor-go"
) )
func TestAlertValidate(t *testing.T) { func TestAlertIsValid(t *testing.T) {
t.Parallel()
cases := []struct { cases := []struct {
alert m.Alert alert m.Alert
expected error expected bool
name string name string
}{ }{
{m.Alert{Command: []string{"echo", "test"}}, nil, "Command only"}, {m.Alert{Command: []string{"echo", "test"}}, true, "Command only"},
{m.Alert{ShellCommand: "echo test"}, nil, "CommandShell only"}, {m.Alert{ShellCommand: "echo test"}, true, "CommandShell only"},
{m.Alert{Command: []string{"echo", "test"}, ShellCommand: "echo test"}, m.ErrInvalidAlert, "Both commands"}, {m.Alert{}, false, "No commands"},
{m.Alert{}, m.ErrInvalidAlert, "No commands"},
} }
for _, c := range cases { for _, c := range cases {
@@ -27,11 +23,8 @@ func TestAlertValidate(t *testing.T) {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
t.Parallel() t.Parallel()
actual := c.alert.Validate() actual := c.alert.IsValid()
hasErr := (actual != nil) if actual != c.expected {
expectErr := (c.expected != nil)
if hasErr != expectErr || !errors.Is(actual, c.expected) {
t.Errorf("expected=%t actual=%t", c.expected, actual) t.Errorf("expected=%t actual=%t", c.expected, actual)
} }
}) })
+111 -92
View File
@@ -6,26 +6,21 @@ import (
"time" "time"
"git.iamthefij.com/iamthefij/slog" "git.iamthefij.com/iamthefij/slog"
/*
* "github.com/hashicorp/hcl/v2"
* "github.com/hashicorp/hcl/v2/gohcl"
*/
"github.com/hashicorp/hcl/v2/hclsimple" "github.com/hashicorp/hcl/v2/hclsimple"
) )
var ( var errInvalidConfig = errors.New("Invalid configuration")
ErrLoadingConfig = errors.New("Failed to load or parse configuration")
ErrConfigInit = errors.New("Failed to initialize configuration")
ErrInvalidConfig = errors.New("Invalid configuration")
ErrNoAlerts = errors.New("No alerts provided")
ErrInvalidAlert = errors.New("Invalid alert configuration")
ErrNoMonitors = errors.New("No monitors provided")
ErrInvalidMonitor = errors.New("Invalid monitor configuration")
ErrUnknownAlert = errors.New("Unknown alert")
)
// Config type is contains all provided user configuration // Config type is contains all provided user configuration
type Config struct { type Config struct {
CheckIntervalStr string `hcl:"check_interval"` CheckIntervalStr string `hcl:"check_interval"`
CheckInterval time.Duration CheckInterval time.Duration
DefaultAlertAfter int `hcl:"default_alert_after,optional"` DefaultAlertAfter *int `hcl:"default_alert_after,optional"`
DefaultAlertEvery *int `hcl:"default_alert_every,optional"` DefaultAlertEvery *int `hcl:"default_alert_every,optional"`
DefaultAlertDown []string `hcl:"default_alert_down,optional"` DefaultAlertDown []string `hcl:"default_alert_down,optional"`
DefaultAlertUp []string `hcl:"default_alert_up,optional"` DefaultAlertUp []string `hcl:"default_alert_up,optional"`
@@ -35,77 +30,6 @@ type Config struct {
alertLookup map[string]*Alert alertLookup map[string]*Alert
} }
// Init performs extra initialization on top of loading the config from file
func (config *Config) Init() (err error) {
config.CheckInterval, err = time.ParseDuration(config.CheckIntervalStr)
if err != nil {
return fmt.Errorf("failed to parse top level check_interval duration: %w", err)
}
if config.DefaultAlertAfter == 0 {
minAlertAfter := 1
config.DefaultAlertAfter = minAlertAfter
}
if config.DefaultAlertEvery == nil {
defaultDefaultAlertEvery := -1
config.DefaultAlertEvery = &defaultDefaultAlertEvery
}
for _, monitor := range config.Monitors {
if err = monitor.Init(
config.DefaultAlertAfter,
config.DefaultAlertEvery,
config.DefaultAlertDown,
config.DefaultAlertUp,
); err != nil {
return
}
}
err = config.BuildAllTemplates()
return
}
// IsValid checks config validity and returns true if valid
func (config Config) IsValid() error {
var err error
// Validate alerts
if len(config.Alerts) == 0 {
err = errors.Join(err, ErrNoAlerts)
}
for _, alert := range config.Alerts {
err = errors.Join(err, alert.Validate())
}
// Validate monitors
if len(config.Monitors) == 0 {
err = errors.Join(err, ErrNoMonitors)
}
for _, monitor := range config.Monitors {
err = errors.Join(err, monitor.Validate())
// Check that all Monitor alerts actually exist
for _, isUp := range []bool{true, false} {
for _, alertName := range monitor.GetAlertNames(isUp) {
if _, ok := config.GetAlert(alertName); !ok {
err = errors.Join(
err,
fmt.Errorf("%w: %s. %w: %s", ErrInvalidMonitor, monitor.Name, ErrUnknownAlert, alertName),
)
}
}
}
}
return err
}
// GetAlert returns an alert by name
func (c Config) GetAlert(name string) (*Alert, bool) { func (c Config) GetAlert(name string) (*Alert, bool) {
if c.alertLookup == nil { if c.alertLookup == nil {
c.alertLookup = map[string]*Alert{} c.alertLookup = map[string]*Alert{}
@@ -130,24 +54,119 @@ func (c *Config) BuildAllTemplates() (err error) {
return return
} }
// LoadConfig will read config from the given path and parse it // IsValid checks config validity and returns true if valid
func LoadConfig(filePath string) (Config, error) { func (config Config) IsValid() (isValid bool) {
var config Config isValid = true
if err := hclsimple.DecodeFile(filePath, nil, &config); err != nil { // Validate alerts
return config, errors.Join(ErrLoadingConfig, err) if len(config.Alerts) == 0 {
// This should never happen because there is a default alert named 'log' for now
slog.Errorf("Invalid alert configuration: Must provide at least one alert")
isValid = false
}
for _, alert := range config.Alerts {
if !alert.IsValid() {
slog.Errorf("Invalid alert configuration: %+v", alert.Name)
isValid = false
}
}
// Validate monitors
if len(config.Monitors) == 0 {
slog.Errorf("Invalid monitor configuration: Must provide at least one monitor")
isValid = false
}
for _, monitor := range config.Monitors {
if !monitor.IsValid() {
slog.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.GetAlert(alertName); !ok {
slog.Errorf(
"Invalid monitor configuration: %s. Unknown alert %s",
monitor.Name, alertName,
)
isValid = false
}
}
}
}
return isValid
}
// Init performs extra initialization on top of loading the config from file
func (config *Config) Init() (err error) {
config.CheckInterval, err = time.ParseDuration(config.CheckIntervalStr)
if err != nil {
return fmt.Errorf("failed to parse top level check_interval duration: %w", err)
}
for _, monitor := range config.Monitors {
// TODO: Move this to a Monitor.Init() method
// Parse the check_interval string into a time.Duration
if monitor.CheckIntervalStr != nil {
monitor.CheckInterval, err = time.ParseDuration(*monitor.CheckIntervalStr)
if err != nil {
return fmt.Errorf("failed to parse check_interval duration for monitor %s: %w", monitor.Name, err)
}
}
// Set default values for monitor alerts
if monitor.AlertAfter == 0 && config.DefaultAlertAfter != nil {
monitor.AlertAfter = *config.DefaultAlertAfter
} else if monitor.AlertAfter == 0 {
monitor.AlertAfter = 1
}
if monitor.AlertEvery == nil {
monitor.AlertEvery = config.DefaultAlertEvery
}
if monitor.AlertDown == nil {
monitor.AlertDown = config.DefaultAlertDown
}
if monitor.AlertUp == nil {
monitor.AlertUp = config.DefaultAlertUp
}
}
err = config.BuildAllTemplates()
return
}
// LoadConfig will read config from the given path and parse it
func LoadConfig(filePath string) (config Config, err error) {
err = hclsimple.DecodeFile(filePath, nil, &config)
if err != nil {
return
} }
slog.Debugf("Config values:\n%v\n", config) slog.Debugf("Config values:\n%v\n", config)
// Finish initializing configuration // Finish initializing configuration
if err := config.Init(); err != nil { if err = config.Init(); err != nil {
return config, errors.Join(ErrConfigInit, err) return
} }
if err := config.IsValid(); err != nil { if !config.IsValid() {
return config, errors.Join(ErrInvalidConfig, err) err = errInvalidConfig
return
} }
return config, nil return config, err
} }
+10 -100
View File
@@ -1,27 +1,23 @@
package main_test package main_test
import ( import (
"errors"
"testing" "testing"
"time"
m "git.iamthefij.com/iamthefij/minitor-go/v2" m "git.iamthefij.com/iamthefij/minitor-go"
) )
func TestLoadConfig(t *testing.T) { func TestLoadConfig(t *testing.T) {
cases := []struct { cases := []struct {
configPath string configPath string
expectedErr error expectErr bool
name string name string
}{ }{
{"./test/does-not-exist", m.ErrLoadingConfig, "Invalid config path"}, {"./test/does-not-exist", true, "Invalid config path"},
{"./test/invalid-config-wrong-hcl-type.hcl", m.ErrLoadingConfig, "Incorrect HCL type"}, {"./test/invalid-config-missing-alerts.hcl", true, "Invalid config missing alerts"},
{"./test/invalid-config-missing-alerts.hcl", m.ErrNoAlerts, "Invalid config missing alerts"}, {"./test/invalid-config-type.hcl", true, "Invalid config type for key"},
{"./test/invalid-config-missing-alerts.hcl", m.ErrInvalidConfig, "Invalid config general"}, {"./test/invalid-config-unknown-alert.hcl", true, "Invalid config unknown alert"},
{"./test/invalid-config-invalid-duration.hcl", m.ErrConfigInit, "Invalid config type for key"}, {"./test/valid-config-default-values.hcl", false, "Valid config file with default values"},
{"./test/invalid-config-unknown-alert.hcl", m.ErrUnknownAlert, "Invalid config unknown alert"}, {"./test/valid-config.hcl", false, "Valid config file"},
{"./test/valid-config-default-values.hcl", nil, "Valid config file with default values"},
{"./test/valid-config.hcl", nil, "Valid config file"},
} }
for _, c := range cases { for _, c := range cases {
c := c c := c
@@ -31,100 +27,14 @@ func TestLoadConfig(t *testing.T) {
_, err := m.LoadConfig(c.configPath) _, err := m.LoadConfig(c.configPath)
hasErr := (err != nil) hasErr := (err != nil)
expectErr := (c.expectedErr != nil)
if hasErr != expectErr || !errors.Is(err, c.expectedErr) { if hasErr != c.expectErr {
t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectedErr, err) t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectErr, err)
} }
}) })
} }
} }
func TestDefaultConfig(t *testing.T) {
cases := []struct {
configPath string
expectedResult m.Config
name string
}{
{
"./test/valid-config-default-values.hcl",
m.Config{
CheckInterval: 1 * time.Second,
DefaultAlertAfter: 2,
DefaultAlertEvery: Ptr(0),
DefaultAlertDown: []string{"log_command"},
},
"override defaults",
},
{
"./test/valid-config.hcl",
m.Config{
CheckInterval: 30 * time.Second,
DefaultAlertAfter: 1,
DefaultAlertEvery: Ptr(-1),
DefaultAlertDown: []string{},
},
"default defaults",
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
config, err := m.LoadConfig(c.configPath)
if err != nil {
t.Errorf("Got error when loading config file %q: %s", c.configPath, err)
}
// Test Config has default values
if config.DefaultAlertAfter != c.expectedResult.DefaultAlertAfter {
t.Errorf("Got unexpected DefaultAlertAfter from file %q: expected=%v actual=%v", c.configPath, c.expectedResult.DefaultAlertAfter, config.DefaultAlertAfter)
}
if *config.DefaultAlertEvery != *c.expectedResult.DefaultAlertEvery {
t.Errorf("Got unexpected DefaultAlertEvery from file %q: expected=%v actual=%v", c.configPath, *c.expectedResult.DefaultAlertEvery, *config.DefaultAlertEvery)
}
if !m.EqualSliceString(config.DefaultAlertUp, c.expectedResult.DefaultAlertUp) {
t.Errorf("Got unexpected DefaultAlertUp from file %q: expected=%v actual=%v", c.configPath, c.expectedResult.DefaultAlertUp, config.DefaultAlertUp)
}
if !m.EqualSliceString(config.DefaultAlertDown, c.expectedResult.DefaultAlertDown) {
t.Errorf("Got unexpected DefaultAlertDown from file %q: expected=%v actual=%v", c.configPath, c.expectedResult.DefaultAlertDown, config.DefaultAlertDown)
}
// Check that monitor defaults propagate
var defaultMonitor *m.Monitor
for _, monitor := range config.Monitors {
if monitor.Name == "Default" {
defaultMonitor = monitor
}
}
if defaultMonitor == nil {
t.Errorf("failed to find default monitor in %q", c.configPath)
}
if defaultMonitor.AlertAfter != c.expectedResult.DefaultAlertAfter {
t.Errorf("Got unexpected AlertAfter from file %q: expected=%v actual=%v", c.configPath, c.expectedResult.DefaultAlertAfter, defaultMonitor.AlertAfter)
}
if *defaultMonitor.AlertEvery != *c.expectedResult.DefaultAlertEvery {
t.Errorf("Got unexpected AlertEvery from file %q: expected=%v actual=%v", c.configPath, *c.expectedResult.DefaultAlertEvery, *defaultMonitor.AlertEvery)
}
if !m.EqualSliceString(defaultMonitor.AlertUp, c.expectedResult.DefaultAlertUp) {
t.Errorf("Got unexpected AlertUp from file %q: expected=%v actual=%v", c.configPath, c.expectedResult.DefaultAlertUp, defaultMonitor.AlertUp)
}
// NOTE: Can't compare AlertDown because default is empty and that is invalid
})
}
}
// TestMultiLineConfig is a more complicated test stepping through the parsing // TestMultiLineConfig is a more complicated test stepping through the parsing
// and execution of mutli-line strings presented in YAML // and execution of mutli-line strings presented in YAML
func TestMultiLineConfig(t *testing.T) { func TestMultiLineConfig(t *testing.T) {
-29
View File
@@ -1,29 +0,0 @@
check_interval = "1s"
monitor "mdstat_raid" {
command = [
"sh",
"-c",
"grep -q '\\[U_\\|_U\\]' /host_proc/mdstat && exit 1 || exit 0"
]
check_interval = "30s"
alert_after = 1
alert_every = 0
alert_down = ["email_alert"]
alert_up = ["email_recovery"]
}
alert "email_alert" {
command = [
"sh",
"-c",
"EMAIL=$EMAIL_RECIPIENT; printf 'Subject: RAID ALERT\nTo: %s\n\nRAID degraded\n' \"$EMAIL\" | /usr/local/bin/sendmail -t || true"
]
}
alert "email_recovery" {
command = [
"sh",
"-c",
"EMAIL=$EMAIL_RECIPIENT; printf 'Subject: RAID ALERT\nTo: %s\n\nRAID clean\n' \"$EMAIL\" | /usr/local/bin/sendmail -t || true"
]
}
+11 -13
View File
@@ -1,27 +1,25 @@
module git.iamthefij.com/iamthefij/minitor-go/v2 module git.iamthefij.com/iamthefij/minitor-go
go 1.25.0 go 1.20
require ( require (
git.iamthefij.com/iamthefij/slog v1.3.0 git.iamthefij.com/iamthefij/slog v1.3.0
github.com/hashicorp/hcl/v2 v2.11.1 github.com/hashicorp/hcl/v2 v2.11.1
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.19.0
) )
require ( require (
github.com/agext/levenshtein v1.2.1 // indirect github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.12.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/zclconf/go-cty v1.8.0 // indirect github.com/zclconf/go-cty v1.8.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/sys v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/text v0.28.0 // indirect google.golang.org/protobuf v1.32.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
) )
+19 -38
View File
@@ -8,8 +8,8 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
@@ -18,43 +18,33 @@ github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJbcc= github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJbcc=
github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
@@ -62,10 +52,6 @@ github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q
github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA= github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA=
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -76,21 +62,16 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+2 -2
View File
@@ -119,7 +119,7 @@ func SendStartupAlerts(config *Config, alertNames []string) error {
func main() { func main() {
showVersion := flag.Bool("version", false, "Display the version of minitor and exit") showVersion := flag.Bool("version", false, "Display the version of minitor and exit")
configPath := flag.String("config", "config.hcl", "Alternate configuration path (default: config.hcl)") configPath := flag.String("config", "config.yml", "Alternate configuration path (default: config.yml)")
startupAlerts := flag.String("startup-alerts", "", "List of alerts to run on startup. This can help determine unhealthy alerts early on. (default \"\")") startupAlerts := flag.String("startup-alerts", "", "List of alerts to run on startup. This can help determine unhealthy alerts early on. (default \"\")")
flag.BoolVar(&slog.DebugLevel, "debug", false, "Enables debug logs (default: false)") flag.BoolVar(&slog.DebugLevel, "debug", false, "Enables debug logs (default: false)")
@@ -136,7 +136,7 @@ func main() {
// Load configuration // Load configuration
config, err := LoadConfig(*configPath) config, err := LoadConfig(*configPath)
slog.OnErrFatalf(err, "Error loading config") slog.OnErrFatalf(err, "Error loading config: %v", err)
// Serve metrics exporter, if specified // Serve metrics exporter, if specified
if ExportMetrics { if ExportMetrics {
+1 -1
View File
@@ -3,7 +3,7 @@ package main_test
import ( import (
"testing" "testing"
m "git.iamthefij.com/iamthefij/minitor-go/v2" m "git.iamthefij.com/iamthefij/minitor-go"
) )
func Ptr[T any](v T) *T { func Ptr[T any](v T) *T {
+31 -94
View File
@@ -1,8 +1,6 @@
package main package main
import ( import (
"errors"
"fmt"
"math" "math"
"os/exec" "os/exec"
"time" "time"
@@ -17,7 +15,6 @@ type Monitor struct { //nolint:maligned
CheckInterval time.Duration CheckInterval time.Duration
Name string `hcl:"name,label"` Name string `hcl:"name,label"`
AlertCount int
AlertAfter int `hcl:"alert_after,optional"` AlertAfter int `hcl:"alert_after,optional"`
AlertEvery *int `hcl:"alert_every,optional"` AlertEvery *int `hcl:"alert_every,optional"`
AlertDown []string `hcl:"alert_down,optional"` AlertDown []string `hcl:"alert_down,optional"`
@@ -26,6 +23,7 @@ type Monitor struct { //nolint:maligned
ShellCommand string `hcl:"shell_command,optional"` ShellCommand string `hcl:"shell_command,optional"`
// Other values // Other values
alertCount int
failureCount int failureCount int
lastCheck time.Time lastCheck time.Time
lastSuccess time.Time lastSuccess time.Time
@@ -33,91 +31,30 @@ type Monitor struct { //nolint:maligned
lastCheckDuration time.Duration lastCheckDuration time.Duration
} }
// Init initializes the Monitor with default values // IsValid returns a boolean indicating if the Monitor has been correctly
func (monitor *Monitor) Init(defaultAlertAfter int, defaultAlertEvery *int, defaultAlertDown []string, defaultAlertUp []string) error { // configured
// Parse the check_interval string into a time.Duration func (monitor Monitor) IsValid() bool {
if monitor.CheckIntervalStr != nil { // TODO: Refactor and return an error containing more information on what was invalid
var err error
monitor.CheckInterval, err = time.ParseDuration(*monitor.CheckIntervalStr)
if err != nil {
return fmt.Errorf("failed to parse check_interval duration for monitor %s: %w", monitor.Name, err)
}
}
// Set default values for monitor alerts
if monitor.AlertAfter == 0 {
minAlertAfter := 1
monitor.AlertAfter = max(defaultAlertAfter, minAlertAfter)
}
if monitor.AlertEvery == nil {
monitor.AlertEvery = defaultAlertEvery
}
if len(monitor.AlertDown) == 0 {
monitor.AlertDown = defaultAlertDown
}
if len(monitor.AlertUp) == 0 {
monitor.AlertUp = defaultAlertUp
}
return nil
}
// Validate checks that the Monitor is properly configured and returns errors if not
func (monitor Monitor) Validate() error {
hasCommand := len(monitor.Command) > 0 hasCommand := len(monitor.Command) > 0
hasShellCommand := monitor.ShellCommand != "" hasShellCommand := monitor.ShellCommand != ""
hasValidAlertAfter := monitor.AlertAfter > 0 hasValidAlertAfter := monitor.AlertAfter > 0
hasAlertDown := len(monitor.AlertDown) > 0 hasAlertDown := len(monitor.AlertDown) > 0
var err error
hasAtLeastOneCommand := hasCommand || hasShellCommand hasAtLeastOneCommand := hasCommand || hasShellCommand
if !hasAtLeastOneCommand {
err = errors.Join(err, fmt.Errorf(
"%w: monitor %s has no command or shell_command configured",
ErrInvalidMonitor,
monitor.Name,
))
}
hasAtMostOneCommand := !(hasCommand && hasShellCommand) hasAtMostOneCommand := !(hasCommand && hasShellCommand)
if !hasAtMostOneCommand {
err = errors.Join(err, fmt.Errorf(
"%w: monitor %s has both command and shell_command configured",
ErrInvalidMonitor,
monitor.Name,
))
}
if !hasValidAlertAfter { return hasAtLeastOneCommand &&
err = errors.Join(err, fmt.Errorf( hasAtMostOneCommand &&
"%w: monitor %s has invalid alert_after value %d. Must be greater than 0", hasValidAlertAfter &&
ErrInvalidMonitor, hasAlertDown
monitor.Name,
monitor.AlertAfter,
))
}
if !hasAlertDown {
err = errors.Join(err, fmt.Errorf(
"%w: monitor %s has no alert_down configured. Configure one here or add a default_alert_down",
ErrInvalidMonitor,
monitor.Name,
))
}
return err
} }
func (monitor Monitor) LastOutput() string { func (monitor Monitor) LastOutput() string {
return monitor.lastOutput return monitor.lastOutput
} }
// ShouldCheck returns a boolean indicating if the Monitor is ready to be be checked again // ShouldCheck returns a boolean indicating if the Monitor is ready to be
// be checked again
func (monitor Monitor) ShouldCheck() bool { func (monitor Monitor) ShouldCheck() bool {
if monitor.lastCheck.IsZero() || monitor.CheckInterval == 0 { if monitor.lastCheck.IsZero() || monitor.CheckInterval == 0 {
return true return true
@@ -128,7 +65,8 @@ func (monitor Monitor) ShouldCheck() bool {
return sinceLastCheck >= monitor.CheckInterval return sinceLastCheck >= monitor.CheckInterval
} }
// Check will run the command configured by the Monitor and return a status and a possible AlertNotice // Check will run the command configured by the Monitor and return a status
// 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 len(monitor.Command) > 0 { if len(monitor.Command) > 0 {
@@ -149,9 +87,9 @@ func (monitor *Monitor) Check() (bool, *AlertNotice) {
isSuccess := (err == nil) isSuccess := (err == nil)
if isSuccess { if isSuccess {
alertNotice = monitor.Success() alertNotice = monitor.success()
} else { } else {
alertNotice = monitor.Failure() alertNotice = monitor.failure()
} }
slog.Debugf("Command output: %s", monitor.lastOutput) slog.Debugf("Command output: %s", monitor.lastOutput)
@@ -167,18 +105,9 @@ func (monitor *Monitor) Check() (bool, *AlertNotice) {
return isSuccess, alertNotice return isSuccess, alertNotice
} }
// GetAlertNames gives a list of alert names for a given monitor status
func (monitor Monitor) GetAlertNames(up bool) []string {
if up {
return monitor.AlertUp
}
return monitor.AlertDown
}
// IsUp returns the status of the current monitor // IsUp returns the status of the current monitor
func (monitor Monitor) IsUp() bool { func (monitor Monitor) IsUp() bool {
return monitor.AlertCount == 0 return monitor.alertCount == 0
} }
// LastCheckMilliseconds gives number of miliseconds the last check ran for // LastCheckMilliseconds gives number of miliseconds the last check ran for
@@ -186,20 +115,20 @@ func (monitor Monitor) LastCheckMilliseconds() int64 {
return monitor.lastCheckDuration.Milliseconds() return monitor.lastCheckDuration.Milliseconds()
} }
func (monitor *Monitor) Success() (notice *AlertNotice) { func (monitor *Monitor) success() (notice *AlertNotice) {
if !monitor.IsUp() { if !monitor.IsUp() {
// Alert that we have recovered // Alert that we have recovered
notice = monitor.createAlertNotice(true) notice = monitor.createAlertNotice(true)
} }
monitor.failureCount = 0 monitor.failureCount = 0
monitor.AlertCount = 0 monitor.alertCount = 0
monitor.lastSuccess = time.Now() monitor.lastSuccess = time.Now()
return return
} }
func (monitor *Monitor) Failure() (notice *AlertNotice) { 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.AlertAfter { if monitor.failureCount < monitor.AlertAfter {
@@ -231,25 +160,33 @@ func (monitor *Monitor) Failure() (notice *AlertNotice) {
} }
default: default:
// Handle negative numbers indicating an exponential backoff // Handle negative numbers indicating an exponential backoff
if failureCount >= int(math.Pow(2, float64(monitor.AlertCount))-1) { //nolint:mnd if failureCount >= int(math.Pow(2, float64(monitor.alertCount))-1) { //nolint:gomnd
notice = monitor.createAlertNotice(false) notice = monitor.createAlertNotice(false)
} }
} }
// If we're going to alert, increment count // If we're going to alert, increment count
if notice != nil { if notice != nil {
monitor.AlertCount++ monitor.alertCount++
notice.AlertCount = monitor.AlertCount
} }
return notice return notice
} }
// GetAlertNames gives a list of alert names for a given monitor status
func (monitor Monitor) GetAlertNames(up bool) []string {
if up {
return monitor.AlertUp
}
return monitor.AlertDown
}
func (monitor Monitor) createAlertNotice(isUp bool) *AlertNotice { func (monitor Monitor) createAlertNotice(isUp bool) *AlertNotice {
// TODO: Maybe add something about recovery status here // TODO: Maybe add something about recovery status here
return &AlertNotice{ return &AlertNotice{
MonitorName: monitor.Name, MonitorName: monitor.Name,
AlertCount: monitor.AlertCount, AlertCount: monitor.alertCount,
FailureCount: monitor.failureCount, FailureCount: monitor.failureCount,
LastCheckOutput: monitor.lastOutput, LastCheckOutput: monitor.lastOutput,
LastSuccess: monitor.lastSuccess, LastSuccess: monitor.lastSuccess,
+11 -55
View File
@@ -1,27 +1,25 @@
package main_test package main_test
import ( import (
"errors"
"reflect" "reflect"
"testing" "testing"
"time" "time"
m "git.iamthefij.com/iamthefij/minitor-go/v2" m "git.iamthefij.com/iamthefij/minitor-go"
) )
func TestMonitorValidate(t *testing.T) { // TestMonitorIsValid tests the Monitor.IsValid()
t.Parallel() func TestMonitorIsValid(t *testing.T) {
cases := []struct { cases := []struct {
monitor m.Monitor monitor m.Monitor
expected error expected bool
name string name string
}{ }{
{m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, nil, "Command only"}, {m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, true, "Command only"},
{m.Monitor{AlertAfter: 1, ShellCommand: "echo test", AlertDown: []string{"log"}}, nil, "CommandShell only"}, {m.Monitor{AlertAfter: 1, ShellCommand: "echo test", AlertDown: []string{"log"}}, true, "CommandShell only"},
{m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}}, m.ErrInvalidMonitor, "No AlertDown"}, {m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}}, false, "No AlertDown"},
{m.Monitor{AlertAfter: 1, AlertDown: []string{"log"}}, m.ErrInvalidMonitor, "No commands"}, {m.Monitor{AlertAfter: 1, AlertDown: []string{"log"}}, false, "No commands"},
{m.Monitor{AlertAfter: -1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, m.ErrInvalidMonitor, "Invalid alert threshold, -1"}, {m.Monitor{AlertAfter: -1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, false, "Invalid alert threshold, -1"},
} }
for _, c := range cases { for _, c := range cases {
@@ -30,11 +28,8 @@ func TestMonitorValidate(t *testing.T) {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
t.Parallel() t.Parallel()
actual := c.monitor.Validate() actual := c.monitor.IsValid()
hasErr := (actual != nil) if actual != c.expected {
expectErr := (c.expected != nil)
if hasErr != expectErr || !errors.Is(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)
} }
}) })
@@ -116,45 +111,6 @@ func TestMonitorGetAlertNames(t *testing.T) {
} }
} }
func TestMonitorAlertCount(t *testing.T) {
alertEvery := 1
cases := []struct {
checkSuccess bool
alertCount int
name string
}{
{false, 1, "First failure and first alert"},
{false, 2, "Second failure and first alert"},
{true, 2, "Success should preserve past alert count"},
{false, 1, "First failure and first alert after success"},
}
// Unlike previous tests, this one requires a static Monitor with repeated
// calls to the failure method
monitor := m.Monitor{AlertAfter: 1, AlertEvery: &alertEvery}
for _, c := range cases {
t.Logf("Testing case %s", c.name)
var notice *m.AlertNotice
if c.checkSuccess {
notice = monitor.Success()
} else {
notice = monitor.Failure()
}
if notice == nil {
t.Fatalf("failure(%v) expected notice, got nil", c.name)
}
if notice.AlertCount != c.alertCount {
t.Errorf("failure(%v), expected=%v actual=%v", c.name, c.alertCount, notice.AlertCount)
t.Logf("Case failed: %s", c.name)
}
}
}
// TestMonitorFailureAlertAfter tests that alerts will not trigger until // TestMonitorFailureAlertAfter tests that alerts will not trigger until
// hitting the threshold provided by AlertAfter // hitting the threshold provided by AlertAfter
func TestMonitorFailureAlertAfter(t *testing.T) { func TestMonitorFailureAlertAfter(t *testing.T) {
-3
View File
@@ -1,3 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}
+4 -4
View File
@@ -38,15 +38,15 @@ alert "mailgun_down" {
-F to=me@minitor.mon \ -F to=me@minitor.mon \
-F text="Our monitor failed" \ -F text="Our monitor failed" \
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}"
EOF EOF
} }
alert "sms_down" { alert "sms_down" {
shell_command = <<-EOF shell_command = <<-EOF
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" \
-u "$${ACCOUNT_SID}:$${AUTH_TOKEN}" -u "${ACCOUNT_SID}:${AUTH_TOKEN}"
EOF EOF
} }
-12
View File
@@ -1,12 +0,0 @@
check_interval = "1s"
alert "log_command" {
command = "should be a list"
}
monitor "Command" {
command = ["echo", "$PATH"]
alert_down = ["log_command"]
alert_every = 2
check_interval = "10s"
}
+1 -6
View File
@@ -1,11 +1,6 @@
check_interval = "1s" check_interval = "1s"
default_alert_down = ["log_command"] default_alert_down = ["log_command"]
default_alert_every = 0 default_alert_after = 1
default_alert_after = 2
monitor "Default" {
command = ["echo"]
}
monitor "Command" { monitor "Command" {
command = ["echo", "$PATH"] command = ["echo", "$PATH"]
-5
View File
@@ -8,11 +8,6 @@ alert "log_shell" {
shell_command = "echo \"Failure on {{.MonitorName}} User is $USER\"" shell_command = "echo \"Failure on {{.MonitorName}} User is $USER\""
} }
monitor "Default" {
command = ["echo"]
alert_down = ["log_command"]
}
monitor "Command" { monitor "Command" {
command = ["echo", "$PATH"] command = ["echo", "$PATH"]
alert_down = ["log_command", "log_shell"] alert_down = ["log_command", "log_shell"]
+25
View File
@@ -0,0 +1,25 @@
---
check_interval: 1s
monitors:
- name: Command
command: ["echo", "$PATH"]
alert_down: ["log_command", "log_shell"]
alert_every: 0
check_interval: 10s
- name: Shell
command: >
echo 'Some string with stuff';
echo 'another line';
echo $PATH;
exit 1
alert_down: ["log_command", "log_shell"]
alert_after: 5
alert_every: 0
check_interval: 1m
alerts:
log_command:
command: ["echo", "regular", '"command!!!"', "{{.MonitorName}}"]
log_shell:
command: echo "Failure on {{.MonitorName}} User is $USER"