Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cac6c94d7 | ||
|
|
e0af17a599 | ||
|
|
a0a6b8199a |
+86
-39
@@ -1,53 +1,100 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: kubernetes
|
||||
name: default
|
||||
name: test
|
||||
|
||||
node_selector:
|
||||
zone: dev
|
||||
steps:
|
||||
- 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:
|
||||
event:
|
||||
- push
|
||||
- tag
|
||||
|
||||
workspace:
|
||||
path: /drone/src
|
||||
refs:
|
||||
- refs/heads/master
|
||||
- refs/tags/v*
|
||||
|
||||
steps:
|
||||
- name: pull image to dockerhub
|
||||
image: docker.io/owncloudci/drone-docker-buildx:4
|
||||
privileged: true
|
||||
settings:
|
||||
cache-from: [ "safebox/minitor" ]
|
||||
repo: safebox/minitor
|
||||
tags: latest
|
||||
username:
|
||||
from_secret: dockerhub-username
|
||||
password:
|
||||
from_secret: dockerhub-password
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
- name: build all binaries
|
||||
image: golang:1.20
|
||||
environment:
|
||||
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
|
||||
commands:
|
||||
- make all
|
||||
|
||||
- name: build multiarch from dev
|
||||
image: docker.io/owncloudci/drone-docker-buildx:4
|
||||
privileged: true
|
||||
- name: compress binaries for release
|
||||
image: ubuntu
|
||||
commands:
|
||||
- find ./dist -type f -executable -execdir tar -czvf {}.tar.gz {} \;
|
||||
when:
|
||||
event: tag
|
||||
|
||||
- name: upload gitea release
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
cache-from: [ "registry.dev.format.hu/minitor" ]
|
||||
registry: registry.dev.format.hu
|
||||
repo: registry.dev.format.hu/minitor
|
||||
tags: latest
|
||||
dockerfile: Dockerfile
|
||||
username:
|
||||
from_secret: dev-hu-registry-username
|
||||
password:
|
||||
from_secret: dev-hu-registry-password
|
||||
title: ${DRONE_TAG}
|
||||
files: dist/*.tar.gz
|
||||
checksum:
|
||||
- md5
|
||||
- sha1
|
||||
- sha256
|
||||
- sha512
|
||||
base_url:
|
||||
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:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- linux/arm
|
||||
username:
|
||||
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
@@ -14,7 +14,6 @@
|
||||
|
||||
# User configuration
|
||||
config.yml
|
||||
config.hcl
|
||||
|
||||
# Output binary
|
||||
minitor
|
||||
|
||||
+12
-28
@@ -1,13 +1,16 @@
|
||||
version: "2"
|
||||
---
|
||||
linters:
|
||||
enable:
|
||||
- errname
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- gofumpt
|
||||
- goimports
|
||||
- gomnd
|
||||
- goprintffuncname
|
||||
- misspell
|
||||
- mnd
|
||||
- tagliatelle
|
||||
- tenv
|
||||
- testpackage
|
||||
- thelper
|
||||
- tparallel
|
||||
@@ -16,37 +19,18 @@ linters:
|
||||
- wsl
|
||||
disable:
|
||||
- gochecknoglobals
|
||||
settings:
|
||||
|
||||
linters-settings:
|
||||
gosec:
|
||||
excludes:
|
||||
- G204
|
||||
tagliatelle:
|
||||
case:
|
||||
rules:
|
||||
json: snake
|
||||
yaml: snake
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gosec
|
||||
path: _test\.go
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofumpt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-yaml
|
||||
@@ -11,10 +11,10 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: check-merge-conflict
|
||||
- repo: https://github.com/golangci/golangci-lint
|
||||
rev: v2.7.2
|
||||
rev: v1.52.2
|
||||
hooks:
|
||||
- id: golangci-lint
|
||||
- repo: https://github.com/hadolint/hadolint
|
||||
rev: refs/pull/1152/head
|
||||
rev: v2.12.1-beta
|
||||
hooks:
|
||||
- id: hadolint-github
|
||||
- id: hadolint
|
||||
|
||||
+7
-113
@@ -1,129 +1,23 @@
|
||||
FROM golang:1.25 AS gomail-builder
|
||||
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 alpine:3.18
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
# hadolint ignore=DL3018
|
||||
RUN apk --no-cache add bash=~5 curl=~8 jq=~1 bind-tools=~9 tzdata
|
||||
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
|
||||
COPY default_config.hcl /app/config.hcl
|
||||
RUN chmod -R 755 /app/scripts
|
||||
|
||||
# Copy minitor in
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
COPY ./dist/minitor-${TARGETOS}-${TARGETARCH} ./minitor
|
||||
|
||||
# Drop to non-root user
|
||||
USER minitor
|
||||
|
||||
|
||||
@@ -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:
|
||||
@@ -17,7 +17,7 @@ I'm running a few small services and found Sensu, Consul, Nagios, etc. to all be
|
||||
Install and execute with:
|
||||
|
||||
```bash
|
||||
go install github.com/iamthefij/minitor-go/v2@latest
|
||||
go install github.com/iamthefij/minitor-go@latest
|
||||
minitor
|
||||
```
|
||||
|
||||
@@ -50,17 +50,15 @@ You can configure the timezone for the container by passing a `TZ` env variable.
|
||||
|
||||
## 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`.
|
||||
|
||||
```hcl
|
||||
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.
|
||||
|
||||
The global configurations are:
|
||||
|
||||
|key|value|
|
||||
|---|---|
|
||||
|`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_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_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.|
|
||||
|`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.|
|
||||
|`monitor`|block listing monitors. Detailed description below|
|
||||
@@ -77,7 +75,7 @@ monitor "example" {
|
||||
alert_up = ["log"]
|
||||
check_interval = "1m"
|
||||
alert_after = 1
|
||||
alert_every = -1
|
||||
alert_every = 0
|
||||
}
|
||||
```
|
||||
|
||||
@@ -169,48 +167,6 @@ minitor -metrics
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -37,32 +37,13 @@ type AlertNotice struct {
|
||||
LastCheckOutput string
|
||||
}
|
||||
|
||||
// Validate checks that the Alert is properly configured and returns errors if not
|
||||
func (alert Alert) Validate() error {
|
||||
hasCommand := len(alert.Command) > 0
|
||||
hasShellCommand := alert.ShellCommand != ""
|
||||
// IsValid returns a boolean indicating if the Alert has been correctly
|
||||
// configured
|
||||
func (alert Alert) IsValid() bool {
|
||||
hasAtLeastOneCommand := alert.Command != nil || alert.ShellCommand != ""
|
||||
hasAtMostOneCommand := alert.Command == nil || alert.ShellCommand == ""
|
||||
|
||||
var err error
|
||||
|
||||
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
|
||||
return hasAtLeastOneCommand && hasAtMostOneCommand
|
||||
}
|
||||
|
||||
// BuildTemplates compiles command templates for the Alert
|
||||
@@ -101,14 +82,14 @@ func (alert *Alert) BuildTemplates() error {
|
||||
}
|
||||
|
||||
switch {
|
||||
case alert.Command != nil:
|
||||
case alert.commandTemplate == nil && alert.Command != nil:
|
||||
alert.commandTemplate = []*template.Template{}
|
||||
for i, cmdPart := range alert.Command {
|
||||
alert.commandTemplate = append(alert.commandTemplate, template.Must(
|
||||
template.New(alert.Name+fmt.Sprint(i)).Funcs(timeFormatFuncs).Parse(cmdPart),
|
||||
))
|
||||
}
|
||||
case alert.ShellCommand != "":
|
||||
case alert.commandShellTemplate == nil && alert.ShellCommand != "":
|
||||
shellCmd := alert.ShellCommand
|
||||
|
||||
alert.commandShellTemplate = template.Must(
|
||||
|
||||
+8
-15
@@ -1,24 +1,20 @@
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
m "git.iamthefij.com/iamthefij/minitor-go/v2"
|
||||
m "git.iamthefij.com/iamthefij/minitor-go"
|
||||
)
|
||||
|
||||
func TestAlertValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
func TestAlertIsValid(t *testing.T) {
|
||||
cases := []struct {
|
||||
alert m.Alert
|
||||
expected error
|
||||
expected bool
|
||||
name string
|
||||
}{
|
||||
{m.Alert{Command: []string{"echo", "test"}}, nil, "Command only"},
|
||||
{m.Alert{ShellCommand: "echo test"}, nil, "CommandShell only"},
|
||||
{m.Alert{Command: []string{"echo", "test"}, ShellCommand: "echo test"}, m.ErrInvalidAlert, "Both commands"},
|
||||
{m.Alert{}, m.ErrInvalidAlert, "No commands"},
|
||||
{m.Alert{Command: []string{"echo", "test"}}, true, "Command only"},
|
||||
{m.Alert{ShellCommand: "echo test"}, true, "CommandShell only"},
|
||||
{m.Alert{}, false, "No commands"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -27,11 +23,8 @@ func TestAlertValidate(t *testing.T) {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual := c.alert.Validate()
|
||||
hasErr := (actual != nil)
|
||||
expectErr := (c.expected != nil)
|
||||
|
||||
if hasErr != expectErr || !errors.Is(actual, c.expected) {
|
||||
actual := c.alert.IsValid()
|
||||
if actual != c.expected {
|
||||
t.Errorf("expected=%t actual=%t", c.expected, actual)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,26 +6,21 @@ import (
|
||||
"time"
|
||||
|
||||
"git.iamthefij.com/iamthefij/slog"
|
||||
/*
|
||||
* "github.com/hashicorp/hcl/v2"
|
||||
* "github.com/hashicorp/hcl/v2/gohcl"
|
||||
*/
|
||||
"github.com/hashicorp/hcl/v2/hclsimple"
|
||||
)
|
||||
|
||||
var (
|
||||
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")
|
||||
)
|
||||
var errInvalidConfig = errors.New("Invalid configuration")
|
||||
|
||||
// Config type is contains all provided user configuration
|
||||
type Config struct {
|
||||
CheckIntervalStr string `hcl:"check_interval"`
|
||||
CheckInterval time.Duration
|
||||
|
||||
DefaultAlertAfter int `hcl:"default_alert_after,optional"`
|
||||
DefaultAlertAfter *int `hcl:"default_alert_after,optional"`
|
||||
DefaultAlertEvery *int `hcl:"default_alert_every,optional"`
|
||||
DefaultAlertDown []string `hcl:"default_alert_down,optional"`
|
||||
DefaultAlertUp []string `hcl:"default_alert_up,optional"`
|
||||
@@ -35,77 +30,6 @@ type Config struct {
|
||||
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) {
|
||||
if c.alertLookup == nil {
|
||||
c.alertLookup = map[string]*Alert{}
|
||||
@@ -130,24 +54,119 @@ func (c *Config) BuildAllTemplates() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// LoadConfig will read config from the given path and parse it
|
||||
func LoadConfig(filePath string) (Config, error) {
|
||||
var config Config
|
||||
// IsValid checks config validity and returns true if valid
|
||||
func (config Config) IsValid() (isValid bool) {
|
||||
isValid = true
|
||||
|
||||
if err := hclsimple.DecodeFile(filePath, nil, &config); err != nil {
|
||||
return config, errors.Join(ErrLoadingConfig, err)
|
||||
// Validate alerts
|
||||
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)
|
||||
|
||||
// Finish initializing configuration
|
||||
if err := config.Init(); err != nil {
|
||||
return config, errors.Join(ErrConfigInit, err)
|
||||
if err = config.Init(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := config.IsValid(); err != nil {
|
||||
return config, errors.Join(ErrInvalidConfig, err)
|
||||
if !config.IsValid() {
|
||||
err = errInvalidConfig
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return config, nil
|
||||
return config, err
|
||||
}
|
||||
|
||||
+10
-100
@@ -1,27 +1,23 @@
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
m "git.iamthefij.com/iamthefij/minitor-go/v2"
|
||||
m "git.iamthefij.com/iamthefij/minitor-go"
|
||||
)
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
cases := []struct {
|
||||
configPath string
|
||||
expectedErr error
|
||||
expectErr bool
|
||||
name string
|
||||
}{
|
||||
{"./test/does-not-exist", m.ErrLoadingConfig, "Invalid config path"},
|
||||
{"./test/invalid-config-wrong-hcl-type.hcl", m.ErrLoadingConfig, "Incorrect HCL type"},
|
||||
{"./test/invalid-config-missing-alerts.hcl", m.ErrNoAlerts, "Invalid config missing alerts"},
|
||||
{"./test/invalid-config-missing-alerts.hcl", m.ErrInvalidConfig, "Invalid config general"},
|
||||
{"./test/invalid-config-invalid-duration.hcl", m.ErrConfigInit, "Invalid config type for key"},
|
||||
{"./test/invalid-config-unknown-alert.hcl", m.ErrUnknownAlert, "Invalid config unknown alert"},
|
||||
{"./test/valid-config-default-values.hcl", nil, "Valid config file with default values"},
|
||||
{"./test/valid-config.hcl", nil, "Valid config file"},
|
||||
{"./test/does-not-exist", true, "Invalid config path"},
|
||||
{"./test/invalid-config-missing-alerts.hcl", true, "Invalid config missing alerts"},
|
||||
{"./test/invalid-config-type.hcl", true, "Invalid config type for key"},
|
||||
{"./test/invalid-config-unknown-alert.hcl", true, "Invalid config unknown alert"},
|
||||
{"./test/valid-config-default-values.hcl", false, "Valid config file with default values"},
|
||||
{"./test/valid-config.hcl", false, "Valid config file"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
@@ -31,100 +27,14 @@ func TestLoadConfig(t *testing.T) {
|
||||
|
||||
_, err := m.LoadConfig(c.configPath)
|
||||
hasErr := (err != nil)
|
||||
expectErr := (c.expectedErr != nil)
|
||||
|
||||
if hasErr != expectErr || !errors.Is(err, c.expectedErr) {
|
||||
t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectedErr, err)
|
||||
if hasErr != c.expectErr {
|
||||
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
|
||||
// and execution of mutli-line strings presented in YAML
|
||||
func TestMultiLineConfig(t *testing.T) {
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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 (
|
||||
git.iamthefij.com/iamthefij/slog v1.3.0
|
||||
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 (
|
||||
github.com/agext/levenshtein v1.2.1 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // 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.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.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.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
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.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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.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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
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/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.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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
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 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/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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
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/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/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
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.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/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
||||
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/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
|
||||
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-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
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-20190412213103-97732733099d/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.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
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.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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
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=
|
||||
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/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
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=
|
||||
|
||||
@@ -119,7 +119,7 @@ func SendStartupAlerts(config *Config, alertNames []string) error {
|
||||
|
||||
func main() {
|
||||
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 \"\")")
|
||||
|
||||
flag.BoolVar(&slog.DebugLevel, "debug", false, "Enables debug logs (default: false)")
|
||||
@@ -136,7 +136,7 @@ func main() {
|
||||
|
||||
// Load configuration
|
||||
config, err := LoadConfig(*configPath)
|
||||
slog.OnErrFatalf(err, "Error loading config")
|
||||
slog.OnErrFatalf(err, "Error loading config: %v", err)
|
||||
|
||||
// Serve metrics exporter, if specified
|
||||
if ExportMetrics {
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ package main_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
m "git.iamthefij.com/iamthefij/minitor-go/v2"
|
||||
m "git.iamthefij.com/iamthefij/minitor-go"
|
||||
)
|
||||
|
||||
func Ptr[T any](v T) *T {
|
||||
|
||||
+31
-94
@@ -1,8 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os/exec"
|
||||
"time"
|
||||
@@ -17,7 +15,6 @@ type Monitor struct { //nolint:maligned
|
||||
CheckInterval time.Duration
|
||||
|
||||
Name string `hcl:"name,label"`
|
||||
AlertCount int
|
||||
AlertAfter int `hcl:"alert_after,optional"`
|
||||
AlertEvery *int `hcl:"alert_every,optional"`
|
||||
AlertDown []string `hcl:"alert_down,optional"`
|
||||
@@ -26,6 +23,7 @@ type Monitor struct { //nolint:maligned
|
||||
ShellCommand string `hcl:"shell_command,optional"`
|
||||
|
||||
// Other values
|
||||
alertCount int
|
||||
failureCount int
|
||||
lastCheck time.Time
|
||||
lastSuccess time.Time
|
||||
@@ -33,91 +31,30 @@ type Monitor struct { //nolint:maligned
|
||||
lastCheckDuration time.Duration
|
||||
}
|
||||
|
||||
// Init initializes the Monitor with default values
|
||||
func (monitor *Monitor) Init(defaultAlertAfter int, defaultAlertEvery *int, defaultAlertDown []string, defaultAlertUp []string) error {
|
||||
// Parse the check_interval string into a time.Duration
|
||||
if monitor.CheckIntervalStr != nil {
|
||||
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 {
|
||||
// IsValid returns a boolean indicating if the Monitor has been correctly
|
||||
// configured
|
||||
func (monitor Monitor) IsValid() bool {
|
||||
// TODO: Refactor and return an error containing more information on what was invalid
|
||||
hasCommand := len(monitor.Command) > 0
|
||||
hasShellCommand := monitor.ShellCommand != ""
|
||||
hasValidAlertAfter := monitor.AlertAfter > 0
|
||||
hasAlertDown := len(monitor.AlertDown) > 0
|
||||
|
||||
var err error
|
||||
|
||||
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)
|
||||
if !hasAtMostOneCommand {
|
||||
err = errors.Join(err, fmt.Errorf(
|
||||
"%w: monitor %s has both command and shell_command configured",
|
||||
ErrInvalidMonitor,
|
||||
monitor.Name,
|
||||
))
|
||||
}
|
||||
|
||||
if !hasValidAlertAfter {
|
||||
err = errors.Join(err, fmt.Errorf(
|
||||
"%w: monitor %s has invalid alert_after value %d. Must be greater than 0",
|
||||
ErrInvalidMonitor,
|
||||
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
|
||||
return hasAtLeastOneCommand &&
|
||||
hasAtMostOneCommand &&
|
||||
hasValidAlertAfter &&
|
||||
hasAlertDown
|
||||
}
|
||||
|
||||
func (monitor Monitor) LastOutput() string {
|
||||
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 {
|
||||
if monitor.lastCheck.IsZero() || monitor.CheckInterval == 0 {
|
||||
return true
|
||||
@@ -128,7 +65,8 @@ func (monitor Monitor) ShouldCheck() bool {
|
||||
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) {
|
||||
var cmd *exec.Cmd
|
||||
if len(monitor.Command) > 0 {
|
||||
@@ -149,9 +87,9 @@ func (monitor *Monitor) Check() (bool, *AlertNotice) {
|
||||
|
||||
isSuccess := (err == nil)
|
||||
if isSuccess {
|
||||
alertNotice = monitor.Success()
|
||||
alertNotice = monitor.success()
|
||||
} else {
|
||||
alertNotice = monitor.Failure()
|
||||
alertNotice = monitor.failure()
|
||||
}
|
||||
|
||||
slog.Debugf("Command output: %s", monitor.lastOutput)
|
||||
@@ -167,18 +105,9 @@ func (monitor *Monitor) Check() (bool, *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
|
||||
func (monitor Monitor) IsUp() bool {
|
||||
return monitor.AlertCount == 0
|
||||
return monitor.alertCount == 0
|
||||
}
|
||||
|
||||
// LastCheckMilliseconds gives number of miliseconds the last check ran for
|
||||
@@ -186,20 +115,20 @@ func (monitor Monitor) LastCheckMilliseconds() int64 {
|
||||
return monitor.lastCheckDuration.Milliseconds()
|
||||
}
|
||||
|
||||
func (monitor *Monitor) Success() (notice *AlertNotice) {
|
||||
func (monitor *Monitor) success() (notice *AlertNotice) {
|
||||
if !monitor.IsUp() {
|
||||
// Alert that we have recovered
|
||||
notice = monitor.createAlertNotice(true)
|
||||
}
|
||||
|
||||
monitor.failureCount = 0
|
||||
monitor.AlertCount = 0
|
||||
monitor.alertCount = 0
|
||||
monitor.lastSuccess = time.Now()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (monitor *Monitor) Failure() (notice *AlertNotice) {
|
||||
func (monitor *Monitor) failure() (notice *AlertNotice) {
|
||||
monitor.failureCount++
|
||||
// If we haven't hit the minimum failures, we can exit
|
||||
if monitor.failureCount < monitor.AlertAfter {
|
||||
@@ -231,25 +160,33 @@ func (monitor *Monitor) Failure() (notice *AlertNotice) {
|
||||
}
|
||||
default:
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// If we're going to alert, increment count
|
||||
if notice != nil {
|
||||
monitor.AlertCount++
|
||||
notice.AlertCount = monitor.AlertCount
|
||||
monitor.alertCount++
|
||||
}
|
||||
|
||||
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 {
|
||||
// TODO: Maybe add something about recovery status here
|
||||
return &AlertNotice{
|
||||
MonitorName: monitor.Name,
|
||||
AlertCount: monitor.AlertCount,
|
||||
AlertCount: monitor.alertCount,
|
||||
FailureCount: monitor.failureCount,
|
||||
LastCheckOutput: monitor.lastOutput,
|
||||
LastSuccess: monitor.lastSuccess,
|
||||
|
||||
+11
-55
@@ -1,27 +1,25 @@
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
m "git.iamthefij.com/iamthefij/minitor-go/v2"
|
||||
m "git.iamthefij.com/iamthefij/minitor-go"
|
||||
)
|
||||
|
||||
func TestMonitorValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// TestMonitorIsValid tests the Monitor.IsValid()
|
||||
func TestMonitorIsValid(t *testing.T) {
|
||||
cases := []struct {
|
||||
monitor m.Monitor
|
||||
expected error
|
||||
expected bool
|
||||
name string
|
||||
}{
|
||||
{m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, nil, "Command only"},
|
||||
{m.Monitor{AlertAfter: 1, ShellCommand: "echo test", AlertDown: []string{"log"}}, nil, "CommandShell only"},
|
||||
{m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}}, m.ErrInvalidMonitor, "No AlertDown"},
|
||||
{m.Monitor{AlertAfter: 1, AlertDown: []string{"log"}}, m.ErrInvalidMonitor, "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"}}, true, "Command only"},
|
||||
{m.Monitor{AlertAfter: 1, ShellCommand: "echo test", AlertDown: []string{"log"}}, true, "CommandShell only"},
|
||||
{m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}}, false, "No AlertDown"},
|
||||
{m.Monitor{AlertAfter: 1, AlertDown: []string{"log"}}, false, "No commands"},
|
||||
{m.Monitor{AlertAfter: -1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, false, "Invalid alert threshold, -1"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -30,11 +28,8 @@ func TestMonitorValidate(t *testing.T) {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual := c.monitor.Validate()
|
||||
hasErr := (actual != nil)
|
||||
expectErr := (c.expected != nil)
|
||||
|
||||
if hasErr != expectErr || !errors.Is(actual, c.expected) {
|
||||
actual := c.monitor.IsValid()
|
||||
if actual != c.expected {
|
||||
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
|
||||
// hitting the threshold provided by AlertAfter
|
||||
func TestMonitorFailureAlertAfter(t *testing.T) {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||
}
|
||||
+4
-4
@@ -38,15 +38,15 @@ alert "mailgun_down" {
|
||||
-F to=me@minitor.mon \
|
||||
-F text="Our monitor failed" \
|
||||
https://api.mailgun.net/v3/minitor.mon/messages \
|
||||
-u "api:$${MAILGUN_API_KEY}"
|
||||
-u "api:${MAILGUN_API_KEY}"
|
||||
EOF
|
||||
}
|
||||
|
||||
alert "sms_down" {
|
||||
shell_command = <<-EOF
|
||||
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" \
|
||||
-u "$${ACCOUNT_SID}:$${AUTH_TOKEN}"
|
||||
-F "From=${AVAILABLE_NUMBER}" -F "To=${MY_PHONE}" \
|
||||
"https://api.twilio.com/2010-04-01/Accounts/${ACCOUNT_SID}/Messages" \
|
||||
-u "${ACCOUNT_SID}:${AUTH_TOKEN}"
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -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,11 +1,6 @@
|
||||
check_interval = "1s"
|
||||
default_alert_down = ["log_command"]
|
||||
default_alert_every = 0
|
||||
default_alert_after = 2
|
||||
|
||||
monitor "Default" {
|
||||
command = ["echo"]
|
||||
}
|
||||
default_alert_after = 1
|
||||
|
||||
monitor "Command" {
|
||||
command = ["echo", "$PATH"]
|
||||
|
||||
@@ -8,11 +8,6 @@ alert "log_shell" {
|
||||
shell_command = "echo \"Failure on {{.MonitorName}} User is $USER\""
|
||||
}
|
||||
|
||||
monitor "Default" {
|
||||
command = ["echo"]
|
||||
alert_down = ["log_command"]
|
||||
}
|
||||
|
||||
monitor "Command" {
|
||||
command = ["echo", "$PATH"]
|
||||
alert_down = ["log_command", "log_shell"]
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user