Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4fb75610a | ||
|
|
0ae7c6dbdf | ||
|
|
a06ed3540c | ||
|
|
bcbac39cad | ||
|
|
c18e9c8771 | ||
|
|
eb2987d3bc | ||
|
|
945c1b1ce0 | ||
|
|
b0ea3dc6d4 | ||
|
|
5c97f2e5c4 | ||
|
|
85afa3d9ef | ||
|
|
fb9d637614 | ||
|
|
98be873220 | ||
|
|
f59848fb61 | ||
|
|
e74fe89cab | ||
|
|
1bc8ab5dac | ||
|
|
f21dce1cc6 | ||
|
|
cfcdf04990 | ||
|
|
9892af48d1 | ||
|
|
c8e914d1b8 | ||
|
|
845604c54c | ||
|
|
49acca1c79 | ||
|
|
2bdafd908d | ||
|
|
b349ada44e | ||
|
|
fbf92d924c | ||
|
|
fd292b005c | ||
|
|
3ef06fb78d | ||
|
|
4aef7b7458 | ||
|
|
be2e8121c5 | ||
|
|
9e20c00dde | ||
|
|
3fb418151b | ||
|
|
df1c7aa74b | ||
|
|
7c72eabd6b | ||
|
|
0535bdf156 | ||
|
|
03f0ab69fe | ||
|
|
9ec62528d9 | ||
|
|
312821fa8d | ||
|
|
32745c816c | ||
|
|
3f6c8f5a22 | ||
|
|
67d7e0574e | ||
|
|
7604138c9e | ||
|
|
f58b4c1495 | ||
|
|
6a2b44673e | ||
|
|
01cca50532 | ||
|
|
2789aa63e4 | ||
|
|
37db4b2db0 | ||
|
|
41a1dbeceb | ||
|
|
c02d64d674 | ||
|
|
46f4561bea | ||
|
|
a1e0e9698b | ||
|
|
ded4e129a1 | ||
|
|
95cb24ac04 | ||
|
|
e6447b615f | ||
|
|
2680eabd40 | ||
|
|
23340e823f | ||
|
|
99b8723abc | ||
|
|
7d87c3d036 | ||
|
|
deec04bf0d |
+8
-44
@@ -4,7 +4,7 @@ name: test
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: test
|
- name: test
|
||||||
image: golang:1.17
|
image: golang:1.21
|
||||||
environment:
|
environment:
|
||||||
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
|
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
|
||||||
commands:
|
commands:
|
||||||
@@ -30,7 +30,7 @@ trigger:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: build all binaries
|
- name: build all binaries
|
||||||
image: golang:1.17
|
image: golang:1.21
|
||||||
environment:
|
environment:
|
||||||
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
|
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
|
||||||
commands:
|
commands:
|
||||||
@@ -60,51 +60,15 @@ steps:
|
|||||||
when:
|
when:
|
||||||
event: tag
|
event: tag
|
||||||
|
|
||||||
- name: push image - arm
|
- name: Build and publish docker images
|
||||||
image: plugins/docker
|
image: thegeeklab/drone-docker-buildx
|
||||||
settings:
|
settings:
|
||||||
repo: iamthefij/minitor-go
|
repo: iamthefij/minitor-go
|
||||||
auto_tag: true
|
auto_tag: true
|
||||||
auto_tag_suffix: linux-arm
|
platforms:
|
||||||
username:
|
- linux/amd64
|
||||||
from_secret: docker_username
|
- linux/arm64
|
||||||
password:
|
- linux/arm
|
||||||
from_secret: docker_password
|
|
||||||
build_args:
|
|
||||||
- ARCH=arm
|
|
||||||
- REPO=arm32v7
|
|
||||||
|
|
||||||
- name: push image - arm64
|
|
||||||
image: plugins/docker
|
|
||||||
settings:
|
|
||||||
repo: iamthefij/minitor-go
|
|
||||||
auto_tag: true
|
|
||||||
auto_tag_suffix: linux-arm64
|
|
||||||
username:
|
|
||||||
from_secret: docker_username
|
|
||||||
password:
|
|
||||||
from_secret: docker_password
|
|
||||||
build_args:
|
|
||||||
- ARCH=arm64
|
|
||||||
- REPO=arm64v8
|
|
||||||
|
|
||||||
- name: push image - amd64
|
|
||||||
image: plugins/docker
|
|
||||||
settings:
|
|
||||||
repo: iamthefij/minitor-go
|
|
||||||
auto_tag: true
|
|
||||||
auto_tag_suffix: linux-amd64
|
|
||||||
username:
|
|
||||||
from_secret: docker_username
|
|
||||||
password:
|
|
||||||
from_secret: docker_password
|
|
||||||
|
|
||||||
- name: publish manifest
|
|
||||||
image: plugins/manifest
|
|
||||||
settings:
|
|
||||||
spec: manifest.tmpl
|
|
||||||
auto_tag: true
|
|
||||||
ignore_missing: true
|
|
||||||
username:
|
username:
|
||||||
from_secret: docker_username
|
from_secret: docker_username
|
||||||
password:
|
password:
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
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 }}
|
||||||
Vendored
+1
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
# User configuration
|
# User configuration
|
||||||
config.yml
|
config.yml
|
||||||
|
config.hcl
|
||||||
|
|
||||||
# Output binary
|
# Output binary
|
||||||
minitor
|
minitor
|
||||||
|
|||||||
+28
-13
@@ -1,17 +1,13 @@
|
|||||||
---
|
version: "2"
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
- errname
|
- errname
|
||||||
- errorlint
|
- errorlint
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- gofumpt
|
|
||||||
- goimports
|
|
||||||
- gomnd
|
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
- misspell
|
- misspell
|
||||||
- promlinter
|
- mnd
|
||||||
- tagliatelle
|
- tagliatelle
|
||||||
- tenv
|
|
||||||
- testpackage
|
- testpackage
|
||||||
- thelper
|
- thelper
|
||||||
- tparallel
|
- tparallel
|
||||||
@@ -20,18 +16,37 @@ linters:
|
|||||||
- wsl
|
- wsl
|
||||||
disable:
|
disable:
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
|
settings:
|
||||||
linters-settings:
|
|
||||||
gosec:
|
gosec:
|
||||||
excludes:
|
excludes:
|
||||||
- G204
|
- G204
|
||||||
tagliatelle:
|
tagliatelle:
|
||||||
case:
|
case:
|
||||||
rules:
|
rules:
|
||||||
|
json: snake
|
||||||
yaml: snake
|
yaml: snake
|
||||||
|
exclusions:
|
||||||
issues:
|
generated: lax
|
||||||
exclude-rules:
|
presets:
|
||||||
- path: _test\.go
|
- comments
|
||||||
linters:
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
rules:
|
||||||
|
- linters:
|
||||||
- gosec
|
- gosec
|
||||||
|
path: _test\.go
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
|||||||
@@ -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.50.1
|
rev: v2.7.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint
|
- id: golangci-lint
|
||||||
- repo: https://github.com/hadolint/hadolint
|
- repo: https://github.com/hadolint/hadolint
|
||||||
rev: v2.12.1-beta
|
rev: refs/pull/1152/head
|
||||||
hooks:
|
hooks:
|
||||||
- id: hadolint
|
- id: hadolint-github
|
||||||
|
|||||||
+6
-5
@@ -1,11 +1,11 @@
|
|||||||
ARG REPO=library
|
FROM alpine:3.23
|
||||||
FROM ${REPO}/alpine:3.12
|
|
||||||
|
|
||||||
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.0 curl=~7.79 jq=~1.6
|
# 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
|
||||||
@@ -15,8 +15,9 @@ COPY ./scripts /app/scripts
|
|||||||
RUN chmod -R 755 /app/scripts
|
RUN chmod -R 755 /app/scripts
|
||||||
|
|
||||||
# Copy minitor in
|
# Copy minitor in
|
||||||
ARG ARCH=amd64
|
ARG TARGETOS
|
||||||
COPY ./dist/minitor-linux-${ARCH} ./minitor
|
ARG TARGETARCH
|
||||||
|
COPY ./dist/minitor-${TARGETOS}-${TARGETARCH} ./minitor
|
||||||
|
|
||||||
# Drop to non-root user
|
# Drop to non-root user
|
||||||
USER minitor
|
USER minitor
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
ARG REPO=library
|
FROM golang:1.25 AS builder
|
||||||
FROM golang:1.17 AS builder
|
|
||||||
|
|
||||||
RUN mkdir /app
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY ./go.mod ./go.sum /app/
|
COPY ./go.mod ./go.sum /app/
|
||||||
@@ -9,12 +7,13 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY ./*.go /app/
|
COPY ./*.go /app/
|
||||||
|
|
||||||
ARG ARCH=amd64
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ENV CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH}
|
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 ${REPO}/alpine:3.12
|
FROM alpine:3.23
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
|
|
||||||
@@ -22,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.0 curl=~7.79 jq=~1.6
|
# 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,6 +1,6 @@
|
|||||||
DOCKER_TAG ?= minitor-go-${USER}
|
DOCKER_TAG ?= minitor-go-${USER}
|
||||||
VERSION ?= $(shell git describe --tags --dirty)
|
VERSION ?= $(shell git describe --tags --dirty)
|
||||||
GOFILES = *.go
|
GOFILES = *.go go.mod go.sum
|
||||||
# Multi-arch targets are generated from this
|
# Multi-arch targets are generated from this
|
||||||
TARGET_ALIAS = minitor-linux-amd64 minitor-linux-arm minitor-linux-arm64 minitor-darwin-amd64
|
TARGET_ALIAS = minitor-linux-amd64 minitor-linux-arm minitor-linux-arm64 minitor-darwin-amd64
|
||||||
TARGETS = $(addprefix dist/,$(TARGET_ALIAS))
|
TARGETS = $(addprefix dist/,$(TARGET_ALIAS))
|
||||||
@@ -63,7 +63,7 @@ docker-build:
|
|||||||
|
|
||||||
.PHONY: docker-run
|
.PHONY: docker-run
|
||||||
docker-run: docker-build
|
docker-run: docker-build
|
||||||
docker run --rm -v $(shell pwd)/config.yml:/root/config.yml $(DOCKER_TAG)
|
docker run --rm -v $(shell pwd)/sample-config.hcl:/root/config.hcl $(DOCKER_TAG)
|
||||||
|
|
||||||
## Multi-arch targets
|
## Multi-arch targets
|
||||||
$(TARGETS): $(GOFILES)
|
$(TARGETS): $(GOFILES)
|
||||||
@@ -79,11 +79,11 @@ $(TARGET_ALIAS):
|
|||||||
# Arch specific docker build targets
|
# Arch specific docker build targets
|
||||||
.PHONY: docker-build-arm
|
.PHONY: docker-build-arm
|
||||||
docker-build-arm: dist/minitor-linux-arm
|
docker-build-arm: dist/minitor-linux-arm
|
||||||
docker build --build-arg REPO=arm32v7 --build-arg ARCH=arm . -t ${DOCKER_TAG}-linux-arm
|
docker build --platform linux/arm . -t ${DOCKER_TAG}-linux-arm
|
||||||
|
|
||||||
.PHONY: docker-build-arm64
|
.PHONY: docker-build-arm64
|
||||||
docker-build-arm64: dist/minitor-linux-arm64
|
docker-build-arm64: dist/minitor-linux-arm64
|
||||||
docker build --build-arg REPO=arm64v8 --build-arg ARCH=arm64 . -t ${DOCKER_TAG}-linux-arm64
|
docker build --platform linux/arm64 . -t ${DOCKER_TAG}-linux-arm64
|
||||||
|
|
||||||
# Cross run on host architechture
|
# Cross run on host architechture
|
||||||
.PHONY: docker-run-arm
|
.PHONY: docker-run-arm
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ A minimal monitoring system
|
|||||||
|
|
||||||
## What does it do?
|
## What does it do?
|
||||||
|
|
||||||
Minitor accepts a YAML configuration file with a set of commands to run and a set of alerts to execute when those commands fail. It is designed to be as simple as possible and relies on other command line tools to do checks and issue alerts.
|
Minitor accepts an HCL configuration file with a set of commands to run and a set of alerts to execute when those commands fail. Minitor has a narow feature set and instead follows a principle to outsource to other command line tools when possible. Thus, it relies on other command line tools to do checks and issue alerts. To make getting started a bit easier, Minitor includes a few scripts to help with common tasks.
|
||||||
|
|
||||||
## But why?
|
## But why?
|
||||||
|
|
||||||
@@ -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 get github.com/iamthefij/minitor-go
|
go install github.com/iamthefij/minitor-go/v2@latest
|
||||||
minitor
|
minitor
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ If locally developing you can use:
|
|||||||
make run
|
make run
|
||||||
```
|
```
|
||||||
|
|
||||||
It will read the contents of `config.yml` and begin its loop. You could also run it directly and provide a new config file via the `-config` argument.
|
It will read the contents of `sample-config.hcl` and begin its loop. You could also run it directly and provide a new config file via the `-config` argument.
|
||||||
|
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
@@ -38,51 +38,83 @@ You can pull this repository directly from Docker:
|
|||||||
docker pull iamthefij/minitor-go:latest
|
docker pull iamthefij/minitor-go:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
The Docker image uses a default `config.yml` that is copied from `sample-config.yml`. This won't really do anything for you, so when you run the Docker image, you should supply your own `config.yml` file:
|
The Docker image uses a default `config.hcl` copied from `sample-config.hcl`. This won't really do anything for you, so when you run the Docker image, you should supply your own `config.hcl` file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -v $PWD/config.yml:/app/config.yml iamthefij/minitor-go:latest
|
docker run -v $PWD/sample-config.hcl:/app/config.hcl iamthefij/minitor-go:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Images are provided for `amd64`, `arm`, and `arm64` architechtures.
|
Images are provided for `amd64`, `arm`, and `arm64` architechtures.
|
||||||
|
|
||||||
|
You can configure the timezone for the container by passing a `TZ` env variable. Eg. `TZ=America/Los_Angeles`.
|
||||||
|
|
||||||
## Configuring
|
## Configuring
|
||||||
|
|
||||||
In this repo, you can explore the `sample-config.yml` file for an example, but the general structure is as follows. It should be noted that environment variable interpolation happens on load of the YAML 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.|
|
||||||
|`monitors`|List of all monitors. Detailed description below|
|
|`default_alert_after`|A default value used as an `alert_after` value for a monitor if not specified. Defaults 1, which will alert immediately.|
|
||||||
|`alerts`|List of all alerts. Detailed description below|
|
|`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_up`|Default up alerts to used by a monitor in case none are provided.|
|
||||||
|
|`monitor`|block listing monitors. Detailed description below|
|
||||||
|
|`alert`|List of all alerts. Detailed description below|
|
||||||
|
|
||||||
### Monitors
|
### Monitors
|
||||||
|
|
||||||
All monitors should be listed under `monitors`.
|
Represent your monitors as blocks with a label indicating the name of the monitor.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
monitor "example" {
|
||||||
|
command = ["echo", "Hello, World!"]
|
||||||
|
alert_down = ["log"]
|
||||||
|
alert_up = ["log"]
|
||||||
|
check_interval = "1m"
|
||||||
|
alert_after = 1
|
||||||
|
alert_every = -1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Each monitor allows the following configuration:
|
Each monitor allows the following configuration:
|
||||||
|
|
||||||
|key|value|
|
|key|value|
|
||||||
|---|---|
|
|---|---|
|
||||||
|`name`|Name of the monitor running. This will show up in messages and logs.|
|
|`name`|Name of the monitor running. This will show up in messages and logs.|
|
||||||
|`command`|Specifies the command that should be executed, either in exec or shell form. This command's exit value will determine whether the check is successful|
|
|`command`|A list of strings representing a command to be executed. This command's exit value will determine whether the check is successful. This value is mutually exclusive to `shell_command`|
|
||||||
|
|`shell_command`|A single string that represents a shell command to be executed. This command's exit value will determine whether the check is successful. This value is mutually exclusive to `command`|
|
||||||
|`alert_down`|A list of Alerts to be triggered when the monitor is in a "down" state|
|
|`alert_down`|A list of Alerts to be triggered when the monitor is in a "down" state|
|
||||||
|`alert_up`|A list of Alerts to be triggered when the monitor moves to an "up" state|
|
|`alert_up`|A list of Alerts to be triggered when the monitor moves to an "up" state|
|
||||||
|`check_interval`|The interval at which this monitor should be checked. This must be greater than the global `check_interval` value|
|
|`check_interval`|The interval at which this monitor should be checked. This must be greater than the global `check_interval` value|
|
||||||
|`alert_after`|Allows specifying the number of failed checks before an alert should be triggered|
|
|`alert_after`|Allows specifying the number of failed checks before an alert should be triggered. A value of 1 will start sending alerts after the first failure.|
|
||||||
|`alert_every`|Allows specifying how often an alert should be retriggered. There are a few magic numbers here. Defaults to `-1` for an exponential backoff. Setting to `0` disables re-alerting. Positive values will allow retriggering after the specified number of checks|
|
|`alert_every`|Allows specifying how often an alert should be retriggered. There are a few magic numbers here. Defaults to `-1` for an exponential backoff. Setting to `0` disables re-alerting. Positive values will allow retriggering after the specified number of checks|
|
||||||
|
|
||||||
### Alerts
|
### Alerts
|
||||||
|
|
||||||
Alerts exist as objects keyed under `alerts`. Their key should be the name of the Alert. This is used in your monitor setup in `alert_down` and `alert_up`.
|
Represent your alerts as blocks with a lable indicating the name of the alert. The name will be used in your monitor setup in `alert_down` and `alert_up`.
|
||||||
|
|
||||||
Eachy alert allows the following configuration:
|
```hcl
|
||||||
|
monitor "example" {
|
||||||
|
command = ["false"]
|
||||||
|
alert_down = ["log"]
|
||||||
|
}
|
||||||
|
|
||||||
|
alert "log" {
|
||||||
|
shell_command = "echo '{{.MonitorName}} is down!'"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each alert allows the following configuration:
|
||||||
|
|
||||||
|key|value|
|
|key|value|
|
||||||
|---|---|
|
|---|---|
|
||||||
|`command`|Specifies the command that should be executed, either in exec or shell form. This is the command that will be run when the alert is executed. This can be templated with environment variables or the variables shown in the table below|
|
|`command`|Specifies the command that should be executed in exec form. This is the command that will be run when the alert is executed. This can be templated with environment variables or the variables shown in the table below. This value is mutually exclusive to `shell_command`|
|
||||||
|
|`shell_command`|Specifies a shell command as a single string. This is the command that will be run when the alert is executed. This can be templated with environment variables or the variables shown in the table below. This value is mutually exclusive to `command`|
|
||||||
|
|
||||||
Also, when alerts are executed, they will be passed through Go's format function with arguments for some attributes of the Monitor. The following monitor specific variables can be referenced using Go formatting syntax:
|
Also, when alerts are executed, they will be passed through Go's format function with arguments for some attributes of the Monitor. The following monitor specific variables can be referenced using Go formatting syntax:
|
||||||
|
|
||||||
@@ -91,10 +123,38 @@ Also, when alerts are executed, they will be passed through Go's format function
|
|||||||
|`{{.AlertCount}}`|Number of times this monitor has alerted|
|
|`{{.AlertCount}}`|Number of times this monitor has alerted|
|
||||||
|`{{.FailureCount}}`|The total number of sequential failed checks for this monitor|
|
|`{{.FailureCount}}`|The total number of sequential failed checks for this monitor|
|
||||||
|`{{.LastCheckOutput}}`|The last returned value from the check command to either stderr or stdout|
|
|`{{.LastCheckOutput}}`|The last returned value from the check command to either stderr or stdout|
|
||||||
|`{{.LastSuccess}}`|The ISO datetime of the last successful check|
|
|`{{.LastSuccess}}`|The datetime of the last successful check as a go Time struct|
|
||||||
|`{{.MonitorName}}`|The name of the monitor that failed and triggered the alert|
|
|`{{.MonitorName}}`|The name of the monitor that failed and triggered the alert|
|
||||||
|`{{.IsUp}}`|Indicates if the monitor that is alerting is up or not. Can be used in a conditional message template|
|
|`{{.IsUp}}`|Indicates if the monitor that is alerting is up or not. Can be used in a conditional message template|
|
||||||
|
|
||||||
|
To provide flexible formatting, the following non-standard functions are available in templates:
|
||||||
|
|
||||||
|
|func|description|
|
||||||
|
|---|---|
|
||||||
|
|`ANSIC <Time>`|Formats provided time in ANSIC format|
|
||||||
|
|`UnixDate <Time>`|Formats provided time in UnixDate format|
|
||||||
|
|`RubyDate <Time>`|Formats provided time in RubyDate format|
|
||||||
|
|`RFC822Z <Time>`|Formats provided time in RFC822Z format|
|
||||||
|
|`RFC850 <Time>`|Formats provided time in RFC850 format|
|
||||||
|
|`RFC1123 <Time>`|Formats provided time in RFC1123 format|
|
||||||
|
|`RFC1123Z <Time>`|Formats provided time in RFC1123Z format|
|
||||||
|
|`RFC3339 <Time>`|Formats provided time in RFC3339 format|
|
||||||
|
|`RFC3339Nano <Time>`|Formats provided time in RFC3339Nano format|
|
||||||
|
|`FormatTime <Time> <string template>`|Formats provided time according to provided template|
|
||||||
|
|`InTZ <Time> <string timezone name>`|Converts provided time to parsed timezone from the provided name|
|
||||||
|
|
||||||
|
For more information, check out the [Go documentation for the time module](https://pkg.go.dev/time@go1.20.7#pkg-constants).
|
||||||
|
|
||||||
|
#### Running alerts on startup
|
||||||
|
|
||||||
|
It's not the best feeling to find out your alerts are broken when you're expecting to be alerted about another failure. To avoid this and provide early insight into broken alerts, it is possible to specify a list of alerts to run when Minitor starts up. This can be done using the command line flag `-startup-alerts`. This flag accepts a comma separated list of strings and will run a test of each of those alerts. Minitor will then respond as it typically does for any failed alert. This can be used to allow you time to correct when initially launching, and to allow schedulers to more easily detect a failed deployment of Minitor.
|
||||||
|
|
||||||
|
Eg.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
minitor -startup-alerts=log_down,log_up -config ./config.hcl
|
||||||
|
```
|
||||||
|
|
||||||
### Metrics
|
### Metrics
|
||||||
|
|
||||||
Minitor supports exporting metrics for [Prometheus](https://prometheus.io/). Prometheus is an open source tool for reading and querying metrics from different sources. Combined with another tool, [Grafana](https://grafana.com/), it allows building of charts and dashboards. You could also opt to just use Minitor to log check results, and instead do your alerting with Grafana.
|
Minitor supports exporting metrics for [Prometheus](https://prometheus.io/). Prometheus is an open source tool for reading and querying metrics from different sources. Combined with another tool, [Grafana](https://grafana.com/), it allows building of charts and dashboards. You could also opt to just use Minitor to log check results, and instead do your alerting with Grafana.
|
||||||
@@ -109,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.
|
||||||
@@ -116,52 +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.
|
|
||||||
|
|
||||||
### Differences from Python version
|
|
||||||
|
|
||||||
Templating for Alert messages has been updated. In the Python version, `str.format(...)` was used with certain keys passed in that could be used to format messages. In the Go version, we use a struct, `AlertNotice` defined in `alert.go` and the built in Go templating format. Eg.
|
|
||||||
|
|
||||||
minitor-py:
|
|
||||||
```yaml
|
|
||||||
alerts:
|
|
||||||
log:
|
|
||||||
command: 'echo {monitor_name}'
|
|
||||||
```
|
|
||||||
|
|
||||||
minitor-go:
|
|
||||||
```yaml
|
|
||||||
alerts:
|
|
||||||
log:
|
|
||||||
command: 'echo {{.MonitorName}}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Interval durations have changed from being an integer number of seconds to a duration string supported by Go, for example:
|
|
||||||
|
|
||||||
minitor-py:
|
|
||||||
```yaml
|
|
||||||
check_interval: 90
|
|
||||||
```
|
|
||||||
|
|
||||||
minitor-go:
|
|
||||||
```yaml
|
|
||||||
check_interval: 1m30s
|
|
||||||
```
|
|
||||||
|
|
||||||
For the time being, legacy configs for the Python version of Minitor should be compatible if you apply the `-py-compat` flag when running Minitor. Eventually, this flag will go away when later breaking changes are introduced.
|
|
||||||
|
|
||||||
## Future
|
|
||||||
|
|
||||||
Future, potentially breaking changes
|
|
||||||
|
|
||||||
- [ ] Consider value of templating vs injecting values into Env variables
|
|
||||||
- [ ] Async checking
|
|
||||||
- [ ] Revisit metrics and see if they all make sense
|
|
||||||
- [ ] Consider dropping `alert_up` and `alert_down` in favor of using Go templates that offer more control of messaging (Breaking)
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -21,63 +20,99 @@ var (
|
|||||||
|
|
||||||
// Alert is a config driven mechanism for sending a notice
|
// Alert is a config driven mechanism for sending a notice
|
||||||
type Alert struct {
|
type Alert struct {
|
||||||
Name string
|
Name string `hcl:"name,label"`
|
||||||
Command CommandOrShell
|
Command []string `hcl:"command,optional"`
|
||||||
|
ShellCommand string `hcl:"shell_command,optional"`
|
||||||
commandTemplate []*template.Template
|
commandTemplate []*template.Template
|
||||||
commandShellTemplate *template.Template
|
commandShellTemplate *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
// AlertNotice captures the context for an alert to be sent
|
// AlertNotice captures the context for an alert to be sent
|
||||||
type AlertNotice struct {
|
type AlertNotice struct {
|
||||||
AlertCount int16
|
AlertCount int
|
||||||
FailureCount int16
|
FailureCount int
|
||||||
IsUp bool
|
IsUp bool
|
||||||
LastSuccess time.Time
|
LastSuccess time.Time
|
||||||
MonitorName string
|
MonitorName string
|
||||||
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
|
||||||
return !alert.Command.Empty()
|
hasShellCommand := alert.ShellCommand != ""
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
hasAtLeastOneCommand := hasCommand || hasShellCommand
|
||||||
|
if !hasAtLeastOneCommand {
|
||||||
|
err = errors.Join(err, fmt.Errorf(
|
||||||
|
"%w: alert %s has no command or shell_command configured",
|
||||||
|
ErrInvalidAlert,
|
||||||
|
alert.Name,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAtMostOneCommand := !(hasCommand && hasShellCommand)
|
||||||
|
if !hasAtMostOneCommand {
|
||||||
|
err = errors.Join(err, fmt.Errorf(
|
||||||
|
"%w: alert %s has both command and shell_command configured",
|
||||||
|
ErrInvalidAlert,
|
||||||
|
alert.Name,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildTemplates compiles command templates for the Alert
|
// BuildTemplates compiles command templates for the Alert
|
||||||
func (alert *Alert) BuildTemplates() error {
|
func (alert *Alert) BuildTemplates() error {
|
||||||
// TODO: Remove legacy template support later after 1.0
|
|
||||||
legacy := strings.NewReplacer(
|
|
||||||
"{alert_count}", "{{.AlertCount}}",
|
|
||||||
"{alert_message}", "{{.MonitorName}} check has failed {{.FailureCount}} times",
|
|
||||||
"{failure_count}", "{{.FailureCount}}",
|
|
||||||
"{last_output}", "{{.LastCheckOutput}}",
|
|
||||||
"{last_success}", "{{.LastSuccess}}",
|
|
||||||
"{monitor_name}", "{{.MonitorName}}",
|
|
||||||
)
|
|
||||||
|
|
||||||
slog.Debugf("Building template for alert %s", alert.Name)
|
slog.Debugf("Building template for alert %s", alert.Name)
|
||||||
|
|
||||||
switch {
|
// Time format func factory
|
||||||
case alert.commandTemplate == nil && alert.Command.Command != nil:
|
tff := func(formatString string) func(time.Time) string {
|
||||||
alert.commandTemplate = []*template.Template{}
|
return func(t time.Time) string {
|
||||||
for i, cmdPart := range alert.Command.Command {
|
return t.Format(formatString)
|
||||||
if PyCompat {
|
}
|
||||||
cmdPart = legacy.Replace(cmdPart)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create some functions for formatting datetimes in popular formats
|
||||||
|
timeFormatFuncs := template.FuncMap{
|
||||||
|
"ANSIC": tff(time.ANSIC),
|
||||||
|
"UnixDate": tff(time.UnixDate),
|
||||||
|
"RubyDate": tff(time.RubyDate),
|
||||||
|
"RFC822Z": tff(time.RFC822Z),
|
||||||
|
"RFC850": tff(time.RFC850),
|
||||||
|
"RFC1123": tff(time.RFC1123),
|
||||||
|
"RFC1123Z": tff(time.RFC1123Z),
|
||||||
|
"RFC3339": tff(time.RFC3339),
|
||||||
|
"RFC3339Nano": tff(time.RFC3339Nano),
|
||||||
|
"FormatTime": func(t time.Time, timeFormat string) string {
|
||||||
|
return t.Format(timeFormat)
|
||||||
|
},
|
||||||
|
"InTZ": func(t time.Time, tzName string) (time.Time, error) {
|
||||||
|
tz, err := time.LoadLocation(tzName)
|
||||||
|
if err != nil {
|
||||||
|
return t, fmt.Errorf("failed to convert time to specified tz: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.In(tz), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case alert.Command != nil:
|
||||||
|
alert.commandTemplate = []*template.Template{}
|
||||||
|
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)).Parse(cmdPart),
|
template.New(alert.Name+fmt.Sprint(i)).Funcs(timeFormatFuncs).Parse(cmdPart),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
case alert.commandShellTemplate == nil && alert.Command.ShellCommand != "":
|
case alert.ShellCommand != "":
|
||||||
shellCmd := alert.Command.ShellCommand
|
shellCmd := alert.ShellCommand
|
||||||
|
|
||||||
if PyCompat {
|
|
||||||
shellCmd = legacy.Replace(shellCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
alert.commandShellTemplate = template.Must(
|
alert.commandShellTemplate = template.Must(
|
||||||
template.New(alert.Name).Parse(shellCmd),
|
template.New(alert.Name).Funcs(timeFormatFuncs).Parse(shellCmd),
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("No template provided for alert %s: %w", alert.Name, errNoTemplate)
|
return fmt.Errorf("No template provided for alert %s: %w", alert.Name, errNoTemplate)
|
||||||
@@ -137,7 +172,7 @@ func (alert Alert) Send(notice AlertNotice) (outputStr string, err error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf(
|
err = fmt.Errorf(
|
||||||
"Alert '%s' failed to send. Returned %v: %w",
|
"Alert %s failed to send. Returned %w: %w",
|
||||||
alert.Name,
|
alert.Name,
|
||||||
err,
|
err,
|
||||||
ErrAlertFailed,
|
ErrAlertFailed,
|
||||||
@@ -146,16 +181,3 @@ func (alert Alert) Send(notice AlertNotice) (outputStr string, err error) {
|
|||||||
|
|
||||||
return outputStr, err
|
return outputStr, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLogAlert creates an alert that does basic logging using echo
|
|
||||||
func NewLogAlert() *Alert {
|
|
||||||
return &Alert{
|
|
||||||
Name: "log",
|
|
||||||
Command: CommandOrShell{
|
|
||||||
Command: []string{
|
|
||||||
"echo",
|
|
||||||
"{{.MonitorName}} {{if .IsUp}}has recovered{{else}}check has failed {{.FailureCount}} times{{end}}",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+51
-77
@@ -1,107 +1,86 @@
|
|||||||
package main
|
package main_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
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 Alert
|
alert m.Alert
|
||||||
expected bool
|
expected error
|
||||||
name string
|
name string
|
||||||
}{
|
}{
|
||||||
{Alert{Command: CommandOrShell{Command: []string{"echo", "test"}}}, true, "Command only"},
|
{m.Alert{Command: []string{"echo", "test"}}, nil, "Command only"},
|
||||||
{Alert{Command: CommandOrShell{ShellCommand: "echo test"}}, true, "CommandShell only"},
|
{m.Alert{ShellCommand: "echo test"}, nil, "CommandShell only"},
|
||||||
{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 {
|
||||||
log.Printf("Testing case %s", c.name)
|
c := c
|
||||||
|
|
||||||
actual := c.alert.IsValid()
|
t.Run(c.name, func(t *testing.T) {
|
||||||
if actual != c.expected {
|
t.Parallel()
|
||||||
t.Errorf("IsValid(%v), expected=%t actual=%t", c.name, c.expected, actual)
|
|
||||||
log.Printf("Case failed: %s", c.name)
|
actual := c.alert.Validate()
|
||||||
|
hasErr := (actual != nil)
|
||||||
|
expectErr := (c.expected != nil)
|
||||||
|
|
||||||
|
if hasErr != expectErr || !errors.Is(actual, c.expected) {
|
||||||
|
t.Errorf("expected=%t actual=%t", c.expected, actual)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
log.Println("-----")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertSend(t *testing.T) {
|
func TestAlertSend(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
alert Alert
|
alert m.Alert
|
||||||
notice AlertNotice
|
notice m.AlertNotice
|
||||||
expectedOutput string
|
expectedOutput string
|
||||||
expectErr bool
|
expectErr bool
|
||||||
name string
|
name string
|
||||||
pyCompat bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Alert{Command: CommandOrShell{Command: []string{"echo", "{{.MonitorName}}"}}},
|
m.Alert{Command: []string{"echo", "{{.MonitorName}}"}},
|
||||||
AlertNotice{MonitorName: "test"},
|
m.AlertNotice{MonitorName: "test"},
|
||||||
"test\n",
|
"test\n",
|
||||||
false,
|
false,
|
||||||
"Command with template",
|
"Command with template",
|
||||||
false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Alert{Command: CommandOrShell{ShellCommand: "echo {{.MonitorName}}"}},
|
m.Alert{ShellCommand: "echo {{.MonitorName}}"},
|
||||||
AlertNotice{MonitorName: "test"},
|
m.AlertNotice{MonitorName: "test"},
|
||||||
"test\n",
|
"test\n",
|
||||||
false,
|
false,
|
||||||
"Command shell with template",
|
"Command shell with template",
|
||||||
false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Alert{Command: CommandOrShell{Command: []string{"echo", "{{.Bad}}"}}},
|
m.Alert{Command: []string{"echo", "{{.Bad}}"}},
|
||||||
AlertNotice{MonitorName: "test"},
|
m.AlertNotice{MonitorName: "test"},
|
||||||
"",
|
"",
|
||||||
true,
|
true,
|
||||||
"Command with bad template",
|
"Command with bad template",
|
||||||
false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Alert{Command: CommandOrShell{ShellCommand: "echo {{.Bad}}"}},
|
m.Alert{ShellCommand: "echo {{.Bad}}"},
|
||||||
AlertNotice{MonitorName: "test"},
|
m.AlertNotice{MonitorName: "test"},
|
||||||
"",
|
"",
|
||||||
true,
|
true,
|
||||||
"Command shell with bad template",
|
"Command shell with bad template",
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Alert{Command: CommandOrShell{ShellCommand: "echo {alert_message}"}},
|
|
||||||
AlertNotice{MonitorName: "test", FailureCount: 1},
|
|
||||||
"test check has failed 1 times\n",
|
|
||||||
false,
|
|
||||||
"Command shell with legacy template",
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
// Test default log alert down
|
|
||||||
{
|
|
||||||
*NewLogAlert(),
|
|
||||||
AlertNotice{MonitorName: "Test", FailureCount: 1, IsUp: false},
|
|
||||||
"Test check has failed 1 times\n",
|
|
||||||
false,
|
|
||||||
"Default log alert down",
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
// Test default log alert up
|
|
||||||
{
|
|
||||||
*NewLogAlert(),
|
|
||||||
AlertNotice{MonitorName: "Test", IsUp: true},
|
|
||||||
"Test has recovered\n",
|
|
||||||
false,
|
|
||||||
"Default log alert up",
|
|
||||||
false,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
log.Printf("Testing case %s", c.name)
|
c := c
|
||||||
// Set PyCompat to value of compat flag
|
|
||||||
PyCompat = c.pyCompat
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
err := c.alert.BuildTemplates()
|
err := c.alert.BuildTemplates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,54 +92,49 @@ func TestAlertSend(t *testing.T) {
|
|||||||
|
|
||||||
if output != c.expectedOutput {
|
if output != c.expectedOutput {
|
||||||
t.Errorf("Send(%v output), expected=%v actual=%v", c.name, c.expectedOutput, output)
|
t.Errorf("Send(%v output), expected=%v actual=%v", c.name, c.expectedOutput, output)
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasErr != c.expectErr {
|
if hasErr != c.expectErr {
|
||||||
t.Errorf("Send(%v err), expected=%v actual=%v", c.name, "Err", err)
|
t.Errorf("Send(%v err), expected=%v actual=%v", c.name, "Err", err)
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
// Set PyCompat back to default value
|
|
||||||
PyCompat = false
|
|
||||||
|
|
||||||
log.Println("-----")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertSendNoTemplates(t *testing.T) {
|
func TestAlertSendNoTemplates(t *testing.T) {
|
||||||
alert := Alert{}
|
alert := m.Alert{}
|
||||||
notice := AlertNotice{}
|
notice := m.AlertNotice{}
|
||||||
|
|
||||||
output, err := alert.Send(notice)
|
output, err := alert.Send(notice)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Send(no template), expected=%v actual=%v", "Err", output)
|
t.Errorf("Send(no template), expected=%v actual=%v", "Err", output)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("-----")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertBuildTemplate(t *testing.T) {
|
func TestAlertBuildTemplate(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
alert Alert
|
alert m.Alert
|
||||||
expectErr bool
|
expectErr bool
|
||||||
name string
|
name string
|
||||||
}{
|
}{
|
||||||
{Alert{Command: CommandOrShell{Command: []string{"echo", "test"}}}, false, "Command only"},
|
{m.Alert{Command: []string{"echo", "test"}}, false, "Command only"},
|
||||||
{Alert{Command: CommandOrShell{ShellCommand: "echo test"}}, false, "CommandShell only"},
|
{m.Alert{ShellCommand: "echo test"}, false, "CommandShell only"},
|
||||||
{Alert{}, true, "No commands"},
|
{m.Alert{}, true, "No commands"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
log.Printf("Testing case %s", c.name)
|
c := c
|
||||||
|
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
err := c.alert.BuildTemplates()
|
err := c.alert.BuildTemplates()
|
||||||
hasErr := (err != nil)
|
hasErr := (err != nil)
|
||||||
|
|
||||||
if hasErr != c.expectErr {
|
if hasErr != c.expectErr {
|
||||||
t.Errorf("IsValid(%v), expected=%t actual=%t", c.name, c.expectErr, err)
|
t.Errorf("IsValid(%v), expected=%t actual=%t", c.name, c.expectErr, err)
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("-----")
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,141 +2,126 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.iamthefij.com/iamthefij/slog"
|
"git.iamthefij.com/iamthefij/slog"
|
||||||
"gopkg.in/yaml.v2"
|
"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 {
|
||||||
CheckInterval SecondsOrDuration `yaml:"check_interval"`
|
CheckIntervalStr string `hcl:"check_interval"`
|
||||||
Monitors []*Monitor
|
CheckInterval time.Duration
|
||||||
Alerts map[string]*Alert
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommandOrShell type wraps a string or list of strings
|
DefaultAlertAfter int `hcl:"default_alert_after,optional"`
|
||||||
// for executing a command directly or in a shell
|
DefaultAlertEvery *int `hcl:"default_alert_every,optional"`
|
||||||
type CommandOrShell struct {
|
DefaultAlertDown []string `hcl:"default_alert_down,optional"`
|
||||||
ShellCommand string
|
DefaultAlertUp []string `hcl:"default_alert_up,optional"`
|
||||||
Command []string
|
Monitors []*Monitor `hcl:"monitor,block"`
|
||||||
}
|
Alerts []*Alert `hcl:"alert,block"`
|
||||||
|
|
||||||
// Empty checks if the Command has a value
|
alertLookup map[string]*Alert
|
||||||
func (cos CommandOrShell) Empty() bool {
|
|
||||||
return (cos.ShellCommand == "" && cos.Command == nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalYAML allows unmarshalling either a string or slice of strings
|
|
||||||
// and parsing them as either a command or a shell command.
|
|
||||||
func (cos *CommandOrShell) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
var cmd []string
|
|
||||||
err := unmarshal(&cmd)
|
|
||||||
// Error indicates this is shell command
|
|
||||||
if err != nil {
|
|
||||||
var shellCmd string
|
|
||||||
|
|
||||||
err := unmarshal(&shellCmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cos.ShellCommand = shellCmd
|
|
||||||
} else {
|
|
||||||
cos.Command = cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SecondsOrDuration wraps a duration value for parsing a duration or seconds from YAML
|
|
||||||
// NOTE: This should be removed in favor of only parsing durations once compatibility is broken
|
|
||||||
type SecondsOrDuration struct {
|
|
||||||
value time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value returns a duration value
|
|
||||||
func (sod SecondsOrDuration) Value() time.Duration {
|
|
||||||
return sod.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalYAML allows unmarshalling a duration value or seconds if an int was provided
|
|
||||||
func (sod *SecondsOrDuration) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
var seconds int64
|
|
||||||
err := unmarshal(&seconds)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
sod.value = time.Second * time.Duration(seconds)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error indicates that we don't have an int
|
|
||||||
err = unmarshal(&sod.value)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValid checks config validity and returns true if valid
|
|
||||||
func (config Config) IsValid() (isValid bool) {
|
|
||||||
isValid = true
|
|
||||||
|
|
||||||
// Validate alerts
|
|
||||||
if config.Alerts == nil || 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
|
|
||||||
} else {
|
|
||||||
slog.Debugf("Loaded alert %s", alert.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate monitors
|
|
||||||
if config.Monitors == nil || 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.Alerts[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
|
// Init performs extra initialization on top of loading the config from file
|
||||||
func (config *Config) Init() (err error) {
|
func (config *Config) Init() (err error) {
|
||||||
for name, alert := range config.Alerts {
|
config.CheckInterval, err = time.ParseDuration(config.CheckIntervalStr)
|
||||||
alert.Name = name
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse top level check_interval duration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DefaultAlertAfter == 0 {
|
||||||
|
minAlertAfter := 1
|
||||||
|
config.DefaultAlertAfter = minAlertAfter
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DefaultAlertEvery == nil {
|
||||||
|
defaultDefaultAlertEvery := -1
|
||||||
|
config.DefaultAlertEvery = &defaultDefaultAlertEvery
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, monitor := range config.Monitors {
|
||||||
|
if err = monitor.Init(
|
||||||
|
config.DefaultAlertAfter,
|
||||||
|
config.DefaultAlertEvery,
|
||||||
|
config.DefaultAlertDown,
|
||||||
|
config.DefaultAlertUp,
|
||||||
|
); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = config.BuildAllTemplates()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid checks config validity and returns true if valid
|
||||||
|
func (config Config) IsValid() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Validate alerts
|
||||||
|
if len(config.Alerts) == 0 {
|
||||||
|
err = errors.Join(err, ErrNoAlerts)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, alert := range config.Alerts {
|
||||||
|
err = errors.Join(err, alert.Validate())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate monitors
|
||||||
|
if len(config.Monitors) == 0 {
|
||||||
|
err = errors.Join(err, ErrNoMonitors)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, monitor := range config.Monitors {
|
||||||
|
err = errors.Join(err, monitor.Validate())
|
||||||
|
|
||||||
|
// Check that all Monitor alerts actually exist
|
||||||
|
for _, isUp := range []bool{true, false} {
|
||||||
|
for _, alertName := range monitor.GetAlertNames(isUp) {
|
||||||
|
if _, ok := config.GetAlert(alertName); !ok {
|
||||||
|
err = errors.Join(
|
||||||
|
err,
|
||||||
|
fmt.Errorf("%w: %s. %w: %s", ErrInvalidMonitor, monitor.Name, ErrUnknownAlert, alertName),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlert returns an alert by name
|
||||||
|
func (c Config) GetAlert(name string) (*Alert, bool) {
|
||||||
|
if c.alertLookup == nil {
|
||||||
|
c.alertLookup = map[string]*Alert{}
|
||||||
|
for _, alert := range c.Alerts {
|
||||||
|
c.alertLookup[alert.Name] = alert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := c.alertLookup[name]
|
||||||
|
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildAllTemplates builds all alert templates
|
||||||
|
func (c *Config) BuildAllTemplates() (err error) {
|
||||||
|
for _, alert := range c.Alerts {
|
||||||
if err = alert.BuildTemplates(); err != nil {
|
if err = alert.BuildTemplates(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -146,41 +131,23 @@ func (config *Config) Init() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
data, err := ioutil.ReadFile(filePath)
|
var config Config
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = yaml.Unmarshal(data, &config)
|
if err := hclsimple.DecodeFile(filePath, nil, &config); err != nil {
|
||||||
if err != nil {
|
return config, errors.Join(ErrLoadingConfig, err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Debugf("Config values:\n%v\n", config)
|
slog.Debugf("Config values:\n%v\n", config)
|
||||||
|
|
||||||
// Add log alert if not present
|
|
||||||
if PyCompat {
|
|
||||||
// Initialize alerts list if not present
|
|
||||||
if config.Alerts == nil {
|
|
||||||
config.Alerts = map[string]*Alert{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := config.Alerts["log"]; !ok {
|
|
||||||
config.Alerts["log"] = NewLogAlert()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|||||||
+133
-83
@@ -1,143 +1,193 @@
|
|||||||
package main
|
package main_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
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
|
||||||
pyCompat bool
|
|
||||||
}{
|
}{
|
||||||
{"./test/valid-config.yml", false, "Valid config file", false},
|
{"./test/does-not-exist", m.ErrLoadingConfig, "Invalid config path"},
|
||||||
{"./test/valid-default-log-alert.yml", false, "Valid config file with default log alert PyCompat", true},
|
{"./test/invalid-config-wrong-hcl-type.hcl", m.ErrLoadingConfig, "Incorrect HCL type"},
|
||||||
{"./test/valid-default-log-alert.yml", true, "Invalid config file no log alert", false},
|
{"./test/invalid-config-missing-alerts.hcl", m.ErrNoAlerts, "Invalid config missing alerts"},
|
||||||
{"./test/does-not-exist", true, "Invalid config path", false},
|
{"./test/invalid-config-missing-alerts.hcl", m.ErrInvalidConfig, "Invalid config general"},
|
||||||
{"./test/invalid-config-type.yml", true, "Invalid config type for key", false},
|
{"./test/invalid-config-invalid-duration.hcl", m.ErrConfigInit, "Invalid config type for key"},
|
||||||
{"./test/invalid-config-missing-alerts.yml", true, "Invalid config missing alerts", false},
|
{"./test/invalid-config-unknown-alert.hcl", m.ErrUnknownAlert, "Invalid config unknown alert"},
|
||||||
{"./test/invalid-config-unknown-alert.yml", true, "Invalid config unknown alert", false},
|
{"./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 {
|
||||||
|
c := c
|
||||||
|
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := m.LoadConfig(c.configPath)
|
||||||
|
hasErr := (err != nil)
|
||||||
|
expectErr := (c.expectedErr != nil)
|
||||||
|
|
||||||
|
if hasErr != expectErr || !errors.Is(err, c.expectedErr) {
|
||||||
|
t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectedErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
for _, c := range cases {
|
||||||
log.Printf("Testing case %s", c.name)
|
c := c
|
||||||
// Set PyCompat based on compatibility mode
|
|
||||||
PyCompat = c.pyCompat
|
|
||||||
_, err := LoadConfig(c.configPath)
|
|
||||||
hasErr := (err != nil)
|
|
||||||
|
|
||||||
if hasErr != c.expectErr {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectErr, err)
|
t.Parallel()
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set PyCompat to default value
|
config, err := m.LoadConfig(c.configPath)
|
||||||
PyCompat = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIntervalParsing(t *testing.T) {
|
|
||||||
log.Printf("Testing case TestIntervalParsing")
|
|
||||||
|
|
||||||
config, err := LoadConfig("./test/valid-config.yml")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failed loading config: %v", err)
|
t.Errorf("Got error when loading config file %q: %s", c.configPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
oneSecond := time.Second
|
// Test Config has default values
|
||||||
tenSeconds := 10 * time.Second
|
if config.DefaultAlertAfter != c.expectedResult.DefaultAlertAfter {
|
||||||
oneMinute := time.Minute
|
t.Errorf("Got unexpected DefaultAlertAfter from file %q: expected=%v actual=%v", c.configPath, c.expectedResult.DefaultAlertAfter, config.DefaultAlertAfter)
|
||||||
|
|
||||||
// validate top level interval seconds represented as an int
|
|
||||||
if config.CheckInterval.Value() != oneSecond {
|
|
||||||
t.Errorf("Incorrectly parsed int seconds. expected=%v actual=%v", oneSecond, config.CheckInterval)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Monitors[0].CheckInterval.Value() != tenSeconds {
|
if *config.DefaultAlertEvery != *c.expectedResult.DefaultAlertEvery {
|
||||||
t.Errorf("Incorrectly parsed seconds duration. expected=%v actual=%v", oneSecond, config.CheckInterval)
|
t.Errorf("Got unexpected DefaultAlertEvery from file %q: expected=%v actual=%v", c.configPath, *c.expectedResult.DefaultAlertEvery, *config.DefaultAlertEvery)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Monitors[1].CheckInterval.Value() != oneMinute {
|
if !m.EqualSliceString(config.DefaultAlertUp, c.expectedResult.DefaultAlertUp) {
|
||||||
t.Errorf("Incorrectly parsed seconds duration. expected=%v actual=%v", oneSecond, config.CheckInterval)
|
t.Errorf("Got unexpected DefaultAlertUp from file %q: expected=%v actual=%v", c.configPath, c.expectedResult.DefaultAlertUp, config.DefaultAlertUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("-----")
|
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) {
|
||||||
log.Println("Testing multi-line string config")
|
t.Parallel()
|
||||||
|
|
||||||
config, err := LoadConfig("./test/valid-verify-multi-line.yml")
|
config, err := m.LoadConfig("./test/valid-verify-multi-line.hcl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("TestMultiLineConfig(load), expected=no_error actual=%v", err)
|
t.Fatalf("TestMultiLineConfig(load), expected=no_error actual=%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("-----")
|
t.Run("Test Monitor with Indented Multi-Line String", func(t *testing.T) {
|
||||||
log.Println("TestMultiLineConfig(parse > string)")
|
// Verify indented heredoc is as expected
|
||||||
|
expected := "echo 'Some string with stuff'\necho \"<angle brackets>\"\nexit 1\n"
|
||||||
expected := "echo 'Some string with stuff'; echo \"<angle brackets>\"; exit 1\n"
|
actual := config.Monitors[0].ShellCommand
|
||||||
actual := config.Monitors[0].Command.ShellCommand
|
|
||||||
|
|
||||||
if expected != actual {
|
if expected != actual {
|
||||||
t.Errorf("TestMultiLineConfig(>) failed")
|
t.Error("Heredoc mismatch")
|
||||||
t.Logf("string expected=`%v`", expected)
|
t.Errorf("string expected=`%v`", expected)
|
||||||
t.Logf("string actual =`%v`", actual)
|
t.Errorf("string actual =`%v`", actual)
|
||||||
t.Logf("bytes expected=%v", []byte(expected))
|
|
||||||
t.Logf("bytes actual =%v", []byte(actual))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("-----")
|
// Run the monitor and verify the output
|
||||||
log.Println("TestMultiLineConfig(execute > string)")
|
|
||||||
|
|
||||||
_, notice := config.Monitors[0].Check()
|
_, notice := config.Monitors[0].Check()
|
||||||
if notice == nil {
|
if notice == nil {
|
||||||
t.Fatalf("Did not receive an alert notice")
|
t.Fatal("Did not receive an alert notice and should have")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify the output of the monitor is as expected
|
||||||
expected = "Some string with stuff\n<angle brackets>\n"
|
expected = "Some string with stuff\n<angle brackets>\n"
|
||||||
actual = notice.LastCheckOutput
|
actual = notice.LastCheckOutput
|
||||||
|
|
||||||
if expected != actual {
|
if expected != actual {
|
||||||
t.Errorf("TestMultiLineConfig(execute > string) check failed")
|
t.Error("Output mismatch")
|
||||||
t.Logf("string expected=`%v`", expected)
|
t.Errorf("string expected=`%v`", expected)
|
||||||
t.Logf("string actual =`%v`", actual)
|
t.Errorf("string actual =`%v`", actual)
|
||||||
t.Logf("bytes expected=%v", []byte(expected))
|
}
|
||||||
t.Logf("bytes actual =%v", []byte(actual))
|
})
|
||||||
|
|
||||||
|
t.Run("Test Alert with Multi-Line String", func(t *testing.T) {
|
||||||
|
alert, ok := config.GetAlert("log_shell")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Could not find expected alert 'log_shell'")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("-----")
|
expected := " echo 'Some string with stuff'\n echo '<angle brackets>'\n"
|
||||||
log.Println("TestMultiLineConfig(parse | string)")
|
actual := alert.ShellCommand
|
||||||
|
|
||||||
expected = "echo 'Some string with stuff'\necho '<angle brackets>'\n"
|
|
||||||
actual = config.Alerts["log_shell"].Command.ShellCommand
|
|
||||||
|
|
||||||
if expected != actual {
|
if expected != actual {
|
||||||
t.Errorf("TestMultiLineConfig(|) failed")
|
t.Error("Heredoc mismatch")
|
||||||
t.Logf("string expected=`%v`", expected)
|
t.Errorf("string expected=`%v`", expected)
|
||||||
t.Logf("string actual =`%v`", actual)
|
t.Errorf("string actual =`%v`", actual)
|
||||||
t.Logf("bytes expected=%v", []byte(expected))
|
|
||||||
t.Logf("bytes actual =%v", []byte(actual))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("-----")
|
actual, err = alert.Send(m.AlertNotice{})
|
||||||
log.Println("TestMultiLineConfig(execute | string)")
|
|
||||||
|
|
||||||
actual, err = config.Alerts["log_shell"].Send(AlertNotice{})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Execution of alert failed")
|
t.Fatal("Execution of alert failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
expected = "Some string with stuff\n<angle brackets>\n"
|
expected = "Some string with stuff\n<angle brackets>\n"
|
||||||
if expected != actual {
|
if expected != actual {
|
||||||
t.Errorf("TestMultiLineConfig(execute | string) check failed")
|
t.Error("Output mismatch")
|
||||||
t.Logf("string expected=`%v`", expected)
|
t.Errorf("string expected=`%v`", expected)
|
||||||
t.Logf("string actual =`%v`", actual)
|
t.Errorf("string actual =`%v`", actual)
|
||||||
t.Logf("bytes expected=%v", []byte(expected))
|
|
||||||
t.Logf("bytes actual =%v", []byte(actual))
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
module git.iamthefij.com/iamthefij/minitor-go
|
module git.iamthefij.com/iamthefij/minitor-go/v2
|
||||||
|
|
||||||
go 1.15
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.iamthefij.com/iamthefij/slog v1.3.0
|
git.iamthefij.com/iamthefij/slog v1.3.0
|
||||||
github.com/prometheus/client_golang v1.2.1
|
github.com/hashicorp/hcl/v2 v2.11.1
|
||||||
gopkg.in/yaml.v2 v2.2.4
|
github.com/prometheus/client_golang v1.23.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/agext/levenshtein v1.2.1 // indirect
|
||||||
|
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.66.1 // indirect
|
||||||
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
|
github.com/zclconf/go-cty v1.8.0 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,80 +1,96 @@
|
|||||||
git.iamthefij.com/iamthefij/slog v1.3.0 h1:4Hu5PQvDrW5e3FrTS3q2iIXW0iPvhNY/9qJsqDR3K3I=
|
git.iamthefij.com/iamthefij/slog v1.3.0 h1:4Hu5PQvDrW5e3FrTS3q2iIXW0iPvhNY/9qJsqDR3K3I=
|
||||||
git.iamthefij.com/iamthefij/slog v1.3.0/go.mod h1:1RUj4hcCompZkAxXCRfUX786tb3cM/Zpkn97dGfUfbg=
|
git.iamthefij.com/iamthefij/slog v1.3.0/go.mod h1:1RUj4hcCompZkAxXCRfUX786tb3cM/Zpkn97dGfUfbg=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
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.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
|
||||||
github.com/golang/protobuf v1.2.0/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.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJbcc=
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||||
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8=
|
|
||||||
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||||
|
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
||||||
|
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||||
|
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
|
||||||
|
github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA=
|
||||||
|
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
|
||||||
|
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.iamthefij.com/iamthefij/slog"
|
"git.iamthefij.com/iamthefij/slog"
|
||||||
@@ -17,16 +18,13 @@ var (
|
|||||||
// Metrics contains all active metrics
|
// Metrics contains all active metrics
|
||||||
Metrics = NewMetrics()
|
Metrics = NewMetrics()
|
||||||
|
|
||||||
// PyCompat enables support for legacy Python templates
|
|
||||||
PyCompat = false
|
|
||||||
|
|
||||||
// version of minitor being run
|
// version of minitor being run
|
||||||
version = "dev"
|
version = "dev"
|
||||||
|
|
||||||
errUnknownAlert = errors.New("unknown alert")
|
errUnknownAlert = errors.New("unknown alert")
|
||||||
)
|
)
|
||||||
|
|
||||||
func sendAlerts(config *Config, monitor *Monitor, alertNotice *AlertNotice) error {
|
func SendAlerts(config *Config, monitor *Monitor, alertNotice *AlertNotice) error {
|
||||||
slog.Debugf("Received an alert notice from %s", alertNotice.MonitorName)
|
slog.Debugf("Received an alert notice from %s", alertNotice.MonitorName)
|
||||||
alertNames := monitor.GetAlertNames(alertNotice.IsUp)
|
alertNames := monitor.GetAlertNames(alertNotice.IsUp)
|
||||||
|
|
||||||
@@ -41,7 +39,7 @@ func sendAlerts(config *Config, monitor *Monitor, alertNotice *AlertNotice) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, alertName := range alertNames {
|
for _, alertName := range alertNames {
|
||||||
if alert, ok := config.Alerts[alertName]; ok {
|
if alert, ok := config.GetAlert(alertName); ok {
|
||||||
output, err := alert.Send(*alertNotice)
|
output, err := alert.Send(*alertNotice)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Errorf(
|
slog.Errorf(
|
||||||
@@ -67,7 +65,7 @@ func sendAlerts(config *Config, monitor *Monitor, alertNotice *AlertNotice) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkMonitors(config *Config) error {
|
func CheckMonitors(config *Config) error {
|
||||||
// TODO: Run this in goroutines and capture exceptions
|
// TODO: Run this in goroutines and capture exceptions
|
||||||
for _, monitor := range config.Monitors {
|
for _, monitor := range config.Monitors {
|
||||||
if monitor.ShouldCheck() {
|
if monitor.ShouldCheck() {
|
||||||
@@ -76,10 +74,10 @@ func checkMonitors(config *Config) error {
|
|||||||
|
|
||||||
// Track status metrics
|
// Track status metrics
|
||||||
Metrics.SetMonitorStatus(monitor.Name, monitor.IsUp())
|
Metrics.SetMonitorStatus(monitor.Name, monitor.IsUp())
|
||||||
Metrics.CountCheck(monitor.Name, success, monitor.LastCheckSeconds(), hasAlert)
|
Metrics.CountCheck(monitor.Name, success, monitor.LastCheckMilliseconds(), hasAlert)
|
||||||
|
|
||||||
if alertNotice != nil {
|
if alertNotice != nil {
|
||||||
err := sendAlerts(config, monitor, alertNotice)
|
err := SendAlerts(config, monitor, alertNotice)
|
||||||
// If there was an error in sending an alert, exit early and bubble it up
|
// If there was an error in sending an alert, exit early and bubble it up
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -91,13 +89,41 @@ func checkMonitors(config *Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SendStartupAlerts(config *Config, alertNames []string) error {
|
||||||
|
for _, alertName := range alertNames {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
alert, ok := config.GetAlert(alertName)
|
||||||
|
if !ok {
|
||||||
|
err = fmt.Errorf("unknown alert %s: %w", alertName, errUnknownAlert)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
_, err = alert.Send(AlertNotice{
|
||||||
|
AlertCount: 0,
|
||||||
|
FailureCount: 0,
|
||||||
|
IsUp: true,
|
||||||
|
LastSuccess: time.Now(),
|
||||||
|
MonitorName: fmt.Sprintf("First Run Alert Test: %s", alert.Name),
|
||||||
|
LastCheckOutput: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
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 \"\")")
|
||||||
|
|
||||||
flag.BoolVar(&slog.DebugLevel, "debug", false, "Enables debug logs (default: false)")
|
flag.BoolVar(&slog.DebugLevel, "debug", false, "Enables debug logs (default: false)")
|
||||||
flag.BoolVar(&ExportMetrics, "metrics", false, "Enables prometheus metrics exporting (default: false)")
|
flag.BoolVar(&ExportMetrics, "metrics", false, "Enables prometheus metrics exporting (default: false)")
|
||||||
flag.BoolVar(&PyCompat, "py-compat", false, "Enables support for legacy Python Minitor config. Will eventually be removed. (default: false)")
|
|
||||||
flag.IntVar(&MetricsPort, "metrics-port", MetricsPort, "The port that Prometheus metrics should be exported on, if enabled. (default: 8080)")
|
flag.IntVar(&MetricsPort, "metrics-port", MetricsPort, "The port that Prometheus metrics should be exported on, if enabled. (default: 8080)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -110,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 {
|
||||||
@@ -119,11 +145,19 @@ func main() {
|
|||||||
go ServeMetrics()
|
go ServeMetrics()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *startupAlerts != "" {
|
||||||
|
alertNames := strings.Split(*startupAlerts, ",")
|
||||||
|
|
||||||
|
err = SendStartupAlerts(&config, alertNames)
|
||||||
|
|
||||||
|
slog.OnErrPanicf(err, "Error running startup alerts")
|
||||||
|
}
|
||||||
|
|
||||||
// Start main loop
|
// Start main loop
|
||||||
for {
|
for {
|
||||||
err = checkMonitors(&config)
|
err = CheckMonitors(&config)
|
||||||
slog.OnErrPanicf(err, "Error checking monitors")
|
slog.OnErrPanicf(err, "Error checking monitors")
|
||||||
|
|
||||||
time.Sleep(config.CheckInterval.Value())
|
time.Sleep(config.CheckInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+139
-83
@@ -1,136 +1,192 @@
|
|||||||
package main
|
package main_test
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
m "git.iamthefij.com/iamthefij/minitor-go/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Ptr[T any](v T) *T {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckConfig tests the checkConfig function
|
||||||
|
// It also tests results for potentially invalid configuration. For example, no alerts
|
||||||
func TestCheckMonitors(t *testing.T) {
|
func TestCheckMonitors(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
config Config
|
config m.Config
|
||||||
expectErr bool
|
expectFailureError bool
|
||||||
|
expectRecoverError bool
|
||||||
name string
|
name string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
config: Config{},
|
config: m.Config{
|
||||||
expectErr: false,
|
CheckIntervalStr: "1s",
|
||||||
name: "Empty",
|
Monitors: []*m.Monitor{
|
||||||
},
|
|
||||||
{
|
|
||||||
config: Config{
|
|
||||||
Monitors: []*Monitor{
|
|
||||||
{
|
{
|
||||||
Name: "Success",
|
Name: "Success",
|
||||||
Command: CommandOrShell{Command: []string{"true"}},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectErr: false,
|
expectFailureError: false,
|
||||||
name: "Monitor success, no alerts",
|
expectRecoverError: false,
|
||||||
|
name: "No alerts",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config: Config{
|
config: m.Config{
|
||||||
Monitors: []*Monitor{
|
CheckIntervalStr: "1s",
|
||||||
|
Monitors: []*m.Monitor{
|
||||||
{
|
{
|
||||||
Name: "Failure",
|
Name: "Failure",
|
||||||
Command: CommandOrShell{Command: []string{"false"}},
|
|
||||||
AlertAfter: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectErr: false,
|
|
||||||
name: "Monitor failure, no alerts",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: Config{
|
|
||||||
Monitors: []*Monitor{
|
|
||||||
{
|
|
||||||
Name: "Success",
|
|
||||||
Command: CommandOrShell{Command: []string{"ls"}},
|
|
||||||
alertCount: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectErr: false,
|
|
||||||
name: "Monitor recovery, no alerts",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: Config{
|
|
||||||
Monitors: []*Monitor{
|
|
||||||
{
|
|
||||||
Name: "Failure",
|
|
||||||
Command: CommandOrShell{Command: []string{"false"}},
|
|
||||||
AlertDown: []string{"unknown"},
|
AlertDown: []string{"unknown"},
|
||||||
AlertAfter: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
name: "Monitor failure, unknown alerts",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: Config{
|
|
||||||
Monitors: []*Monitor{
|
|
||||||
{
|
|
||||||
Name: "Success",
|
|
||||||
Command: CommandOrShell{Command: []string{"true"}},
|
|
||||||
AlertUp: []string{"unknown"},
|
AlertUp: []string{"unknown"},
|
||||||
alertCount: 1,
|
AlertAfter: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectErr: true,
|
expectFailureError: true,
|
||||||
name: "Monitor recovery, unknown alerts",
|
expectRecoverError: true,
|
||||||
|
name: "Unknown alerts",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config: Config{
|
config: m.Config{
|
||||||
Monitors: []*Monitor{
|
CheckIntervalStr: "1s",
|
||||||
|
Monitors: []*m.Monitor{
|
||||||
{
|
{
|
||||||
Name: "Failure",
|
Name: "Failure",
|
||||||
Command: CommandOrShell{Command: []string{"false"}},
|
|
||||||
AlertDown: []string{"good"},
|
AlertDown: []string{"good"},
|
||||||
|
AlertUp: []string{"good"},
|
||||||
AlertAfter: 1,
|
AlertAfter: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Alerts: map[string]*Alert{
|
Alerts: []*m.Alert{{
|
||||||
"good": {
|
Name: "good",
|
||||||
Command: CommandOrShell{Command: []string{"true"}},
|
Command: []string{"true"},
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
},
|
expectFailureError: false,
|
||||||
},
|
expectRecoverError: false,
|
||||||
expectErr: false,
|
name: "Successful alert",
|
||||||
name: "Monitor failure, successful alert",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config: Config{
|
config: m.Config{
|
||||||
Monitors: []*Monitor{
|
CheckIntervalStr: "1s",
|
||||||
|
Monitors: []*m.Monitor{
|
||||||
{
|
{
|
||||||
Name: "Failure",
|
Name: "Failure",
|
||||||
Command: CommandOrShell{Command: []string{"false"}},
|
|
||||||
AlertDown: []string{"bad"},
|
AlertDown: []string{"bad"},
|
||||||
|
AlertUp: []string{"bad"},
|
||||||
AlertAfter: 1,
|
AlertAfter: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Alerts: map[string]*Alert{
|
Alerts: []*m.Alert{{
|
||||||
"bad": {
|
|
||||||
Name: "bad",
|
Name: "bad",
|
||||||
Command: CommandOrShell{Command: []string{"false"}},
|
Command: []string{"false"},
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
},
|
expectFailureError: true,
|
||||||
},
|
expectRecoverError: true,
|
||||||
expectErr: true,
|
name: "Failing alert",
|
||||||
name: "Monitor failure, bad alert",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
|
c := c
|
||||||
|
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
err := c.config.Init()
|
err := c.config.Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("checkMonitors(%s): unexpected error reading config: %v", c.name, err)
|
t.Errorf("checkMonitors(%s): unexpected error reading config: %v", c.name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = checkMonitors(&c.config)
|
for _, check := range []struct {
|
||||||
|
shellCmd string
|
||||||
|
name string
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{"false", "Failure", c.expectFailureError}, {"true", "Success", c.expectRecoverError},
|
||||||
|
} {
|
||||||
|
// Set the shell command for this check
|
||||||
|
c.config.Monitors[0].ShellCommand = check.shellCmd
|
||||||
|
|
||||||
|
// Run the check
|
||||||
|
err = m.CheckMonitors(&c.config)
|
||||||
|
|
||||||
|
// Check the results
|
||||||
|
if err == nil && check.expectErr {
|
||||||
|
t.Errorf("checkMonitors(%s:%s): Expected error, the code did not error", c.name, check.name)
|
||||||
|
} else if err != nil && !check.expectErr {
|
||||||
|
t.Errorf("checkMonitors(%s:%s): Did not expect an error, but we got one anyway: %v", c.name, check.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirstRunAlerts(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
config m.Config
|
||||||
|
expectErr bool
|
||||||
|
startupAlerts []string
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
config: m.Config{
|
||||||
|
CheckIntervalStr: "1s",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
startupAlerts: []string{"missing"},
|
||||||
|
name: "Unknown",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: m.Config{
|
||||||
|
CheckIntervalStr: "1s",
|
||||||
|
Alerts: []*m.Alert{
|
||||||
|
{
|
||||||
|
Name: "good",
|
||||||
|
Command: []string{"true"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
startupAlerts: []string{"good"},
|
||||||
|
name: "Successful alert",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: m.Config{
|
||||||
|
CheckIntervalStr: "1s",
|
||||||
|
Alerts: []*m.Alert{
|
||||||
|
{
|
||||||
|
Name: "bad",
|
||||||
|
Command: []string{"false"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
startupAlerts: []string{"bad"},
|
||||||
|
name: "Failed alert",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
c := c
|
||||||
|
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := c.config.Init()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("sendFirstRunAlerts(%s): unexpected error reading config: %v", c.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.SendStartupAlerts(&c.config, c.startupAlerts)
|
||||||
if err == nil && c.expectErr {
|
if err == nil && c.expectErr {
|
||||||
t.Errorf("checkMonitors(%s): Expected panic, the code did not panic", c.name)
|
t.Errorf("sendFirstRunAlerts(%s): Expected error, the code did not error", c.name)
|
||||||
} else if err != nil && !c.expectErr {
|
} else if err != nil && !c.expectErr {
|
||||||
t.Errorf("checkMonitors(%s): Did not expect an error, but we got one anyway: %v", c.name, err)
|
t.Errorf("sendFirstRunAlerts(%s): Did not expect an error, but we got one anyway: %v", c.name, err)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
image: iamthefij/minitor-go:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
|
|
||||||
{{#if build.tags}}
|
|
||||||
tags:
|
|
||||||
{{#each build.tags}}
|
|
||||||
- {{this}}
|
|
||||||
{{/each}}
|
|
||||||
{{/if}}
|
|
||||||
manifests:
|
|
||||||
-
|
|
||||||
image: iamthefij/minitor-go:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
|
|
||||||
platform:
|
|
||||||
architecture: amd64
|
|
||||||
os: linux
|
|
||||||
-
|
|
||||||
image: iamthefij/minitor-go:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
|
|
||||||
platform:
|
|
||||||
architecture: arm64
|
|
||||||
os: linux
|
|
||||||
variant: v8
|
|
||||||
-
|
|
||||||
image: iamthefij/minitor-go:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
|
|
||||||
platform:
|
|
||||||
architecture: arm
|
|
||||||
os: linux
|
|
||||||
variant: v7
|
|
||||||
+4
-4
@@ -43,14 +43,14 @@ func NewMetrics() *MinitorMetrics {
|
|||||||
),
|
),
|
||||||
checkTime: prometheus.NewGaugeVec(
|
checkTime: prometheus.NewGaugeVec(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "minitor_check_seconds",
|
Name: "minitor_check_milliseconds",
|
||||||
Help: "Time in miliseconds that a check ran for",
|
Help: "Time in miliseconds that a check ran for",
|
||||||
},
|
},
|
||||||
[]string{"monitor", "status"},
|
[]string{"monitor", "status"},
|
||||||
),
|
),
|
||||||
monitorStatus: prometheus.NewGaugeVec(
|
monitorStatus: prometheus.NewGaugeVec(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "minitor_monitor_up",
|
Name: "minitor_monitor_up_count",
|
||||||
Help: "Status of currently responsive monitors",
|
Help: "Status of currently responsive monitors",
|
||||||
},
|
},
|
||||||
[]string{"monitor"},
|
[]string{"monitor"},
|
||||||
@@ -77,7 +77,7 @@ func (metrics *MinitorMetrics) SetMonitorStatus(monitor string, isUp bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CountCheck counts the result of a particular Monitor check
|
// CountCheck counts the result of a particular Monitor check
|
||||||
func (metrics *MinitorMetrics) CountCheck(monitor string, isSuccess bool, secs float64, isAlert bool) {
|
func (metrics *MinitorMetrics) CountCheck(monitor string, isSuccess bool, ms int64, isAlert bool) {
|
||||||
status := "failure"
|
status := "failure"
|
||||||
if isSuccess {
|
if isSuccess {
|
||||||
status = "success"
|
status = "success"
|
||||||
@@ -94,7 +94,7 @@ func (metrics *MinitorMetrics) CountCheck(monitor string, isSuccess bool, secs f
|
|||||||
|
|
||||||
metrics.checkTime.With(
|
metrics.checkTime.With(
|
||||||
prometheus.Labels{"monitor": monitor, "status": status},
|
prometheus.Labels{"monitor": monitor, "status": status},
|
||||||
).Set(secs)
|
).Set(float64(ms))
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountAlert counts an alert
|
// CountAlert counts an alert
|
||||||
|
|||||||
+177
-105
@@ -1,6 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
@@ -11,51 +13,130 @@ import (
|
|||||||
// Monitor represents a particular periodic check of a command
|
// Monitor represents a particular periodic check of a command
|
||||||
type Monitor struct { //nolint:maligned
|
type Monitor struct { //nolint:maligned
|
||||||
// Config values
|
// Config values
|
||||||
AlertAfter int16 `yaml:"alert_after"`
|
CheckIntervalStr *string `hcl:"check_interval,optional"`
|
||||||
AlertEvery int16 `yaml:"alert_every"`
|
CheckInterval time.Duration
|
||||||
CheckInterval SecondsOrDuration `yaml:"check_interval"`
|
|
||||||
Name string
|
Name string `hcl:"name,label"`
|
||||||
AlertDown []string `yaml:"alert_down"`
|
AlertCount int
|
||||||
AlertUp []string `yaml:"alert_up"`
|
AlertAfter int `hcl:"alert_after,optional"`
|
||||||
Command CommandOrShell
|
AlertEvery *int `hcl:"alert_every,optional"`
|
||||||
|
AlertDown []string `hcl:"alert_down,optional"`
|
||||||
|
AlertUp []string `hcl:"alert_up,optional"`
|
||||||
|
Command []string `hcl:"command,optional"`
|
||||||
|
ShellCommand string `hcl:"shell_command,optional"`
|
||||||
|
|
||||||
// Other values
|
// Other values
|
||||||
alertCount int16
|
failureCount int
|
||||||
failureCount int16
|
|
||||||
lastCheck time.Time
|
lastCheck time.Time
|
||||||
lastSuccess time.Time
|
lastSuccess time.Time
|
||||||
lastOutput string
|
lastOutput string
|
||||||
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
|
||||||
return (!monitor.Command.Empty() &&
|
if monitor.CheckIntervalStr != nil {
|
||||||
monitor.getAlertAfter() > 0 &&
|
var err error
|
||||||
monitor.AlertDown != 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldCheck returns a boolean indicating if the Monitor is ready to be
|
// Set default values for monitor alerts
|
||||||
// be checked again
|
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
|
||||||
|
hasShellCommand := monitor.ShellCommand != ""
|
||||||
|
hasValidAlertAfter := monitor.AlertAfter > 0
|
||||||
|
hasAlertDown := len(monitor.AlertDown) > 0
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
hasAtLeastOneCommand := hasCommand || hasShellCommand
|
||||||
|
if !hasAtLeastOneCommand {
|
||||||
|
err = errors.Join(err, fmt.Errorf(
|
||||||
|
"%w: monitor %s has no command or shell_command configured",
|
||||||
|
ErrInvalidMonitor,
|
||||||
|
monitor.Name,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAtMostOneCommand := !(hasCommand && hasShellCommand)
|
||||||
|
if !hasAtMostOneCommand {
|
||||||
|
err = errors.Join(err, fmt.Errorf(
|
||||||
|
"%w: monitor %s has both command and shell_command configured",
|
||||||
|
ErrInvalidMonitor,
|
||||||
|
monitor.Name,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasValidAlertAfter {
|
||||||
|
err = errors.Join(err, fmt.Errorf(
|
||||||
|
"%w: monitor %s has invalid alert_after value %d. Must be greater than 0",
|
||||||
|
ErrInvalidMonitor,
|
||||||
|
monitor.Name,
|
||||||
|
monitor.AlertAfter,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasAlertDown {
|
||||||
|
err = errors.Join(err, fmt.Errorf(
|
||||||
|
"%w: monitor %s has no alert_down configured. Configure one here or add a default_alert_down",
|
||||||
|
ErrInvalidMonitor,
|
||||||
|
monitor.Name,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor Monitor) LastOutput() string {
|
||||||
|
return monitor.lastOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() {
|
if monitor.lastCheck.IsZero() || monitor.CheckInterval == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
sinceLastCheck := time.Since(monitor.lastCheck)
|
sinceLastCheck := time.Since(monitor.lastCheck)
|
||||||
|
|
||||||
return sinceLastCheck >= monitor.CheckInterval.Value()
|
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 monitor.Command.Command != nil {
|
if len(monitor.Command) > 0 {
|
||||||
cmd = exec.Command(monitor.Command.Command[0], monitor.Command.Command[1:]...)
|
cmd = exec.Command(monitor.Command[0], monitor.Command[1:]...)
|
||||||
|
} else if monitor.ShellCommand != "" {
|
||||||
|
cmd = ShellCommand(monitor.ShellCommand)
|
||||||
} else {
|
} else {
|
||||||
cmd = ShellCommand(monitor.Command.ShellCommand)
|
slog.Fatalf("Monitor %s has no command configured", monitor.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkStartTime := time.Now()
|
checkStartTime := time.Now()
|
||||||
@@ -68,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)
|
||||||
@@ -86,84 +167,6 @@ func (monitor *Monitor) Check() (bool, *AlertNotice) {
|
|||||||
return isSuccess, alertNotice
|
return isSuccess, alertNotice
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsUp returns the status of the current monitor
|
|
||||||
func (monitor Monitor) IsUp() bool {
|
|
||||||
return monitor.alertCount == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// LastCheckSeconds gives number of seconds the last check ran for
|
|
||||||
func (monitor Monitor) LastCheckSeconds() float64 {
|
|
||||||
return monitor.lastCheckDuration.Seconds()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (monitor *Monitor) success() (notice *AlertNotice) {
|
|
||||||
if !monitor.IsUp() {
|
|
||||||
// Alert that we have recovered
|
|
||||||
notice = monitor.createAlertNotice(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
monitor.failureCount = 0
|
|
||||||
monitor.alertCount = 0
|
|
||||||
monitor.lastSuccess = time.Now()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (monitor *Monitor) failure() (notice *AlertNotice) {
|
|
||||||
monitor.failureCount++
|
|
||||||
// If we haven't hit the minimum failures, we can exit
|
|
||||||
if monitor.failureCount < monitor.getAlertAfter() {
|
|
||||||
slog.Debugf(
|
|
||||||
"%s failed but did not hit minimum failures. "+
|
|
||||||
"Count: %v alert after: %v",
|
|
||||||
monitor.Name,
|
|
||||||
monitor.failureCount,
|
|
||||||
monitor.getAlertAfter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take number of failures after minimum
|
|
||||||
failureCount := (monitor.failureCount - monitor.getAlertAfter())
|
|
||||||
|
|
||||||
// Use alert cadence to determine if we should alert
|
|
||||||
switch {
|
|
||||||
case monitor.AlertEvery > 0:
|
|
||||||
// Handle integer number of failures before alerting
|
|
||||||
if failureCount%monitor.AlertEvery == 0 {
|
|
||||||
notice = monitor.createAlertNotice(false)
|
|
||||||
}
|
|
||||||
case monitor.AlertEvery == 0:
|
|
||||||
// Handle alerting on first failure only
|
|
||||||
if failureCount == 0 {
|
|
||||||
notice = monitor.createAlertNotice(false)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// Handle negative numbers indicating an exponential backoff
|
|
||||||
if failureCount >= int16(math.Pow(2, float64(monitor.alertCount))-1) { //nolint:gomnd
|
|
||||||
notice = monitor.createAlertNotice(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're going to alert, increment count
|
|
||||||
if notice != nil {
|
|
||||||
monitor.alertCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
return notice
|
|
||||||
}
|
|
||||||
|
|
||||||
func (monitor Monitor) getAlertAfter() int16 {
|
|
||||||
// TODO: Come up with a better way than this method
|
|
||||||
// Zero is one!
|
|
||||||
if monitor.AlertAfter == 0 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return monitor.AlertAfter
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAlertNames gives a list of alert names for a given monitor status
|
// GetAlertNames gives a list of alert names for a given monitor status
|
||||||
func (monitor Monitor) GetAlertNames(up bool) []string {
|
func (monitor Monitor) GetAlertNames(up bool) []string {
|
||||||
if up {
|
if up {
|
||||||
@@ -173,11 +176,80 @@ func (monitor Monitor) GetAlertNames(up bool) []string {
|
|||||||
return monitor.AlertDown
|
return monitor.AlertDown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsUp returns the status of the current monitor
|
||||||
|
func (monitor Monitor) IsUp() bool {
|
||||||
|
return monitor.AlertCount == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastCheckMilliseconds gives number of miliseconds the last check ran for
|
||||||
|
func (monitor Monitor) LastCheckMilliseconds() int64 {
|
||||||
|
return monitor.lastCheckDuration.Milliseconds()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor *Monitor) Success() (notice *AlertNotice) {
|
||||||
|
if !monitor.IsUp() {
|
||||||
|
// Alert that we have recovered
|
||||||
|
notice = monitor.createAlertNotice(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor.failureCount = 0
|
||||||
|
monitor.AlertCount = 0
|
||||||
|
monitor.lastSuccess = time.Now()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor *Monitor) Failure() (notice *AlertNotice) {
|
||||||
|
monitor.failureCount++
|
||||||
|
// If we haven't hit the minimum failures, we can exit
|
||||||
|
if monitor.failureCount < monitor.AlertAfter {
|
||||||
|
slog.Debugf(
|
||||||
|
"%s failed but did not hit minimum failures. "+
|
||||||
|
"Count: %v alert after: %v",
|
||||||
|
monitor.Name,
|
||||||
|
monitor.failureCount,
|
||||||
|
monitor.AlertAfter,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take number of failures after minimum
|
||||||
|
failureCount := (monitor.failureCount - monitor.AlertAfter)
|
||||||
|
|
||||||
|
// Use alert cadence to determine if we should alert
|
||||||
|
switch {
|
||||||
|
case monitor.AlertEvery == nil, *monitor.AlertEvery == 0:
|
||||||
|
// Handle alerting on first failure only
|
||||||
|
if failureCount == 0 {
|
||||||
|
notice = monitor.createAlertNotice(false)
|
||||||
|
}
|
||||||
|
case *monitor.AlertEvery > 0:
|
||||||
|
// Handle integer number of failures before alerting
|
||||||
|
if failureCount%*monitor.AlertEvery == 0 {
|
||||||
|
notice = monitor.createAlertNotice(false)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Handle negative numbers indicating an exponential backoff
|
||||||
|
if failureCount >= int(math.Pow(2, float64(monitor.AlertCount))-1) { //nolint:mnd
|
||||||
|
notice = monitor.createAlertNotice(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're going to alert, increment count
|
||||||
|
if notice != nil {
|
||||||
|
monitor.AlertCount++
|
||||||
|
notice.AlertCount = monitor.AlertCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return notice
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
+146
-135
@@ -1,171 +1,196 @@
|
|||||||
package main
|
package main_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"errors"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
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 Monitor
|
monitor m.Monitor
|
||||||
expected bool
|
expected error
|
||||||
name string
|
name string
|
||||||
}{
|
}{
|
||||||
{Monitor{Command: CommandOrShell{Command: []string{"echo", "test"}}, AlertDown: []string{"log"}}, true, "Command only"},
|
{m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, nil, "Command only"},
|
||||||
{Monitor{Command: CommandOrShell{ShellCommand: "echo test"}, AlertDown: []string{"log"}}, true, "CommandShell only"},
|
{m.Monitor{AlertAfter: 1, ShellCommand: "echo test", AlertDown: []string{"log"}}, nil, "CommandShell only"},
|
||||||
{Monitor{Command: CommandOrShell{Command: []string{"echo", "test"}}}, false, "No AlertDown"},
|
{m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}}, m.ErrInvalidMonitor, "No AlertDown"},
|
||||||
{Monitor{AlertDown: []string{"log"}}, false, "No commands"},
|
{m.Monitor{AlertAfter: 1, AlertDown: []string{"log"}}, m.ErrInvalidMonitor, "No commands"},
|
||||||
{Monitor{Command: CommandOrShell{Command: []string{"echo", "test"}}, AlertDown: []string{"log"}, AlertAfter: -1}, 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 {
|
||||||
log.Printf("Testing case %s", c.name)
|
c := c
|
||||||
|
|
||||||
actual := c.monitor.IsValid()
|
t.Run(c.name, func(t *testing.T) {
|
||||||
if actual != c.expected {
|
t.Parallel()
|
||||||
|
|
||||||
|
actual := c.monitor.Validate()
|
||||||
|
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)
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
log.Println("-----")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMonitorShouldCheck tests the Monitor.ShouldCheck()
|
// TestMonitorShouldCheck tests the Monitor.ShouldCheck()
|
||||||
func TestMonitorShouldCheck(t *testing.T) {
|
func TestMonitorShouldCheck(t *testing.T) {
|
||||||
timeNow := time.Now()
|
t.Parallel()
|
||||||
timeTenSecAgo := time.Now().Add(time.Second * -10)
|
|
||||||
timeTwentySecAgo := time.Now().Add(time.Second * -20)
|
|
||||||
|
|
||||||
cases := []struct {
|
// Create a monitor that should check every second and then verify it checks with some sleeps
|
||||||
monitor Monitor
|
monitor := m.Monitor{ShellCommand: "true", CheckInterval: time.Second}
|
||||||
expected bool
|
|
||||||
name string
|
if !monitor.ShouldCheck() {
|
||||||
}{
|
t.Errorf("New monitor should be ready to check")
|
||||||
{Monitor{}, true, "Empty"},
|
|
||||||
{Monitor{lastCheck: timeNow, CheckInterval: SecondsOrDuration{time.Second * 15}}, false, "Just checked"},
|
|
||||||
{Monitor{lastCheck: timeTenSecAgo, CheckInterval: SecondsOrDuration{time.Second * 15}}, false, "-10s"},
|
|
||||||
{Monitor{lastCheck: timeTwentySecAgo, CheckInterval: SecondsOrDuration{time.Second * 15}}, true, "-20s"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
monitor.Check()
|
||||||
actual := c.monitor.ShouldCheck()
|
|
||||||
if actual != c.expected {
|
if monitor.ShouldCheck() {
|
||||||
t.Errorf("ShouldCheck(%v), expected=%t actual=%t", c.name, c.expected, actual)
|
t.Errorf("Monitor should not be ready to check after a check")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
if !monitor.ShouldCheck() {
|
||||||
|
t.Errorf("Monitor should be ready to check after a second")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMonitorIsUp tests the Monitor.IsUp()
|
// TestMonitorIsUp tests the Monitor.IsUp()
|
||||||
func TestMonitorIsUp(t *testing.T) {
|
func TestMonitorIsUp(t *testing.T) {
|
||||||
cases := []struct {
|
t.Parallel()
|
||||||
monitor Monitor
|
|
||||||
expected bool
|
// Creating a monitor that should alert after 2 failures. The monitor should be considered up until we reach two failed checks
|
||||||
name string
|
monitor := m.Monitor{ShellCommand: "false", AlertAfter: 2}
|
||||||
}{
|
if !monitor.IsUp() {
|
||||||
{Monitor{}, true, "Empty"},
|
t.Errorf("New monitor should be considered up")
|
||||||
{Monitor{alertCount: 1}, false, "Has alert"},
|
|
||||||
{Monitor{alertCount: -1}, false, "Negative alerts"},
|
|
||||||
{Monitor{alertCount: 0}, true, "No alerts"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
monitor.Check()
|
||||||
log.Printf("Testing case %s", c.name)
|
|
||||||
|
|
||||||
actual := c.monitor.IsUp()
|
if !monitor.IsUp() {
|
||||||
if actual != c.expected {
|
t.Errorf("Monitor should be considered up with one failure and no alerts")
|
||||||
t.Errorf("IsUp(%v), expected=%t actual=%t", c.name, c.expected, actual)
|
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("-----")
|
monitor.Check()
|
||||||
|
|
||||||
|
if monitor.IsUp() {
|
||||||
|
t.Errorf("Monitor should be considered down with one alert")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMonitorGetAlertNames tests that proper alert names are returned
|
// TestMonitorGetAlertNames tests that proper alert names are returned
|
||||||
func TestMonitorGetAlertNames(t *testing.T) {
|
func TestMonitorGetAlertNames(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
monitor Monitor
|
monitor m.Monitor
|
||||||
up bool
|
up bool
|
||||||
expected []string
|
expected []string
|
||||||
name string
|
name string
|
||||||
}{
|
}{
|
||||||
{Monitor{}, true, nil, "Empty up"},
|
{m.Monitor{}, true, nil, "Empty up"},
|
||||||
{Monitor{}, false, nil, "Empty down"},
|
{m.Monitor{}, false, nil, "Empty down"},
|
||||||
{Monitor{AlertUp: []string{"alert"}}, true, []string{"alert"}, "Return up"},
|
{m.Monitor{AlertUp: []string{"alert"}}, true, []string{"alert"}, "Return up"},
|
||||||
{Monitor{AlertDown: []string{"alert"}}, false, []string{"alert"}, "Return down"},
|
{m.Monitor{AlertDown: []string{"alert"}}, false, []string{"alert"}, "Return down"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
log.Printf("Testing case %s", c.name)
|
c := c
|
||||||
|
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
actual := c.monitor.GetAlertNames(c.up)
|
actual := c.monitor.GetAlertNames(c.up)
|
||||||
if !EqualSliceString(actual, c.expected) {
|
if !reflect.DeepEqual(actual, c.expected) {
|
||||||
t.Errorf("GetAlertNames(%v), expected=%v actual=%v", c.name, c.expected, actual)
|
t.Errorf("GetAlertNames(%v), expected=%v actual=%v", c.name, c.expected, actual)
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
log.Println("-----")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMonitorSuccess tests the Monitor.success()
|
func TestMonitorAlertCount(t *testing.T) {
|
||||||
func TestMonitorSuccess(t *testing.T) {
|
alertEvery := 1
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
monitor Monitor
|
checkSuccess bool
|
||||||
expectNotice bool
|
alertCount int
|
||||||
name string
|
name string
|
||||||
}{
|
}{
|
||||||
{Monitor{}, false, "Empty"},
|
{false, 1, "First failure and first alert"},
|
||||||
{Monitor{alertCount: 0}, false, "No alerts"},
|
{false, 2, "Second failure and first alert"},
|
||||||
{Monitor{alertCount: 1}, true, "Has 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 {
|
for _, c := range cases {
|
||||||
log.Printf("Testing case %s", c.name)
|
t.Logf("Testing case %s", c.name)
|
||||||
|
|
||||||
notice := c.monitor.success()
|
var notice *m.AlertNotice
|
||||||
hasNotice := (notice != nil)
|
if c.checkSuccess {
|
||||||
|
notice = monitor.Success()
|
||||||
if hasNotice != c.expectNotice {
|
} else {
|
||||||
t.Errorf("success(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice)
|
notice = monitor.Failure()
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("-----")
|
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) {
|
||||||
|
var alertEveryOne int = 1
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
monitor Monitor
|
monitor m.Monitor
|
||||||
|
numChecks int
|
||||||
expectNotice bool
|
expectNotice bool
|
||||||
name string
|
name string
|
||||||
}{
|
}{
|
||||||
{Monitor{AlertAfter: 1}, true, "Empty"}, // Defaults to true because and AlertEvery default to 0
|
{m.Monitor{ShellCommand: "false", AlertAfter: 1}, 1, true, "Empty After 1"}, // Defaults to true because and AlertEvery default to 0
|
||||||
{Monitor{failureCount: 0, AlertAfter: 1, AlertEvery: 1}, true, "Alert after 1: first failure"},
|
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: &alertEveryOne}, 1, true, "Alert after 1: first failure"},
|
||||||
{Monitor{failureCount: 1, AlertAfter: 1, AlertEvery: 1}, true, "Alert after 1: second failure"},
|
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: &alertEveryOne}, 2, true, "Alert after 1: second failure"},
|
||||||
{Monitor{failureCount: 0, AlertAfter: 20, AlertEvery: 1}, false, "Alert after 20: first failure"},
|
{m.Monitor{ShellCommand: "false", AlertAfter: 20, AlertEvery: &alertEveryOne}, 1, false, "Alert after 20: first failure"},
|
||||||
{Monitor{failureCount: 19, AlertAfter: 20, AlertEvery: 1}, true, "Alert after 20: 20th failure"},
|
{m.Monitor{ShellCommand: "false", AlertAfter: 20, AlertEvery: &alertEveryOne}, 20, true, "Alert after 20: 20th failure"},
|
||||||
{Monitor{failureCount: 20, AlertAfter: 20, AlertEvery: 1}, true, "Alert after 20: 21st failure"},
|
{m.Monitor{ShellCommand: "false", AlertAfter: 20, AlertEvery: &alertEveryOne}, 21, true, "Alert after 20: 21st failure"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
log.Printf("Testing case %s", c.name)
|
c := c
|
||||||
|
|
||||||
notice := c.monitor.failure()
|
t.Run(c.name, func(t *testing.T) {
|
||||||
hasNotice := (notice != nil)
|
t.Parallel()
|
||||||
|
|
||||||
|
hasNotice := false
|
||||||
|
|
||||||
|
for i := 0; i < c.numChecks; i++ {
|
||||||
|
_, notice := c.monitor.Check()
|
||||||
|
hasNotice = (notice != nil)
|
||||||
|
}
|
||||||
|
|
||||||
if hasNotice != c.expectNotice {
|
if hasNotice != c.expectNotice {
|
||||||
t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice)
|
t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice)
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
log.Println("-----")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,53 +198,42 @@ func TestMonitorFailureAlertAfter(t *testing.T) {
|
|||||||
// on the expected intervals
|
// on the expected intervals
|
||||||
func TestMonitorFailureAlertEvery(t *testing.T) {
|
func TestMonitorFailureAlertEvery(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
monitor Monitor
|
monitor m.Monitor
|
||||||
expectNotice bool
|
expectedNotice []bool
|
||||||
name string
|
name string
|
||||||
}{
|
}{
|
||||||
/*
|
{m.Monitor{ShellCommand: "false", AlertAfter: 1}, []bool{true}, "No AlertEvery set"}, // Defaults to true because AlertAfter and AlertEvery default to nil
|
||||||
TODO: Actually found a bug in original implementation. There is an inconsistency in the way AlertAfter is treated.
|
|
||||||
For "First alert only" (ie. AlertEvery=0), it is the number of failures to ignore before alerting, so AlertAfter=1
|
|
||||||
will ignore the first failure and alert on the second failure
|
|
||||||
For other intervals (ie. AlertEvery=1), it is essentially indexed on one. Essentially making AlertAfter=1 trigger
|
|
||||||
on the first failure.
|
|
||||||
|
|
||||||
For usabilty, this should be consistent. Consistent with what though? minitor-py? Or itself? Dun dun duuuunnnnn!
|
|
||||||
*/
|
|
||||||
{Monitor{AlertAfter: 1}, true, "Empty"}, // Defaults to true because AlertAfter and AlertEvery default to 0
|
|
||||||
// Alert first time only, after 1
|
// Alert first time only, after 1
|
||||||
{Monitor{failureCount: 0, AlertAfter: 1, AlertEvery: 0}, true, "Alert first time only after 1: first failure"},
|
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(0)}, []bool{true, false, false}, "Alert first time only after 1"},
|
||||||
{Monitor{failureCount: 1, AlertAfter: 1, AlertEvery: 0}, false, "Alert first time only after 1: second failure"},
|
|
||||||
{Monitor{failureCount: 2, AlertAfter: 1, AlertEvery: 0}, false, "Alert first time only after 1: third failure"},
|
|
||||||
// Alert every time, after 1
|
// Alert every time, after 1
|
||||||
{Monitor{failureCount: 0, AlertAfter: 1, AlertEvery: 1}, true, "Alert every time after 1: first failure"},
|
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(1)}, []bool{true, true, true}, "Alert every time after 1"},
|
||||||
{Monitor{failureCount: 1, AlertAfter: 1, AlertEvery: 1}, true, "Alert every time after 1: second failure"},
|
|
||||||
{Monitor{failureCount: 1, AlertAfter: 1, AlertEvery: 1}, true, "Alert every time after 1: third failure"},
|
|
||||||
// Alert every other time, after 1
|
// Alert every other time, after 1
|
||||||
{Monitor{failureCount: 0, AlertAfter: 1, AlertEvery: 2}, true, "Alert every other time after 1: first failure"},
|
{m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(2)}, []bool{true, false, true, false}, "Alert every other time after 1"},
|
||||||
{Monitor{failureCount: 1, AlertAfter: 1, AlertEvery: 2}, false, "Alert every other time after 1: second failure"},
|
|
||||||
{Monitor{failureCount: 2, AlertAfter: 1, AlertEvery: 2}, true, "Alert every other time after 1: third failure"},
|
|
||||||
{Monitor{failureCount: 3, AlertAfter: 1, AlertEvery: 2}, false, "Alert every other time after 1: fourth failure"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
log.Printf("Testing case %s", c.name)
|
c := c
|
||||||
|
|
||||||
notice := c.monitor.failure()
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for i, expectNotice := range c.expectedNotice {
|
||||||
|
_, notice := c.monitor.Check()
|
||||||
hasNotice := (notice != nil)
|
hasNotice := (notice != nil)
|
||||||
|
|
||||||
if hasNotice != c.expectNotice {
|
if hasNotice != expectNotice {
|
||||||
t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice)
|
t.Errorf("failed %s check %d: expected=%t actual=%t", c.name, i, expectNotice, hasNotice)
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
log.Println("-----")
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMonitorFailureExponential tests that alerts will trigger
|
// TestMonitorFailureExponential tests that alerts will trigger
|
||||||
// with an exponential backoff after repeated failures
|
// with an exponential backoff after repeated failures
|
||||||
func TestMonitorFailureExponential(t *testing.T) {
|
func TestMonitorFailureExponential(t *testing.T) {
|
||||||
|
var alertEveryExp int = -1
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
expectNotice bool
|
expectNotice bool
|
||||||
name string
|
name string
|
||||||
@@ -236,20 +250,18 @@ func TestMonitorFailureExponential(t *testing.T) {
|
|||||||
|
|
||||||
// Unlike previous tests, this one requires a static Monitor with repeated
|
// Unlike previous tests, this one requires a static Monitor with repeated
|
||||||
// calls to the failure method
|
// calls to the failure method
|
||||||
monitor := Monitor{failureCount: 0, AlertAfter: 1, AlertEvery: -1}
|
monitor := m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: &alertEveryExp}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
log.Printf("Testing case %s", c.name)
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
// NOTE: These tests are not parallel because they rely on the state of the Monitor
|
||||||
notice := monitor.failure()
|
_, notice := monitor.Check()
|
||||||
hasNotice := (notice != nil)
|
hasNotice := (notice != nil)
|
||||||
|
|
||||||
if hasNotice != c.expectNotice {
|
if hasNotice != c.expectNotice {
|
||||||
t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice)
|
t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice)
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
log.Println("-----")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,53 +274,52 @@ func TestMonitorCheck(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
monitor Monitor
|
monitor m.Monitor
|
||||||
expect expected
|
expect expected
|
||||||
name string
|
name string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Monitor{Command: CommandOrShell{Command: []string{"echo", "success"}}},
|
m.Monitor{AlertAfter: 1, Command: []string{"echo", "success"}},
|
||||||
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
|
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
|
||||||
"Test successful command",
|
"Test successful command",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Monitor{Command: CommandOrShell{ShellCommand: "echo success"}},
|
m.Monitor{AlertAfter: 1, ShellCommand: "echo success"},
|
||||||
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
|
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
|
||||||
"Test successful command shell",
|
"Test successful command shell",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Monitor{Command: CommandOrShell{Command: []string{"total", "failure"}}},
|
m.Monitor{AlertAfter: 1, Command: []string{"total", "failure"}},
|
||||||
expected{isSuccess: false, hasNotice: true, lastOutput: ""},
|
expected{isSuccess: false, hasNotice: true, lastOutput: ""},
|
||||||
"Test failed command",
|
"Test failed command",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Monitor{Command: CommandOrShell{ShellCommand: "false"}},
|
m.Monitor{AlertAfter: 1, ShellCommand: "false"},
|
||||||
expected{isSuccess: false, hasNotice: true, lastOutput: ""},
|
expected{isSuccess: false, hasNotice: true, lastOutput: ""},
|
||||||
"Test failed command shell",
|
"Test failed command shell",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
log.Printf("Testing case %s", c.name)
|
c := c
|
||||||
|
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
isSuccess, notice := c.monitor.Check()
|
isSuccess, notice := c.monitor.Check()
|
||||||
if isSuccess != c.expect.isSuccess {
|
if isSuccess != c.expect.isSuccess {
|
||||||
t.Errorf("Check(%v) (success), expected=%t actual=%t", c.name, c.expect.isSuccess, isSuccess)
|
t.Errorf("Check(%v) (success), expected=%t actual=%t", c.name, c.expect.isSuccess, isSuccess)
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasNotice := (notice != nil)
|
hasNotice := (notice != nil)
|
||||||
if hasNotice != c.expect.hasNotice {
|
if hasNotice != c.expect.hasNotice {
|
||||||
t.Errorf("Check(%v) (notice), expected=%t actual=%t", c.name, c.expect.hasNotice, hasNotice)
|
t.Errorf("Check(%v) (notice), expected=%t actual=%t", c.name, c.expect.hasNotice, hasNotice)
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastOutput := c.monitor.lastOutput
|
lastOutput := c.monitor.LastOutput()
|
||||||
if lastOutput != c.expect.lastOutput {
|
if lastOutput != c.expect.lastOutput {
|
||||||
t.Errorf("Check(%v) (output), expected=%v actual=%v", c.name, c.expect.lastOutput, lastOutput)
|
t.Errorf("Check(%v) (output), expected=%v actual=%v", c.name, c.expect.lastOutput, lastOutput)
|
||||||
log.Printf("Case failed: %s", c.name)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
log.Println("-----")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
check_interval = "5s"
|
||||||
|
|
||||||
|
monitor "Fake Website" {
|
||||||
|
command = ["curl", "-s", "-o", "/dev/null", "https://minitor.mon"]
|
||||||
|
alert_down = ["log_down", "mailgun_down", "sms_down"]
|
||||||
|
alert_up = ["log_up", "email_up"]
|
||||||
|
check_interval = "10s" # Must be at minimum the global `check_interval`
|
||||||
|
alert_after = 3
|
||||||
|
alert_every = -1 # Defaults to -1 for exponential backoff. 0 to disable repeating
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor "Real Website" {
|
||||||
|
command = ["curl", "-s", "-o", "/dev/null", "https://google.com"]
|
||||||
|
alert_down = ["log_down", "mailgun_down", "sms_down"]
|
||||||
|
alert_up = ["log_up", "email_up"]
|
||||||
|
check_interval = "5s"
|
||||||
|
alert_after = 3
|
||||||
|
alert_every = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
alert "log_down" {
|
||||||
|
command = ["echo", "Minitor failure for {{.MonitorName}}"]
|
||||||
|
}
|
||||||
|
|
||||||
|
alert "log_up" {
|
||||||
|
command = ["echo", "Minitor recovery for {{.MonitorName}}"]
|
||||||
|
}
|
||||||
|
|
||||||
|
alert "email_up" {
|
||||||
|
command = ["sendmail", "me@minitor.mon", "Recovered: {monitor_name}", "We're back!"]
|
||||||
|
}
|
||||||
|
|
||||||
|
alert "mailgun_down" {
|
||||||
|
shell_command = <<-EOF
|
||||||
|
curl -s -X POST \
|
||||||
|
-F subject="Alert! {{.MonitorName}} failed" \
|
||||||
|
-F from="Minitor <minitor@minitor.mon>" \
|
||||||
|
-F to=me@minitor.mon \
|
||||||
|
-F text="Our monitor failed" \
|
||||||
|
https://api.mailgun.net/v3/minitor.mon/messages \
|
||||||
|
-u "api:$${MAILGUN_API_KEY}"
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
alert "sms_down" {
|
||||||
|
shell_command = <<-EOF
|
||||||
|
curl -s -X POST -F "Body=Failure! {{.MonitorName}} has failed" \
|
||||||
|
-F "From=$${AVAILABLE_NUMBER}" -F "To=$${MY_PHONE}" \
|
||||||
|
"https://api.twilio.com/2010-04-01/Accounts/$${ACCOUNT_SID}/Messages" \
|
||||||
|
-u "$${ACCOUNT_SID}:$${AUTH_TOKEN}"
|
||||||
|
EOF
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
check_interval: 5
|
|
||||||
|
|
||||||
monitors:
|
|
||||||
- name: Fake Website
|
|
||||||
command: ["curl", "-s", "-o", "/dev/null", "https://minitor.mon"]
|
|
||||||
alert_down: [log_down, mailgun_down, sms_down]
|
|
||||||
alert_up: [log_up, email_up]
|
|
||||||
check_interval: 10 # Must be at minimum the global `check_interval`
|
|
||||||
alert_after: 3
|
|
||||||
alert_every: -1 # Defaults to -1 for exponential backoff. 0 to disable repeating
|
|
||||||
- name: Real Website
|
|
||||||
command: ["curl", "-s", "-o", "/dev/null", "https://google.com"]
|
|
||||||
alert_down: [log_down, mailgun_down, sms_down]
|
|
||||||
alert_up: [log_up, email_up]
|
|
||||||
check_interval: 5
|
|
||||||
alert_after: 3
|
|
||||||
alert_every: -1
|
|
||||||
|
|
||||||
alerts:
|
|
||||||
log_down:
|
|
||||||
command: ["echo", "Minitor failure for {{.MonitorName}}"]
|
|
||||||
log_up:
|
|
||||||
command: ["echo", "Minitor recovery for {{.MonitorName}}"]
|
|
||||||
email_up:
|
|
||||||
command:
|
|
||||||
[sendmail, "me@minitor.mon", "Recovered: {monitor_name}", "We're back!"]
|
|
||||||
mailgun_down:
|
|
||||||
command: >
|
|
||||||
curl -s -X POST
|
|
||||||
-F subject="Alert! {{.MonitorName}} failed"
|
|
||||||
-F from="Minitor <minitor@minitor.mon>"
|
|
||||||
-F to=me@minitor.mon
|
|
||||||
-F text="Our monitor failed"
|
|
||||||
https://api.mailgun.net/v3/minitor.mon/messages
|
|
||||||
-u "api:${MAILGUN_API_KEY}"
|
|
||||||
sms_down:
|
|
||||||
command: >
|
|
||||||
curl -s -X POST -F "Body=Failure! {{.MonitorName}} has failed"
|
|
||||||
-F "From=${AVAILABLE_NUMBER}" -F "To=${MY_PHONE}"
|
|
||||||
"https://api.twilio.com/2010-04-01/Accounts/${ACCOUNT_SID}/Messages"
|
|
||||||
-u "${ACCOUNT_SID}:${AUTH_TOKEN}"
|
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
# Used for a basic HTTP health check
|
||||||
|
# Avoids output from non-errors and will fail if the HTTP response is unsuccessful
|
||||||
|
|
||||||
|
curl --silent --show-error --fail -o /dev/null "$@"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
check_interval = "woops, I'm not an int!"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
check_interval = "1s"
|
||||||
|
|
||||||
|
monitor "Command" {
|
||||||
|
command = ["echo", "$PATH"]
|
||||||
|
alert_down = [ "alert_down", "log_shell", "log_command" ]
|
||||||
|
alert_every = 0
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
check_interval: 1
|
|
||||||
|
|
||||||
monitors:
|
|
||||||
- name: Command
|
|
||||||
command: ['echo', '$PATH']
|
|
||||||
alert_down: [ 'alert_down', 'log_shell', 'log_command' ]
|
|
||||||
# alert_every: -1
|
|
||||||
alert_every: 0
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
check_interval: woops, I'm not an int!
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
check_interval = "1s"
|
||||||
|
|
||||||
|
monitor "Command" {
|
||||||
|
command = ["echo", "$PATH"]
|
||||||
|
alert_down = ["not_log"]
|
||||||
|
alert_every = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
alert "log" {
|
||||||
|
command = ["true"]
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
check_interval: 1
|
|
||||||
|
|
||||||
monitors:
|
|
||||||
- name: Command
|
|
||||||
command: ['echo', '$PATH']
|
|
||||||
alert_down: [ 'not_log']
|
|
||||||
# alert_every: -1
|
|
||||||
alert_every: 0
|
|
||||||
|
|
||||||
|
|
||||||
alerts:
|
|
||||||
log:
|
|
||||||
command: ['true']
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
check_interval = "1s"
|
||||||
|
default_alert_down = ["log_command"]
|
||||||
|
default_alert_every = 0
|
||||||
|
default_alert_after = 2
|
||||||
|
|
||||||
|
monitor "Default" {
|
||||||
|
command = ["echo"]
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor "Command" {
|
||||||
|
command = ["echo", "$PATH"]
|
||||||
|
}
|
||||||
|
|
||||||
|
alert "log_command" {
|
||||||
|
command = ["echo", "default", "'command!!!'", "{{.MonitorName}}"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
check_interval = "1s"
|
||||||
|
|
||||||
|
alert "log_command" {
|
||||||
|
command = ["echo", "regular", "'command!!!'", "{{.MonitorName}}"]
|
||||||
|
}
|
||||||
|
|
||||||
|
alert "log_shell" {
|
||||||
|
shell_command = "echo \"Failure on {{.MonitorName}} User is $USER\""
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor "Default" {
|
||||||
|
command = ["echo"]
|
||||||
|
alert_down = ["log_command"]
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor "Command" {
|
||||||
|
command = ["echo", "$PATH"]
|
||||||
|
alert_down = ["log_command", "log_shell"]
|
||||||
|
alert_every = 2
|
||||||
|
check_interval = "10s"
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor "Shell" {
|
||||||
|
shell_command = <<-EOF
|
||||||
|
echo 'Some string with stuff'
|
||||||
|
echo 'another line'
|
||||||
|
echo $PATH
|
||||||
|
exit 1
|
||||||
|
EOF
|
||||||
|
alert_down = ["log_command", "log_shell"]
|
||||||
|
alert_after = 5
|
||||||
|
alert_every = 0
|
||||||
|
check_interval = "1m"
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
check_interval: 1
|
|
||||||
|
|
||||||
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"
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
check_interval: 1
|
|
||||||
|
|
||||||
monitors:
|
|
||||||
- name: Command
|
|
||||||
command: ['echo', '$PATH']
|
|
||||||
alert_down: ['log']
|
|
||||||
alert_every: 0
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
check_interval = "1s"
|
||||||
|
|
||||||
|
monitor "Shell" {
|
||||||
|
shell_command = <<-EOF
|
||||||
|
echo 'Some string with stuff'
|
||||||
|
echo "<angle brackets>"
|
||||||
|
exit 1
|
||||||
|
EOF
|
||||||
|
alert_down = ["log_shell"]
|
||||||
|
alert_after = 1
|
||||||
|
alert_every = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
alert "log_shell" {
|
||||||
|
shell_command = <<EOF
|
||||||
|
echo 'Some string with stuff'
|
||||||
|
echo '<angle brackets>'
|
||||||
|
EOF
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
check_interval: 1
|
|
||||||
|
|
||||||
monitors:
|
|
||||||
- name: Shell
|
|
||||||
command: >
|
|
||||||
echo 'Some string with stuff';
|
|
||||||
echo "<angle brackets>";
|
|
||||||
exit 1
|
|
||||||
alert_down: ['log_shell']
|
|
||||||
alert_after: 1
|
|
||||||
alert_every: 0
|
|
||||||
|
|
||||||
alerts:
|
|
||||||
log_shell:
|
|
||||||
command: |
|
|
||||||
echo 'Some string with stuff'
|
|
||||||
echo '<angle brackets>'
|
|
||||||
+10
-1
@@ -1,6 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestUtilEqualSliceString(t *testing.T) {
|
func TestUtilEqualSliceString(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
@@ -21,6 +24,11 @@ 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.Parallel()
|
||||||
|
|
||||||
actual := EqualSliceString(c.a, c.b)
|
actual := EqualSliceString(c.a, c.b)
|
||||||
if actual != c.expected {
|
if actual != c.expected {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
@@ -28,5 +36,6 @@ func TestUtilEqualSliceString(t *testing.T) {
|
|||||||
c.a, c.b, c.expected, actual,
|
c.a, c.b, c.expected, actual,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user