aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-17 23:55:15 -0800
committerFuwn <[email protected]>2026-01-17 23:55:15 -0800
commited47c63253cd08818cbc2bff68af6c16d30490e1 (patch)
tree01e1f9d2ad69c1d9fc7679263cfa0047daf48e4a /cmd
parentfeat: Terminal aesthetic (diff)
downloadkaze-ed47c63253cd08818cbc2bff68af6c16d30490e1.tar.xz
kaze-ed47c63253cd08818cbc2bff68af6c16d30490e1.zip
feat: Hot reload configuration
Diffstat (limited to 'cmd')
-rw-r--r--cmd/kaze/main.go246
1 files changed, 246 insertions, 0 deletions
diff --git a/cmd/kaze/main.go b/cmd/kaze/main.go
new file mode 100644
index 0000000..fe58bd2
--- /dev/null
+++ b/cmd/kaze/main.go
@@ -0,0 +1,246 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log/slog"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "syscall"
+ "time"
+
+ "github.com/Fuwn/kaze/internal/config"
+ "github.com/Fuwn/kaze/internal/monitor"
+ "github.com/Fuwn/kaze/internal/server"
+ "github.com/Fuwn/kaze/internal/storage"
+ "github.com/fsnotify/fsnotify"
+)
+
+var (
+ version = "dev"
+ commit = "none"
+ date = "unknown"
+)
+
+func main() {
+ // Parse flags
+ configPath := flag.String("config", "config.yaml", "Path to configuration file")
+ showVersion := flag.Bool("version", false, "Show version information")
+ debug := flag.Bool("debug", false, "Enable debug logging")
+ flag.Parse()
+
+ if *showVersion {
+ fmt.Printf("kaze %s (commit: %s, built: %s)\n", version, commit, date)
+ os.Exit(0)
+ }
+
+ // Setup logging
+ logLevel := slog.LevelInfo
+ if *debug {
+ logLevel = slog.LevelDebug
+ }
+
+ logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
+ Level: logLevel,
+ ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
+ // Simplify time format
+ if a.Key == slog.TimeKey {
+ return slog.Attr{
+ Key: a.Key,
+ Value: slog.StringValue(time.Now().Format("15:04:05")),
+ }
+ }
+ return a
+ },
+ }))
+
+ logger.Info("starting kaze", "version", version)
+
+ // Load configuration
+ cfg, err := config.Load(*configPath)
+ if err != nil {
+ logger.Error("failed to load configuration", "error", err)
+ os.Exit(1)
+ }
+ logger.Info("loaded configuration",
+ "groups", len(cfg.Groups),
+ "incidents", len(cfg.Incidents))
+
+ // Initialize storage
+ store, err := storage.New(cfg.Storage.Path, cfg.Storage.HistoryDays)
+ if err != nil {
+ logger.Error("failed to initialize storage", "error", err)
+ os.Exit(1)
+ }
+ defer store.Close()
+ logger.Info("initialized storage", "path", cfg.Storage.Path, "history_days", cfg.Storage.HistoryDays)
+
+ // Initialize scheduler
+ sched, err := monitor.NewScheduler(cfg, store, logger)
+ if err != nil {
+ logger.Error("failed to initialize scheduler", "error", err)
+ os.Exit(1)
+ }
+
+ // Initialize HTTP server
+ srv, err := server.New(cfg, store, sched, logger)
+ if err != nil {
+ logger.Error("failed to initialize server", "error", err)
+ os.Exit(1)
+ }
+
+ // Setup graceful shutdown
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
+
+ // Setup file watcher for config hot reload
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ logger.Error("failed to create file watcher", "error", err)
+ } else {
+ defer watcher.Close()
+
+ absConfigPath, err := filepath.Abs(*configPath)
+ if err != nil {
+ logger.Warn("failed to get absolute config path", "error", err)
+ } else {
+ if err := watcher.Add(absConfigPath); err != nil {
+ logger.Warn("failed to watch config file", "error", err, "path", absConfigPath)
+ } else {
+ logger.Debug("watching config file for changes", "path", absConfigPath)
+ }
+ }
+ }
+
+ // Start scheduler
+ sched.Start()
+
+ // Start HTTP server in a goroutine
+ go func() {
+ if err := srv.Start(); err != nil {
+ logger.Error("server error", "error", err)
+ cancel()
+ }
+ }()
+
+ logger.Info("kaze is running",
+ "address", fmt.Sprintf("http://%s:%d", cfg.Server.Host, cfg.Server.Port))
+
+ // Reload function
+ reloadConfig := func() {
+ logger.Info("reloading configuration...")
+
+ newCfg, err := config.Load(*configPath)
+ if err != nil {
+ logger.Error("failed to reload configuration", "error", err)
+ return
+ }
+
+ // Validate the new configuration
+ if len(newCfg.Groups) == 0 {
+ logger.Error("invalid configuration: no monitor groups defined")
+ return
+ }
+
+ // Stop current scheduler
+ sched.Stop()
+ logger.Debug("stopped scheduler")
+
+ // Update config reference
+ cfg = newCfg
+
+ // Create new scheduler with updated config
+ newSched, err := monitor.NewScheduler(cfg, store, logger)
+ if err != nil {
+ logger.Error("failed to create new scheduler", "error", err)
+ // Restart old scheduler
+ sched.Start()
+ return
+ }
+
+ // Replace scheduler
+ sched = newSched
+ sched.Start()
+
+ // Stop old server
+ shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
+ if err := srv.Stop(shutdownCtx); err != nil {
+ logger.Error("error stopping old server", "error", err)
+ }
+ shutdownCancel()
+
+ // Create new server with updated config
+ newSrv, err := server.New(cfg, store, sched, logger)
+ if err != nil {
+ logger.Error("failed to create new server", "error", err)
+ // Try to restart old server
+ go func() {
+ if err := srv.Start(); err != nil {
+ logger.Error("server error", "error", err)
+ }
+ }()
+ return
+ }
+
+ // Replace server
+ srv = newSrv
+
+ // Start new server
+ go func() {
+ if err := srv.Start(); err != nil {
+ logger.Error("server error", "error", err)
+ cancel()
+ }
+ }()
+
+ logger.Info("configuration reloaded successfully",
+ "groups", len(cfg.Groups),
+ "incidents", len(cfg.Incidents))
+ }
+
+ // Wait for shutdown signal or config changes
+ for {
+ select {
+ case sig := <-sigCh:
+ if sig == syscall.SIGHUP {
+ logger.Info("received SIGHUP, reloading configuration")
+ reloadConfig()
+ } else {
+ logger.Info("received signal, shutting down", "signal", sig)
+ goto shutdown
+ }
+ case event := <-watcher.Events:
+ if event.Op&fsnotify.Write == fsnotify.Write {
+ logger.Info("config file modified, reloading", "file", event.Name)
+ // Debounce: wait a bit for writes to complete
+ time.Sleep(100 * time.Millisecond)
+ reloadConfig()
+ }
+ case err := <-watcher.Errors:
+ logger.Error("file watcher error", "error", err)
+ case <-ctx.Done():
+ goto shutdown
+ }
+ }
+
+shutdown:
+
+ // Graceful shutdown
+ shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer shutdownCancel()
+
+ // Stop scheduler first
+ sched.Stop()
+
+ // Stop HTTP server
+ if err := srv.Stop(shutdownCtx); err != nil {
+ logger.Error("error stopping server", "error", err)
+ }
+
+ logger.Info("kaze stopped")
+}