diff options
| author | Fuwn <[email protected]> | 2026-01-17 23:55:15 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-17 23:55:15 -0800 |
| commit | ed47c63253cd08818cbc2bff68af6c16d30490e1 (patch) | |
| tree | 01e1f9d2ad69c1d9fc7679263cfa0047daf48e4a /cmd | |
| parent | feat: Terminal aesthetic (diff) | |
| download | kaze-ed47c63253cd08818cbc2bff68af6c16d30490e1.tar.xz kaze-ed47c63253cd08818cbc2bff68af6c16d30490e1.zip | |
feat: Hot reload configuration
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/kaze/main.go | 246 |
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") +} |