Container events are Ham Lab's own record, not Docker's short memory

docker events is a live stream with a small in-memory backlog: ask for thirty days and you
get a few minutes, and a daemon restart loses the lot. That is why Dockhand kept its own
copy. A watcher now follows the stream and appends lifecycle events to a ring beside the
config, flushed every thirty seconds, so the question of what happened to a container last
week has an answer. Health-check exec events are excluded — three per probe, thousands a
day, and the health panel says it better.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-24 19:55:36 -07:00
co-authored by Claude Opus 5
parent b3e1c495f0
commit a70f3e2ed2
5 changed files with 239 additions and 66 deletions
-59
View File
@@ -439,65 +439,6 @@ func (s *Server) dockerHealth(w http.ResponseWriter, r *http.Request) {
})
}
// dockerEvents is what has happened to one container lately — started, died, killed, the
// health checks going bad. Asked of Docker on demand rather than kept: Docker already has
// the history, and a copy of it would only go stale.
func (s *Server) dockerEvents(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if !dockerRef.MatchString(id) {
jsonErr(w, 400, "bad container")
return
}
since := r.URL.Query().Get("since")
if !regexp.MustCompile(`^[0-9]+[mhd]$`).MatchString(since) {
since = "7d"
}
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
out, err := dockerRun(ctx, "events", "--since", since, "--until", "0s",
"--filter", "container="+id, "--format", "{{json .}}")
if err != nil {
jsonErr(w, 500, err.Error())
return
}
events := []map[string]any{}
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if !strings.HasPrefix(line, "{") {
continue
}
var e struct {
Action string `json:"Action"`
Time int64 `json:"time"`
TimeNano int64 `json:"timeNano"`
Actor map[string]any `json:"Actor"`
}
if json.Unmarshal([]byte(line), &e) != nil {
continue
}
m := map[string]any{"action": e.Action, "t": e.Time * 1000}
if e.TimeNano > 0 {
m["t"] = e.TimeNano / 1e6
}
if at, ok := e.Actor["Attributes"].(map[string]any); ok {
// the interesting ones: why it stopped, and what the health check said
for _, k := range []string{"exitCode", "signal", "name", "image"} {
if v, ok := at[k]; ok && v != "" {
m[k] = v
}
}
}
events = append(events, m)
}
// newest first, and bounded: a flapping container can produce thousands
for i, j := 0, len(events)-1; i < j; i, j = i+1, j-1 {
events[i], events[j] = events[j], events[i]
}
if len(events) > 400 {
events = events[:400]
}
jsonOK(w, 200, map[string]any{"events": events, "since": since})
}
// dockerExec opens a shell inside a container, as a terminal session like any other — so
// it reattaches, resizes and scrolls back the same way, and works on a managed machine
// through the same proxy.