mirror of
https://github.com/aziis98/statuspage.git
synced 2026-10-06 22:55:21 +00:00
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
198 lines
4.2 KiB
Go
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
|
|
}
|