用 docker logs -f --tail=0 启动子进程并 bufio.Scanner 逐行读取最轻量;或调 Docker API 用 ContainerLogs 获取流,需处理权限、ANSI 控制符、多行日志及连接泄漏;结合 events API 动态跟踪容器启停,用 sync.Map 管理日志流,结构化解析 JSON 或正则日志为 LogEntry。
docker logs 实时抓取容器日志并转给 Go 程序处理Go 本身不直接读取 Docker 容器的 stdout/stderr,得靠 docker logs 命令或 Docker API。最轻量、最常用的方式是调用 docker logs -f --tail=0 启动一个持续流式输出的子进程,再用 bufio.Scanner 逐行读取。
注意点:
--tail=0 表示从当前最新日志开始(不是从头),避免重复拉取历史日志-f 才能保持连接不断开;不加的话命令执行完就退出,Go 程序收不到后续日志docker logs -f 不会自动重连,需在 Go 层做断连检测和重试逻辑Cmd.Stdout 为 os.Pipe(),避免缓冲区满导致子进程阻塞cmd := exec.Command("docker", "logs", "-f", "--tail=0", "my-app")
stdout, _ := cmd.StdoutPipe()
scanner := bufio.NewScanner(stdout)
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
for scanner.Scan() {
line := scanner.Text()
// 处理单行日志,比如解析 JSON、打时间戳、转发到 Kafka
}
github.com/docker/docker/api/types 直接调 Docker Engine API 获取日志流比 shell 命令更可控,适合嵌入长期运行的 DevOps 工具中。关键在于构造正确的 ContainerLogsOptions 并用 Client.ContainerLogs 获取 io.ReadCloser。
常见陷阱:
unix:///var/run/docker.sock,Go 程序必须有读该 socket 的权限(常被忽略,报 permission denied)Follow: true 才能持续读新日志;Tail: "0"(字符串)才等效于 --tail=0,填整数会 panicdocker/pkg/stdcopy.StdCopy 或手动按 \r\n/\n 拆分ReadCloser 会导致连接泄漏,尤其在轮询多个容器时真实场景中容器频繁重建,硬编码容器名或 ID 会立刻失效。必须结合 Docker events API 动态跟踪。
推荐做法:
Client.Events,监听 start / die / destroy 事件start 事件,提取 Actor.Attributes["name"] 或 Actor.ID,触发新日志流采集die 事件,查本地是否已有该容器的日志 reader,有则 Close() 并清理 goroutinesync.Map 存储 containerID → *logReader 映射,避免并发 map 写冲突别依赖容器名唯一性——同名容器重建后 ID 变了,但 name 可能复用;优先用 ID 关联日志流。
裸日志(如 2025-05-12T10:23:45Z INFO api.go:123 user login success)很难过滤分析。应在 Go 层做初步解析,至少提取时间、级别、服务名、traceID。
实操建议:
^(?P
{"level":"info","ts":"2025-05-12T10:23:45Z","msg":"login success"}),直接 json.Unmarshal,跳过正则开销raw: true 标记,避免丢日志type LogEntry { Timestamp time.Time; Level string; Service string; TraceID string; Message string },方便后续写入 ES 或发 Kafka真正难的不是解析,而是当几十个容器同时输出乱序、跨行、无界日志时,保证每条记录的时间戳准确、上下文不丢失——这需要在 reader goroutine 内部做小缓冲和行边界判
定,不能简单按 Scanner 的 Scan() 切分。