init work of uptime

This commit is contained in:
Blake Ridgway
2026-03-22 11:30:31 -05:00
parent 854cba4c24
commit f0db70c840
18 changed files with 2252 additions and 0 deletions

183
cmd/arcline-uptime/main.go Normal file
View File

@@ -0,0 +1,183 @@
package main
import (
"context"
"flag"
"fmt"
"log/slog"
"os"
"os/signal"
"strings"
"syscall"
"time"
"arclineit/arcline-uptime/internal/alert"
"arclineit/arcline-uptime/internal/config"
"arclineit/arcline-uptime/internal/monitor"
"arclineit/arcline-uptime/internal/scheduler"
"arclineit/arcline-uptime/internal/store"
"arclineit/arcline-uptime/internal/web"
)
var version = "dev"
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
switch os.Args[1] {
case "start":
cmdStart(os.Args[2:])
case "check":
cmdCheck(os.Args[2:])
case "list":
cmdList(os.Args[2:])
case "version":
fmt.Println(version)
default:
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", os.Args[1])
printUsage()
os.Exit(1)
}
}
func printUsage() {
fmt.Fprintf(os.Stderr, "arcline-uptime %s\n\nUsage:\n", version)
fmt.Fprintf(os.Stderr, " arcline-uptime start [--config FILE] [--db FILE]\n")
fmt.Fprintf(os.Stderr, " arcline-uptime check [--config FILE] [--monitor NAME]\n")
fmt.Fprintf(os.Stderr, " arcline-uptime list [--config FILE]\n")
fmt.Fprintf(os.Stderr, " arcline-uptime version\n")
}
func cmdStart(args []string) {
fs := flag.NewFlagSet("start", flag.ExitOnError)
configPath := fs.String("config", "uptime.yaml", "path to config file")
dbPath := fs.String("db", "uptime.db", "path to SQLite database")
fs.Parse(args)
cfg, err := config.Load(*configPath)
if err != nil {
fmt.Fprintf(os.Stderr, "config: %v\n", err)
os.Exit(1)
}
// Configure structured logging as early as possible.
setupLogging(cfg.Global.LogFormat)
db, err := store.Open(*dbPath)
if err != nil {
slog.Error("open database", "error", err)
os.Exit(1)
}
defer db.Close()
checkers, err := monitor.BuildCheckers(cfg.Monitors, time.Duration(cfg.Global.Timeout)*time.Second)
if err != nil {
slog.Error("build monitors", "error", err)
os.Exit(1)
}
alerters, err := alert.BuildNamedAlerters(cfg.Alerts)
if err != nil {
slog.Error("build alerters", "error", err)
os.Exit(1)
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if cfg.Dashboard.Enabled {
srv := web.NewServer(cfg, db)
go func() {
slog.Info("dashboard listening", "addr", cfg.Dashboard.Listen)
if err := srv.ListenAndServe(); err != nil {
slog.Error("dashboard server", "error", err)
}
}()
}
sched := scheduler.New(checkers, alerters, cfg.Monitors, db, cfg.Global)
slog.Info("starting",
"monitors", len(checkers),
"interval", cfg.Global.CheckInterval,
"retention_days", cfg.Global.RetentionDays,
)
sched.Start(ctx)
<-ctx.Done()
slog.Info("shutting down")
sched.Wait()
}
func cmdCheck(args []string) {
fs := flag.NewFlagSet("check", flag.ExitOnError)
configPath := fs.String("config", "uptime.yaml", "path to config file")
monitorName := fs.String("monitor", "", "monitor name (omit to check all)")
fs.Parse(args)
cfg, err := config.Load(*configPath)
if err != nil {
fmt.Fprintf(os.Stderr, "config: %v\n", err)
os.Exit(1)
}
checkers, err := monitor.BuildCheckers(cfg.Monitors, time.Duration(cfg.Global.Timeout)*time.Second)
if err != nil {
fmt.Fprintf(os.Stderr, "monitors: %v\n", err)
os.Exit(1)
}
exitCode := 0
for _, c := range checkers {
if *monitorName != "" && c.Name() != *monitorName {
continue
}
r := c.Check()
if r.Up {
fmt.Printf("[OK] %-30s %dms\n", r.MonitorName, r.ResponseTime.Milliseconds())
} else {
fmt.Printf("[DOWN] %-30s %s\n", r.MonitorName, r.Error)
exitCode = 1
}
}
os.Exit(exitCode)
}
func cmdList(args []string) {
fs := flag.NewFlagSet("list", flag.ExitOnError)
configPath := fs.String("config", "uptime.yaml", "path to config file")
fs.Parse(args)
cfg, err := config.Load(*configPath)
if err != nil {
fmt.Fprintf(os.Stderr, "config: %v\n", err)
os.Exit(1)
}
fmt.Printf("%-30s %-6s %-10s %-40s\n", "NAME", "TYPE", "INTERVAL", "TARGET")
fmt.Printf("%s %s %s %s\n",
strings.Repeat("-", 30), strings.Repeat("-", 6),
strings.Repeat("-", 10), strings.Repeat("-", 40))
for _, m := range cfg.Monitors {
target := m.URL
if target == "" {
target = fmt.Sprintf("%s:%d", m.Host, m.Port)
}
interval := fmt.Sprintf("%ds", m.Interval)
fmt.Printf("%-30s %-6s %-10s %-40s\n", m.Name, m.Type, interval, target)
}
}
func setupLogging(format string) {
var h slog.Handler
opts := &slog.HandlerOptions{Level: slog.LevelInfo}
if format == "json" {
h = slog.NewJSONHandler(os.Stdout, opts)
} else {
h = slog.NewTextHandler(os.Stdout, opts)
}
slog.SetDefault(slog.New(h))
}