Compare commits

..

41 Commits

Author SHA1 Message Date
Ian Fijolek
e262afdb1f Merge branch 'master' into next-major 2026-01-13 21:45:27 -08:00
Renovate Bot
a5268ae1f6 Update actions/setup-python action to v6 2026-01-14 05:45:09 +00:00
Renovate Bot
16ad16d873 Update actions/setup-go action to v6 2026-01-14 05:44:37 +00:00
Ian Fijolek
f4fb75610a Update variable interpolation for hcl 2026-01-13 21:43:57 -08:00
Ian Fijolek
0ae7c6dbdf Update default config file to config.hcl 2026-01-13 21:43:03 -08:00
Ian Fijolek
a06ed3540c Remove extra spaces in Makefile 2026-01-13 21:13:19 -08:00
Renovate Bot
200cfd1a2d Update actions/checkout action to v6 2026-01-07 00:02:31 +00:00
Ian Fijolek
bcbac39cad Add migration instructions 2026-01-05 16:32:59 -08:00
Ian Fijolek
afacf40ec8 Update build to better detect tags and versions 2026-01-05 16:20:51 -08:00
Ian Fijolek
c18e9c8771 Update readme with better default descriptions 2025-12-11 16:41:17 -08:00
Ian Fijolek
eb2987d3bc Tidy again and update readme 2025-12-11 16:37:02 -08:00
Ian Fijolek
945c1b1ce0 Update module path to v2 2025-12-11 16:34:18 -08:00
Ian Fijolek
b0ea3dc6d4 Bump go version 2025-12-11 16:26:05 -08:00
Ian Fijolek
5c97f2e5c4 Update defaults and add tests for them 2025-12-11 16:18:26 -08:00
Ian Fijolek
85afa3d9ef Merge branch 'master' into next-major 2025-12-11 16:17:38 -08:00
Renovate Bot
0a7aab7030 Update actions/checkout action to v6 2025-12-12 00:02:32 +00:00
IamTheFij
fb9d637614 Merge pull request 'Update actions/setup-go action to v6' (#13) from renovate/actions-setup-go-6.x into master
Reviewed-on: https://git.iamthefij.com/iamthefij/minitor-go/pulls/13
2025-12-11 23:09:33 +00:00
Renovate Bot
98be873220 Update actions/setup-go action to v6 2025-12-11 23:09:33 +00:00
IamTheFij
f59848fb61 Merge pull request 'Update actions/setup-python action to v6' (#14) from renovate/actions-setup-python-6.x into master
Reviewed-on: https://git.iamthefij.com/iamthefij/minitor-go/pulls/14
2025-12-11 23:09:11 +00:00
Renovate Bot
e74fe89cab Update actions/setup-python action to v6 2025-12-11 23:09:11 +00:00
IamTheFij
1bc8ab5dac Merge pull request 'Update alpine Docker tag to v3.23' (#7) from renovate/alpine-3.x into master
Reviewed-on: https://git.iamthefij.com/iamthefij/minitor-go/pulls/7
2025-12-11 22:33:11 +00:00
Renovate Bot
f21dce1cc6 Update alpine Docker tag to v3.23 2025-12-11 22:33:11 +00:00
IamTheFij
cfcdf04990 Merge pull request 'Update module github.com/prometheus/client_golang to v1.23.2' (#9) from renovate/github.com-prometheus-client_golang-1.x into master
Reviewed-on: https://git.iamthefij.com/iamthefij/minitor-go/pulls/9
2025-12-11 22:29:30 +00:00
Renovate Bot
9892af48d1 Update module github.com/prometheus/client_golang to v1.23.2 2025-12-11 20:46:36 +00:00
IamTheFij
c8e914d1b8 Merge pull request 'Switch to gitea actions' (#11) from gitea-actions into master
Reviewed-on: https://git.iamthefij.com/iamthefij/minitor-go/pulls/11
2025-12-11 19:07:28 +00:00
Ian Fijolek
845604c54c Use temporary hadolint hook location 2025-12-11 11:03:39 -08:00
Ian Fijolek
49acca1c79 Switch to gitea actions 2025-12-10 23:17:42 -08:00
IamTheFij
2bdafd908d Merge pull request 'Update golang Docker tag to v1.25' (#8) from renovate/golang-1.x into master
Reviewed-on: https://git.iamthefij.com/iamthefij/minitor-go/pulls/8
2025-12-10 19:27:55 +00:00
Renovate Bot
b349ada44e Update golang Docker tag to v1.25 2025-12-10 19:23:55 +00:00
IamTheFij
fbf92d924c Merge pull request 'Configure Renovate' (#6) from renovate/configure into master
Reviewed-on: https://git.iamthefij.com/iamthefij/minitor-go/pulls/6
2025-12-10 19:19:21 +00:00
Renovate Bot
fd292b005c Add renovate.json 2025-12-10 18:23:06 +00:00
Ian Fijolek
3ef06fb78d Stop getting errors when tzdata updates 2025-02-13 13:28:06 -08:00
Ian Fijolek
4aef7b7458 Remove redundant logging 2025-02-13 12:17:59 -08:00
Ian Fijolek
be2e8121c5 Refactor validation for alert and monitor to return errors 2025-02-13 12:17:59 -08:00
Ian Fijolek
9e20c00dde Move monitor init to it's own method and refactor config validate to return err 2025-02-13 12:17:59 -08:00
Ian Fijolek
3fb418151b Update readme and update some test files to be better examples 2025-02-13 12:17:59 -08:00
Ian Fijolek
df1c7aa74b Refactor test package and some field types
Fairly big test refactor and changing some of the fields from pointers
2025-02-13 12:17:59 -08:00
Ian Fijolek
7c72eabd6b Initial step of hcl migration 2025-02-13 12:17:46 -08:00
IamTheFij
0535bdf156 Fix incorrect alertCount 2025-02-13 12:12:01 -08:00
Ian Fijolek
03f0ab69fe Add documentation for default_alert_every 2025-02-13 11:26:39 -08:00
Ian Fijolek
312821fa8d Add pycompat warning 2024-11-14 13:09:23 -08:00
26 changed files with 684 additions and 293 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ name: test
steps: steps:
- name: test - name: test
image: golang:1.20 image: golang:1.21
environment: environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}} VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
commands: commands:
@@ -30,7 +30,7 @@ trigger:
steps: steps:
- name: build all binaries - name: build all binaries
image: golang:1.20 image: golang:1.21
environment: environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}} VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
commands: commands:
+111
View File
@@ -0,0 +1,111 @@
name: ci
on:
push:
branches:
- main
tags:
- "v*"
pull_request:
branches:
- main
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Run tests
run: make test
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Set up Python
uses: actions/setup-python@v6
- name: Run pre-commit
uses: https://git.iamthefij.com/iamthefij/pre-commit-action@v3.0.2
release:
runs-on: ubuntu-latest
needs: test
if: "${{ github.event_name != 'pull_request' }}"
steps:
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Build binaries
env:
VERSION: "${{ github.REF_NAME }}"
run: make all
# Package binaries and create release if this is a tagged build
- name: Compress binaries
if: "${{ github.ref_type == 'tag' }}"
run: find ./dist -type f -executable -execdir tar -czvf {}.tar.gz {} \;
- name: Upload release
uses: https://gitea.com/actions/gitea-release-action@v1
if: "${{ github.ref_type == 'tag' }}"
with:
files: |-
dist/*.tar.gz
md5sum: true
sha256sum: true
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
# list of Docker images to use as base name for tags
images: |
${{ github.REPOSITORY }}
# generate Docker tags based on the following events/attributes
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Login to Docker Hub
uses: docker/login-action@v3
if: "${{ github.event_name != 'pull_request' }}"
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
# Use path context so we can access pre-compiled binaries
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: |
linux/amd64
linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Vendored
+1
View File
@@ -14,6 +14,7 @@
# User configuration # User configuration
config.yml config.yml
config.hcl
# Output binary # Output binary
minitor minitor
+28 -12
View File
@@ -1,16 +1,13 @@
--- 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
@@ -19,18 +16,37 @@ 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:
issues: generated: lax
exclude-rules: presets:
- path: _test\.go - comments
linters: - common-false-positives
- 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: v4.4.0 rev: v6.0.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: v1.52.2 rev: v2.7.2
hooks: hooks:
- id: golangci-lint - id: golangci-lint
- repo: https://github.com/hadolint/hadolint - repo: https://github.com/hadolint/hadolint
rev: v2.12.1-beta rev: refs/pull/1152/head
hooks: hooks:
- id: hadolint - id: hadolint-github
+3 -2
View File
@@ -1,10 +1,11 @@
FROM alpine:3.18 FROM alpine:3.23
RUN mkdir /app RUN mkdir /app
WORKDIR /app/ WORKDIR /app/
# Add common checking tools # Add common checking tools
RUN apk --no-cache add bash=~5 curl=~8 jq=~1 bind-tools=~9 tzdata~=2024a # hadolint ignore=DL3018
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
+4 -3
View File
@@ -1,4 +1,4 @@
FROM golang:1.20 AS builder FROM golang:1.25 AS builder
WORKDIR /app WORKDIR /app
@@ -13,7 +13,7 @@ ARG VERSION=dev
ENV CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=${TARGETARCH} ENV CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=${TARGETARCH}
RUN go build -ldflags "-X main.version=${VERSION}" -a -installsuffix nocgo -o minitor . RUN go build -ldflags "-X main.version=${VERSION}" -a -installsuffix nocgo -o minitor .
FROM alpine:3.18 FROM alpine:3.23
RUN mkdir /app RUN mkdir /app
WORKDIR /app/ WORKDIR /app/
@@ -21,7 +21,8 @@ WORKDIR /app/
COPY --from=builder /app/minitor . COPY --from=builder /app/minitor .
# Add common checking tools # Add common checking tools
RUN apk --no-cache add bash=~5 curl=~8 jq=~1 bind-tools=~9 tzdata~=2024a # hadolint ignore=DL3018
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
+49 -13
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@latest go install github.com/iamthefij/minitor-go/v2@latest
minitor minitor
``` ```
@@ -50,15 +50,17 @@ 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. It should be noted that environment variable interpolation happens on load of the HCL file. 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
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 or 0.| |`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.| |`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_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|
@@ -75,7 +77,7 @@ monitor "example" {
alert_up = ["log"] alert_up = ["log"]
check_interval = "1m" check_interval = "1m"
alert_after = 1 alert_after = 1
alert_every = 0 alert_every = -1
} }
``` ```
@@ -167,6 +169,48 @@ 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.
@@ -174,11 +218,3 @@ 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.
+27 -8
View File
@@ -37,13 +37,32 @@ type AlertNotice struct {
LastCheckOutput string LastCheckOutput string
} }
// IsValid returns a boolean indicating if the Alert has been correctly // Validate checks that the Alert is properly configured and returns errors if not
// configured func (alert Alert) Validate() error {
func (alert Alert) IsValid() bool { hasCommand := len(alert.Command) > 0
hasAtLeastOneCommand := alert.Command != nil || alert.ShellCommand != "" hasShellCommand := alert.ShellCommand != ""
hasAtMostOneCommand := alert.Command == nil || alert.ShellCommand == ""
return hasAtLeastOneCommand && hasAtMostOneCommand 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
} }
// BuildTemplates compiles command templates for the Alert // BuildTemplates compiles command templates for the Alert
@@ -82,14 +101,14 @@ func (alert *Alert) BuildTemplates() error {
} }
switch { switch {
case alert.commandTemplate == nil && alert.Command != nil: case 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.commandShellTemplate == nil && alert.ShellCommand != "": case alert.ShellCommand != "":
shellCmd := alert.ShellCommand shellCmd := alert.ShellCommand
alert.commandShellTemplate = template.Must( alert.commandShellTemplate = template.Must(
+15 -8
View File
@@ -1,20 +1,24 @@
package main_test package main_test
import ( import (
"errors"
"testing" "testing"
m "git.iamthefij.com/iamthefij/minitor-go" m "git.iamthefij.com/iamthefij/minitor-go/v2"
) )
func TestAlertIsValid(t *testing.T) { func TestAlertValidate(t *testing.T) {
t.Parallel()
cases := []struct { cases := []struct {
alert m.Alert alert m.Alert
expected bool expected error
name string name string
}{ }{
{m.Alert{Command: []string{"echo", "test"}}, true, "Command only"}, {m.Alert{Command: []string{"echo", "test"}}, nil, "Command only"},
{m.Alert{ShellCommand: "echo test"}, true, "CommandShell only"}, {m.Alert{ShellCommand: "echo test"}, nil, "CommandShell only"},
{m.Alert{}, false, "No commands"}, {m.Alert{Command: []string{"echo", "test"}, ShellCommand: "echo test"}, m.ErrInvalidAlert, "Both commands"},
{m.Alert{}, m.ErrInvalidAlert, "No commands"},
} }
for _, c := range cases { for _, c := range cases {
@@ -23,8 +27,11 @@ func TestAlertIsValid(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.IsValid() actual := c.alert.Validate()
if actual != c.expected { hasErr := (actual != nil)
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)
} }
}) })
+92 -111
View File
@@ -6,21 +6,26 @@ 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 errInvalidConfig = errors.New("Invalid configuration") 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")
)
// 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"`
@@ -30,6 +35,77 @@ 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{}
@@ -54,119 +130,24 @@ func (c *Config) BuildAllTemplates() (err error) {
return return
} }
// IsValid checks config validity and returns true if valid
func (config Config) IsValid() (isValid bool) {
isValid = true
// 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 // LoadConfig will read config from the given path and parse it
func LoadConfig(filePath string) (config Config, err error) { func LoadConfig(filePath string) (Config, error) {
err = hclsimple.DecodeFile(filePath, nil, &config) var config Config
if err != nil {
return if err := hclsimple.DecodeFile(filePath, nil, &config); err != nil {
return config, errors.Join(ErrLoadingConfig, err)
} }
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 return config, errors.Join(ErrConfigInit, err)
} }
if !config.IsValid() { if err := config.IsValid(); err != nil {
err = errInvalidConfig return config, errors.Join(ErrInvalidConfig, err)
return
} }
return config, err return config, nil
} }
+100 -10
View File
@@ -1,23 +1,27 @@
package main_test package main_test
import ( import (
"errors"
"testing" "testing"
"time"
m "git.iamthefij.com/iamthefij/minitor-go" m "git.iamthefij.com/iamthefij/minitor-go/v2"
) )
func TestLoadConfig(t *testing.T) { func TestLoadConfig(t *testing.T) {
cases := []struct { cases := []struct {
configPath string configPath string
expectErr bool expectedErr error
name string name string
}{ }{
{"./test/does-not-exist", true, "Invalid config path"}, {"./test/does-not-exist", m.ErrLoadingConfig, "Invalid config path"},
{"./test/invalid-config-missing-alerts.hcl", true, "Invalid config missing alerts"}, {"./test/invalid-config-wrong-hcl-type.hcl", m.ErrLoadingConfig, "Incorrect HCL type"},
{"./test/invalid-config-type.hcl", true, "Invalid config type for key"}, {"./test/invalid-config-missing-alerts.hcl", m.ErrNoAlerts, "Invalid config missing alerts"},
{"./test/invalid-config-unknown-alert.hcl", true, "Invalid config unknown alert"}, {"./test/invalid-config-missing-alerts.hcl", m.ErrInvalidConfig, "Invalid config general"},
{"./test/valid-config-default-values.hcl", false, "Valid config file with default values"}, {"./test/invalid-config-invalid-duration.hcl", m.ErrConfigInit, "Invalid config type for key"},
{"./test/valid-config.hcl", false, "Valid config file"}, {"./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"},
} }
for _, c := range cases { for _, c := range cases {
c := c c := c
@@ -27,14 +31,100 @@ 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 != c.expectErr { if hasErr != expectErr || !errors.Is(err, c.expectedErr) {
t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectErr, err) t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectedErr, 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) {
+13 -11
View File
@@ -1,25 +1,27 @@
module git.iamthefij.com/iamthefij/minitor-go module git.iamthefij.com/iamthefij/minitor-go/v2
go 1.20 go 1.25.0
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.19.0 github.com/prometheus/client_golang v1.23.2
) )
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.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.7.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/prometheus/client_model v0.5.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/common v0.66.1 // 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
golang.org/x/sys v0.16.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/sys v0.35.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect golang.org/x/text v0.28.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
) )
+38 -19
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.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.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,33 +18,43 @@ 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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
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=
@@ -52,6 +62,10 @@ 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=
@@ -62,16 +76,21 @@ 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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
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.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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=
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.yml", "Alternate configuration path (default: config.yml)") configPath := flag.String("config", "config.hcl", "Alternate configuration path (default: config.hcl)")
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: %v", err) slog.OnErrFatalf(err, "Error loading config")
// 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" m "git.iamthefij.com/iamthefij/minitor-go/v2"
) )
func Ptr[T any](v T) *T { func Ptr[T any](v T) *T {
+96 -33
View File
@@ -1,6 +1,8 @@
package main package main
import ( import (
"errors"
"fmt"
"math" "math"
"os/exec" "os/exec"
"time" "time"
@@ -15,6 +17,7 @@ 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"`
@@ -23,7 +26,6 @@ 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
@@ -31,30 +33,91 @@ type Monitor struct { //nolint:maligned
lastCheckDuration time.Duration lastCheckDuration time.Duration
} }
// IsValid returns a boolean indicating if the Monitor has been correctly // Init initializes the Monitor with default values
// configured func (monitor *Monitor) Init(defaultAlertAfter int, defaultAlertEvery *int, defaultAlertDown []string, defaultAlertUp []string) error {
func (monitor Monitor) IsValid() bool { // Parse the check_interval string into a time.Duration
// TODO: Refactor and return an error containing more information on what was invalid 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 {
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
hasAtLeastOneCommand := hasCommand || hasShellCommand var err error
hasAtMostOneCommand := !(hasCommand && hasShellCommand)
return hasAtLeastOneCommand && hasAtLeastOneCommand := hasCommand || hasShellCommand
hasAtMostOneCommand && if !hasAtLeastOneCommand {
hasValidAlertAfter && err = errors.Join(err, fmt.Errorf(
hasAlertDown "%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
} }
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 // ShouldCheck returns a boolean indicating if the Monitor is ready to be be checked again
// 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
@@ -65,8 +128,7 @@ 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 // Check will run the command configured by the Monitor and return a status and a possible AlertNotice
// and a possible AlertNotice
func (monitor *Monitor) Check() (bool, *AlertNotice) { func (monitor *Monitor) Check() (bool, *AlertNotice) {
var cmd *exec.Cmd var cmd *exec.Cmd
if len(monitor.Command) > 0 { if len(monitor.Command) > 0 {
@@ -87,9 +149,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)
@@ -105,9 +167,18 @@ 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
@@ -115,20 +186,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 {
@@ -160,33 +231,25 @@ 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:gomnd if failureCount >= int(math.Pow(2, float64(monitor.AlertCount))-1) { //nolint:mnd
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,
+55 -11
View File
@@ -1,25 +1,27 @@
package main_test package main_test
import ( import (
"errors"
"reflect" "reflect"
"testing" "testing"
"time" "time"
m "git.iamthefij.com/iamthefij/minitor-go" m "git.iamthefij.com/iamthefij/minitor-go/v2"
) )
// TestMonitorIsValid tests the Monitor.IsValid() func TestMonitorValidate(t *testing.T) {
func TestMonitorIsValid(t *testing.T) { t.Parallel()
cases := []struct { cases := []struct {
monitor m.Monitor monitor m.Monitor
expected bool expected error
name string name string
}{ }{
{m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, true, "Command only"}, {m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, nil, "Command only"},
{m.Monitor{AlertAfter: 1, ShellCommand: "echo test", AlertDown: []string{"log"}}, true, "CommandShell only"}, {m.Monitor{AlertAfter: 1, ShellCommand: "echo test", AlertDown: []string{"log"}}, nil, "CommandShell only"},
{m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}}, false, "No AlertDown"}, {m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}}, m.ErrInvalidMonitor, "No AlertDown"},
{m.Monitor{AlertAfter: 1, AlertDown: []string{"log"}}, false, "No commands"}, {m.Monitor{AlertAfter: 1, AlertDown: []string{"log"}}, m.ErrInvalidMonitor, "No commands"},
{m.Monitor{AlertAfter: -1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, false, "Invalid alert threshold, -1"}, {m.Monitor{AlertAfter: -1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, m.ErrInvalidMonitor, "Invalid alert threshold, -1"},
} }
for _, c := range cases { for _, c := range cases {
@@ -28,8 +30,11 @@ func TestMonitorIsValid(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.IsValid() actual := c.monitor.Validate()
if actual != c.expected { hasErr := (actual != nil)
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)
} }
}) })
@@ -111,6 +116,45 @@ 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
@@ -0,0 +1,3 @@
{
"$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
@@ -0,0 +1,12 @@
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"
}
+6 -1
View File
@@ -1,6 +1,11 @@
check_interval = "1s" check_interval = "1s"
default_alert_down = ["log_command"] default_alert_down = ["log_command"]
default_alert_after = 1 default_alert_every = 0
default_alert_after = 2
monitor "Default" {
command = ["echo"]
}
monitor "Command" { monitor "Command" {
command = ["echo", "$PATH"] command = ["echo", "$PATH"]
+5
View File
@@ -8,6 +8,11 @@ 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
@@ -1,25 +0,0 @@
---
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"