Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor/globals #61

Merged
merged 9 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ If a "ping" does not arrive in the given interval & grace period, Heartbeats wil
## Flags

```yaml
-c, --config string Path to the configuration file (default "./deploy/config.yaml")
-l, --listen-address string Address to listen on (default "localhost:8080")
-c, --config string Path to the configuration file (default "./deploy/config.yaml")
-l, --listen-address string Address to listen on (default "localhost:8080")
-s, --site-root string Site root for the heartbeat service (default "http://<listenAddress>")
-m, --max-size int Maximum size of the cache (default 100)
-r, --reduce int Amount to reduce when max size is exceeded (default 10)
-v, --verbose Enable verbose logging
--version Show version and exit
-h, --help Show help and exit
-m, --max-size int Maximum size of the cache (default 1000)
-r, --reduce int Percentage to reduce when max size is exceeded (default 25)
-v, --verbose Enable verbose logging
--version Show version and exit
-h, --help Show help and exit
```

## Environment Variables
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.22.0

require (
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/alecthomas/kingpin/v2 v2.4.0
github.com/prometheus/client_golang v1.19.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/afero v1.11.0
Expand All @@ -15,6 +16,7 @@ require (
require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
Expand All @@ -28,6 +30,7 @@ require (
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/xhit/go-str2duration/v2 v2.1.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
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=
Expand Down Expand Up @@ -49,9 +53,12 @@ github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNo
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
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/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand All @@ -64,6 +71,7 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh
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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
95 changes: 74 additions & 21 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,101 @@ import (
"embed"
"fmt"
"heartbeats/pkg/config"
"heartbeats/pkg/flags"
"heartbeats/pkg/heartbeat"
"heartbeats/pkg/history"
"heartbeats/pkg/logger"
"heartbeats/pkg/notify"
"heartbeats/pkg/server"
"os"

"github.com/alecthomas/kingpin/v2"
)

const version = "0.6.7"
const version = "0.6.10"

//go:embed web
var templates embed.FS

func run(ctx context.Context, verbose bool) error {
config.App.Version = version
var (
configPath = kingpin.Flag("config", "Path to the configuration file").
Short('c').
Envar("HEARTBEATS_CONFIG").
Default("./deploy/config.yaml").
String()
listenAddress = kingpin.Flag("listen-address", "Address to listen on").
Short('l').
Envar("HEARTBEATS_LISTEN_ADDRESS").
Default("localhost:8080").
String()
siteRoot = kingpin.
Flag("site-root", "Site root for the heartbeat service").
Short('s').
Envar("HEARTBEATS_SITE_ROOT").
Default("http://<listenaddress>").
String()
maxSize = kingpin.Flag("max-size", "Maximum size of the cache").
Short('m').
Envar("HEARTBEATS_MAX_SIZE").
Default("1000").
Int()
reduce = kingpin.Flag("reduce", "Percentage to reduce when max size is exceeded").
Short('r').
Envar("HEARTBEATS_REDUCE").
Default("25").
Int()
verbose = kingpin.Flag("verbose", "Enable verbose logging").
Short('v').
Envar("HEARTBEATS_VERBOSE").
Bool()
)

result := flags.ParseFlags(os.Args, os.Stdout)
// run initializes and runs the server with the provided context and verbosity settings.
func run(ctx context.Context, verbose bool) error {
kingpin.CommandLine.Name = "heartbeats"
kingpin.UsageTemplate(kingpin.CompactUsageTemplate)
kingpin.Parse()

if result.Err != nil {
fmt.Fprintf(os.Stderr, "error parsing flags: %v\n", result.Err)
os.Exit(1)
}
log := logger.NewLogger(verbose)

if result.ShowHelp {
os.Exit(0)
}
heartbeatsStore := heartbeat.NewStore()
notificationStore := notify.NewStore()
historyStore := history.NewStore()

if result.ShowVersion {
fmt.Println(version)
os.Exit(0)
if err := config.Read(
*configPath,
history.Config{
MaxSize: *maxSize,
Reduce: *reduce,
},
heartbeatsStore,
notificationStore,
historyStore,
); err != nil {
return fmt.Errorf("error reading config file. %v", err)
}

log := logger.NewLogger(verbose)

if err := config.App.Read(); err != nil {
return fmt.Errorf("Error reading config file: %v", err)
if err := config.Validate(heartbeatsStore, notificationStore); err != nil {
return fmt.Errorf("Error validating config file. %v", err)
}

return server.Run(ctx, config.App.Server.ListenAddress, templates, log)
return server.Run(
ctx,
log,
version,
server.Config{
ListenAddress: *listenAddress,
SiteRoot: *siteRoot,
},
templates,
heartbeatsStore,
notificationStore,
historyStore,
)
}

func main() {
ctx := context.Background()
if err := run(ctx, config.App.Verbose); err != nil {
if err := run(ctx, *verbose); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
Expand Down
111 changes: 60 additions & 51 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,9 @@ import (
"gopkg.in/yaml.v3"
)

// App is the global configuration instance.
var App = &Config{
HeartbeatStore: heartbeat.NewStore(),
NotificationStore: notify.NewStore(),
}

// HistoryStore is the global HistoryStore.
var HistoryStore = history.NewStore()

// Cache configuration structure.
type Cache struct {
MaxSize int `yaml:"maxSize"` // Maximum size of the cache
Reduce int `yaml:"reduce"` // Amount to reduce when max size is exceeded
}

// Server configuration structure.
type Server struct {
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 `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 from the file specified in the Config struct.
func (c *Config) Read() error {
content, err := os.ReadFile(c.Path)
// Read reads the configuration from a specified file and processes it.
func Read(path string, historyConfig history.Config, heartbeatsStore *heartbeat.Store, notificationStore *notify.Store, historyStore *history.Store) error {
content, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read config file. %w", err)
}
Expand All @@ -54,19 +22,32 @@ func (c *Config) Read() error {
return fmt.Errorf("failed to unmarshal raw config. %w", err)
}

if err := c.processNotifications(rawConfig["notifications"]); err != nil {
if err := processNotifications(rawConfig["notifications"], notificationStore); err != nil {
return err
}

if err := processHeartbeats(rawConfig["heartbeats"], heartbeatsStore, historyStore, historyConfig.MaxSize, historyConfig.Reduce); err != nil {
return err
}

return nil
}

// Validate validates the configuration file.
func Validate(heartbeatStore *heartbeat.Store, notificationStore *notify.Store) error {
if err := validateNotifications(notificationStore); err != nil {
return err
}

if err := c.processHeartbeats(rawConfig["heartbeats"]); err != nil {
if err := validateHeartbeats(heartbeatStore, notificationStore); err != nil {
return err
}

return nil
}

// processNotifications handles the unmarshaling and processing of notification configurations.
func (c *Config) processNotifications(rawNotifications interface{}) error {
// processNotifications processes the raw notification configurations and updates the notification store.
func processNotifications(rawNotifications interface{}, notificationStore *notify.Store) error {
notifications, ok := rawNotifications.(map[string]interface{})
if !ok {
return fmt.Errorf("failed to unmarshal notifications")
Expand All @@ -83,36 +64,35 @@ func (c *Config) processNotifications(rawNotifications interface{}) error {
return fmt.Errorf("failed to unmarshal notification '%s'. %w", name, err)
}

if err := c.NotificationStore.Add(name, &notification); err != nil {
if err := notificationStore.Add(name, &notification); err != nil {
return fmt.Errorf("failed to add notification '%s'. %w", name, err)
}

if err := c.updateSlackNotification(name, &notification); err != nil {
if err := updateSlackNotification(name, &notification, notificationStore); err != nil {
return err
}
}

return nil
}

// updateSlackNotification updates the Slack notification with a default color template if not set.
func (c *Config) updateSlackNotification(name string, notification *notify.Notification) error {
// updateSlackNotification sets a default color template for Slack notifications if not provided and updates the notification store.
func updateSlackNotification(name string, notification *notify.Notification, notificationStore *notify.Store) 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 {
if err := notificationStore.Update(name, notification); err != nil {
return fmt.Errorf("failed to update notification '%s'. %w", notification.Name, err)
}
}
return nil
}

// processHeartbeats handles the unmarshaling and processing of heartbeat configurations.
func (c *Config) processHeartbeats(rawHeartbeats interface{}) error {
// processHeartbeats processes and adds heartbeats to the store, creating their respective histories.
func processHeartbeats(rawHeartbeats interface{}, heartbeatStore *heartbeat.Store, historyStore *history.Store, maxSize, reduce int) 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 {
Expand All @@ -124,19 +104,48 @@ func (c *Config) processHeartbeats(rawHeartbeats interface{}) error {
return fmt.Errorf("failed to unmarshal heartbeat '%s'. %w", name, err)
}

if err := c.HeartbeatStore.Add(name, &hb); err != nil {
if err := heartbeatStore.Add(name, &hb); err != nil {
return fmt.Errorf("failed to add heartbeat '%s'. %w", hb.Name, err)
}

historyInstance, err := history.NewHistory(c.Cache.MaxSize, c.Cache.Reduce)
historyInstance, err := history.NewHistory(maxSize, reduce)
if err != nil {
return fmt.Errorf("failed to add heartbeat '%s'. %w", hb.Name, err)
return fmt.Errorf("failed to create history for heartbeat '%s'. %w", name, err)
}

if err := HistoryStore.Add(name, historyInstance); err != nil {
if err := historyStore.Add(name, historyInstance); err != nil {
return fmt.Errorf("failed to create heartbeat history for '%s'. %w", name, err)
}
}

return nil
}

// validateNotifications validates the notification configurations.
func validateNotifications(notificationStore *notify.Store) error {
var hbDummy heartbeat.Heartbeat
for _, notification := range notificationStore.GetAll() {
if notification.Enabled != nil && !*notification.Enabled {
continue
}

if err := notification.ValidateTemplate(&hbDummy); err != nil {
return fmt.Errorf("cannot validate templates. %s", err)
}
}

return nil
}

// validateHeartbeats validates the heartbeat configurations.
func validateHeartbeats(heartbeatStore *heartbeat.Store, notificationStore *notify.Store) error {
for name, heartbeat := range heartbeatStore.GetAll() {
for _, notification := range heartbeat.Notifications {
if exists := notificationStore.Get(notification); exists == nil {
return fmt.Errorf("notification '%s' not found for heartbeat '%s'.", notification, name)
}
}
}

return nil
}
Loading