Add web auth, infinite-scroll, agent admission and review fixes across agent/server/web
This commit is contained in:
@@ -3,6 +3,7 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -24,6 +25,9 @@ type App struct {
|
||||
Log *slog.Logger
|
||||
|
||||
lastDropLog time.Time
|
||||
cfgMu sync.RWMutex
|
||||
schedulerMu sync.RWMutex
|
||||
scheduler *scheduler.Manager
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -36,47 +40,61 @@ const (
|
||||
|
||||
func (a *App) Run(ctx context.Context) error {
|
||||
events := make(chan scheduler.Event, 256)
|
||||
cfg := a.config()
|
||||
|
||||
// Account drops accumulated during spool Open (e.g. shrunk limits on restart).
|
||||
a.recordSpoolDrops(a.Spool.Drops())
|
||||
a.Metrics.ChecksConfigured.Set(float64(len(a.Cfg.Checks)))
|
||||
a.Metrics.BuildInfo.WithLabelValues(a.Version, boolLabel(a.Cfg.PushesToServer()), boolLabel(a.Cfg.ExposesMetrics())).Set(1)
|
||||
for _, c := range a.Cfg.Checks {
|
||||
a.Metrics.ConfigLoadSuccess.Set(1)
|
||||
a.Metrics.ChecksConfigured.Set(float64(len(cfg.Checks)))
|
||||
a.Metrics.BuildInfo.WithLabelValues(a.Version, boolLabel(cfg.PushesToServer()), boolLabel(cfg.ExposesMetrics())).Set(1)
|
||||
for _, c := range cfg.Checks {
|
||||
a.Metrics.CheckInterval.WithLabelValues(c.ID).Set(c.Interval.Duration.Seconds())
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
if a.Cfg.ExposesMetrics() {
|
||||
if cfg.ExposesMetrics() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := a.Metrics.Serve(ctx, a.Cfg.Metrics.Listen); err != nil {
|
||||
if err := a.Metrics.Serve(ctx, cfg.Metrics.Listen); err != nil {
|
||||
a.Log.Error("metrics server", "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
mgr := scheduler.NewManager(ctx, a.AgentID, events, scheduler.Hooks{
|
||||
OnSkipped: func(id string) { a.Metrics.CheckSkipped.WithLabelValues(id).Inc() },
|
||||
OnResult: func(ev scheduler.Event) {
|
||||
a.Metrics.CheckRuns.WithLabelValues(ev.CheckID, ev.Status).Inc()
|
||||
a.Metrics.CheckDuration.WithLabelValues(ev.CheckID).Observe(float64(ev.DurationMs) / 1000.0)
|
||||
a.Metrics.CheckStatus.WithLabelValues(ev.CheckID).Set(metrics.StatusCode(ev.Status))
|
||||
a.Metrics.CheckExitCode.WithLabelValues(ev.CheckID).Set(float64(ev.ExitCode))
|
||||
ts := float64(ev.ObservedAt.Unix())
|
||||
a.Metrics.CheckLastRun.WithLabelValues(ev.CheckID).Set(ts)
|
||||
if ev.Status == "ok" {
|
||||
a.Metrics.CheckLastSuccess.WithLabelValues(ev.CheckID).Set(ts)
|
||||
}
|
||||
},
|
||||
OnEventChannelFull: func() { a.Metrics.EventsChannelFull.Inc() },
|
||||
OnResourceLimitFailure: func(id string, resource string) {
|
||||
a.Metrics.ResourceLimitApplyFailures.WithLabelValues(id, resource).Inc()
|
||||
},
|
||||
OnStopped: func(id string) { a.Metrics.DeleteCheck(id) },
|
||||
})
|
||||
mgr.Update(cfg.Checks)
|
||||
a.schedulerMu.Lock()
|
||||
a.scheduler = mgr
|
||||
a.schedulerMu.Unlock()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
scheduler.Run(ctx, a.AgentID, a.Cfg.Checks, events, scheduler.Hooks{
|
||||
OnSkipped: func(id string) { a.Metrics.CheckSkipped.WithLabelValues(id).Inc() },
|
||||
OnResult: func(ev scheduler.Event) {
|
||||
a.Metrics.CheckRuns.WithLabelValues(ev.CheckID, ev.Status).Inc()
|
||||
a.Metrics.CheckDuration.WithLabelValues(ev.CheckID).Observe(float64(ev.DurationMs) / 1000.0)
|
||||
a.Metrics.CheckStatus.WithLabelValues(ev.CheckID).Set(metrics.StatusCode(ev.Status))
|
||||
a.Metrics.CheckExitCode.WithLabelValues(ev.CheckID).Set(float64(ev.ExitCode))
|
||||
ts := float64(ev.ObservedAt.Unix())
|
||||
a.Metrics.CheckLastRun.WithLabelValues(ev.CheckID).Set(ts)
|
||||
if ev.Status == "ok" {
|
||||
a.Metrics.CheckLastSuccess.WithLabelValues(ev.CheckID).Set(ts)
|
||||
}
|
||||
},
|
||||
})
|
||||
<-ctx.Done()
|
||||
mgr.Stop()
|
||||
mgr.Wait()
|
||||
}()
|
||||
|
||||
if a.Cfg.PushesToServer() {
|
||||
if cfg.PushesToServer() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -128,18 +146,19 @@ func (a *App) spoolWriter(ctx context.Context, events <-chan scheduler.Event) {
|
||||
}
|
||||
|
||||
func (a *App) heartbeatLoop(ctx context.Context) {
|
||||
interval := a.Cfg.Server.HeartbeatInterval.Duration
|
||||
attempt := 0
|
||||
for {
|
||||
cfg := a.config()
|
||||
interval := cfg.Server.HeartbeatInterval.Duration
|
||||
hb := client.HeartbeatRequest{
|
||||
AgentID: a.AgentID,
|
||||
ObservedAt: time.Now().UTC(),
|
||||
Hostname: a.Cfg.Hostname,
|
||||
Hostname: cfg.Hostname,
|
||||
Features: client.AgentFeatures{
|
||||
Push: a.Cfg.PushesToServer(),
|
||||
Metrics: a.Cfg.ExposesMetrics(),
|
||||
Push: cfg.PushesToServer(),
|
||||
Metrics: cfg.ExposesMetrics(),
|
||||
},
|
||||
Labels: a.Cfg.HeartbeatLabels(a.Version),
|
||||
Labels: cfg.HeartbeatLabels(a.Version),
|
||||
}
|
||||
a.Metrics.SendAttempts.WithLabelValues("heartbeat").Inc()
|
||||
err := a.Client.SendHeartbeat(ctx, hb)
|
||||
@@ -167,9 +186,14 @@ func (a *App) heartbeatLoop(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (a *App) sendLoop(ctx context.Context) {
|
||||
interval := a.Cfg.Server.BatchInterval.Duration
|
||||
attempt := 0
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
interval := a.config().Server.BatchInterval.Duration
|
||||
items, err := a.Spool.Peek(client.MaxBatchEvents)
|
||||
if err != nil {
|
||||
a.Log.Error("spool peek", "err", err)
|
||||
@@ -217,6 +241,74 @@ func (a *App) sendLoop(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Reload applies a validated hot-reload config if immutable fields match.
|
||||
func (a *App) Reload(newCfg *config.Config, newAgentID string) error {
|
||||
oldCfg := a.config()
|
||||
if err := validateReload(oldCfg, newCfg, a.AgentID, newAgentID); err != nil {
|
||||
a.RecordReloadFailure()
|
||||
return err
|
||||
}
|
||||
a.cfgMu.Lock()
|
||||
a.Cfg = newCfg
|
||||
a.cfgMu.Unlock()
|
||||
|
||||
a.Metrics.ConfigLoadSuccess.Set(1)
|
||||
a.Metrics.ConfigReloadTotal.WithLabelValues("success").Inc()
|
||||
a.Metrics.ConfigLastReloadSuccess.Set(float64(time.Now().Unix()))
|
||||
a.Metrics.ChecksConfigured.Set(float64(len(newCfg.Checks)))
|
||||
for _, c := range newCfg.Checks {
|
||||
a.Metrics.CheckInterval.WithLabelValues(c.ID).Set(c.Interval.Duration.Seconds())
|
||||
}
|
||||
|
||||
a.schedulerMu.RLock()
|
||||
mgr := a.scheduler
|
||||
a.schedulerMu.RUnlock()
|
||||
if mgr != nil {
|
||||
mgr.Update(newCfg.Checks)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecordReloadFailure updates config reload metrics after a failed reload attempt.
|
||||
func (a *App) RecordReloadFailure() {
|
||||
a.Metrics.ConfigLoadSuccess.Set(0)
|
||||
a.Metrics.ConfigReloadTotal.WithLabelValues("failure").Inc()
|
||||
}
|
||||
|
||||
func (a *App) config() *config.Config {
|
||||
a.cfgMu.RLock()
|
||||
defer a.cfgMu.RUnlock()
|
||||
return a.Cfg
|
||||
}
|
||||
|
||||
func validateReload(oldCfg, newCfg *config.Config, oldAgentID, newAgentID string) error {
|
||||
if oldAgentID != newAgentID {
|
||||
return fmt.Errorf("agent_id change requires restart")
|
||||
}
|
||||
if oldCfg.StateDir != newCfg.StateDir {
|
||||
return fmt.Errorf("state_dir change requires restart")
|
||||
}
|
||||
if oldCfg.PushesToServer() != newCfg.PushesToServer() {
|
||||
return fmt.Errorf("server.enabled change requires restart")
|
||||
}
|
||||
if oldCfg.Server.URL != newCfg.Server.URL {
|
||||
return fmt.Errorf("server.url change requires restart")
|
||||
}
|
||||
if oldCfg.Server.HeartbeatInterval.Duration != newCfg.Server.HeartbeatInterval.Duration {
|
||||
return fmt.Errorf("server.heartbeat_interval change requires restart")
|
||||
}
|
||||
if oldCfg.Server.BatchInterval.Duration != newCfg.Server.BatchInterval.Duration {
|
||||
return fmt.Errorf("server.batch_interval change requires restart")
|
||||
}
|
||||
if oldCfg.ExposesMetrics() != newCfg.ExposesMetrics() {
|
||||
return fmt.Errorf("metrics.enabled change requires restart")
|
||||
}
|
||||
if oldCfg.Metrics.Listen != newCfg.Metrics.Listen {
|
||||
return fmt.Errorf("metrics.listen change requires restart")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sleep returns true if context was cancelled while waiting.
|
||||
func sleep(ctx context.Context, d time.Duration) bool {
|
||||
t := time.NewTimer(d)
|
||||
|
||||
Reference in New Issue
Block a user