Compare commits

..

3 Commits

Author SHA1 Message Date
Ian Fijolek
7cac6c94d7 Update readme and update some test files to be better examples 2024-11-15 12:05:17 -08:00
Ian Fijolek
e0af17a599 Refactor test package and some field types
Fairly big test refactor and changing some of the fields from pointers
2024-11-15 11:37:03 -08:00
Ian Fijolek
a0a6b8199a WIP: Try migration to hcl 2024-11-14 13:42:50 -08:00
22 changed files with 267 additions and 623 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ name: test
steps: steps:
- name: test - name: test
image: golang:1.21 image: golang:1.20
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.21 image: golang:1.20
environment: environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}} VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
commands: commands:
-113
View File
@@ -1,113 +0,0 @@
name: ci
on:
push:
branches:
- main
- master
tags:
- "v*"
pull_request:
branches:
- main
- master
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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@v4
- 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@v4
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Build binaries
env:
VERSION: "${{ vars.REF_NAME }}"
run: make all
# Package binaries and create release if this is a tagged build
- name: Compress binaries
if: "${{ github.event_name == '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.event_name == '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
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
linux/arm
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+20 -36
View File
@@ -1,13 +1,16 @@
version: "2" ---
linters: linters:
enable: enable:
- errname - errname
- errorlint - errorlint
- exhaustive - exhaustive
- gofumpt
- goimports
- gomnd
- goprintffuncname - goprintffuncname
- misspell - misspell
- mnd
- tagliatelle - tagliatelle
- tenv
- testpackage - testpackage
- thelper - thelper
- tparallel - tparallel
@@ -16,37 +19,18 @@ linters:
- wsl - wsl
disable: disable:
- gochecknoglobals - gochecknoglobals
settings:
gosec: linters-settings:
excludes: gosec:
- G204 excludes:
tagliatelle: - G204
case: tagliatelle:
rules: case:
json: snake rules:
yaml: snake yaml: snake
exclusions:
generated: lax issues:
presets: exclude-rules:
- comments - path: _test\.go
- common-false-positives linters:
- legacy - gosec
- std-error-handling
rules:
- linters:
- gosec
path: _test\.go
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
+4 -4
View File
@@ -1,7 +1,7 @@
--- ---
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 rev: v4.4.0
hooks: hooks:
- id: check-added-large-files - id: check-added-large-files
- id: check-yaml - id: check-yaml
@@ -11,10 +11,10 @@ repos:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: check-merge-conflict - id: check-merge-conflict
- repo: https://github.com/golangci/golangci-lint - repo: https://github.com/golangci/golangci-lint
rev: v2.7.2 rev: v1.52.2
hooks: hooks:
- id: golangci-lint - id: golangci-lint
- repo: https://github.com/hadolint/hadolint - repo: https://github.com/hadolint/hadolint
rev: refs/pull/1152/head rev: v2.12.1-beta
hooks: hooks:
- id: hadolint-github - id: hadolint
+2 -3
View File
@@ -1,11 +1,10 @@
FROM alpine:3.23 FROM alpine:3.18
RUN mkdir /app RUN mkdir /app
WORKDIR /app/ WORKDIR /app/
# Add common checking tools # Add common checking tools
# hadolint ignore=DL3018 RUN apk --no-cache add bash=~5 curl=~8 jq=~1 bind-tools=~9 tzdata~=2024a
RUN apk --no-cache add bash=~5 curl=~8 jq=~1 bind-tools=~9 tzdata
# Add minitor user for running as non-root # Add minitor user for running as non-root
RUN addgroup -S minitor && adduser -S minitor -G minitor RUN addgroup -S minitor && adduser -S minitor -G minitor
+3 -4
View File
@@ -1,4 +1,4 @@
FROM golang:1.25 AS builder FROM golang:1.20 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.23 FROM alpine:3.18
RUN mkdir /app RUN mkdir /app
WORKDIR /app/ WORKDIR /app/
@@ -21,8 +21,7 @@ WORKDIR /app/
COPY --from=builder /app/minitor . COPY --from=builder /app/minitor .
# Add common checking tools # Add common checking tools
# hadolint ignore=DL3018 RUN apk --no-cache add bash=~5 curl=~8 jq=~1 bind-tools=~9 tzdata~=2024a
RUN apk --no-cache add bash=~5 curl=~8 jq=~1 bind-tools=~9 tzdata
# Add minitor user for running as non-root # Add minitor user for running as non-root
RUN addgroup -S minitor && adduser -S minitor -G minitor RUN addgroup -S minitor && adduser -S minitor -G minitor
+1 -1
View File
@@ -75,7 +75,7 @@ monitor "example" {
alert_up = ["log"] alert_up = ["log"]
check_interval = "1m" check_interval = "1m"
alert_after = 1 alert_after = 1
alert_every = -1 alert_every = 0
} }
``` ```
+8 -27
View File
@@ -37,32 +37,13 @@ type AlertNotice struct {
LastCheckOutput string LastCheckOutput string
} }
// Validate checks that the Alert is properly configured and returns errors if not // IsValid returns a boolean indicating if the Alert has been correctly
func (alert Alert) Validate() error { // configured
hasCommand := len(alert.Command) > 0 func (alert Alert) IsValid() bool {
hasShellCommand := alert.ShellCommand != "" hasAtLeastOneCommand := alert.Command != nil || alert.ShellCommand != ""
hasAtMostOneCommand := alert.Command == nil || alert.ShellCommand == ""
var err error return hasAtLeastOneCommand && hasAtMostOneCommand
hasAtLeastOneCommand := hasCommand || hasShellCommand
if !hasAtLeastOneCommand {
err = errors.Join(err, fmt.Errorf(
"%w: alert %s has no command or shell_command configured",
ErrInvalidAlert,
alert.Name,
))
}
hasAtMostOneCommand := !(hasCommand && hasShellCommand)
if !hasAtMostOneCommand {
err = errors.Join(err, fmt.Errorf(
"%w: alert %s has both command and shell_command configured",
ErrInvalidAlert,
alert.Name,
))
}
return err
} }
// BuildTemplates compiles command templates for the Alert // BuildTemplates compiles command templates for the Alert
@@ -101,14 +82,14 @@ func (alert *Alert) BuildTemplates() error {
} }
switch { switch {
case alert.Command != nil: case alert.commandTemplate == nil && alert.Command != nil:
alert.commandTemplate = []*template.Template{} alert.commandTemplate = []*template.Template{}
for i, cmdPart := range alert.Command { for i, cmdPart := range alert.Command {
alert.commandTemplate = append(alert.commandTemplate, template.Must( alert.commandTemplate = append(alert.commandTemplate, template.Must(
template.New(alert.Name+fmt.Sprint(i)).Funcs(timeFormatFuncs).Parse(cmdPart), template.New(alert.Name+fmt.Sprint(i)).Funcs(timeFormatFuncs).Parse(cmdPart),
)) ))
} }
case alert.ShellCommand != "": case alert.commandShellTemplate == nil && alert.ShellCommand != "":
shellCmd := alert.ShellCommand shellCmd := alert.ShellCommand
alert.commandShellTemplate = template.Must( alert.commandShellTemplate = template.Must(
+7 -14
View File
@@ -1,24 +1,20 @@
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"
) )
func TestAlertValidate(t *testing.T) { func TestAlertIsValid(t *testing.T) {
t.Parallel()
cases := []struct { cases := []struct {
alert m.Alert alert m.Alert
expected error expected bool
name string name string
}{ }{
{m.Alert{Command: []string{"echo", "test"}}, nil, "Command only"}, {m.Alert{Command: []string{"echo", "test"}}, true, "Command only"},
{m.Alert{ShellCommand: "echo test"}, nil, "CommandShell only"}, {m.Alert{ShellCommand: "echo test"}, true, "CommandShell only"},
{m.Alert{Command: []string{"echo", "test"}, ShellCommand: "echo test"}, m.ErrInvalidAlert, "Both commands"}, {m.Alert{}, false, "No commands"},
{m.Alert{}, m.ErrInvalidAlert, "No commands"},
} }
for _, c := range cases { for _, c := range cases {
@@ -27,11 +23,8 @@ func TestAlertValidate(t *testing.T) {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
t.Parallel() t.Parallel()
actual := c.alert.Validate() actual := c.alert.IsValid()
hasErr := (actual != nil) if actual != c.expected {
expectErr := (c.expected != nil)
if hasErr != expectErr || !errors.Is(actual, c.expected) {
t.Errorf("expected=%t actual=%t", c.expected, actual) t.Errorf("expected=%t actual=%t", c.expected, actual)
} }
}) })
+111 -92
View File
@@ -6,26 +6,21 @@ import (
"time" "time"
"git.iamthefij.com/iamthefij/slog" "git.iamthefij.com/iamthefij/slog"
/*
* "github.com/hashicorp/hcl/v2"
* "github.com/hashicorp/hcl/v2/gohcl"
*/
"github.com/hashicorp/hcl/v2/hclsimple" "github.com/hashicorp/hcl/v2/hclsimple"
) )
var ( var errInvalidConfig = errors.New("Invalid configuration")
ErrLoadingConfig = errors.New("Failed to load or parse configuration")
ErrConfigInit = errors.New("Failed to initialize configuration")
ErrInvalidConfig = errors.New("Invalid configuration")
ErrNoAlerts = errors.New("No alerts provided")
ErrInvalidAlert = errors.New("Invalid alert configuration")
ErrNoMonitors = errors.New("No monitors provided")
ErrInvalidMonitor = errors.New("Invalid monitor configuration")
ErrUnknownAlert = errors.New("Unknown alert")
)
// Config type is contains all provided user configuration // Config type is contains all provided user configuration
type Config struct { type Config struct {
CheckIntervalStr string `hcl:"check_interval"` CheckIntervalStr string `hcl:"check_interval"`
CheckInterval time.Duration CheckInterval time.Duration
DefaultAlertAfter int `hcl:"default_alert_after,optional"` DefaultAlertAfter *int `hcl:"default_alert_after,optional"`
DefaultAlertEvery *int `hcl:"default_alert_every,optional"` DefaultAlertEvery *int `hcl:"default_alert_every,optional"`
DefaultAlertDown []string `hcl:"default_alert_down,optional"` DefaultAlertDown []string `hcl:"default_alert_down,optional"`
DefaultAlertUp []string `hcl:"default_alert_up,optional"` DefaultAlertUp []string `hcl:"default_alert_up,optional"`
@@ -35,77 +30,6 @@ type Config struct {
alertLookup map[string]*Alert alertLookup map[string]*Alert
} }
// Init performs extra initialization on top of loading the config from file
func (config *Config) Init() (err error) {
config.CheckInterval, err = time.ParseDuration(config.CheckIntervalStr)
if err != nil {
return fmt.Errorf("failed to parse top level check_interval duration: %w", err)
}
if config.DefaultAlertAfter == 0 {
minAlertAfter := 1
config.DefaultAlertAfter = minAlertAfter
}
if config.DefaultAlertEvery == nil {
defaultDefaultAlertEvery := -1
config.DefaultAlertEvery = &defaultDefaultAlertEvery
}
for _, monitor := range config.Monitors {
if err = monitor.Init(
config.DefaultAlertAfter,
config.DefaultAlertEvery,
config.DefaultAlertDown,
config.DefaultAlertUp,
); err != nil {
return
}
}
err = config.BuildAllTemplates()
return
}
// IsValid checks config validity and returns true if valid
func (config Config) IsValid() error {
var err error
// Validate alerts
if len(config.Alerts) == 0 {
err = errors.Join(err, ErrNoAlerts)
}
for _, alert := range config.Alerts {
err = errors.Join(err, alert.Validate())
}
// Validate monitors
if len(config.Monitors) == 0 {
err = errors.Join(err, ErrNoMonitors)
}
for _, monitor := range config.Monitors {
err = errors.Join(err, monitor.Validate())
// Check that all Monitor alerts actually exist
for _, isUp := range []bool{true, false} {
for _, alertName := range monitor.GetAlertNames(isUp) {
if _, ok := config.GetAlert(alertName); !ok {
err = errors.Join(
err,
fmt.Errorf("%w: %s. %w: %s", ErrInvalidMonitor, monitor.Name, ErrUnknownAlert, alertName),
)
}
}
}
}
return err
}
// GetAlert returns an alert by name
func (c Config) GetAlert(name string) (*Alert, bool) { func (c Config) GetAlert(name string) (*Alert, bool) {
if c.alertLookup == nil { if c.alertLookup == nil {
c.alertLookup = map[string]*Alert{} c.alertLookup = map[string]*Alert{}
@@ -130,24 +54,119 @@ func (c *Config) BuildAllTemplates() (err error) {
return return
} }
// LoadConfig will read config from the given path and parse it // IsValid checks config validity and returns true if valid
func LoadConfig(filePath string) (Config, error) { func (config Config) IsValid() (isValid bool) {
var config Config isValid = true
if err := hclsimple.DecodeFile(filePath, nil, &config); err != nil { // Validate alerts
return config, errors.Join(ErrLoadingConfig, err) if len(config.Alerts) == 0 {
// This should never happen because there is a default alert named 'log' for now
slog.Errorf("Invalid alert configuration: Must provide at least one alert")
isValid = false
}
for _, alert := range config.Alerts {
if !alert.IsValid() {
slog.Errorf("Invalid alert configuration: %+v", alert.Name)
isValid = false
}
}
// Validate monitors
if len(config.Monitors) == 0 {
slog.Errorf("Invalid monitor configuration: Must provide at least one monitor")
isValid = false
}
for _, monitor := range config.Monitors {
if !monitor.IsValid() {
slog.Errorf("Invalid monitor configuration: %s", monitor.Name)
isValid = false
}
// Check that all Monitor alerts actually exist
for _, isUp := range []bool{true, false} {
for _, alertName := range monitor.GetAlertNames(isUp) {
if _, ok := config.GetAlert(alertName); !ok {
slog.Errorf(
"Invalid monitor configuration: %s. Unknown alert %s",
monitor.Name, alertName,
)
isValid = false
}
}
}
}
return isValid
}
// Init performs extra initialization on top of loading the config from file
func (config *Config) Init() (err error) {
config.CheckInterval, err = time.ParseDuration(config.CheckIntervalStr)
if err != nil {
return fmt.Errorf("failed to parse top level check_interval duration: %w", err)
}
for _, monitor := range config.Monitors {
// TODO: Move this to a Monitor.Init() method
// Parse the check_interval string into a time.Duration
if monitor.CheckIntervalStr != nil {
monitor.CheckInterval, err = time.ParseDuration(*monitor.CheckIntervalStr)
if err != nil {
return fmt.Errorf("failed to parse check_interval duration for monitor %s: %w", monitor.Name, err)
}
}
// Set default values for monitor alerts
if monitor.AlertAfter == 0 && config.DefaultAlertAfter != nil {
monitor.AlertAfter = *config.DefaultAlertAfter
} else if monitor.AlertAfter == 0 {
monitor.AlertAfter = 1
}
if monitor.AlertEvery == nil {
monitor.AlertEvery = config.DefaultAlertEvery
}
if monitor.AlertDown == nil {
monitor.AlertDown = config.DefaultAlertDown
}
if monitor.AlertUp == nil {
monitor.AlertUp = config.DefaultAlertUp
}
}
err = config.BuildAllTemplates()
return
}
// LoadConfig will read config from the given path and parse it
func LoadConfig(filePath string) (config Config, err error) {
err = hclsimple.DecodeFile(filePath, nil, &config)
if err != nil {
return
} }
slog.Debugf("Config values:\n%v\n", config) slog.Debugf("Config values:\n%v\n", config)
// Finish initializing configuration // Finish initializing configuration
if err := config.Init(); err != nil { if err = config.Init(); err != nil {
return config, errors.Join(ErrConfigInit, err) return
} }
if err := config.IsValid(); err != nil { if !config.IsValid() {
return config, errors.Join(ErrInvalidConfig, err) err = errInvalidConfig
return
} }
return config, nil return config, err
} }
+11 -101
View File
@@ -1,27 +1,23 @@
package main_test package main_test
import ( import (
"errors"
"testing" "testing"
"time"
m "git.iamthefij.com/iamthefij/minitor-go" m "git.iamthefij.com/iamthefij/minitor-go"
) )
func TestLoadConfig(t *testing.T) { func TestLoadConfig(t *testing.T) {
cases := []struct { cases := []struct {
configPath string configPath string
expectedErr error expectErr bool
name string name string
}{ }{
{"./test/does-not-exist", m.ErrLoadingConfig, "Invalid config path"}, {"./test/does-not-exist", true, "Invalid config path"},
{"./test/invalid-config-wrong-hcl-type.hcl", m.ErrLoadingConfig, "Incorrect HCL type"}, {"./test/invalid-config-missing-alerts.hcl", true, "Invalid config missing alerts"},
{"./test/invalid-config-missing-alerts.hcl", m.ErrNoAlerts, "Invalid config missing alerts"}, {"./test/invalid-config-type.hcl", true, "Invalid config type for key"},
{"./test/invalid-config-missing-alerts.hcl", m.ErrInvalidConfig, "Invalid config general"}, {"./test/invalid-config-unknown-alert.hcl", true, "Invalid config unknown alert"},
{"./test/invalid-config-invalid-duration.hcl", m.ErrConfigInit, "Invalid config type for key"}, {"./test/valid-config-default-values.hcl", false, "Valid config file with default values"},
{"./test/invalid-config-unknown-alert.hcl", m.ErrUnknownAlert, "Invalid config unknown alert"}, {"./test/valid-config.hcl", false, "Valid config file"},
{"./test/valid-config-default-values.hcl", nil, "Valid config file with default values"},
{"./test/valid-config.hcl", nil, "Valid config file"},
} }
for _, c := range cases { for _, c := range cases {
c := c c := c
@@ -31,100 +27,14 @@ func TestLoadConfig(t *testing.T) {
_, err := m.LoadConfig(c.configPath) _, err := m.LoadConfig(c.configPath)
hasErr := (err != nil) hasErr := (err != nil)
expectErr := (c.expectedErr != nil)
if hasErr != expectErr || !errors.Is(err, c.expectedErr) { if hasErr != c.expectErr {
t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectedErr, err) t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectErr, err)
} }
}) })
} }
} }
func TestDefaultConfig(t *testing.T) {
cases := []struct {
configPath string
expectedResult m.Config
name string
}{
{
"./test/valid-config-default-values.hcl",
m.Config{
CheckInterval: 1 * time.Second,
DefaultAlertAfter: 2,
DefaultAlertEvery: Ptr(0),
DefaultAlertDown: []string{"log_command"},
},
"override defaults",
},
{
"./test/valid-config.hcl",
m.Config{
CheckInterval: 30 * time.Second,
DefaultAlertAfter: 1,
DefaultAlertEvery: Ptr(-1),
DefaultAlertDown: []string{},
},
"default defaults",
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
config, err := m.LoadConfig(c.configPath)
if err != nil {
t.Errorf("Got error when loading config file %q: %s", c.configPath, err)
}
// Test Config has default values
if config.DefaultAlertAfter != c.expectedResult.DefaultAlertAfter {
t.Errorf("Got unexpected DefaultAlertAfter from file %q: expected=%v actual=%v", c.configPath, c.expectedResult.DefaultAlertAfter, config.DefaultAlertAfter)
}
if *config.DefaultAlertEvery != *c.expectedResult.DefaultAlertEvery {
t.Errorf("Got unexpected DefaultAlertEvery from file %q: expected=%v actual=%v", c.configPath, *c.expectedResult.DefaultAlertEvery, *config.DefaultAlertEvery)
}
if !m.EqualSliceString(config.DefaultAlertUp, c.expectedResult.DefaultAlertUp) {
t.Errorf("Got unexpected DefaultAlertUp from file %q: expected=%v actual=%v", c.configPath, c.expectedResult.DefaultAlertUp, config.DefaultAlertUp)
}
if !m.EqualSliceString(config.DefaultAlertDown, c.expectedResult.DefaultAlertDown) {
t.Errorf("Got unexpected DefaultAlertDown from file %q: expected=%v actual=%v", c.configPath, c.expectedResult.DefaultAlertDown, config.DefaultAlertDown)
}
// Check that monitor defaults propagate
var defaultMonitor *m.Monitor
for _, monitor := range config.Monitors {
if monitor.Name == "Default" {
defaultMonitor = monitor
}
}
if defaultMonitor == nil {
t.Errorf("failed to find default monitor in %q", c.configPath)
}
if defaultMonitor.AlertAfter != c.expectedResult.DefaultAlertAfter {
t.Errorf("Got unexpected AlertAfter from file %q: expected=%v actual=%v", c.configPath, c.expectedResult.DefaultAlertAfter, defaultMonitor.AlertAfter)
}
if *defaultMonitor.AlertEvery != *c.expectedResult.DefaultAlertEvery {
t.Errorf("Got unexpected AlertEvery from file %q: expected=%v actual=%v", c.configPath, *c.expectedResult.DefaultAlertEvery, *defaultMonitor.AlertEvery)
}
if !m.EqualSliceString(defaultMonitor.AlertUp, c.expectedResult.DefaultAlertUp) {
t.Errorf("Got unexpected AlertUp from file %q: expected=%v actual=%v", c.configPath, c.expectedResult.DefaultAlertUp, defaultMonitor.AlertUp)
}
// NOTE: Can't compare AlertDown because default is empty and that is invalid
})
}
}
// TestMultiLineConfig is a more complicated test stepping through the parsing // TestMultiLineConfig is a more complicated test stepping through the parsing
// and execution of mutli-line strings presented in YAML // and execution of mutli-line strings presented in YAML
func TestMultiLineConfig(t *testing.T) { func TestMultiLineConfig(t *testing.T) {
+10 -12
View File
@@ -1,27 +1,25 @@
module git.iamthefij.com/iamthefij/minitor-go module git.iamthefij.com/iamthefij/minitor-go
go 1.23.0 go 1.20
require ( require (
git.iamthefij.com/iamthefij/slog v1.3.0 git.iamthefij.com/iamthefij/slog v1.3.0
github.com/hashicorp/hcl/v2 v2.11.1 github.com/hashicorp/hcl/v2 v2.11.1
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.19.0
) )
require ( require (
github.com/agext/levenshtein v1.2.1 // indirect github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.12.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/zclconf/go-cty v1.8.0 // indirect github.com/zclconf/go-cty v1.8.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/sys v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/text v0.28.0 // indirect google.golang.org/protobuf v1.32.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
) )
+19 -38
View File
@@ -8,8 +8,8 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
@@ -18,43 +18,33 @@ github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJbcc= github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJbcc=
github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
@@ -62,10 +52,6 @@ github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q
github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA= github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA=
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -76,21 +62,16 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+1 -1
View File
@@ -136,7 +136,7 @@ func main() {
// Load configuration // Load configuration
config, err := LoadConfig(*configPath) config, err := LoadConfig(*configPath)
slog.OnErrFatalf(err, "Error loading config") slog.OnErrFatalf(err, "Error loading config: %v", err)
// Serve metrics exporter, if specified // Serve metrics exporter, if specified
if ExportMetrics { if ExportMetrics {
+32 -95
View File
@@ -1,8 +1,6 @@
package main package main
import ( import (
"errors"
"fmt"
"math" "math"
"os/exec" "os/exec"
"time" "time"
@@ -16,8 +14,7 @@ type Monitor struct { //nolint:maligned
CheckIntervalStr *string `hcl:"check_interval,optional"` CheckIntervalStr *string `hcl:"check_interval,optional"`
CheckInterval time.Duration CheckInterval time.Duration
Name string `hcl:"name,label"` Name string `hcl:"name,label"`
AlertCount int
AlertAfter int `hcl:"alert_after,optional"` AlertAfter int `hcl:"alert_after,optional"`
AlertEvery *int `hcl:"alert_every,optional"` AlertEvery *int `hcl:"alert_every,optional"`
AlertDown []string `hcl:"alert_down,optional"` AlertDown []string `hcl:"alert_down,optional"`
@@ -26,6 +23,7 @@ type Monitor struct { //nolint:maligned
ShellCommand string `hcl:"shell_command,optional"` ShellCommand string `hcl:"shell_command,optional"`
// Other values // Other values
alertCount int
failureCount int failureCount int
lastCheck time.Time lastCheck time.Time
lastSuccess time.Time lastSuccess time.Time
@@ -33,91 +31,30 @@ type Monitor struct { //nolint:maligned
lastCheckDuration time.Duration lastCheckDuration time.Duration
} }
// Init initializes the Monitor with default values // IsValid returns a boolean indicating if the Monitor has been correctly
func (monitor *Monitor) Init(defaultAlertAfter int, defaultAlertEvery *int, defaultAlertDown []string, defaultAlertUp []string) error { // configured
// Parse the check_interval string into a time.Duration func (monitor Monitor) IsValid() bool {
if monitor.CheckIntervalStr != nil { // TODO: Refactor and return an error containing more information on what was invalid
var err error
monitor.CheckInterval, err = time.ParseDuration(*monitor.CheckIntervalStr)
if err != nil {
return fmt.Errorf("failed to parse check_interval duration for monitor %s: %w", monitor.Name, err)
}
}
// Set default values for monitor alerts
if monitor.AlertAfter == 0 {
minAlertAfter := 1
monitor.AlertAfter = max(defaultAlertAfter, minAlertAfter)
}
if monitor.AlertEvery == nil {
monitor.AlertEvery = defaultAlertEvery
}
if len(monitor.AlertDown) == 0 {
monitor.AlertDown = defaultAlertDown
}
if len(monitor.AlertUp) == 0 {
monitor.AlertUp = defaultAlertUp
}
return nil
}
// Validate checks that the Monitor is properly configured and returns errors if not
func (monitor Monitor) Validate() error {
hasCommand := len(monitor.Command) > 0 hasCommand := len(monitor.Command) > 0
hasShellCommand := monitor.ShellCommand != "" hasShellCommand := monitor.ShellCommand != ""
hasValidAlertAfter := monitor.AlertAfter > 0 hasValidAlertAfter := monitor.AlertAfter > 0
hasAlertDown := len(monitor.AlertDown) > 0 hasAlertDown := len(monitor.AlertDown) > 0
var err error
hasAtLeastOneCommand := hasCommand || hasShellCommand hasAtLeastOneCommand := hasCommand || hasShellCommand
if !hasAtLeastOneCommand {
err = errors.Join(err, fmt.Errorf(
"%w: monitor %s has no command or shell_command configured",
ErrInvalidMonitor,
monitor.Name,
))
}
hasAtMostOneCommand := !(hasCommand && hasShellCommand) hasAtMostOneCommand := !(hasCommand && hasShellCommand)
if !hasAtMostOneCommand {
err = errors.Join(err, fmt.Errorf(
"%w: monitor %s has both command and shell_command configured",
ErrInvalidMonitor,
monitor.Name,
))
}
if !hasValidAlertAfter { return hasAtLeastOneCommand &&
err = errors.Join(err, fmt.Errorf( hasAtMostOneCommand &&
"%w: monitor %s has invalid alert_after value %d. Must be greater than 0", hasValidAlertAfter &&
ErrInvalidMonitor, hasAlertDown
monitor.Name,
monitor.AlertAfter,
))
}
if !hasAlertDown {
err = errors.Join(err, fmt.Errorf(
"%w: monitor %s has no alert_down configured. Configure one here or add a default_alert_down",
ErrInvalidMonitor,
monitor.Name,
))
}
return err
} }
func (monitor Monitor) LastOutput() string { func (monitor Monitor) LastOutput() string {
return monitor.lastOutput return monitor.lastOutput
} }
// ShouldCheck returns a boolean indicating if the Monitor is ready to be be checked again // ShouldCheck returns a boolean indicating if the Monitor is ready to be
// be checked again
func (monitor Monitor) ShouldCheck() bool { func (monitor Monitor) ShouldCheck() bool {
if monitor.lastCheck.IsZero() || monitor.CheckInterval == 0 { if monitor.lastCheck.IsZero() || monitor.CheckInterval == 0 {
return true return true
@@ -128,7 +65,8 @@ func (monitor Monitor) ShouldCheck() bool {
return sinceLastCheck >= monitor.CheckInterval return sinceLastCheck >= monitor.CheckInterval
} }
// Check will run the command configured by the Monitor and return a status and a possible AlertNotice // Check will run the command configured by the Monitor and return a status
// and a possible AlertNotice
func (monitor *Monitor) Check() (bool, *AlertNotice) { func (monitor *Monitor) Check() (bool, *AlertNotice) {
var cmd *exec.Cmd var cmd *exec.Cmd
if len(monitor.Command) > 0 { if len(monitor.Command) > 0 {
@@ -149,9 +87,9 @@ func (monitor *Monitor) Check() (bool, *AlertNotice) {
isSuccess := (err == nil) isSuccess := (err == nil)
if isSuccess { if isSuccess {
alertNotice = monitor.Success() alertNotice = monitor.success()
} else { } else {
alertNotice = monitor.Failure() alertNotice = monitor.failure()
} }
slog.Debugf("Command output: %s", monitor.lastOutput) slog.Debugf("Command output: %s", monitor.lastOutput)
@@ -167,18 +105,9 @@ func (monitor *Monitor) Check() (bool, *AlertNotice) {
return isSuccess, alertNotice return isSuccess, alertNotice
} }
// GetAlertNames gives a list of alert names for a given monitor status
func (monitor Monitor) GetAlertNames(up bool) []string {
if up {
return monitor.AlertUp
}
return monitor.AlertDown
}
// IsUp returns the status of the current monitor // IsUp returns the status of the current monitor
func (monitor Monitor) IsUp() bool { func (monitor Monitor) IsUp() bool {
return monitor.AlertCount == 0 return monitor.alertCount == 0
} }
// LastCheckMilliseconds gives number of miliseconds the last check ran for // LastCheckMilliseconds gives number of miliseconds the last check ran for
@@ -186,20 +115,20 @@ func (monitor Monitor) LastCheckMilliseconds() int64 {
return monitor.lastCheckDuration.Milliseconds() return monitor.lastCheckDuration.Milliseconds()
} }
func (monitor *Monitor) Success() (notice *AlertNotice) { func (monitor *Monitor) success() (notice *AlertNotice) {
if !monitor.IsUp() { if !monitor.IsUp() {
// Alert that we have recovered // Alert that we have recovered
notice = monitor.createAlertNotice(true) notice = monitor.createAlertNotice(true)
} }
monitor.failureCount = 0 monitor.failureCount = 0
monitor.AlertCount = 0 monitor.alertCount = 0
monitor.lastSuccess = time.Now() monitor.lastSuccess = time.Now()
return return
} }
func (monitor *Monitor) Failure() (notice *AlertNotice) { func (monitor *Monitor) failure() (notice *AlertNotice) {
monitor.failureCount++ monitor.failureCount++
// If we haven't hit the minimum failures, we can exit // If we haven't hit the minimum failures, we can exit
if monitor.failureCount < monitor.AlertAfter { if monitor.failureCount < monitor.AlertAfter {
@@ -231,25 +160,33 @@ func (monitor *Monitor) Failure() (notice *AlertNotice) {
} }
default: default:
// Handle negative numbers indicating an exponential backoff // Handle negative numbers indicating an exponential backoff
if failureCount >= int(math.Pow(2, float64(monitor.AlertCount))-1) { //nolint:mnd if failureCount >= int(math.Pow(2, float64(monitor.alertCount))-1) { //nolint:gomnd
notice = monitor.createAlertNotice(false) notice = monitor.createAlertNotice(false)
} }
} }
// If we're going to alert, increment count // If we're going to alert, increment count
if notice != nil { if notice != nil {
monitor.AlertCount++ monitor.alertCount++
notice.AlertCount = monitor.AlertCount
} }
return notice return notice
} }
// GetAlertNames gives a list of alert names for a given monitor status
func (monitor Monitor) GetAlertNames(up bool) []string {
if up {
return monitor.AlertUp
}
return monitor.AlertDown
}
func (monitor Monitor) createAlertNotice(isUp bool) *AlertNotice { func (monitor Monitor) createAlertNotice(isUp bool) *AlertNotice {
// TODO: Maybe add something about recovery status here // TODO: Maybe add something about recovery status here
return &AlertNotice{ return &AlertNotice{
MonitorName: monitor.Name, MonitorName: monitor.Name,
AlertCount: monitor.AlertCount, AlertCount: monitor.alertCount,
FailureCount: monitor.failureCount, FailureCount: monitor.failureCount,
LastCheckOutput: monitor.lastOutput, LastCheckOutput: monitor.lastOutput,
LastSuccess: monitor.lastSuccess, LastSuccess: monitor.lastSuccess,
+10 -54
View File
@@ -1,7 +1,6 @@
package main_test package main_test
import ( import (
"errors"
"reflect" "reflect"
"testing" "testing"
"time" "time"
@@ -9,19 +8,18 @@ import (
m "git.iamthefij.com/iamthefij/minitor-go" m "git.iamthefij.com/iamthefij/minitor-go"
) )
func TestMonitorValidate(t *testing.T) { // TestMonitorIsValid tests the Monitor.IsValid()
t.Parallel() func TestMonitorIsValid(t *testing.T) {
cases := []struct { cases := []struct {
monitor m.Monitor monitor m.Monitor
expected error expected bool
name string name string
}{ }{
{m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, nil, "Command only"}, {m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, true, "Command only"},
{m.Monitor{AlertAfter: 1, ShellCommand: "echo test", AlertDown: []string{"log"}}, nil, "CommandShell only"}, {m.Monitor{AlertAfter: 1, ShellCommand: "echo test", AlertDown: []string{"log"}}, true, "CommandShell only"},
{m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}}, m.ErrInvalidMonitor, "No AlertDown"}, {m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}}, false, "No AlertDown"},
{m.Monitor{AlertAfter: 1, AlertDown: []string{"log"}}, m.ErrInvalidMonitor, "No commands"}, {m.Monitor{AlertAfter: 1, AlertDown: []string{"log"}}, false, "No commands"},
{m.Monitor{AlertAfter: -1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, m.ErrInvalidMonitor, "Invalid alert threshold, -1"}, {m.Monitor{AlertAfter: -1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, false, "Invalid alert threshold, -1"},
} }
for _, c := range cases { for _, c := range cases {
@@ -30,11 +28,8 @@ func TestMonitorValidate(t *testing.T) {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
t.Parallel() t.Parallel()
actual := c.monitor.Validate() actual := c.monitor.IsValid()
hasErr := (actual != nil) if actual != c.expected {
expectErr := (c.expected != nil)
if hasErr != expectErr || !errors.Is(actual, c.expected) {
t.Errorf("IsValid(%v), expected=%t actual=%t", c.name, c.expected, actual) t.Errorf("IsValid(%v), expected=%t actual=%t", c.name, c.expected, actual)
} }
}) })
@@ -116,45 +111,6 @@ func TestMonitorGetAlertNames(t *testing.T) {
} }
} }
func TestMonitorAlertCount(t *testing.T) {
alertEvery := 1
cases := []struct {
checkSuccess bool
alertCount int
name string
}{
{false, 1, "First failure and first alert"},
{false, 2, "Second failure and first alert"},
{true, 2, "Success should preserve past alert count"},
{false, 1, "First failure and first alert after success"},
}
// Unlike previous tests, this one requires a static Monitor with repeated
// calls to the failure method
monitor := m.Monitor{AlertAfter: 1, AlertEvery: &alertEvery}
for _, c := range cases {
t.Logf("Testing case %s", c.name)
var notice *m.AlertNotice
if c.checkSuccess {
notice = monitor.Success()
} else {
notice = monitor.Failure()
}
if notice == nil {
t.Fatalf("failure(%v) expected notice, got nil", c.name)
}
if notice.AlertCount != c.alertCount {
t.Errorf("failure(%v), expected=%v actual=%v", c.name, c.alertCount, notice.AlertCount)
t.Logf("Case failed: %s", c.name)
}
}
}
// TestMonitorFailureAlertAfter tests that alerts will not trigger until // TestMonitorFailureAlertAfter tests that alerts will not trigger until
// hitting the threshold provided by AlertAfter // hitting the threshold provided by AlertAfter
func TestMonitorFailureAlertAfter(t *testing.T) { func TestMonitorFailureAlertAfter(t *testing.T) {
-3
View File
@@ -1,3 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}
-12
View File
@@ -1,12 +0,0 @@
check_interval = "1s"
alert "log_command" {
command = "should be a list"
}
monitor "Command" {
command = ["echo", "$PATH"]
alert_down = ["log_command"]
alert_every = 2
check_interval = "10s"
}
+1 -6
View File
@@ -1,11 +1,6 @@
check_interval = "1s" check_interval = "1s"
default_alert_down = ["log_command"] default_alert_down = ["log_command"]
default_alert_every = 0 default_alert_after = 1
default_alert_after = 2
monitor "Default" {
command = ["echo"]
}
monitor "Command" { monitor "Command" {
command = ["echo", "$PATH"] command = ["echo", "$PATH"]
-5
View File
@@ -8,11 +8,6 @@ alert "log_shell" {
shell_command = "echo \"Failure on {{.MonitorName}} User is $USER\"" shell_command = "echo \"Failure on {{.MonitorName}} User is $USER\""
} }
monitor "Default" {
command = ["echo"]
alert_down = ["log_command"]
}
monitor "Command" { monitor "Command" {
command = ["echo", "$PATH"] command = ["echo", "$PATH"]
alert_down = ["log_command", "log_shell"] alert_down = ["log_command", "log_shell"]
+25
View File
@@ -0,0 +1,25 @@
---
check_interval: 1s
monitors:
- name: Command
command: ["echo", "$PATH"]
alert_down: ["log_command", "log_shell"]
alert_every: 0
check_interval: 10s
- name: Shell
command: >
echo 'Some string with stuff';
echo 'another line';
echo $PATH;
exit 1
alert_down: ["log_command", "log_shell"]
alert_after: 5
alert_every: 0
check_interval: 1m
alerts:
log_command:
command: ["echo", "regular", '"command!!!"', "{{.MonitorName}}"]
log_shell:
command: echo "Failure on {{.MonitorName}} User is $USER"