Import source: internal/config/config
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,166 @@
|
|||||||
|
// 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user