aboutsummaryrefslogtreecommitdiff
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
parentfeat: Terminal aesthetic (diff)
downloadkaze-ed47c63253cd08818cbc2bff68af6c16d30490e1.tar.xz
kaze-ed47c63253cd08818cbc2bff68af6c16d30490e1.zip
feat: Hot reload configuration
-rw-r--r--.gitignore2
-rw-r--r--cmd/kaze/main.go246
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--ideas.md97
5 files changed, 347 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore
index 557cc2f..449c2fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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")
+}
diff --git a/go.mod b/go.mod
index 9e4a0d4..3507605 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index 31b30b0..4e9294b 100644
--- a/go.sum
+++ b/go.sum
@@ -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._