Add health/metrics server and proper cleanup to unified agent
- Add /healthz (liveness) and /readyz (readiness) endpoints - Add /metrics endpoint with Prometheus metrics (pulse_agent_info, pulse_agent_up) - Properly call dockerAgent.Close() on shutdown - New config: -health-addr flag and PULSE_HEALTH_ADDR env (default :9191) - Set to empty string to disable health server
This commit is contained in:
parent
7ab68cf682
commit
b6221dcc27
1 changed files with 119 additions and 19 deletions
|
|
@ -4,12 +4,17 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/agentupdate"
|
"github.com/rcourtman/pulse-go-rewrite/internal/agentupdate"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/dockeragent"
|
"github.com/rcourtman/pulse-go-rewrite/internal/dockeragent"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/hostagent"
|
"github.com/rcourtman/pulse-go-rewrite/internal/hostagent"
|
||||||
|
|
@ -20,6 +25,17 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Version = "dev"
|
Version = "dev"
|
||||||
|
|
||||||
|
// Prometheus metrics
|
||||||
|
agentInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Name: "pulse_agent_info",
|
||||||
|
Help: "Information about the Pulse agent",
|
||||||
|
}, []string{"version", "host_enabled", "docker_enabled"})
|
||||||
|
|
||||||
|
agentUp = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Name: "pulse_agent_up",
|
||||||
|
Help: "Whether the Pulse agent is running (1 = up, 0 = down)",
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
type multiValue []string
|
type multiValue []string
|
||||||
|
|
@ -48,11 +64,10 @@ func main() {
|
||||||
logger.Fatal().Err(err).Msg("Windows service failed")
|
logger.Fatal().Err(err).Msg("Windows service failed")
|
||||||
}
|
}
|
||||||
if ranAsService {
|
if ranAsService {
|
||||||
// Service handled everything, exit normally
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Setup Context & Signal Handling (for non-service mode)
|
// 4. Setup Context & Signal Handling
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|
@ -66,7 +81,21 @@ func main() {
|
||||||
Bool("auto_update", !cfg.DisableAutoUpdate).
|
Bool("auto_update", !cfg.DisableAutoUpdate).
|
||||||
Msg("Starting Pulse Unified Agent")
|
Msg("Starting Pulse Unified Agent")
|
||||||
|
|
||||||
// 5. Start Auto-Updater
|
// 5. Set prometheus info metric
|
||||||
|
agentInfo.WithLabelValues(
|
||||||
|
Version,
|
||||||
|
fmt.Sprintf("%t", cfg.EnableHost),
|
||||||
|
fmt.Sprintf("%t", cfg.EnableDocker),
|
||||||
|
).Set(1)
|
||||||
|
agentUp.Set(1)
|
||||||
|
|
||||||
|
// 6. Start Health/Metrics Server
|
||||||
|
var ready atomic.Bool
|
||||||
|
if cfg.HealthAddr != "" {
|
||||||
|
startHealthServer(ctx, cfg.HealthAddr, &ready, &logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Start Auto-Updater
|
||||||
updater := agentupdate.New(agentupdate.Config{
|
updater := agentupdate.New(agentupdate.Config{
|
||||||
PulseURL: cfg.PulseURL,
|
PulseURL: cfg.PulseURL,
|
||||||
APIToken: cfg.APIToken,
|
APIToken: cfg.APIToken,
|
||||||
|
|
@ -83,25 +112,22 @@ func main() {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// 6. Start Host Agent (if enabled)
|
// 8. Start Host Agent (if enabled)
|
||||||
if cfg.EnableHost {
|
if cfg.EnableHost {
|
||||||
hostCfg := hostagent.Config{
|
hostCfg := hostagent.Config{
|
||||||
PulseURL: cfg.PulseURL,
|
PulseURL: cfg.PulseURL,
|
||||||
APIToken: cfg.APIToken,
|
APIToken: cfg.APIToken,
|
||||||
Interval: cfg.Interval,
|
Interval: cfg.Interval,
|
||||||
HostnameOverride: cfg.HostnameOverride,
|
HostnameOverride: cfg.HostnameOverride,
|
||||||
AgentID: cfg.AgentID, // Shared ID? Or separate? Usually separate for now.
|
AgentID: cfg.AgentID,
|
||||||
AgentType: "unified",
|
AgentType: "unified",
|
||||||
AgentVersion: Version, // Pass unified agent version
|
AgentVersion: Version,
|
||||||
Tags: cfg.Tags,
|
Tags: cfg.Tags,
|
||||||
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||||
LogLevel: cfg.LogLevel,
|
LogLevel: cfg.LogLevel,
|
||||||
Logger: &logger,
|
Logger: &logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
// If AgentID is set globally, we might want to suffix it or let the agents derive their own.
|
|
||||||
// For now, let's pass it through. If it's empty, agents derive their own.
|
|
||||||
|
|
||||||
agent, err := hostagent.New(hostCfg)
|
agent, err := hostagent.New(hostCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal().Err(err).Msg("Failed to initialize host agent")
|
logger.Fatal().Err(err).Msg("Failed to initialize host agent")
|
||||||
|
|
@ -113,7 +139,8 @@ func main() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Start Docker Agent (if enabled)
|
// 9. Start Docker Agent (if enabled)
|
||||||
|
var dockerAgent *dockeragent.Agent
|
||||||
if cfg.EnableDocker {
|
if cfg.EnableDocker {
|
||||||
dockerCfg := dockeragent.Config{
|
dockerCfg := dockeragent.Config{
|
||||||
PulseURL: cfg.PulseURL,
|
PulseURL: cfg.PulseURL,
|
||||||
|
|
@ -122,12 +149,11 @@ func main() {
|
||||||
HostnameOverride: cfg.HostnameOverride,
|
HostnameOverride: cfg.HostnameOverride,
|
||||||
AgentID: cfg.AgentID,
|
AgentID: cfg.AgentID,
|
||||||
AgentType: "unified",
|
AgentType: "unified",
|
||||||
AgentVersion: Version, // Pass unified agent version
|
AgentVersion: Version,
|
||||||
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||||
DisableAutoUpdate: true, // Unified agent handles updates
|
DisableAutoUpdate: true,
|
||||||
LogLevel: cfg.LogLevel,
|
LogLevel: cfg.LogLevel,
|
||||||
Logger: &logger,
|
Logger: &logger,
|
||||||
// Docker specific defaults
|
|
||||||
SwarmScope: "node",
|
SwarmScope: "node",
|
||||||
IncludeContainers: true,
|
IncludeContainers: true,
|
||||||
IncludeServices: true,
|
IncludeServices: true,
|
||||||
|
|
@ -135,28 +161,92 @@ func main() {
|
||||||
CollectDiskMetrics: true,
|
CollectDiskMetrics: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
agent, err := dockeragent.New(dockerCfg)
|
dockerAgent, err = dockeragent.New(dockerCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal().Err(err).Msg("Failed to initialize docker agent")
|
logger.Fatal().Err(err).Msg("Failed to initialize docker agent")
|
||||||
}
|
}
|
||||||
// Docker agent has a Close method we should call, but errgroup doesn't make defer easy here.
|
|
||||||
// Ideally we wrap this. For now, we rely on OS cleanup or context cancellation.
|
|
||||||
|
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
logger.Info().Msg("Docker agent module started")
|
logger.Info().Msg("Docker agent module started")
|
||||||
return agent.Run(ctx)
|
return dockerAgent.Run(ctx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Wait for all agents to exit
|
// Mark as ready after all agents started
|
||||||
|
ready.Store(true)
|
||||||
|
|
||||||
|
// 10. Wait for all agents to exit
|
||||||
if err := g.Wait(); err != nil && err != context.Canceled {
|
if err := g.Wait(); err != nil && err != context.Canceled {
|
||||||
logger.Error().Err(err).Msg("Agent terminated with error")
|
logger.Error().Err(err).Msg("Agent terminated with error")
|
||||||
|
agentUp.Set(0)
|
||||||
|
cleanupDockerAgent(dockerAgent, &logger)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 11. Cleanup
|
||||||
|
agentUp.Set(0)
|
||||||
|
cleanupDockerAgent(dockerAgent, &logger)
|
||||||
|
|
||||||
logger.Info().Msg("Pulse Unified Agent stopped")
|
logger.Info().Msg("Pulse Unified Agent stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanupDockerAgent(agent *dockeragent.Agent, logger *zerolog.Logger) {
|
||||||
|
if agent == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := agent.Close(); err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("Failed to close docker agent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startHealthServer(ctx context.Context, addr string, ready *atomic.Bool, logger *zerolog.Logger) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// Liveness probe - always returns 200 if server is running
|
||||||
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Readiness probe - returns 200 only when agents are initialized
|
||||||
|
mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if ready.Load() {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
w.Write([]byte("not ready"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Prometheus metrics
|
||||||
|
mux.Handle("/metrics", promhttp.Handler())
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: mux,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
IdleTimeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil && err != http.ErrServerClosed {
|
||||||
|
logger.Warn().Err(err).Msg("Failed to shut down health server")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
logger.Info().Str("addr", addr).Msg("Health/metrics server listening")
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
logger.Warn().Err(err).Msg("Health server stopped unexpectedly")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
PulseURL string
|
PulseURL string
|
||||||
APIToken string
|
APIToken string
|
||||||
|
|
@ -174,6 +264,9 @@ type Config struct {
|
||||||
|
|
||||||
// Auto-update
|
// Auto-update
|
||||||
DisableAutoUpdate bool
|
DisableAutoUpdate bool
|
||||||
|
|
||||||
|
// Health/metrics server
|
||||||
|
HealthAddr string
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig() Config {
|
func loadConfig() Config {
|
||||||
|
|
@ -189,6 +282,7 @@ func loadConfig() Config {
|
||||||
envEnableHost := utils.GetenvTrim("PULSE_ENABLE_HOST")
|
envEnableHost := utils.GetenvTrim("PULSE_ENABLE_HOST")
|
||||||
envEnableDocker := utils.GetenvTrim("PULSE_ENABLE_DOCKER")
|
envEnableDocker := utils.GetenvTrim("PULSE_ENABLE_DOCKER")
|
||||||
envDisableAutoUpdate := utils.GetenvTrim("PULSE_DISABLE_AUTO_UPDATE")
|
envDisableAutoUpdate := utils.GetenvTrim("PULSE_DISABLE_AUTO_UPDATE")
|
||||||
|
envHealthAddr := utils.GetenvTrim("PULSE_HEALTH_ADDR")
|
||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
defaultInterval := 30 * time.Second
|
defaultInterval := 30 * time.Second
|
||||||
|
|
@ -208,6 +302,11 @@ func loadConfig() Config {
|
||||||
defaultEnableDocker = utils.ParseBool(envEnableDocker)
|
defaultEnableDocker = utils.ParseBool(envEnableDocker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultHealthAddr := envHealthAddr
|
||||||
|
if defaultHealthAddr == "" {
|
||||||
|
defaultHealthAddr = ":9191"
|
||||||
|
}
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
urlFlag := flag.String("url", envURL, "Pulse server URL")
|
urlFlag := flag.String("url", envURL, "Pulse server URL")
|
||||||
tokenFlag := flag.String("token", envToken, "Pulse API token")
|
tokenFlag := flag.String("token", envToken, "Pulse API token")
|
||||||
|
|
@ -220,6 +319,7 @@ func loadConfig() Config {
|
||||||
enableHostFlag := flag.Bool("enable-host", defaultEnableHost, "Enable Host Agent module")
|
enableHostFlag := flag.Bool("enable-host", defaultEnableHost, "Enable Host Agent module")
|
||||||
enableDockerFlag := flag.Bool("enable-docker", defaultEnableDocker, "Enable Docker Agent module")
|
enableDockerFlag := flag.Bool("enable-docker", defaultEnableDocker, "Enable Docker Agent module")
|
||||||
disableAutoUpdateFlag := flag.Bool("disable-auto-update", utils.ParseBool(envDisableAutoUpdate), "Disable automatic updates")
|
disableAutoUpdateFlag := flag.Bool("disable-auto-update", utils.ParseBool(envDisableAutoUpdate), "Disable automatic updates")
|
||||||
|
healthAddrFlag := flag.String("health-addr", defaultHealthAddr, "Health/metrics server address (empty to disable)")
|
||||||
showVersion := flag.Bool("version", false, "Print the agent version and exit")
|
showVersion := flag.Bool("version", false, "Print the agent version and exit")
|
||||||
|
|
||||||
var tagFlags multiValue
|
var tagFlags multiValue
|
||||||
|
|
@ -263,10 +363,10 @@ func loadConfig() Config {
|
||||||
EnableHost: *enableHostFlag,
|
EnableHost: *enableHostFlag,
|
||||||
EnableDocker: *enableDockerFlag,
|
EnableDocker: *enableDockerFlag,
|
||||||
DisableAutoUpdate: *disableAutoUpdateFlag,
|
DisableAutoUpdate: *disableAutoUpdateFlag,
|
||||||
|
HealthAddr: strings.TrimSpace(*healthAddrFlag),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers (duplicated from existing agents for now, to be moved to shared pkg later)
|
|
||||||
func gatherTags(env string, flags []string) []string {
|
func gatherTags(env string, flags []string) []string {
|
||||||
tags := make([]string, 0)
|
tags := make([]string, 0)
|
||||||
if env != "" {
|
if env != "" {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue