Files
Dash/apps/backend/cmd/dash-agent/docker.go
T
Tomas DvorakandDevin f3143af351 feat(api): phase 5 — alerts, agent depth, subdomains, auth, boards
P5b: notifier transport registry (webhook/slack/discord/telegram/
gotify/ntfy/smtp via net/smtp), per-monitor+domain alert rules
(failsBeforeDown, latencyWarn, certDays, mute).

P5c: per-container cpu/mem, optional SMART/ZFS/GPU exec-or-skip
collectors, crt.sh subdomain discovery with daily sweep.

P5d: opt-in auth (bcrypt, sqlite sessions, 7d rotating cookie, login
rate limit, public-route bypass) and multiple boards (boards table,
sections.board_id, /api/boards CRUD, slug routing).

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-22 12:49:44 +02:00

122 lines
3.3 KiB
Go

package main
// Docker collector: container liveness plus per-container CPU/memory from
// /containers/{id}/stats?stream=false. All best-effort — no socket, no data.
import (
"context"
"encoding/json"
"io"
"net"
"net/http"
"strings"
"sync"
"time"
)
const dockerSock = "/var/run/docker.sock"
var dockerClient = &http.Client{
Timeout: 4 * time.Second,
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return (&net.Dialer{Timeout: 2 * time.Second}).DialContext(ctx, "unix", dockerSock)
},
},
}
// dockerStatsJSON is the subset of /containers/{id}/stats we consume.
type dockerStatsJSON struct {
CPUStats struct {
CPUUsage struct {
TotalUsage uint64 `json:"total_usage"`
} `json:"cpu_usage"`
SystemCPUUsage uint64 `json:"system_cpu_usage"`
OnlineCPUs uint32 `json:"online_cpus"`
} `json:"cpu_stats"`
PreCPUStats struct {
CPUUsage struct {
TotalUsage uint64 `json:"total_usage"`
} `json:"cpu_usage"`
SystemCPUUsage uint64 `json:"system_cpu_usage"`
} `json:"precpu_stats"`
MemoryStats struct {
Usage uint64 `json:"usage"`
Limit uint64 `json:"limit"`
} `json:"memory_stats"`
}
// containerCPUPct applies the Docker dashboard formula: cpu delta over
// system delta, scaled by online CPUs. 0 when deltas don't advance.
func containerCPUPct(s *dockerStatsJSON) float64 {
cpuDelta := float64(s.CPUStats.CPUUsage.TotalUsage) - float64(s.PreCPUStats.CPUUsage.TotalUsage)
sysDelta := float64(s.CPUStats.SystemCPUUsage) - float64(s.PreCPUStats.SystemCPUUsage)
online := s.CPUStats.OnlineCPUs
if online == 0 {
online = 1
}
if cpuDelta <= 0 || sysDelta <= 0 {
return 0
}
return cpuDelta / sysDelta * float64(online) * 100
}
// readContainers lists containers and fans out to per-container stats.
// Stats fetches are bounded: 8 concurrent, each under the client timeout.
func readContainers() []containerStat {
if _, err := net.DialTimeout("unix", dockerSock, time.Second); err != nil {
return nil
}
res, err := dockerClient.Get("http://d/containers/json?all=1")
if err != nil {
return nil
}
defer res.Body.Close()
var raw []struct {
ID string `json:"Id"`
Names []string `json:"Names"`
State string `json:"State"`
}
if json.NewDecoder(io.LimitReader(res.Body, 4<<20)).Decode(&raw) != nil {
return nil
}
out := make([]containerStat, len(raw))
var wg sync.WaitGroup
sem := make(chan struct{}, 8)
for i, r := range raw {
name := ""
if len(r.Names) > 0 {
name = strings.TrimPrefix(r.Names[0], "/")
}
out[i] = containerStat{Name: name, State: r.State}
if r.State != "running" || r.ID == "" {
continue
}
wg.Add(1)
sem <- struct{}{}
go func(i int, id string) {
defer wg.Done()
defer func() { <-sem }()
fillContainerStats(&out[i], id)
}(i, r.ID)
}
wg.Wait()
return out
}
// fillContainerStats fetches one container's one-shot stats into cs.
func fillContainerStats(cs *containerStat, id string) {
res, err := dockerClient.Get("http://d/containers/" + id + "/stats?stream=false")
if err != nil {
return
}
defer res.Body.Close()
var st dockerStatsJSON
if json.NewDecoder(io.LimitReader(res.Body, 4<<20)).Decode(&st) != nil {
return
}
cs.CPU = containerCPUPct(&st)
cs.MemUsed = float64(st.MemoryStats.Usage)
cs.MemLimit = float64(st.MemoryStats.Limit)
}