mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-10-28 07:41:27 +00:00
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>
This commit is contained in:
1 parent
8973be3b41
commit
f3143af351
33 files changed
+2772
-126
No files matched your search
@@ -0,0 +1,121 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user