// Package config loads and validates the targets.yaml file. package config import ( "fmt" "os" "time" "gopkg.in/yaml.v3" ) // Duration wraps time.Duration so YAML values like "30s" parse naturally. type Duration struct{ time.Duration } func (d *Duration) UnmarshalYAML(value *yaml.Node) error { var s string if err := value.Decode(&s); err != nil { return err } dur, err := time.ParseDuration(s) if err != nil { return fmt.Errorf("invalid duration %q: %w", s, err) } d.Duration = dur return nil } // CheckType enumerates the supported probe kinds. type CheckType string const ( CheckHTTP CheckType = "http" CheckTCP CheckType = "tcp" CheckTLS CheckType = "tls" CheckICMP CheckType = "icmp" CheckDocker CheckType = "docker" ) // Target is one monitored endpoint. type Target struct { ID string `yaml:"id"` Name string `yaml:"name"` Type CheckType `yaml:"type"` // Type-specific endpoints. URL string `yaml:"url"` // http Address string `yaml:"address"` // tcp, tls (host:port) Host string `yaml:"host"` // icmp Container string `yaml:"container"` // docker Socket string `yaml:"socket"` // docker (default /var/run/docker.sock) Interval Duration `yaml:"interval"` Timeout Duration `yaml:"timeout"` // Flap detection: N consecutive failures before "down", // M consecutive successes before "up" again. FailThreshold int `yaml:"fail_threshold"` RecoverThreshold int `yaml:"recover_threshold"` ExpectStatus int `yaml:"expect_status"` // http: exact status (0 = any < 400) WarnDays int `yaml:"warn_days"` // tls: degraded when cert expires sooner DegradedLatencyMS float64 `yaml:"degraded_latency_ms"` // any: degraded above this latency } // Settings holds server-level options. type Settings struct { Listen string `yaml:"listen"` DB string `yaml:"db"` HistoryDays int `yaml:"history_days"` } // Config is the full parsed targets.yaml. type Config struct { Settings Settings `yaml:"settings"` Targets []Target `yaml:"targets"` } // Load reads, parses, validates and applies defaults to a config file. func Load(path string) (*Config, error) { raw, err := os.ReadFile(path) if err != nil { return nil, err } var cfg Config if err := yaml.Unmarshal(raw, &cfg); err != nil { return nil, fmt.Errorf("parse %s: %w", path, err) } if err := cfg.validate(); err != nil { return nil, err } cfg.applyDefaults() return &cfg, nil } func (c *Config) validate() error { if len(c.Targets) == 0 { return fmt.Errorf("no targets defined") } seen := map[string]bool{} for i, t := range c.Targets { if t.ID == "" { return fmt.Errorf("target %d: missing id", i) } if seen[t.ID] { return fmt.Errorf("duplicate target id %q", t.ID) } seen[t.ID] = true switch t.Type { case CheckHTTP: if t.URL == "" { return fmt.Errorf("target %q: http check requires url", t.ID) } case CheckTCP, CheckTLS: if t.Address == "" { return fmt.Errorf("target %q: %s check requires address (host:port)", t.ID, t.Type) } case CheckICMP: if t.Host == "" { return fmt.Errorf("target %q: icmp check requires host", t.ID) } case CheckDocker: if t.Container == "" { return fmt.Errorf("target %q: docker check requires container", t.ID) } default: return fmt.Errorf("target %q: unknown check type %q", t.ID, t.Type) } } return nil } func (c *Config) applyDefaults() { if c.Settings.Listen == "" { c.Settings.Listen = ":8080" } if c.Settings.DB == "" { c.Settings.DB = "dash.db" } if c.Settings.HistoryDays <= 0 { c.Settings.HistoryDays = 30 } for i := range c.Targets { t := &c.Targets[i] if t.Name == "" { t.Name = t.ID } if t.Interval.Duration <= 0 { t.Interval.Duration = 30 * time.Second } if t.Timeout.Duration <= 0 { t.Timeout.Duration = 5 * time.Second } if t.FailThreshold <= 0 { t.FailThreshold = 3 } if t.RecoverThreshold <= 0 { t.RecoverThreshold = 2 } if t.Type == CheckTLS && t.WarnDays <= 0 { t.WarnDays = 14 } if t.Type == CheckDocker && t.Socket == "" { t.Socket = "/var/run/docker.sock" } } }