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 | |
| parent | feat: Terminal aesthetic (diff) | |
| download | kaze-ed47c63253cd08818cbc2bff68af6c16d30490e1.tar.xz kaze-ed47c63253cd08818cbc2bff68af6c16d30490e1.zip | |
feat: Hot reload configuration
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | cmd/kaze/main.go | 246 | ||||
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | ideas.md | 97 |
5 files changed, 347 insertions, 1 deletions
@@ -1,5 +1,5 @@ # Binaries -kaze +/kaze *.exe *.exe~ *.dll 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") +} @@ -9,6 +9,7 @@ require ( require ( github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect @@ -1,5 +1,7 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/ideas.md b/ideas.md new file mode 100644 index 0000000..d2f3dcf --- /dev/null +++ b/ideas.md @@ -0,0 +1,97 @@ +# Kaze - Future Ideas & Enhancements + +## 1. Style & Theme Options + +### Theme System +- Keep the terminal theme as the new default +- Add it as an optional theme alongside the OpenCode style +- Make further refinements to the terminal theme +- Theme switcher between "terminal" and "opencode" styles via config +- Configurable color palette (allow users to use colored statuses if desired) + +### Terminal Theme Enhancements +- **ASCII art/box drawing characters**: Use `┌─┐│└┘` for group borders instead of CSS borders +- **Terminal-style status indicators**: Replace colored dots with `[✓]` `[!]` `[✗]` text symbols +- **Command-line feel**: Add a terminal prompt aesthetic to the header (e.g., `user@kaze:~$`) +- **Blinking cursor**: Add a subtle cursor effect somewhere for extra terminal authenticity +- **Monospace badge styling**: Make SSL days, status badges look more terminal-like + +## 2. UI/UX Enhancements + +### Visual Improvements +- ASCII box drawing for cleaner section separation +- Terminal symbols for better status visualization +- More authentic terminal experience with shell-like elements +- Responsive design improvements for mobile/tablet + +### Interactive Features +- Keyboard shortcuts for navigation +- Search/filter monitors +- Bulk actions on monitors +- Quick status overview in browser tab title + +## 3. Functional Features + +### Monitoring +- API endpoints for programmatic access +- Export/download status reports +- Custom alert thresholds per monitor +- Historical data visualization +- Response time graphs/charts +- Uptime trends and analytics + +### Notifications & Alerts +- Webhook support for status changes +- Email notifications +- Slack/Discord integration +- Custom alert rules per monitor +- Alert escalation policies + +### Monitor Types +- ICMP ping monitoring +- DNS resolution checks +- Database connection monitoring +- Custom script/command execution +- WebSocket monitoring +- GraphQL endpoint monitoring + +## 4. Configuration & Management + +### Config Enhancements +- Web UI for configuration management +- Monitor templates for common services +- Group-level configuration inheritance +- Environment variable substitution in config + +### Data & Storage +- Multiple database backend support (PostgreSQL, MySQL) +- Data retention policies +- Automated backups +- Data export in multiple formats (CSV, JSON) +- Metrics aggregation and rollup + +### Security & Access +- API authentication/authorization +- Read-only mode for public status pages +- Custom branding options +- Password-protected status pages +- IP whitelist/blacklist + +### Advanced Features +- Multi-region monitoring +- Distributed monitoring agents +- Maintenance windows +- SLA tracking and reporting +- Custom status page domains +- Embed widgets for external sites + +## Implementation Priority + +Priority levels to consider: +- **High**: Core functionality improvements, critical UX issues +- **Medium**: Nice-to-have features that enhance usability +- **Low**: Experimental or niche features + +--- + +_Note: This document captures ideas for future development. Not all ideas may be implemented, and priorities may change based on user feedback and project goals._ |