package config import ( "os" "time" ) type Config struct { Addr string DBPath string ArchiveAfter time.Duration // StaleAfter is how long a firing alert may go without a refreshing webhook // before the sweeper treats it as resolved. It must exceed Alertmanager's // repeat_interval (default 4h), which is what refreshes the alert. StaleAfter time.Duration // DeadmanMatchers selects the alerts that are heartbeats rather than // problems: receiving one opens no incident, and the absence of one does. // // ";" separates matchers, "," the label conditions within one, "=" is exact // equality — `alertname=Watchdog,cluster=prod; alertname=Heartbeat`. Every // matcher must name an alertname. See api.ParseDeadmanConfig. DeadmanMatchers string // DeadmanTimeout is how long a heartbeat may go unheard before its switch is // declared dead. It must be *shorter* than the Alertmanager repeat_interval // of the route carrying the heartbeat — the opposite of StaleAfter, and the // reason a dead man's switch usually wants a route of its own. Zero disables // dead man's switch handling entirely. DeadmanTimeout time.Duration // DeadmanSeverity is the severity a dead man's switch incident opens at. // These incidents have no member alerts to derive one from. DeadmanSeverity string // NtfyURL is the ntfy server push notifications are published to. Empty // disables notifications entirely. NtfyURL string // NtfyToken is an optional bearer token for an access-controlled ntfy. NtfyToken string // NtfyFallbackTopic receives incidents that open with nobody on call. NtfyFallbackTopic string // PublicURL is the base URL a phone uses to reach this server, used for the // link and the Acknowledge button inside a notification. Without it // notifications carry neither. PublicURL string // NotifyRepeat is how long an incident may sit unacknowledged before it is // notified again. Zero disables reminders. NotifyRepeat time.Duration } func Load() Config { addr := os.Getenv("TERDUT_ADDR") if addr == "" { addr = ":8080" } dbPath := os.Getenv("TERDUT_DB_PATH") if dbPath == "" { dbPath = "terdut.db" } deadmanMatchers := os.Getenv("TERDUT_DEADMAN_MATCHERS") if deadmanMatchers == "" { deadmanMatchers = "alertname=Watchdog" } deadmanSeverity := os.Getenv("TERDUT_DEADMAN_SEVERITY") if deadmanSeverity == "" { deadmanSeverity = "critical" } return Config{ Addr: addr, DBPath: dbPath, ArchiveAfter: duration("TERDUT_ARCHIVE_AFTER", 7*24*time.Hour), StaleAfter: duration("TERDUT_STALE_AFTER", 6*time.Hour), DeadmanMatchers: deadmanMatchers, DeadmanTimeout: duration("TERDUT_DEADMAN_TIMEOUT", 15*time.Minute), DeadmanSeverity: deadmanSeverity, NtfyURL: os.Getenv("TERDUT_NTFY_URL"), NtfyToken: os.Getenv("TERDUT_NTFY_TOKEN"), NtfyFallbackTopic: os.Getenv("TERDUT_NTFY_FALLBACK_TOPIC"), PublicURL: os.Getenv("TERDUT_PUBLIC_URL"), NotifyRepeat: duration("TERDUT_NOTIFY_REPEAT", 15*time.Minute), } } // duration reads a time.ParseDuration-formatted env var. An unset or // unparseable value falls back to def rather than failing startup: a typo in one // tuning knob should not take the server down. func duration(env string, def time.Duration) time.Duration { if s := os.Getenv(env); s != "" { if d, err := time.ParseDuration(s); err == nil { return d } } return def }