Docker Compose CLI 中docker compose logs命令解析:日志聚合、跟随输出与源码实现
【免费下载链接】composeDefine and run multi-container applications with Docker项目地址: https://gitcode.com/GitHub_Trending/compose/compose
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 的ContainerLogsAPI(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/LogConsumerAPI 定义 | 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 模式下自动接入重启容器日志等行为,都有了源码层面的确定答案。
【免费下载链接】composeDefine and run multi-container applications with Docker项目地址: https://gitcode.com/GitHub_Trending/compose/compose
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考