package main import ( "context" "flag" "log/slog" "os" "os/signal" "path/filepath" "syscall" "time" "github.com/monlet/agent/internal/agentkey" "github.com/monlet/agent/internal/app" "github.com/monlet/agent/internal/client" "github.com/monlet/agent/internal/config" "github.com/monlet/agent/internal/ids" "github.com/monlet/agent/internal/metrics" "github.com/monlet/agent/internal/spool" ) var version = "0.1.0" const shutdownTimeout = 30 * time.Second type reloadTarget interface { RecordReloadFailure() Reload(*config.Config, string) error } func main() { cfgPath := flag.String("config", "config.toml", "path to TOML config") flag.Parse() log := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) cfg, err := config.Load(*cfgPath) if err != nil { log.Error("load config", "err", err) os.Exit(1) } agentID, err := ids.ResolveAgentID(cfg.AgentID, cfg.Hostname) if err != nil { log.Error("agent_id", "err", err) os.Exit(1) } sp, err := spool.Open(filepath.Join(cfg.StateDir, "spool"), spool.DefaultMaxEvents, spool.DefaultMaxBytes) if err != nil { log.Error("open spool", "err", err) os.Exit(1) } key := "" if cfg.PushesToServer() { key, err = agentkey.LoadOrCreate(cfg.StateDir) if err != nil { log.Error("agent key", "err", err) os.Exit(1) } } a := &app.App{ Cfg: cfg, AgentID: agentID, Version: version, Client: client.New(cfg.Server.URL, key, agentID), Spool: sp, Metrics: metrics.New(), Log: log, } ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() startShutdownWatchdog(ctx, shutdownTimeout, log, os.Exit) hup := make(chan os.Signal, 1) signal.Notify(hup, syscall.SIGHUP) defer signal.Stop(hup) go runReloadLoop(ctx, *cfgPath, hup, a, log) log.Info("monlet-agent starting", "agent_id", agentID, "version", version, "checks", len(cfg.Checks)) if err := a.Run(ctx); err != nil { log.Error("run", "err", err) os.Exit(1) } log.Info("monlet-agent stopped") } func startShutdownWatchdog(ctx context.Context, timeout time.Duration, log *slog.Logger, exit func(int)) { go func() { <-ctx.Done() t := time.NewTimer(timeout) defer t.Stop() <-t.C log.Error("shutdown timeout exceeded", "timeout", timeout.String()) exit(1) }() } func runReloadLoop(ctx context.Context, cfgPath string, hup <-chan os.Signal, target reloadTarget, log *slog.Logger) { for { select { case <-ctx.Done(): return case <-hup: reloadOnce(cfgPath, target, log) } } } func reloadOnce(cfgPath string, target reloadTarget, log *slog.Logger) { nextCfg, err := config.Load(cfgPath) if err != nil { target.RecordReloadFailure() log.Warn("reload config failed", "err", err) return } nextAgentID, err := ids.ResolveAgentID(nextCfg.AgentID, nextCfg.Hostname) if err != nil { target.RecordReloadFailure() log.Warn("reload agent_id failed", "err", err) return } if err := target.Reload(nextCfg, nextAgentID); err != nil { log.Warn("reload rejected", "err", err) return } log.Info("reload complete", "checks", len(nextCfg.Checks)) }