Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f6c924089 | ||
|
|
e614fb35b5 | ||
|
|
3323a562c9 | ||
|
|
c40841ef61 | ||
|
|
59ce31c35f | ||
|
|
39359ffb1e | ||
|
|
7d92458a7a | ||
|
|
723cf7d127 | ||
|
|
fd8e81cb1a | ||
|
|
0e291719c9 | ||
|
|
884fbaceac | ||
|
|
e262afdb1f | ||
|
|
a5268ae1f6 | ||
|
|
16ad16d873 | ||
|
|
f4fb75610a | ||
|
|
0ae7c6dbdf | ||
|
|
a06ed3540c | ||
|
|
200cfd1a2d | ||
|
|
bcbac39cad | ||
|
|
afacf40ec8 | ||
|
|
c18e9c8771 | ||
|
|
eb2987d3bc | ||
|
|
945c1b1ce0 | ||
|
|
b0ea3dc6d4 | ||
|
|
5c97f2e5c4 | ||
|
|
85afa3d9ef | ||
|
|
0a7aab7030 | ||
|
|
fb9d637614 | ||
|
|
98be873220 | ||
|
|
f59848fb61 | ||
|
|
e74fe89cab | ||
|
|
1bc8ab5dac | ||
|
|
f21dce1cc6 | ||
|
|
cfcdf04990 | ||
|
|
9892af48d1 | ||
|
|
c8e914d1b8 | ||
|
|
845604c54c | ||
|
|
49acca1c79 | ||
|
|
2bdafd908d | ||
|
|
b349ada44e | ||
|
|
fbf92d924c | ||
|
|
fd292b005c | ||
|
|
3ef06fb78d | ||
|
|
4aef7b7458 | ||
|
|
be2e8121c5 | ||
|
|
9e20c00dde | ||
|
|
3fb418151b | ||
|
|
df1c7aa74b | ||
|
|
7c72eabd6b | ||
|
|
0535bdf156 | ||
|
|
03f0ab69fe | ||
|
|
312821fa8d |
+2
-2
@@ -4,7 +4,7 @@ name: test
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: test
|
- name: test
|
||||||
image: golang:1.20
|
image: golang:1.26
|
||||||
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.26
|
||||||
environment:
|
environment:
|
||||||
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
|
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
|
||||||
commands:
|
commands:
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
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.1.0
|
||||||
|
|
||||||
|
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
|
||||||
|
linux/arm/v7
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
Vendored
+1
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
# User configuration
|
# User configuration
|
||||||
config.yml
|
config.yml
|
||||||
|
config.hcl
|
||||||
|
|
||||||
# Output binary
|
# Output binary
|
||||||
minitor
|
minitor
|
||||||
|
|||||||
+21
-23
@@ -1,36 +1,34 @@
|
|||||||
---
|
version: "2"
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
|
- errcheck
|
||||||
- errname
|
- errname
|
||||||
- errorlint
|
- errorlint
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- gofumpt
|
- gosec
|
||||||
- goimports
|
- govet
|
||||||
- gomnd
|
- ineffassign
|
||||||
- goprintffuncname
|
|
||||||
- misspell
|
- misspell
|
||||||
- tagliatelle
|
- mnd
|
||||||
- tenv
|
- modernize
|
||||||
|
- staticcheck
|
||||||
|
- tagalign
|
||||||
- testpackage
|
- testpackage
|
||||||
- thelper
|
- thelper
|
||||||
- tparallel
|
- tparallel
|
||||||
- unconvert
|
- unconvert
|
||||||
|
- unused
|
||||||
- wrapcheck
|
- wrapcheck
|
||||||
- wsl
|
- wsl_v5
|
||||||
disable:
|
exclusions:
|
||||||
- gochecknoglobals
|
generated: lax
|
||||||
|
presets:
|
||||||
linters-settings:
|
- common-false-positives
|
||||||
gosec:
|
|
||||||
excludes:
|
|
||||||
- G204
|
|
||||||
tagliatelle:
|
|
||||||
case:
|
|
||||||
rules:
|
rules:
|
||||||
yaml: snake
|
- linters:
|
||||||
|
|
||||||
issues:
|
|
||||||
exclude-rules:
|
|
||||||
- path: _test\.go
|
|
||||||
linters:
|
|
||||||
- gosec
|
- gosec
|
||||||
|
path: _test\.go
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
|||||||
@@ -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.10.1
|
||||||
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
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.20 AS builder
|
FROM golang:1.26 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
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.iamthefij.com/iamthefij/slog"
|
"git.iamthefij.com/iamthefij/slog/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -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
-11
@@ -1,30 +1,36 @@
|
|||||||
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 {
|
||||||
c := c
|
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -70,7 +76,6 @@ func TestAlertSend(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
c := c
|
|
||||||
|
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -116,7 +121,6 @@ func TestAlertBuildTemplate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
c := c
|
|
||||||
|
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|||||||
@@ -5,22 +5,27 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.iamthefij.com/iamthefij/slog"
|
"git.iamthefij.com/iamthefij/slog/v2"
|
||||||
/*
|
|
||||||
* "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
|
||||||
}
|
}
|
||||||
|
|||||||
+99
-11
@@ -1,40 +1,128 @@
|
|||||||
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
|
|
||||||
|
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
_, 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: new(0),
|
||||||
|
DefaultAlertDown: []string{"log_command"},
|
||||||
|
},
|
||||||
|
"override defaults",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"./test/valid-config.hcl",
|
||||||
|
m.Config{
|
||||||
|
CheckInterval: 30 * time.Second,
|
||||||
|
DefaultAlertAfter: 1,
|
||||||
|
DefaultAlertEvery: new(-1),
|
||||||
|
DefaultAlertDown: []string{},
|
||||||
|
},
|
||||||
|
"default defaults",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
module git.iamthefij.com/iamthefij/minitor-go
|
module git.iamthefij.com/iamthefij/minitor-go/v2
|
||||||
|
|
||||||
go 1.20
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.iamthefij.com/iamthefij/slog v1.3.0
|
git.iamthefij.com/iamthefij/slog/v2 v2.0.1
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.iamthefij.com/iamthefij/slog v1.3.0 h1:4Hu5PQvDrW5e3FrTS3q2iIXW0iPvhNY/9qJsqDR3K3I=
|
git.iamthefij.com/iamthefij/slog/v2 v2.0.1 h1:eXf3ga0aMTkm0l6EMYOQpg34S+dQw7cN8o+W1Cg/0C0=
|
||||||
git.iamthefij.com/iamthefij/slog v1.3.0/go.mod h1:1RUj4hcCompZkAxXCRfUX786tb3cM/Zpkn97dGfUfbg=
|
git.iamthefij.com/iamthefij/slog/v2 v2.0.1/go.mod h1:VFjX1e1tfHADyQr2wJBAz0JUQT+K/5FBjzSHgUKOuu8=
|
||||||
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
|
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
|
||||||
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||||
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
|
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
|
||||||
@@ -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=
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.iamthefij.com/iamthefij/slog"
|
"git.iamthefij.com/iamthefij/slog/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -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
-7
@@ -3,13 +3,9 @@ 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 {
|
|
||||||
return &v
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCheckConfig tests the checkConfig function
|
// TestCheckConfig tests the checkConfig function
|
||||||
// It also tests results for potentially invalid configuration. For example, no alerts
|
// It also tests results for potentially invalid configuration. For example, no alerts
|
||||||
func TestCheckMonitors(t *testing.T) {
|
func TestCheckMonitors(t *testing.T) {
|
||||||
@@ -91,7 +87,6 @@ func TestCheckMonitors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
c := c
|
|
||||||
|
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -171,7 +166,6 @@ func TestFirstRunAlerts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
c := c
|
|
||||||
|
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|||||||
+97
-34
@@ -1,11 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.iamthefij.com/iamthefij/slog"
|
"git.iamthefij.com/iamthefij/slog/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Monitor represents a particular periodic check of a command
|
// Monitor represents a particular periodic check of a command
|
||||||
@@ -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,
|
||||||
|
|||||||
+58
-19
@@ -1,35 +1,39 @@
|
|||||||
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 {
|
||||||
c := c
|
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -98,7 +102,6 @@ func TestMonitorGetAlertNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
c := c
|
|
||||||
|
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -111,6 +114,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) {
|
||||||
@@ -131,7 +173,6 @@ func TestMonitorFailureAlertAfter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
c := c
|
|
||||||
|
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -160,15 +201,14 @@ func TestMonitorFailureAlertEvery(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{m.Monitor{ShellCommand: "false", AlertAfter: 1}, []bool{true}, "No AlertEvery set"}, // Defaults to true because AlertAfter and AlertEvery default to nil
|
{m.Monitor{ShellCommand: "false", AlertAfter: 1}, []bool{true}, "No AlertEvery set"}, // Defaults to true because AlertAfter and AlertEvery default to nil
|
||||||
// Alert first time only, after 1
|
// Alert first time only, after 1
|
||||||
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(0)}, []bool{true, false, false}, "Alert first time only after 1"},
|
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: new(0)}, []bool{true, false, false}, "Alert first time only after 1"},
|
||||||
// Alert every time, after 1
|
// Alert every time, after 1
|
||||||
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(1)}, []bool{true, true, true}, "Alert every time after 1"},
|
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: new(1)}, []bool{true, true, true}, "Alert every time after 1"},
|
||||||
// Alert every other time, after 1
|
// Alert every other time, after 1
|
||||||
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(2)}, []bool{true, false, true, false}, "Alert every other time after 1"},
|
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: new(2)}, []bool{true, false, true, false}, "Alert every other time after 1"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
c := c
|
|
||||||
|
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -257,7 +297,6 @@ func TestMonitorCheck(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
c := c
|
|
||||||
|
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
||||||
+4
-4
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -24,7 +24,6 @@ func TestUtilEqualSliceString(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
c := c
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("%v %v", c.a, c.b), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%v %v", c.a, c.b), func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|||||||
Reference in New Issue
Block a user