Move monitor init to it's own method and refactor config validate to return err

This commit is contained in:
IamTheFij 2024-11-15 16:17:15 -08:00
parent 3fb418151b
commit 9e20c00dde
9 changed files with 171 additions and 141 deletions

View File

@ -4,7 +4,7 @@ name: test
steps: steps:
- name: test - name: test
image: golang:1.20 image: golang:1.21
environment: environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}} VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
commands: commands:
@ -30,7 +30,7 @@ trigger:
steps: steps:
- name: build all binaries - name: build all binaries
image: golang:1.20 image: golang:1.21
environment: environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}} VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
commands: commands:

View File

@ -1,4 +1,4 @@
FROM golang:1.20 AS builder FROM golang:1.21 AS builder
WORKDIR /app WORKDIR /app

203
config.go
View File

@ -6,21 +6,26 @@ import (
"time" "time"
"git.iamthefij.com/iamthefij/slog" "git.iamthefij.com/iamthefij/slog"
/*
* "github.com/hashicorp/hcl/v2"
* "github.com/hashicorp/hcl/v2/gohcl"
*/
"github.com/hashicorp/hcl/v2/hclsimple" "github.com/hashicorp/hcl/v2/hclsimple"
) )
var errInvalidConfig = errors.New("Invalid configuration") var (
ErrLoadingConfig = errors.New("Failed to load or parse configuration")
ErrConfigInit = errors.New("Failed to initialize configuration")
ErrInvalidConfig = errors.New("Invalid configuration")
ErrNoAlerts = errors.New("No alerts provided")
ErrInvalidAlert = errors.New("Invalid alert configuration")
ErrNoMonitors = errors.New("No monitors provided")
ErrInvalidMonitor = errors.New("Invalid monitor configuration")
ErrUnknownAlert = errors.New("Unknown alert")
)
// Config type is contains all provided user configuration // Config type is contains all provided user configuration
type Config struct { type Config struct {
CheckIntervalStr string `hcl:"check_interval"` CheckIntervalStr string `hcl:"check_interval"`
CheckInterval time.Duration CheckInterval time.Duration
DefaultAlertAfter *int `hcl:"default_alert_after,optional"` DefaultAlertAfter int `hcl:"default_alert_after,optional"`
DefaultAlertEvery *int `hcl:"default_alert_every,optional"` DefaultAlertEvery *int `hcl:"default_alert_every,optional"`
DefaultAlertDown []string `hcl:"default_alert_down,optional"` DefaultAlertDown []string `hcl:"default_alert_down,optional"`
DefaultAlertUp []string `hcl:"default_alert_up,optional"` DefaultAlertUp []string `hcl:"default_alert_up,optional"`
@ -30,6 +35,76 @@ type Config struct {
alertLookup map[string]*Alert alertLookup map[string]*Alert
} }
// Init performs extra initialization on top of loading the config from file
func (config *Config) Init() (err error) {
config.CheckInterval, err = time.ParseDuration(config.CheckIntervalStr)
if err != nil {
return fmt.Errorf("failed to parse top level check_interval duration: %w", err)
}
if config.DefaultAlertAfter == 0 {
minAlertAfter := 1
config.DefaultAlertAfter = minAlertAfter
}
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 {
if !alert.IsValid() {
err = errors.Join(err, fmt.Errorf("%w: %s", ErrInvalidAlert, alert.Name))
}
}
// Validate monitors
if len(config.Monitors) == 0 {
err = errors.Join(err, ErrNoMonitors)
}
for _, monitor := range config.Monitors {
if !monitor.IsValid() {
err = errors.Join(err, fmt.Errorf("%w: %s", ErrInvalidMonitor, monitor.Name))
}
// Check that all Monitor alerts actually exist
for _, isUp := range []bool{true, false} {
for _, alertName := range monitor.GetAlertNames(isUp) {
if _, ok := config.GetAlert(alertName); !ok {
err = errors.Join(
err,
fmt.Errorf("%w: %s. %w: %s", ErrInvalidMonitor, monitor.Name, ErrUnknownAlert, alertName),
)
}
}
}
}
return err
}
// GetAlert returns an alert by name
func (c Config) GetAlert(name string) (*Alert, bool) { func (c Config) GetAlert(name string) (*Alert, bool) {
if c.alertLookup == nil { if c.alertLookup == nil {
c.alertLookup = map[string]*Alert{} c.alertLookup = map[string]*Alert{}
@ -54,119 +129,25 @@ func (c *Config) BuildAllTemplates() (err error) {
return return
} }
// IsValid checks config validity and returns true if valid
func (config Config) IsValid() (isValid bool) {
isValid = true
// Validate alerts
if len(config.Alerts) == 0 {
// This should never happen because there is a default alert named 'log' for now
slog.Errorf("Invalid alert configuration: Must provide at least one alert")
isValid = false
}
for _, alert := range config.Alerts {
if !alert.IsValid() {
slog.Errorf("Invalid alert configuration: %+v", alert.Name)
isValid = false
}
}
// Validate monitors
if len(config.Monitors) == 0 {
slog.Errorf("Invalid monitor configuration: Must provide at least one monitor")
isValid = false
}
for _, monitor := range config.Monitors {
if !monitor.IsValid() {
slog.Errorf("Invalid monitor configuration: %s", monitor.Name)
isValid = false
}
// Check that all Monitor alerts actually exist
for _, isUp := range []bool{true, false} {
for _, alertName := range monitor.GetAlertNames(isUp) {
if _, ok := config.GetAlert(alertName); !ok {
slog.Errorf(
"Invalid monitor configuration: %s. Unknown alert %s",
monitor.Name, alertName,
)
isValid = false
}
}
}
}
return isValid
}
// Init performs extra initialization on top of loading the config from file
func (config *Config) Init() (err error) {
config.CheckInterval, err = time.ParseDuration(config.CheckIntervalStr)
if err != nil {
return fmt.Errorf("failed to parse top level check_interval duration: %w", err)
}
for _, monitor := range config.Monitors {
// TODO: Move this to a Monitor.Init() method
// Parse the check_interval string into a time.Duration
if monitor.CheckIntervalStr != nil {
monitor.CheckInterval, err = time.ParseDuration(*monitor.CheckIntervalStr)
if err != nil {
return fmt.Errorf("failed to parse check_interval duration for monitor %s: %w", monitor.Name, err)
}
}
// Set default values for monitor alerts
if monitor.AlertAfter == 0 && config.DefaultAlertAfter != nil {
monitor.AlertAfter = *config.DefaultAlertAfter
} else if monitor.AlertAfter == 0 {
monitor.AlertAfter = 1
}
if monitor.AlertEvery == nil {
monitor.AlertEvery = config.DefaultAlertEvery
}
if monitor.AlertDown == nil {
monitor.AlertDown = config.DefaultAlertDown
}
if monitor.AlertUp == nil {
monitor.AlertUp = config.DefaultAlertUp
}
}
err = config.BuildAllTemplates()
return
}
// LoadConfig will read config from the given path and parse it // LoadConfig will read config from the given path and parse it
func LoadConfig(filePath string) (config Config, err error) { func LoadConfig(filePath string) (Config, error) {
err = hclsimple.DecodeFile(filePath, nil, &config) var config Config
if err != nil {
return if err := hclsimple.DecodeFile(filePath, nil, &config); err != nil {
slog.Debugf("Failed to load config from %s: %v", filePath, err)
return config, errors.Join(ErrLoadingConfig, err)
} }
slog.Debugf("Config values:\n%v\n", config) slog.Debugf("Config values:\n%v\n", config)
// Finish initializing configuration // Finish initializing configuration
if err = config.Init(); err != nil { if err := config.Init(); err != nil {
return return config, errors.Join(ErrConfigInit, err)
} }
if !config.IsValid() { if err := config.IsValid(); err != nil {
err = errInvalidConfig return config, errors.Join(ErrInvalidConfig, err)
return
} }
return config, err return config, nil
} }

View File

@ -1,6 +1,7 @@
package main_test package main_test
import ( import (
"errors"
"testing" "testing"
m "git.iamthefij.com/iamthefij/minitor-go" m "git.iamthefij.com/iamthefij/minitor-go"
@ -8,16 +9,18 @@ import (
func TestLoadConfig(t *testing.T) { func TestLoadConfig(t *testing.T) {
cases := []struct { cases := []struct {
configPath string configPath string
expectErr bool expectedErr error
name string name string
}{ }{
{"./test/does-not-exist", true, "Invalid config path"}, {"./test/does-not-exist", m.ErrLoadingConfig, "Invalid config path"},
{"./test/invalid-config-missing-alerts.hcl", true, "Invalid config missing alerts"}, {"./test/invalid-config-wrong-hcl-type.hcl", m.ErrLoadingConfig, "Incorrect HCL type"},
{"./test/invalid-config-type.hcl", true, "Invalid config type for key"}, {"./test/invalid-config-missing-alerts.hcl", m.ErrNoAlerts, "Invalid config missing alerts"},
{"./test/invalid-config-unknown-alert.hcl", true, "Invalid config unknown alert"}, {"./test/invalid-config-missing-alerts.hcl", m.ErrInvalidConfig, "Invalid config general"},
{"./test/valid-config-default-values.hcl", false, "Valid config file with default values"}, {"./test/invalid-config-invalid-duration.hcl", m.ErrConfigInit, "Invalid config type for key"},
{"./test/valid-config.hcl", false, "Valid config file"}, {"./test/invalid-config-unknown-alert.hcl", m.ErrUnknownAlert, "Invalid config unknown alert"},
{"./test/valid-config-default-values.hcl", nil, "Valid config file with default values"},
{"./test/valid-config.hcl", nil, "Valid config file"},
} }
for _, c := range cases { for _, c := range cases {
c := c c := c
@ -27,9 +30,10 @@ func TestLoadConfig(t *testing.T) {
_, err := m.LoadConfig(c.configPath) _, err := m.LoadConfig(c.configPath)
hasErr := (err != nil) hasErr := (err != nil)
expectErr := (c.expectedErr != nil)
if hasErr != c.expectErr { if hasErr != expectErr || !errors.Is(err, c.expectedErr) {
t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectErr, err) t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectedErr, err)
} }
}) })
} }

2
go.mod
View File

@ -1,6 +1,6 @@
module git.iamthefij.com/iamthefij/minitor-go module git.iamthefij.com/iamthefij/minitor-go
go 1.20 go 1.21
require ( require (
git.iamthefij.com/iamthefij/slog v1.3.0 git.iamthefij.com/iamthefij/slog v1.3.0

2
go.sum
View File

@ -24,6 +24,7 @@ github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJb
github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@ -41,6 +42,7 @@ github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5E
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"math" "math"
"os/exec" "os/exec"
"time" "time"
@ -31,8 +32,40 @@ type Monitor struct { //nolint:maligned
lastCheckDuration time.Duration lastCheckDuration time.Duration
} }
// IsValid returns a boolean indicating if the Monitor has been correctly // Init initializes the Monitor with default values
// configured func (monitor *Monitor) Init(defaultAlertAfter int, defaultAlertEvery *int, defaultAlertDown []string, defaultAlertUp []string) error {
// Parse the check_interval string into a time.Duration
if monitor.CheckIntervalStr != nil {
var err error
monitor.CheckInterval, err = time.ParseDuration(*monitor.CheckIntervalStr)
if err != nil {
return fmt.Errorf("failed to parse check_interval duration for monitor %s: %w", monitor.Name, err)
}
}
// Set default values for monitor alerts
if monitor.AlertAfter == 0 {
minAlertAfter := 1
monitor.AlertAfter = max(defaultAlertAfter, minAlertAfter)
}
if monitor.AlertEvery == nil {
monitor.AlertEvery = defaultAlertEvery
}
if len(monitor.AlertDown) == 0 {
monitor.AlertDown = defaultAlertDown
}
if len(monitor.AlertUp) == 0 {
monitor.AlertUp = defaultAlertUp
}
return nil
}
// IsValid returns a boolean indicating if the Monitor has been correctly configured
func (monitor Monitor) IsValid() bool { func (monitor Monitor) IsValid() bool {
// TODO: Refactor and return an error containing more information on what was invalid // TODO: Refactor and return an error containing more information on what was invalid
hasCommand := len(monitor.Command) > 0 hasCommand := len(monitor.Command) > 0
@ -53,8 +86,7 @@ func (monitor Monitor) LastOutput() string {
return monitor.lastOutput return monitor.lastOutput
} }
// ShouldCheck returns a boolean indicating if the Monitor is ready to be // ShouldCheck returns a boolean indicating if the Monitor is ready to be be checked again
// be checked again
func (monitor Monitor) ShouldCheck() bool { func (monitor Monitor) ShouldCheck() bool {
if monitor.lastCheck.IsZero() || monitor.CheckInterval == 0 { if monitor.lastCheck.IsZero() || monitor.CheckInterval == 0 {
return true return true
@ -65,8 +97,7 @@ func (monitor Monitor) ShouldCheck() bool {
return sinceLastCheck >= monitor.CheckInterval return sinceLastCheck >= monitor.CheckInterval
} }
// Check will run the command configured by the Monitor and return a status // Check will run the command configured by the Monitor and return a status and a possible AlertNotice
// and a possible AlertNotice
func (monitor *Monitor) Check() (bool, *AlertNotice) { func (monitor *Monitor) Check() (bool, *AlertNotice) {
var cmd *exec.Cmd var cmd *exec.Cmd
if len(monitor.Command) > 0 { if len(monitor.Command) > 0 {
@ -105,6 +136,15 @@ func (monitor *Monitor) Check() (bool, *AlertNotice) {
return isSuccess, alertNotice return isSuccess, alertNotice
} }
// GetAlertNames gives a list of alert names for a given monitor status
func (monitor Monitor) GetAlertNames(up bool) []string {
if up {
return monitor.AlertUp
}
return monitor.AlertDown
}
// IsUp returns the status of the current monitor // IsUp returns the status of the current monitor
func (monitor Monitor) IsUp() bool { func (monitor Monitor) IsUp() bool {
return monitor.alertCount == 0 return monitor.alertCount == 0
@ -173,15 +213,6 @@ func (monitor *Monitor) failure() (notice *AlertNotice) {
return notice return notice
} }
// GetAlertNames gives a list of alert names for a given monitor status
func (monitor Monitor) GetAlertNames(up bool) []string {
if up {
return monitor.AlertUp
}
return monitor.AlertDown
}
func (monitor Monitor) createAlertNotice(isUp bool) *AlertNotice { func (monitor Monitor) createAlertNotice(isUp bool) *AlertNotice {
// TODO: Maybe add something about recovery status here // TODO: Maybe add something about recovery status here
return &AlertNotice{ return &AlertNotice{

View File

@ -0,0 +1,12 @@
check_interval = "1s"
alert "log_command" {
command = "should be a list"
}
monitor "Command" {
command = ["echo", "$PATH"]
alert_down = ["log_command"]
alert_every = 2
check_interval = "10s"
}