diff --git a/README.md b/README.md index 9872bec..69309dd 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ All Keys marked with `*` will be resolved as described before. ```yaml --- -notifications: # keys must be lowercase! +notifications: dev-slack: enabled: true slack_config: diff --git a/deploy/config.yaml b/deploy/config.yaml index bf83702..b680f5e 100644 --- a/deploy/config.yaml +++ b/deploy/config.yaml @@ -15,7 +15,7 @@ heartbeats: notifications: # must match with notifications - dev-slack - gmail -notifications: # keys must be lowercause +notifications: dev-slack: enabled: true slack_config: diff --git a/deploy/kubernetes/configmap.yaml b/deploy/kubernetes/configmap.yaml index dc40400..3a8c296 100644 --- a/deploy/kubernetes/configmap.yaml +++ b/deploy/kubernetes/configmap.yaml @@ -33,7 +33,7 @@ data: notifications: # must match with notifications - dev-slack - gmail - notifications: # keys must be lowercause + notifications: dev-slack: enabled: true slack_config: diff --git a/go.mod b/go.mod index 1ef9581..d9016c7 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/prometheus/client_golang v1.19.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.19.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -15,32 +15,19 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/google/uuid v1.5.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/imdario/mergo v0.3.16 // indirect - github.com/magiconair/properties v1.8.7 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect golang.org/x/crypto v0.21.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.33.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 61bd189..6cf90c4 100644 --- a/go.sum +++ b/go.sum @@ -8,20 +8,15 @@ 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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= @@ -30,16 +25,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -53,55 +42,24 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k 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/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go index 93fcaa9..b5f6271 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,11 +5,9 @@ import ( "heartbeats/internal/heartbeat" "heartbeats/internal/history" "heartbeats/internal/notify" - "heartbeats/internal/timer" - "reflect" - "time" + "os" - "github.com/spf13/viper" + "gopkg.in/yaml.v3" ) // App is the global configuration instance. @@ -18,6 +16,7 @@ var App = &Config{ NotificationStore: notify.NewStore(), } +// HistoryStore is the global HistoryStore. var HistoryStore = history.NewStore() // Cache configuration structure. @@ -28,144 +27,112 @@ type Cache struct { // Server configuration structure. type Server struct { - SiteRoot string // Site root - ListenAddress string // Address on which the application listens + SiteRoot string `yaml:"siteRoot"` // Site root + ListenAddress string `yaml:"listenAddress"` // Address on which the application listens } // Config holds the entire application configuration. type Config struct { - Version string - Verbose bool - Path string - Server Server - Cache Cache + Version string `yaml:"version"` + Verbose bool `yaml:"verbose"` + Path string `yaml:"path"` + Server Server `yaml:"server"` + Cache Cache `yaml:"cache"` HeartbeatStore *heartbeat.Store `yaml:"heartbeats"` NotificationStore *notify.Store `yaml:"notifications"` } -// Read reads the configuration file and initializes the stores. +// Read reads the configuration from the file specified in the Config struct. func (c *Config) Read() error { - viper.SetConfigFile(c.Path) - - if err := viper.ReadInConfig(); err != nil { + content, err := os.ReadFile(c.Path) + if err != nil { return fmt.Errorf("failed to read config file. %w", err) } - notifications := make(map[string]*notify.Notification) - if err := viper.UnmarshalKey("notifications", ¬ifications); err != nil { - return fmt.Errorf("failed to unmarshal notifications. %w", err) - } - - for name, n := range notifications { - if err := c.NotificationStore.Add(name, n); err != nil { - return fmt.Errorf("failed to add notification '%s'. %w", n.Name, err) - } - - notification := c.NotificationStore.Get(name) - if notification.Type == "slack" && notification.SlackConfig.ColorTemplate == "" { - notification.SlackConfig.ColorTemplate = `{{ if eq .Status "ok" }}good{{ else }}danger{{ end }}` - if err := c.NotificationStore.Update(name, notification); err != nil { - return fmt.Errorf("failed to update notification '%s'. %w", notification.Name, err) - } - } + var rawConfig map[string]interface{} + if err := yaml.Unmarshal(content, &rawConfig); err != nil { + return fmt.Errorf("failed to unmarshal raw config. %w", err) } - heartbeats := make(map[string]*heartbeat.Heartbeat) - if err := viper.UnmarshalKey("heartbeats", &heartbeats, viper.DecodeHook(decodeHookHeartbeats)); err != nil { - return fmt.Errorf("failed to unmarshal heartbeats. %w", err) + if err := c.processNotifications(rawConfig["notifications"]); err != nil { + return err } - for name, h := range heartbeats { - if err := c.HeartbeatStore.Add(name, h); err != nil { - return fmt.Errorf("failed to add heartbeat '%s'. %w", h.Name, err) - } - historyInstance := history.NewHistory(c.Cache.MaxSize, c.Cache.Reduce) - if err := HistoryStore.Add(name, historyInstance); err != nil { - return fmt.Errorf("failed to create heartbeat history for '%s'. %w", name, err) - } + if err := c.processHeartbeats(rawConfig["heartbeats"]); err != nil { + return err } return nil } -// decodeHookHeartbeats is a custom decode hook for the 'heartbeats' configuration section. -func decodeHookHeartbeats(_, to reflect.Type, data interface{}) (interface{}, error) { - if data == nil { - return nil, nil // No data to process - } - - heartbeats, ok := data.(map[string]interface{}) +// processNotifications handles the unmarshaling and processing of notification configurations. +func (c *Config) processNotifications(rawNotifications interface{}) error { + notifications, ok := rawNotifications.(map[string]interface{}) if !ok { - return data, nil // Not the correct type, no error but no processing + return fmt.Errorf("failed to unmarshal notifications") } - result := make(map[string]heartbeat.Heartbeat) - - for name, hb := range heartbeats { - h, ok := hb.(map[string]interface{}) - if !ok { - return data, fmt.Errorf("failed to assert heartbeat as map[string]interface{}") - } - - description, _ := h["description"].(string) - - interval, err := parseTimer(h, "interval") + for name, rawNotification := range notifications { + notificationBytes, err := yaml.Marshal(rawNotification) if err != nil { - return nil, fmt.Errorf("failed to parse interval for heartbeat '%s': %w", name, err) + return fmt.Errorf("failed to marshal notification '%s'. %w", name, err) } - grace, err := parseTimer(h, "grace") - if err != nil { - return nil, fmt.Errorf("failed to parse grace for heartbeat '%s': %w", name, err) + var notification notify.Notification + if err := yaml.Unmarshal(notificationBytes, ¬ification); err != nil { + return fmt.Errorf("failed to unmarshal notification '%s'. %w", name, err) } - var sendResolve *bool - if sr, ok := h["sendresolve"].(bool); ok { // All lowercase because of viper - sendResolve = &sr + if err := c.NotificationStore.Add(name, ¬ification); err != nil { + return fmt.Errorf("failed to add notification '%s'. %w", notification.Name, err) } - notifications, err := parseNotifications(h) - if err != nil { - return nil, fmt.Errorf("failed to parse notifications for heartbeat '%s': %w", name, err) - } - - result[name] = heartbeat.Heartbeat{ - Name: name, - Description: description, - Grace: grace, - Interval: interval, - SendResolve: sendResolve, - Notifications: notifications, + if err := c.updateSlackNotification(name, ¬ification); err != nil { + return err } } - return result, nil + return nil } -// parseTimer parses a timer configuration from the heartbeat configuration. -func parseTimer(h map[string]interface{}, key string) (*timer.Timer, error) { - t := &timer.Timer{} - if value, ok := h[key].(string); ok { - duration, err := time.ParseDuration(value) - if err != nil { - return nil, fmt.Errorf("failed to parse '%s' as duration: %w", key, err) +// updateSlackNotification updates the Slack notification with a default color template if not set. +func (c *Config) updateSlackNotification(name string, notification *notify.Notification) error { + if notification.Type == "slack" && notification.SlackConfig.ColorTemplate == "" { + notification.SlackConfig.ColorTemplate = `{{ if eq .Status "ok" }}good{{ else }}danger{{ end }}` + if err := c.NotificationStore.Update(name, notification); err != nil { + return fmt.Errorf("failed to update notification '%s'. %w", notification.Name, err) } - t.Interval = &duration } - return t, nil + return nil } -// parseNotifications parses the notifications configuration from the heartbeat configuration. -func parseNotifications(h map[string]interface{}) ([]string, error) { - var notifications []string - if notificationList, ok := h["notifications"].([]interface{}); ok { - for _, notificationName := range notificationList { - name, isString := notificationName.(string) - if !isString { - return nil, fmt.Errorf("notification name is not a string") - } - notifications = append(notifications, name) +// processHeartbeats handles the unmarshaling and processing of heartbeat configurations. +func (c *Config) processHeartbeats(rawHeartbeats interface{}) error { + heartbeats, ok := rawHeartbeats.(map[string]interface{}) + if !ok { + return fmt.Errorf("failed to unmarshal heartbeats") + } + + for name, rawHeartbeat := range heartbeats { + heartbeatBytes, err := yaml.Marshal(rawHeartbeat) + if err != nil { + return fmt.Errorf("failed to marshal heartbeat '%s'. %w", name, err) + } + + var hb heartbeat.Heartbeat + if err := yaml.Unmarshal(heartbeatBytes, &hb); err != nil { + return fmt.Errorf("failed to unmarshal heartbeat '%s'. %w", name, err) + } + + if err := c.HeartbeatStore.Add(name, &hb); err != nil { + return fmt.Errorf("failed to add heartbeat '%s'. %w", hb.Name, err) + } + + historyInstance := history.NewHistory(c.Cache.MaxSize, c.Cache.Reduce) + if err := HistoryStore.Add(name, historyInstance); err != nil { + return fmt.Errorf("failed to create heartbeat history for '%s'. %w", name, err) } } - return notifications, nil + + return nil } diff --git a/internal/flags/flags.go b/internal/flags/flags.go index ae4a797..64dff99 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -11,12 +11,6 @@ import ( ) // ParseFlags initializes the command-line flags and sets the values in the global config.App. -// -// Parameters: -// - currentVersion: The current version of the application. -// -// Returns: -// - error: An error if parsing the flags fails. func ParseFlags(currentVersion string) error { var showVersion, showHelp bool diff --git a/internal/handlers/heartbeats.go b/internal/handlers/heartbeats.go index f9b3222..10e7a14 100644 --- a/internal/handlers/heartbeats.go +++ b/internal/handlers/heartbeats.go @@ -7,7 +7,6 @@ import ( "heartbeats/internal/timer" "html/template" "net/http" - "strings" "time" "github.com/Masterminds/sprig" @@ -61,7 +60,7 @@ func Heartbeats(logger logger.Logger, staticFS embed.FS) http.HandlerFunc { for _, h := range heartbeatStore.GetAll() { var notifications []NotificationState for _, notificationName := range h.Notifications { - n := notificationStore.Get(strings.ToLower(notificationName)) + n := notificationStore.Get(notificationName) if n != nil { notifications = append(notifications, NotificationState{ Name: n.Name, diff --git a/internal/heartbeat/heartbeat.go b/internal/heartbeat/heartbeat.go index a41b910..4312873 100644 --- a/internal/heartbeat/heartbeat.go +++ b/internal/heartbeat/heartbeat.go @@ -8,7 +8,6 @@ import ( "heartbeats/internal/metrics" "heartbeats/internal/notify" "heartbeats/internal/timer" - "strings" "sync" "time" @@ -17,15 +16,15 @@ import ( // Heartbeat represents the configuration and state of a monitoring heartbeat. type Heartbeat struct { - Name string `mapstructure:"name" yaml:"name"` - Enabled *bool `mapstructure:"enabled" yaml:"enabled,omitempty"` - Description string `mapstructure:"description" yaml:"description,omitempty"` - LastPing time.Time `mapstructure:"-" yaml:"lastPing,omitempty"` - Interval *timer.Timer `mapstructure:"-" yaml:"interval,omitempty" json:"-"` - Grace *timer.Timer `mapstructure:"-" yaml:"grace,omitempty" json:"-"` - Notifications []string `mapstructure:"-" yaml:"notifications,omitempty"` - Status string `mapstructure:"-" yaml:"status,omitempty"` - SendResolve *bool `mapstructure:"sendResolve" yaml:"sendResolve,omitempty"` + Name string `yaml:"name"` + Enabled *bool `yaml:"enabled,omitempty"` + Description string `yaml:"description,omitempty"` + LastPing time.Time `yaml:"lastPing,omitempty"` + Interval *timer.Timer `yaml:"interval,omitempty" json:"-"` + Grace *timer.Timer `yaml:"grace,omitempty" json:"-"` + Notifications []string `yaml:"notifications,omitempty"` + Status string `yaml:"status,omitempty"` + SendResolve *bool `yaml:"sendResolve,omitempty"` } // Store manages the storage and retrieval of heartbeats. @@ -138,7 +137,7 @@ func (h *Heartbeat) StopTimers() { // SendNotifications sends notifications based on the current status of the heartbeat. func (h *Heartbeat) SendNotifications(ctx context.Context, log logger.Logger, notificationStore *notify.Store, history *history.History, isResolved bool) { for _, n := range h.Notifications { - notification := notificationStore.Get(strings.ToLower(n)) + notification := notificationStore.Get(n) if notification == nil { log.Debugf("%s Notification '%s' not found", EventSend, n) continue diff --git a/internal/notify/notifier/email.go b/internal/notify/notifier/email.go index 4165a25..3c988d8 100644 --- a/internal/notify/notifier/email.go +++ b/internal/notify/notifier/email.go @@ -12,13 +12,13 @@ const EmailType = "email" // MailConfig holds configuration settings for email notifications. type MailConfig struct { - SMTP email.SMTPConfig `mapstructure:"smtp"` - Email email.Email `mapstructure:"email"` + SMTP email.SMTPConfig `yaml:"smtp"` + Email email.Email `yaml:"email"` } // EmailNotifier implements Notifier for sending email notifications. type EmailNotifier struct { - Config MailConfig + Config MailConfig `yaml:"mail_config"` } // Send sends an email notification with the given data and resolution status. diff --git a/internal/notify/notifier/msteams.go b/internal/notify/notifier/msteams.go index 354caf5..90a2c0a 100644 --- a/internal/notify/notifier/msteams.go +++ b/internal/notify/notifier/msteams.go @@ -12,14 +12,14 @@ const MSTeamsType = "msteams" // MSTeamsConfig holds the configuration for MS Teams notifications inside the config file. type MSTeamsConfig struct { - WebhookURL string `mapstructure:"webhook_url"` - Title string `json:"title,omitempty"` - Text string `json:"text,omitempty"` + WebhookURL string `yaml:"webhook_url"` + Title string `yaml:"title"` + Text string `yaml:"text"` } // MSTeamsNotifier Notifier for sending MS Teams notifications. type MSTeamsNotifier struct { - Config MSTeamsConfig + Config MSTeamsConfig `yaml:"msteams_config"` } // Send sends an MS Teams notification with the given data and resolution status. diff --git a/internal/notify/notifier/slack.go b/internal/notify/notifier/slack.go index 2e725fc..f1583b6 100644 --- a/internal/notify/notifier/slack.go +++ b/internal/notify/notifier/slack.go @@ -13,16 +13,16 @@ const SlackType = "slack" // SlackConfig holds the configuration for Slack notifications inside the config file. type SlackConfig struct { - Channel string `mapstructure:"channel"` - Token string `mapstructure:"token"` - Title string `yaml:"title,omitempty"` - Text string `yaml:"text,omitempty"` - ColorTemplate string `mapstructure:"color_template,omitempty" yaml:"colorTemplate"` + Channel string `yaml:"channel"` + Token string `yaml:"token"` + Title string `yaml:"title"` + Text string `yaml:"text"` + ColorTemplate string `yaml:"colorTemplate"` } // SlackNotifier implements Notifier for sending Slack notifications. type SlackNotifier struct { - Config SlackConfig + Config SlackConfig `yaml:"slack_config"` } // Send sends a Slack notification with the given data and resolution status. diff --git a/internal/notify/notify.go b/internal/notify/notify.go index 24bfb86..eabc18c 100644 --- a/internal/notify/notify.go +++ b/internal/notify/notify.go @@ -11,13 +11,13 @@ import ( // Notification represents a single notification configuration, including the type (Slack, Email, etc.) // and its enabled status. type Notification struct { - Name string `mapstructure:"name" yaml:"-"` - Enabled *bool `mapstructure:"enabled,omitempty" yaml:"enabled,omitempty"` - Type string `mapstructure:"type,omitempty" yaml:"type,omitempty"` - SlackConfig *notifier.SlackConfig `mapstructure:"slack_config,omitempty" yaml:"slack_config,omitempty"` - MailConfig *notifier.MailConfig `mapstructure:"mail_config,omitempty" yaml:"mail_config,omitempty"` - MSTeamsConfig *notifier.MSTeamsConfig `mapstructure:"msteams_config,omitempty" yaml:"msteams_config,omitempty"` - Notifier notifier.Notifier `mapstructure:"notifier,omitempty" yaml:"notifier,omitempty"` + Name string `yaml:"-"` + Enabled *bool `yaml:"enabled,omitempty"` + Type string `yaml:"type,omitempty"` + SlackConfig *notifier.SlackConfig `yaml:"slack_config,omitempty"` + MailConfig *notifier.MailConfig `yaml:"mail_config,omitempty"` + MSTeamsConfig *notifier.MSTeamsConfig `yaml:"msteams_config,omitempty"` + Notifier notifier.Notifier `yaml:"notifier,omitempty"` } // String returns the name of the notification. diff --git a/internal/notify/services/email/email.go b/internal/notify/services/email/email.go index 7480fdd..725c507 100644 --- a/internal/notify/services/email/email.go +++ b/internal/notify/services/email/email.go @@ -10,30 +10,30 @@ import ( // SMTPConfig holds the configuration settings for an SMTP server. type SMTPConfig struct { - Host string `mapstructure:"host,omitempty" yaml:"host,omitempty"` - Port int `mapstructure:"port,omitempty" yaml:"port,omitempty"` - From string `mapstructure:"from,omitempty" yaml:"from,omitempty"` - Username string `mapstructure:"username,omitempty" yaml:"username,omitempty"` - Password string `mapstructure:"password,omitempty" yaml:"password,omitempty"` - StartTLS *bool `mapstructure:"startTLS,omitempty" yaml:"startTLS,omitempty"` - SkipInsecureVerify *bool `mapstructure:"skipInsecureVerify,omitempty" yaml:"skipInsecureVerify,omitempty"` + Host string `yaml:"host,omitempty"` + Port int `yaml:"port,omitempty"` + From string `yaml:"from,omitempty"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` + StartTLS *bool `yaml:"startTLS,omitempty"` + SkipInsecureVerify *bool `yaml:"skipInsecureVerify,omitempty"` } // Email represents the structure of an email message. type Email struct { - To []string `mapstructure:"to,omitempty" yaml:"to,omitempty"` - Cc []string `mapstructure:"cc,omitempty" yaml:"cc,omitempty"` - Bcc []string `mapstructure:"bcc,omitempty" yaml:"bcc,omitempty"` - IsHTML bool `mapstructure:"isHTML,omitempty" yaml:"isHTML,omitempty"` - Subject string `mapstructure:"subject,omitempty" yaml:"subject,omitempty"` - Body string `mapstructure:"body,omitempty" yaml:"body,omitempty"` - Attachments []Attachment `mapstructure:"attachments,omitempty" yaml:"attachments,omitempty"` + To []string `yaml:"to,omitempty"` + Cc []string `yaml:"cc,omitempty"` + Bcc []string `yaml:"bcc,omitempty"` + IsHTML bool `yaml:"isHTML,omitempty"` + Subject string `yaml:"subject,omitempty"` + Body string `yaml:"body,omitempty"` + Attachments []Attachment `yaml:"attachments,omitempty"` } // Attachment represents an email attachment. type Attachment struct { - Filename string `mapstructure:"filename"` - Data []byte `mapstructure:"-"` + Filename string + Data []byte } // MailClient handles the connection and sending of emails. diff --git a/internal/server/routes.go b/internal/server/routes.go index 24ac217..b9ce684 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -8,6 +8,7 @@ import ( "net/http" ) +// newRouter creates a new HTTP server mux, setting up routes and handlers func newRouter(logger logger.Logger, staticFS embed.FS) http.Handler { mux := http.NewServeMux() diff --git a/internal/timer/timer.go b/internal/timer/timer.go index 2251898..c16fac3 100644 --- a/internal/timer/timer.go +++ b/internal/timer/timer.go @@ -12,12 +12,29 @@ type TimerCallback func() // Timer encapsulates a time.Timer for scheduling callbacks at specific intervals. type Timer struct { - Timer *time.Timer `mapstructure:"-" yaml:"-"` - Interval *time.Duration `mapstructure:"interval" yaml:"interval,omitempty"` - Mutex sync.Mutex `mapstructure:"-" yaml:"-"` + Timer *time.Timer `yaml:"-"` + Interval *time.Duration `yaml:"interval,omitempty"` + Mutex sync.Mutex `yaml:"-"` cancel context.CancelFunc } +// UnmarshalYAML custom unmarshals a duration string into a Timer. +func (t *Timer) UnmarshalYAML(unmarshal func(interface{}) error) error { + var durationStr string + if err := unmarshal(&durationStr); err != nil { + return err + } + + duration, err := time.ParseDuration(durationStr) + if err != nil { + return err + } + + t.Interval = &duration + + return nil +} + // RunTimer runs the interval timer and executes the callback when elapsed. // The timer respects the context for cancellation. // diff --git a/main.go b/main.go index 017db17..877cf3b 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,7 @@ import ( "os" ) -const version = "0.6.5" +const version = "0.6.6" //go:embed web var templates embed.FS