Files
statuspage/server/config.go
T
aziis98 95eab7f584 statuspage: monitor lab/home machines with a small status page
Go backend probes machines over ICMP/TCP, runs a configurable script over
SSH for checks and numeric metrics, and keeps history in SQLite. Preact +
Vite frontend shows machine groups, per-machine metric plots, and
cross-machine metric aggregates.

- server/: status, history, metrics, aggregate, refresh APIs
- src/: Preact + Vite frontend
- Single YAML config; documented example in example.config.yaml
- SQLite data stored under data.volume/ (bind-mounted in Docker)
- Multi-stage alpine Dockerfile with BuildKit cache mounts
- LICENSE: AGPL-3.0
2026-08-01 02:27:47 +02:00

198 lines
4.2 KiB
Go

package main
import (
"fmt"
"os"
"time"
"gopkg.in/yaml.v3"
)
type Duration struct {
time.Duration
}
func (d *Duration) UnmarshalYAML(node *yaml.Node) error {
var s string
if err := node.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
}
type PingConfig struct {
Interval Duration `yaml:"interval"`
Timeout Duration `yaml:"timeout"`
TCPPort int `yaml:"tcp_port"`
}
type SSHConfig struct {
Interval Duration `yaml:"interval"`
User string `yaml:"user"`
Port int `yaml:"port"`
Key string `yaml:"key"`
Script string `yaml:"script"`
}
type MachineConfig struct {
Name string `yaml:"name"`
Host string `yaml:"host"`
Ping *PingConfig `yaml:"ping"`
SSH *SSHConfig `yaml:"ssh"`
}
type GroupConfig struct {
Name string `yaml:"name"`
Machines []MachineConfig `yaml:"machines"`
}
type MetricRange struct {
Min *float64 `yaml:"min"`
Max *float64 `yaml:"max"`
}
type RawConfig struct {
Title string `yaml:"title"`
Interactive bool `yaml:"interactive"`
SharedMetricWindow bool `yaml:"shared_metric_window"`
Ping *PingConfig `yaml:"ping"`
SSH *SSHConfig `yaml:"ssh"`
Metrics map[string]MetricRange `yaml:"metrics"`
Groups []GroupConfig `yaml:"groups"`
Machines []MachineConfig `yaml:"machines"`
}
type Machine struct {
ID string
Name string
Host string
Group string
Ping PingConfig
SSH SSHConfig
}
type Config struct {
Title string
Interactive bool
SharedMetricWindow bool
Ping PingConfig
SSH SSHConfig
Metrics map[string]MetricRange
Groups []GroupConfig
Machines []Machine
}
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var raw RawConfig
if err := yaml.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("parsing %s: %w", path, err)
}
cfg := &Config{
Title: raw.Title,
Interactive: raw.Interactive,
SharedMetricWindow: raw.SharedMetricWindow,
Ping: PingConfig{
Interval: Duration{time.Second * 10},
Timeout: Duration{time.Second * 2},
TCPPort: 22,
},
SSH: SSHConfig{
Interval: Duration{time.Minute * 10},
User: "root",
Port: 22,
},
}
if raw.Ping != nil {
if raw.Ping.Interval.Duration > 0 {
cfg.Ping.Interval = raw.Ping.Interval
}
if raw.Ping.Timeout.Duration > 0 {
cfg.Ping.Timeout = raw.Ping.Timeout
}
if raw.Ping.TCPPort != 0 {
cfg.Ping.TCPPort = raw.Ping.TCPPort
}
}
if raw.SSH != nil {
if raw.SSH.Interval.Duration > 0 {
cfg.SSH.Interval = raw.SSH.Interval
}
if raw.SSH.User != "" {
cfg.SSH.User = raw.SSH.User
}
if raw.SSH.Port != 0 {
cfg.SSH.Port = raw.SSH.Port
}
cfg.SSH.Key = raw.SSH.Key
cfg.SSH.Script = raw.SSH.Script
}
if cfg.Title == "" {
cfg.Title = "Status Page"
}
cfg.Groups = raw.Groups
cfg.Metrics = raw.Metrics
for _, g := range raw.Groups {
for _, mc := range g.Machines {
cfg.Machines = append(cfg.Machines, resolveMachine(mc, g.Name, cfg))
}
}
for _, mc := range raw.Machines {
cfg.Machines = append(cfg.Machines, resolveMachine(mc, "", cfg))
}
return cfg, nil
}
func resolveMachine(mc MachineConfig, group string, cfg *Config) Machine {
m := Machine{
ID: mc.Host,
Name: mc.Host,
Host: mc.Host,
Group: group,
Ping: cfg.Ping,
SSH: cfg.SSH,
}
if mc.Name != "" {
m.Name = mc.Name
}
if mc.Ping != nil {
if mc.Ping.Interval.Duration > 0 {
m.Ping.Interval = mc.Ping.Interval
}
if mc.Ping.Timeout.Duration > 0 {
m.Ping.Timeout = mc.Ping.Timeout
}
if mc.Ping.TCPPort != 0 {
m.Ping.TCPPort = mc.Ping.TCPPort
}
}
if mc.SSH != nil {
if mc.SSH.Interval.Duration > 0 {
m.SSH.Interval = mc.SSH.Interval
}
if mc.SSH.User != "" {
m.SSH.User = mc.SSH.User
}
if mc.SSH.Port != 0 {
m.SSH.Port = mc.SSH.Port
}
if mc.SSH.Key != "" {
m.SSH.Key = mc.SSH.Key
}
if mc.SSH.Script != "" {
m.SSH.Script = mc.SSH.Script
}
}
return m
}