Collect the logs of the local Docker containers

- docker.go follows every running container through the Docker API
  (events + logs with follow), resumes after a restart from the last
  position saved in /data/docker-state.json, reads DOCKER_BACKFILL (1h)
  of history for new containers, strips terminal color codes and guesses
  the severity from the line (JSON, logfmt, [ERROR], ERROR ...).
- Logs carry source_type=docker, container, container_id, image,
  compose_project, compose_service and stream; host is the Docker host.
- Settings > Sources: one switch per container (grouped by compose
  project), enable/disable all, follow new containers automatically.
  Choices are saved per compose service in /data/docker.json.
- Source filter (syslog / docker) in the filter bar and the live view.
- docker-compose: read-only docker-socket-proxy; Logstream and the proxy
  are labelled logstream.exclude=true and never collected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cedricandClaude Opus 5.5 committed 2026-09-28 16:52:05 +02:00
1 parent aad0fb7c16
commit 085095c46f
12 files changed
+974 -10

No files matched your search

+12 -1
View File
@@ -14,7 +14,8 @@ type Filter struct {
Range string // 5m, 15m, 1h, 6h, 24h, 7d, 30d; anything else = no time limit
Host string
App string
Severity int // highest severity number included (0 = emerg … 7 = debug), -1 = all
Severity int // highest severity number included (0 = emerg … 7 = debug), -1 = all
Source string // "syslog", "docker" or "" for all
}
var validRanges = map[string]bool{"5m": true, "15m": true, "1h": true, "6h": true, "24h": true, "7d": true, "30d": true}
@@ -27,6 +28,7 @@ func FilterFromRequest(r *http.Request) Filter {
Range: q.Get("range"),
Host: q.Get("host"),
App: q.Get("app"),
Source: q.Get("source"),
Severity: -1,
}
if v, err := strconv.Atoi(q.Get("severity")); err == nil && v >= 0 && v <= 7 {
@@ -90,6 +92,12 @@ func (f Filter) filterExpr() string {
if f.App != "" {
parts = append(parts, "app:="+strconv.Quote(f.App))
}
switch f.Source {
case "docker":
parts = append(parts, `source_type:="docker"`)
case "syslog": // also matches logs stored before source_type existed
parts = append(parts, `!(source_type:="docker")`)
}
if f.Severity >= 0 && f.Severity < 7 {
names := make([]string, 0, 8)
for i := 0; i <= f.Severity; i++ {
@@ -173,6 +181,9 @@ func (m *Matcher) Match(e *Entry) bool {
if m.f.App != "" && e.App != m.f.App {
return false
}
if (m.f.Source == "docker") != (e.SourceType == "docker") && m.f.Source != "" {
return false
}
if m.f.Severity >= 0 && e.SevNum > m.f.Severity {
return false
}