From 2f2549d6df7300b04a4550dc060a3d7f684a0b41 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Mon, 19 Jan 2026 04:38:08 -0800 Subject: feat: Add reset_on_next_check flag to wipe monitor history --- cmd/kaze/main.go | 4 +-- config.example.yaml | 5 +++- internal/config/config.go | 70 ++++++++++++++++++++++++++++++++++++------- internal/monitor/scheduler.go | 53 +++++++++++++++++++++++++------- internal/storage/sqlite.go | 21 +++++++++++++ 5 files changed, 128 insertions(+), 25 deletions(-) diff --git a/cmd/kaze/main.go b/cmd/kaze/main.go index fe58bd2..a71c03e 100644 --- a/cmd/kaze/main.go +++ b/cmd/kaze/main.go @@ -78,7 +78,7 @@ func main() { logger.Info("initialized storage", "path", cfg.Storage.Path, "history_days", cfg.Storage.HistoryDays) // Initialize scheduler - sched, err := monitor.NewScheduler(cfg, store, logger) + sched, err := monitor.NewScheduler(cfg, store, logger, *configPath) if err != nil { logger.Error("failed to initialize scheduler", "error", err) os.Exit(1) @@ -155,7 +155,7 @@ func main() { cfg = newCfg // Create new scheduler with updated config - newSched, err := monitor.NewScheduler(cfg, store, logger) + newSched, err := monitor.NewScheduler(cfg, store, logger, *configPath) if err != nil { logger.Error("failed to create new scheduler", "error", err) // Restart old scheduler diff --git a/config.example.yaml b/config.example.yaml index dd15ac1..c3876a7 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -62,7 +62,8 @@ groups: target: "https://example.com" interval: 30s timeout: 10s - retries: 2 # Retry 2 times before marking as down (default: 0) + retries: 2 # Retry 2 times before marking as down (default: 0) + # reset_on_next_check: true # Wipe all historical data on next check and flip to false expected_status: 200 verify_ssl: true @@ -138,6 +139,8 @@ incidents: # timeout: duration - Request timeout (default: 10s) # retries: int - Number of retry attempts before marking as down (default: 0) # Retries are attempted with a 500ms delay between attempts +# reset_on_next_check: bool - When true, wipes all historical data for this monitor on next check +# Automatically flips to false after reset completes # # HTTP/HTTPS specific fields: # expected_status: int - Expected HTTP status code (default: 200) diff --git a/internal/config/config.go b/internal/config/config.go index 1c033e1..8839906 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -63,17 +63,18 @@ type GroupConfig struct { // MonitorConfig represents a single monitor type MonitorConfig struct { - Name string `yaml:"name"` - Type string `yaml:"type"` // http, https, tcp - Target string `yaml:"target"` - Interval Duration `yaml:"interval"` - Timeout Duration `yaml:"timeout"` - Retries int `yaml:"retries,omitempty"` // Number of retry attempts before marking as down - ExpectedStatus int `yaml:"expected_status,omitempty"` - VerifySSL *bool `yaml:"verify_ssl,omitempty"` - Method string `yaml:"method,omitempty"` - Headers map[string]string `yaml:"headers,omitempty"` - Body string `yaml:"body,omitempty"` + Name string `yaml:"name"` + Type string `yaml:"type"` // http, https, tcp, gemini + Target string `yaml:"target"` + Interval Duration `yaml:"interval"` + Timeout Duration `yaml:"timeout"` + Retries int `yaml:"retries,omitempty"` // Number of retry attempts before marking as down + ResetOnNextCheck bool `yaml:"reset_on_next_check,omitempty"` // Wipe monitor data on next check and flip to false + ExpectedStatus int `yaml:"expected_status,omitempty"` + VerifySSL *bool `yaml:"verify_ssl,omitempty"` + Method string `yaml:"method,omitempty"` + Headers map[string]string `yaml:"headers,omitempty"` + Body string `yaml:"body,omitempty"` } // IncidentConfig represents an incident or maintenance @@ -308,3 +309,50 @@ type MonitorWithGroup struct { GroupName string Monitor MonitorConfig } + +// UpdateResetFlag updates the reset_on_next_check flag for a specific monitor in the config file +func UpdateResetFlag(configPath string, monitorName string, value bool) error { + // Read the config file + data, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + // Parse as YAML + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return fmt.Errorf("failed to parse config file: %w", err) + } + + // Find and update the monitor + found := false + for i := range cfg.Groups { + for j := range cfg.Groups[i].Monitors { + if cfg.Groups[i].Monitors[j].Name == monitorName { + cfg.Groups[i].Monitors[j].ResetOnNextCheck = value + found = true + break + } + } + if found { + break + } + } + + if !found { + return fmt.Errorf("monitor %q not found in config", monitorName) + } + + // Marshal back to YAML + newData, err := yaml.Marshal(&cfg) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + // Write back to file + if err := os.WriteFile(configPath, newData, 0644); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + return nil +} diff --git a/internal/monitor/scheduler.go b/internal/monitor/scheduler.go index 5a7e817..1478732 100644 --- a/internal/monitor/scheduler.go +++ b/internal/monitor/scheduler.go @@ -12,23 +12,27 @@ import ( // Scheduler manages and runs all monitors type Scheduler struct { - monitors []Monitor - storage *storage.Storage - logger *slog.Logger - wg sync.WaitGroup - ctx context.Context - cancel context.CancelFunc + monitors []Monitor + monitorCfg map[string]config.MonitorConfig // Monitor configs by name for reset flag checks + configPath string + storage *storage.Storage + logger *slog.Logger + wg sync.WaitGroup + ctx context.Context + cancel context.CancelFunc } // NewScheduler creates a new monitor scheduler -func NewScheduler(cfg *config.Config, store *storage.Storage, logger *slog.Logger) (*Scheduler, error) { +func NewScheduler(cfg *config.Config, store *storage.Storage, logger *slog.Logger, configPath string) (*Scheduler, error) { ctx, cancel := context.WithCancel(context.Background()) s := &Scheduler{ - storage: store, - logger: logger, - ctx: ctx, - cancel: cancel, + monitorCfg: make(map[string]config.MonitorConfig), + configPath: configPath, + storage: store, + logger: logger, + ctx: ctx, + cancel: cancel, } // Create monitors from configuration @@ -40,6 +44,7 @@ func NewScheduler(cfg *config.Config, store *storage.Storage, logger *slog.Logge return nil, err } s.monitors = append(s.monitors, mon) + s.monitorCfg[monCfg.Name] = monCfg // Store config for reset flag checks logger.Info("registered monitor", "name", mon.Name(), "type", mon.Type(), @@ -96,6 +101,32 @@ func (s *Scheduler) runMonitor(mon Monitor) { // executeCheck performs a single check and saves the result func (s *Scheduler) executeCheck(mon Monitor) { + // Check if reset flag is set for this monitor + if monCfg, exists := s.monitorCfg[mon.Name()]; exists && monCfg.ResetOnNextCheck { + s.logger.Info("resetting monitor data", "name", mon.Name()) + + // Delete all historical data for this monitor + if err := s.storage.ResetMonitorData(s.ctx, mon.Name()); err != nil { + s.logger.Error("failed to reset monitor data", + "name", mon.Name(), + "error", err) + } else { + s.logger.Info("monitor data reset complete", "name", mon.Name()) + + // Flip the reset flag to false in the config file + if err := config.UpdateResetFlag(s.configPath, mon.Name(), false); err != nil { + s.logger.Error("failed to update reset flag in config", + "name", mon.Name(), + "error", err) + } else { + // Update in-memory config + monCfg.ResetOnNextCheck = false + s.monitorCfg[mon.Name()] = monCfg + s.logger.Info("reset flag cleared in config", "name", mon.Name()) + } + } + } + // Create a context with timeout for this check checkCtx, cancel := context.WithTimeout(s.ctx, mon.Interval()) defer cancel() diff --git a/internal/storage/sqlite.go b/internal/storage/sqlite.go index e08e4ee..48cf432 100644 --- a/internal/storage/sqlite.go +++ b/internal/storage/sqlite.go @@ -673,6 +673,27 @@ func (s *Storage) Cleanup(ctx context.Context) error { return nil } +// ResetMonitorData deletes all historical data for a specific monitor +func (s *Storage) ResetMonitorData(ctx context.Context, monitorName string) error { + // Delete from check_results + _, err := s.db.ExecContext(ctx, ` + DELETE FROM check_results WHERE monitor_name = ? + `, monitorName) + if err != nil { + return fmt.Errorf("failed to delete check_results for monitor %q: %w", monitorName, err) + } + + // Delete from daily_stats + _, err = s.db.ExecContext(ctx, ` + DELETE FROM daily_stats WHERE monitor_name = ? + `, monitorName) + if err != nil { + return fmt.Errorf("failed to delete daily_stats for monitor %q: %w", monitorName, err) + } + + return nil +} + // Close closes the database connection func (s *Storage) Close() error { return s.db.Close() -- cgit v1.2.3