首页
/ Docker Compose CLI 中 `docker compose logs` 命令解析:日志聚合、跟随输出与源码实现

Docker Compose CLI 中 `docker compose logs` 命令解析:日志聚合、跟随输出与源码实现

2026-09-05 11:57:28作者:伍希望

docker compose logs 是查看 Compose 应用日志的核心命令:它把项目中一个或多个服务的容器日志汇聚到终端,支持按服务过滤、指定副本索引、时间窗口截取、尾行数控制以及实时跟随输出。本文以本仓库的命令参考文档 docs/reference/compose_logs.md 为主体,完整梳理该命令的全部参数,并深入 cmd/compose/logs.gopkg/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 2docker 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.godoLogContainer 时,这些字段被逐字段透传给 Docker Engine 的 ContainerLogs API(TailSinceUntilFollowTimestamps),因此时间解析、相对时间换算等语义完全由 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.gologConsumer 中,它实现了 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):

  1. 每个容器一个 presenterregister(name) 为每个容器名注册展示器;当开启彩色输出(未传 --no-color)时,每个容器会被分配一个循环调色色(nextColor()),空名容器使用单色。
  2. 前缀宽度自适应computeWidth() 遍历所有已注册容器名,取最长名长度加 1 作为统一前缀宽度,setPrefixfmt.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))
}
  1. --no-log-prefix 的语义。在 cmd/compose/logs.go 中,--no-log-prefix 被取反后传入 NewLogConsumer(..., prefix, ...);未开启前缀时日志行不再带"容器名 | "对齐前缀,适合管道重定向场景。
  2. -t/--timestamps 的行为细节。注意时间戳是在 CLI 侧打印时按本地时间生成的:write() 中用 time.Now().Format(jsonmessage.RFC3339NanoFixed) 为每一行补时间戳(cmd/formatter/logs.go)。
  3. 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.Logspkg/compose/logs.go),第一步调用 selectLogsContainerspkg/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.goTestLocalComposeLogs 用上面的 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 模式下通常是流读完毕)。单个容器的读取流程 logContainerdoLogContainer

  1. ContainerInspect 拿容器详情(需要 Config.Tty 字段);
  2. ContainerLogs API 拿到合并的日志流;
  3. 按 TTY 分路解复用pkg/compose/logs.go):非 TTY 容器使用 stdcopy.StdCopy 把 stdout/stderr 多路复用流拆成两条,逐行回调 consumer.Log;TTY 容器则直接 io.Copy 原样透传(TTY 流本身不带 stdcopy 8 字节头)。
  4. 容错处理:若引擎返回 NotImplemented(日志驱动不支持读取日志),只打一条 Warn 而不是使整个命令失败:
if errdefs.IsNotImplemented(err) {
    logrus.Warnf("Can't retrieve logs for %q: %s", getCanonicalContainerName(ctr), err.Error())
    return nil
}

单元测试 TestComposeService_Logs_Demuxpkg/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)值得理解两点:

  1. 事件过滤Start() 消费 Events API 流,按项目过滤器(projectFilter(c.project))+ type=container + one-off 排除标签过滤,并按 com.docker.compose.service 标签判断该容器是否属于当前监听范围(watched(),空服务集表示整个应用)。
  2. 对 restart 策略的精确处理:源码注释详细列出了 Engine 侧各种容器生命周期对应的真实事件序列(进程自行退出、restart policy 重启、stop/kill/rm、OOM 等),onContainerDieContainerInspect 检查 State.Restarting——若容器配置了退即重启策略,事件标记为 Restarting=true,由 printer.HandleEventpkg/compose/printer.go)输出为:
exited with code N (restarting)

而非普通的 exited with code N

  1. 跟随期间新启动/重启的容器自动纳入日志流followStartedContainersLogspkg/compose/logs.go)作为事件监听器,凡收到 ContainerEventStarted 事件就新开一个 goroutine,以该容器的 State.StartedAt 作为 Since 从本次启动点开始流式读取日志。这意味着 follow 模式下容器被重启、scale 扩容新副本后,其日志会自动接入输出。e2e 测试 TestLocalComposeLogsFollowpkg/e2e/logs_test.go)正是验证这一点:先 follow ping 服务,再依次 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-1hello-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.gopkg/compose/monitor.go)负责容器选择、并发流式读取与容器生命周期事件追踪,格式化层(cmd/formatter/logs.go)负责前缀对齐、着色与时间戳。理解这条调用链后,--index 的单服务限制、attach: false 服务的默认排除、-t 时间戳的本地生成、follow 模式下自动接入重启容器日志等行为,都有了源码层面的确定答案。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384