From adda6eea38e6e5d41783e49fdfe8b25a42131341 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 4 Nov 2025 22:16:26 +0000 Subject: [PATCH] Update docker CPU metrics and add OpenRC installer support (Refs #255) --- internal/dockeragent/agent.go | 153 ++++++++++++++++++++++++++------ scripts/install-docker-agent.sh | 130 +++++++++++++++++++++------ 2 files changed, 227 insertions(+), 56 deletions(-) diff --git a/internal/dockeragent/agent.go b/internal/dockeragent/agent.go index cfda9c1..38a152c 100644 --- a/internal/dockeragent/agent.go +++ b/internal/dockeragent/agent.go @@ -81,26 +81,34 @@ const ( // Agent collects Docker metrics and posts them to Pulse. type Agent struct { - cfg Config - docker *client.Client - daemonHost string - runtime RuntimeKind - runtimeVer string - supportsSwarm bool - httpClients map[bool]*http.Client - logger zerolog.Logger - machineID string - hostName string - cpuCount int - targets []TargetConfig - allowedStates map[string]struct{} - stateFilters []string - hostID string + cfg Config + docker *client.Client + daemonHost string + runtime RuntimeKind + runtimeVer string + supportsSwarm bool + httpClients map[bool]*http.Client + logger zerolog.Logger + machineID string + hostName string + cpuCount int + targets []TargetConfig + allowedStates map[string]struct{} + stateFilters []string + hostID string + prevContainerCPU map[string]cpuSample } // ErrStopRequested indicates the agent should terminate gracefully after acknowledging a stop command. var ErrStopRequested = errors.New("docker host stop requested") +type cpuSample struct { + totalUsage uint64 + systemUsage uint64 + onlineCPUs uint32 + read time.Time +} + // New creates a new Docker agent instance. func New(cfg Config) (*Agent, error) { targets, err := normalizeTargets(cfg.Targets) @@ -212,19 +220,20 @@ func New(cfg Config) (*Agent, error) { } agent := &Agent{ - cfg: cfg, - docker: dockerClient, - daemonHost: dockerClient.DaemonHost(), - runtime: runtimeKind, - runtimeVer: info.ServerVersion, - supportsSwarm: runtimeKind == RuntimeDocker, - httpClients: httpClients, - logger: *logger, - machineID: machineID, - hostName: hostName, - targets: cfg.Targets, - allowedStates: make(map[string]struct{}, len(stateFilters)), - stateFilters: stateFilters, + cfg: cfg, + docker: dockerClient, + daemonHost: dockerClient.DaemonHost(), + runtime: runtimeKind, + runtimeVer: info.ServerVersion, + supportsSwarm: runtimeKind == RuntimeDocker, + httpClients: httpClients, + logger: *logger, + machineID: machineID, + hostName: hostName, + targets: cfg.Targets, + allowedStates: make(map[string]struct{}, len(stateFilters)), + stateFilters: stateFilters, + prevContainerCPU: make(map[string]cpuSample), } for _, state := range stateFilters { @@ -702,6 +711,7 @@ func (a *Agent) collectContainers(ctx context.Context) ([]agentsdocker.Container } containers := make([]agentsdocker.Container, 0, len(list)) + active := make(map[string]struct{}, len(list)) for _, summary := range list { if len(a.allowedStates) > 0 { if _, ok := a.allowedStates[strings.ToLower(summary.State)]; !ok { @@ -709,6 +719,8 @@ func (a *Agent) collectContainers(ctx context.Context) ([]agentsdocker.Container } } + active[summary.ID] = struct{}{} + container, err := a.collectContainer(ctx, summary) if err != nil { a.logger.Warn().Str("container", strings.Join(summary.Names, ",")).Err(err).Msg("Failed to collect container stats") @@ -716,9 +728,22 @@ func (a *Agent) collectContainers(ctx context.Context) ([]agentsdocker.Container } containers = append(containers, container) } + a.pruneStaleCPUSamples(active) return containers, nil } +func (a *Agent) pruneStaleCPUSamples(active map[string]struct{}) { + if len(a.prevContainerCPU) == 0 { + return + } + + for id := range a.prevContainerCPU { + if _, ok := active[id]; !ok { + delete(a.prevContainerCPU, id) + } + } +} + func (a *Agent) collectContainer(ctx context.Context, summary types.Container) (agentsdocker.Container, error) { const perContainerTimeout = 15 * time.Second @@ -751,9 +776,11 @@ func (a *Agent) collectContainer(ctx context.Context, summary types.Container) ( return agentsdocker.Container{}, fmt.Errorf("decode stats: %w", err) } - cpuPercent = calculateCPUPercent(stats, a.cpuCount) + cpuPercent = a.calculateContainerCPUPercent(summary.ID, stats) memUsage, memLimit, memPercent = calculateMemoryUsage(stats) blockIO = summarizeBlockIO(stats) + } else { + delete(a.prevContainerCPU, summary.ID) } createdAt := time.Unix(summary.Created, 0) @@ -1256,6 +1283,74 @@ func summarizeBlockIO(stats containertypes.StatsResponse) *agentsdocker.Containe } } +func (a *Agent) calculateContainerCPUPercent(id string, stats containertypes.StatsResponse) float64 { + current := cpuSample{ + totalUsage: stats.CPUStats.CPUUsage.TotalUsage, + systemUsage: stats.CPUStats.SystemUsage, + onlineCPUs: stats.CPUStats.OnlineCPUs, + read: stats.Read, + } + + percent := calculateCPUPercent(stats, a.cpuCount) + if percent > 0 { + a.prevContainerCPU[id] = current + return percent + } + + prev, ok := a.prevContainerCPU[id] + a.prevContainerCPU[id] = current + if !ok { + return 0 + } + + var totalDelta float64 + if current.totalUsage >= prev.totalUsage { + totalDelta = float64(current.totalUsage - prev.totalUsage) + } else { + // Counter likely reset (container restart); fall back to current reading. + totalDelta = float64(current.totalUsage) + } + + if totalDelta <= 0 { + return 0 + } + + onlineCPUs := current.onlineCPUs + if onlineCPUs == 0 { + onlineCPUs = prev.onlineCPUs + } + if onlineCPUs == 0 && a.cpuCount > 0 { + onlineCPUs = uint32(a.cpuCount) + } + if onlineCPUs == 0 { + return 0 + } + + var systemDelta float64 + if current.systemUsage >= prev.systemUsage { + systemDelta = float64(current.systemUsage - prev.systemUsage) + } else if current.systemUsage > 0 { + systemDelta = float64(current.systemUsage) + } + + if systemDelta > 0 { + return safeFloat((totalDelta / systemDelta) * float64(onlineCPUs) * 100.0) + } + + if !prev.read.IsZero() && !current.read.IsZero() { + elapsed := current.read.Sub(prev.read).Seconds() + if elapsed > 0 { + denominator := elapsed * float64(onlineCPUs) * 1e9 + if denominator > 0 { + cpuPercent := (totalDelta / denominator) * 100.0 + return safeFloat(cpuPercent) + } + } + } + + return 0 +} + func calculateCPUPercent(stats containertypes.StatsResponse, hostCPUs int) float64 { totalDelta := float64(stats.CPUStats.CPUUsage.TotalUsage - stats.PreCPUStats.CPUUsage.TotalUsage) systemDelta := float64(stats.CPUStats.SystemUsage - stats.PreCPUStats.SystemUsage) diff --git a/scripts/install-docker-agent.sh b/scripts/install-docker-agent.sh index e368423..8fcc98f 100755 --- a/scripts/install-docker-agent.sh +++ b/scripts/install-docker-agent.sh @@ -1461,6 +1461,28 @@ allow_reenroll_if_needed() { allow_reenroll_if_needed "$AGENT_IDENTIFIER" +# Determine whether to disable auto-update (development server) +NO_AUTO_UPDATE_FLAG="" +if command -v curl &> /dev/null || command -v wget &> /dev/null; then + SERVER_INFO_URL="$PRIMARY_URL/api/server/info" + SERVER_INFO="" + if command -v curl &> /dev/null; then + SERVER_INFO=$(curl -fsSL "$SERVER_INFO_URL" 2>/dev/null || true) + else + SERVER_INFO=$(wget -qO- "$SERVER_INFO_URL" 2>/dev/null || true) + fi + + if [[ -n "$SERVER_INFO" ]] && echo "$SERVER_INFO" | grep -q '"isDevelopment"[[:space:]]*:[[:space:]]*true'; then + NO_AUTO_UPDATE_FLAG=" --no-auto-update" + log_info 'Development server detected – auto-update disabled' + fi + + if [[ -n "$NO_AUTO_UPDATE_FLAG" ]] && ! "$AGENT_PATH" --help 2>&1 | grep -q -- '--no-auto-update'; then + log_warn 'Agent binary lacks --no-auto-update flag; keeping auto-update enabled' + NO_AUTO_UPDATE_FLAG="" + fi +fi + # Check if systemd is available if ! command -v systemctl &> /dev/null || [ ! -d /etc/systemd/system ]; then printf '\n%s\n' '-- Systemd not detected; configuring alternative startup --' @@ -1489,6 +1511,87 @@ EOF log_info 'Log file : /var/log/pulse-docker-agent.log' log_info 'Host visible in Pulse: ~30 seconds' exit 0 + elif command -v rc-service >/dev/null 2>&1 && { [ -x /sbin/openrc-run ] || [ -x /bin/openrc-run ] || [ -x /usr/bin/openrc-run ]; }; then + log_info 'Detected OpenRC environment' + + log_header 'Preparing service environment' + ensure_service_user + ensure_service_home + ensure_docker_group_membership + write_env_file + + OPENRC_SERVICE="/etc/init.d/pulse-docker-agent" + OPENRC_PIDFILE="/run/pulse-docker-agent.pid" + OPENRC_PIDDIR=$(dirname "$OPENRC_PIDFILE") + OPENRC_LOG="/var/log/pulse-docker-agent.log" + OPENRC_ENV_FILE="$ENV_FILE" + OPENRC_OWNER="$SERVICE_USER_ACTUAL:$SERVICE_GROUP_ACTUAL" + + mkdir -p "$(dirname "$OPENRC_LOG")" + if [[ ! -f "$OPENRC_LOG" ]]; then + touch "$OPENRC_LOG" + fi + chown "$SERVICE_USER_ACTUAL":"$SERVICE_GROUP_ACTUAL" "$OPENRC_LOG" >/dev/null 2>&1 || true + chmod 0640 "$OPENRC_LOG" >/dev/null 2>&1 || true + + cat > "$OPENRC_SERVICE" </dev/null 2>&1; then + rc-service pulse-docker-agent stop >/dev/null 2>&1 || true + fi + + if rc-update add pulse-docker-agent default >/dev/null 2>&1; then + log_success 'Added pulse-docker-agent to OpenRC default runlevel' + else + log_warn 'Failed to add service to default runlevel; run: rc-update add pulse-docker-agent default' + fi + + if rc-service pulse-docker-agent start >/dev/null 2>&1; then + log_success 'Started pulse-docker-agent service' + else + log_warn 'Failed to start pulse-docker-agent service; run: rc-service pulse-docker-agent start' + fi + + log_header 'Installation complete' + log_info 'Agent service enabled via OpenRC' + log_info 'Check status : rc-service pulse-docker-agent status' + log_info 'Follow logs : tail -f /var/log/pulse-docker-agent.log' + log_info 'Host visible in Pulse : ~30 seconds' + exit 0 fi log_info 'Manual startup environment detected' @@ -1503,33 +1606,6 @@ EOF fi - -# Check if server is in development mode -NO_AUTO_UPDATE_FLAG="" -if command -v curl &> /dev/null || command -v wget &> /dev/null; then - SERVER_INFO_URL="$PRIMARY_URL/api/server/info" - IS_DEV="false" - - if command -v curl &> /dev/null; then - SERVER_INFO=$(curl -fsSL "$SERVER_INFO_URL" 2>/dev/null || echo "") - elif command -v wget &> /dev/null; then - SERVER_INFO=$(wget -qO- "$SERVER_INFO_URL" 2>/dev/null || echo "") - fi - -if [[ -n "$SERVER_INFO" ]] && echo "$SERVER_INFO" | grep -q '"isDevelopment"[[:space:]]*:[[:space:]]*true'; then - IS_DEV="true" - NO_AUTO_UPDATE_FLAG=" --no-auto-update" - log_info 'Development server detected – auto-update disabled' -fi - -if [[ -n "$NO_AUTO_UPDATE_FLAG" ]]; then - if ! "$AGENT_PATH" --help 2>&1 | grep -q -- '--no-auto-update'; then - log_warn 'Agent binary lacks --no-auto-update flag; keeping auto-update enabled' - NO_AUTO_UPDATE_FLAG="" - fi -fi -fi - log_header 'Preparing service environment' ensure_service_user ensure_service_home