Docker Compose CLI 中 `docker compose logs` 命令解析:日志聚合、跟随输出与源码实现
docker compose logs 是查看 Compose 应用日志的核心命令:它把项目中一个或多个服务的容器日志汇聚到终端,支持按服务过滤、指定副本索引、时间窗口截取、尾行数控制以及实时跟随输出。本文以本仓库的命令参考文档 docs/reference/compose_logs.md 为主体,完整梳理该命令的全部参数,并深入 cmd/compose/logs.go 与 pkg/compose/logs.go 等源码,讲清日志选择、并发流式读取、前缀着色格式化和 --follow 事件监控的实现机制。读完本文,你既能熟练使用该命令的每个选项,也能理解输出格式和跟随行为背后的代码逻辑。
命令概览与用法
参考文档对该命令的描述是:"Displays log output from services"(显示来自各服务的日志输出)。
命令的完整签名为(见 cmd/compose/logs.go):
docker compose logs [OPTIONS] [SERVICE...]
SERVICE...为可选的位置参数:不指定时输出项目中所有服务的日志;指定一个或多个服务名时,只输出对应服务的日志。命令通过completeServiceNames提供 Shell 补全(ValidArgsFunction),服务名可从 Compose 文件中自动补全。- 命令还支持 全局项目选项(如
-f指定 Compose 文件、--project-name指定项目名等),即文档中继承自ProjectOptions的部分。
一个典型的多服务示例如下(与仓库 e2e 测试夹具 pkg/e2e/fixtures/logs-test/compose.yaml 完全一致):
services:
ping:
image: alpine
init: true
command: ping localhost -c ${REPEAT:-1}
hello:
image: alpine
command: echo hello
deploy:
replicas: 2
# 输出所有服务的日志
docker compose logs
# 只看某一个 / 某几个服务
docker compose logs ping
docker compose logs hello ping
# 查看多副本服务中第 2 个容器的日志
docker compose logs --index 2 hello
完整选项说明
以下为参考文档 docs/reference/compose_logs.md 中的全部选项,并结合作用户可见的源码 cmd/compose/logs.go 中各 flag 的注册方式补充了默认值与约束:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
--dry-run |
bool |
以 dry run 模式执行命令 | |
-f, --follow |
bool |
跟随日志输出(实时流式打印) | |
--index |
int |
0 |
当服务有多个副本时,指定要查看的容器索引 |
--no-color |
bool |
输出单色(禁用彩色前缀) | |
--no-log-prefix |
bool |
不打印日志行前缀 | |
--since |
string |
只显示该时间戳之后的日志(如 2013-01-02T13:23:37Z 或相对时间 42m) |
|
-n, --tail |
string |
all |
每个容器显示日志末尾的行数 |
-t, --timestamps |
bool |
显示时间戳 | |
--until |
string |
只显示该时间戳之前的日志(如 2013-01-02T13:23:37Z 或相对时间 42m) |
其中几个选项在源码中有关键行为约束,值得展开:
--index 要求恰好选择一个服务
--index 用于在多副本服务(deploy.replicas > 1 或 --scale 扩容)中定位具体副本。源码在命令的 PreRunE 钩子中做了强校验(cmd/compose/logs.go):
PreRunE: func(cmd *cobra.Command, args []string) error {
if opts.index > 0 && len(args) != 1 {
return errors.New("--index requires one service to be selected")
}
return nil
},
也就是说,--index 值大于 0 时必须且只能指定一个服务名,否则命令直接报错,例如 docker compose logs --index 2 hello 合法,而 docker compose logs --index 2 或 docker compose logs --index 2 hello ping 会被拒绝。
--tail / --since / --until 直通容器日志 API
这三个选项的值被原样放入 API 层的 LogOptions(定义见 pkg/api/api.go):
// LogOptions defines optional parameters for the `Log` API
type LogOptions struct {
Project *types.Project
Index int
Services []string
Tail string
Since string
Until string
Follow bool
Timestamps bool
}
在底层调用 pkg/compose/logs.go 的 doLogContainer 时,这些字段被逐字段透传给 Docker Engine 的 ContainerLogs API(Tail、Since、Until、Follow、Timestamps),因此时间解析、相对时间换算等语义完全由 Engine 侧的容器日志能力决定;Since/Until 既接受 RFC3339 时间戳,也接受如 42m 的相对时长。
日志输出格式的源码实现:前缀、颜色与时间戳
docker compose logs 的多服务输出之所以易读,是因为每行日志都会带上"服务名(副本号) | "这样的彩色前缀,例如:
hello-1 | hello
ping-1 | PING localhost (127.0.0.1) 56(84) bytes of data.
格式化逻辑在 cmd/formatter/logs.go 的 logConsumer 中,它实现了 API 层定义的 LogConsumer 接口(pkg/api/api.go):
type LogConsumer interface {
Log(containerName, message string)
Err(containerName, message string)
Status(container, msg string)
}
关键实现点(cmd/formatter/logs.go):
- 每个容器一个 presenter。
register(name)为每个容器名注册展示器;当开启彩色输出(未传--no-color)时,每个容器会被分配一个循环调色色(nextColor()),空名容器使用单色。 - 前缀宽度自适应。
computeWidth()遍历所有已注册容器名,取最长名长度加 1 作为统一前缀宽度,setPrefix用fmt.Sprintf("%-*s | ", width, name)对齐所有行,因此多服务日志列对齐(见 cmd/formatter/logs.go):
func (p *presenter) setPrefix(width int) {
if p.name == api.WatchLogger {
p.prefix = p.colors(strings.Repeat(" ", width) + " ⦿ ")
return
}
p.prefix = p.colors(fmt.Sprintf("%-"+strconv.Itoa(width)+"s | ", p.name))
}
--no-log-prefix的语义。在 cmd/compose/logs.go 中,--no-log-prefix被取反后传入NewLogConsumer(..., prefix, ...);未开启前缀时日志行不再带"容器名 | "对齐前缀,适合管道重定向场景。-t/--timestamps的行为细节。注意时间戳是在 CLI 侧打印时按本地时间生成的:write()中用time.Now().Format(jsonmessage.RFC3339NanoFixed)为每一行补时间戳(cmd/formatter/logs.go)。- stdout 与 stderr 分流。
Log()写到 stdout,Err()写到 stderr,Status()则用于打印"exited with code N"这类状态行,方便脚本按流捕获。
此外,cmd/compose/logs.go 中还有一个容易忽略的行为:当未显式指定服务时,配置了 attach: false 的服务会被自动排除,除非用户明确点名该服务:
// exclude services configured to ignore output (attach: false), until explicitly selected
if project != nil && len(services) == 0 {
for n, service := range project.Services {
if service.Attach == nil || *service.Attach {
services = append(services, n)
}
}
}
这解释了为什么某些"纯后台"服务在 docker compose logs 中默认看不到,而 docker compose logs that_service 又能正常输出。
容器选择逻辑:如何确定读哪些容器的日志
服务层入口是 composeService.Logs(pkg/compose/logs.go),第一步调用 selectLogsContainers(pkg/compose/logs.go):
- 带
--index:调用getSpecifiedContainer按索引精确定位单个容器(同时排除 one-off 容器,如run产生的一次性容器),直接返回该容器; - 不带
--index:调用getContainers按服务名收集所有相关容器(同样排除 one-off); - 显式
-f compose.yaml但没传服务名时:只考虑该文件里定义的服务(options.Services = options.Project.ServiceNames()),并过滤掉同项目下其他 Compose 文件贡献的容器,避免串味。
e2e 测试 pkg/e2e/logs_test.go 的 TestLocalComposeLogs 用上面的 logs-test 夹具验证了这些行为:聚合全部服务输出、单服务过滤掉其他服务、多服务名同时输出、--index 2 只取 hello-2 而不包含 hello-1。
并发读取:errgroup 与 stdcopy 解复用
Logs 的核心并发模型(pkg/compose/logs.go):
eg, ctx := errgroup.WithContext(ctx)
for _, ctr := range containers {
eg.Go(func() error {
return s.logContainer(ctx, consumer, ctr, options)
})
}
每个容器一个 goroutine 并行拉日志,eg.Wait() 等待全部结束(非 follow 模式下通常是流读完毕)。单个容器的读取流程 logContainer → doLogContainer:
- 先
ContainerInspect拿容器详情(需要Config.Tty字段); - 调
ContainerLogsAPI 拿到合并的日志流; - 按 TTY 分路解复用(pkg/compose/logs.go):非 TTY 容器使用
stdcopy.StdCopy把 stdout/stderr 多路复用流拆成两条,逐行回调consumer.Log;TTY 容器则直接io.Copy原样透传(TTY 流本身不带 stdcopy 8 字节头)。 - 容错处理:若引擎返回
NotImplemented(日志驱动不支持读取日志),只打一条Warn而不是使整个命令失败:
if errdefs.IsNotImplemented(err) {
logrus.Warnf("Can't retrieve logs for %q: %s", getCanonicalContainerName(ctr), err.Error())
return nil
}
单元测试 TestComposeService_Logs_Demux(pkg/compose/logs_test.go)用手工构造的 stdcopy 多路复用流验证了"stdout/stderr 两路写入、解复用后顺序回调"这一行为。
--follow 的深层机制:容器事件监控
-f/--follow 不只是把 Follow: true 传给容器日志 API。当 options.Follow 为真时,Logs 会额外启动一个 monitor 监听本项目容器的引擎事件流(pkg/compose/logs.go):
if options.Follow {
printer := newLogPrinter(consumer)
monitor := newMonitor(s.apiClient(), projectName)
// 按 --index 指定的服务或整个项目过滤要监听的服务
monitor.withListener(printer.HandleEvent)
monitor.withListener(s.followStartedContainersLogs(ctx, eg, consumer, options))
eg.Go(func() error {
// pass ctx so monitor will immediately stop on SIGINT
return monitor.Start(ctx)
})
}
monitor 的实现(pkg/compose/monitor.go)值得理解两点:
- 事件过滤:
Start()消费EventsAPI 流,按项目过滤器(projectFilter(c.project))+type=container+ one-off 排除标签过滤,并按com.docker.compose.service标签判断该容器是否属于当前监听范围(watched(),空服务集表示整个应用)。 - 对 restart 策略的精确处理:源码注释详细列出了 Engine 侧各种容器生命周期对应的真实事件序列(进程自行退出、restart policy 重启、
stop/kill/rm、OOM 等),onContainerDie会ContainerInspect检查State.Restarting——若容器配置了退即重启策略,事件标记为Restarting=true,由printer.HandleEvent(pkg/compose/printer.go)输出为:
exited with code N (restarting)
而非普通的 exited with code N。
- 跟随期间新启动/重启的容器自动纳入日志流:
followStartedContainersLogs(pkg/compose/logs.go)作为事件监听器,凡收到ContainerEventStarted事件就新开一个 goroutine,以该容器的State.StartedAt作为Since从本次启动点开始流式读取日志。这意味着 follow 模式下容器被重启、scale 扩容新副本后,其日志会自动接入输出。e2e 测试TestLocalComposeLogsFollow(pkg/e2e/logs_test.go)正是验证这一点:先 followping服务,再依次up新服务hello、--scale ping=2,并轮询等待输出中出现hello-1、ping-2前缀。
实用操作示例
结合以上机制,几组常见用法:
# 只看最近 42 分钟的日志
docker compose logs --since 42m
# 只看某时间窗口:2024-01-01 之后、2 小时之前产生的日志
docker compose logs --since 2024-01-01T00:00:00Z --until 2h
# 每个容器只取末尾 100 行,便于快速排障
docker compose logs --tail 100
# 实时跟踪并带时间戳(注意时间戳为 CLI 本地时间)
docker compose logs -f -t
# 脚本处理:去掉颜色与前缀,只取 stdout 流
docker compose logs --no-color --no-log-prefix ping 1> /tmp/ping.log
多副本服务的副本索引语义:--index N 选取该服务的第 N 个容器(hello-1、hello-2 …),这要求恰好指定一个服务,否则命令在参数校验阶段即失败。
相关源码与测试索引
| 内容 | 路径 |
|---|---|
命令定义、flag 注册与 --index 校验 |
cmd/compose/logs.go |
LogOptions / LogConsumer API 定义 |
pkg/api/api.go |
Logs 主流程、容器选择、并发读取、follow 接入 |
pkg/compose/logs.go |
| 容器事件监控与 restart 策略事件序列说明 | pkg/compose/monitor.go |
| 退出/重建状态行打印 | pkg/compose/printer.go |
| 前缀对齐、颜色分配、时间戳与 stdout/stderr 分流 | cmd/formatter/logs.go |
| stdcopy 解复用单元测试 | pkg/compose/logs_test.go |
e2e 场景:聚合、过滤、--index、follow |
pkg/e2e/logs_test.go |
| e2e 日志测试夹具 | pkg/e2e/fixtures/logs-test/compose.yaml |
小结
docker compose logs 表面上是一条"把日志打出来"的命令,实际包含四段清晰的职责划分:CLI 层(cmd/compose/logs.go)负责 flag 校验与服务名补全,API 层(pkg/api/api.go)用 LogOptions/LogConsumer 解耦选项与输出,服务层(pkg/compose/logs.go、pkg/compose/monitor.go)负责容器选择、并发流式读取与容器生命周期事件追踪,格式化层(cmd/formatter/logs.go)负责前缀对齐、着色与时间戳。理解这条调用链后,--index 的单服务限制、attach: false 服务的默认排除、-t 时间戳的本地生成、follow 模式下自动接入重启容器日志等行为,都有了源码层面的确定答案。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00