第三章 日志文件结构解析
拿到一份 Cromwell 日志,第一眼可能觉得它又臭又长。其实它的结构非常清晰。我刚开始接触 Cromwell 时,也对着满屏的日志发过懵。后来我总结出了一个规律——你只要找到三个关键区域,就能快速定位问题。
说白了,Cromwell 的日志文件就是一个 WDL 执行过程的录音带。它记录了三件事:我要做什么、我打算怎么做、我实际做了什么。把这三块拆开,你就掌握了日志的命脉。
3.1 日志文件的“三明治”结构
Cromwell 的日志文件大致可以分为三部分。我个人习惯把它叫做“三明治”。上层是 WDL 描述,中间是元数据,下层是执行记录。
| 层次 | 名称 | 主要内容 | 关键词 |
|---|---|---|---|
| 顶层 | WDL 描述块 | Workflow 和 Task 的定义、输入输出声明 | workflow, task, input, output, String, Int, File |
| 中间层 | 元数据块 | 运行状态、启动/结束时间、重试次数、资源需求 | status, start, end, attempt, preemptible, runtime |
| 底层 | 执行记录块 | 具体命令、标准输出/错误、退出码、Cromwell 内部调用轨迹 | command, stdout, stderr, exit_code, call_caching, docker |
为什么要区分这三部分?因为排查问题的思路不一样。出 Bug 了,你通常先看执行记录块,再看元数据块,最后才回头检查 WDL 描述。这个顺序能帮你省下至少一半的时间。我在项目中遇到过多次,新手一上来就盯着 WDL 描述改来改去,结果真正的问题出在运行时依赖上。
3.2 WDL 描述块解析
WDL 描述块是日志的开头部分。它记录了 Workflow 和 Task 的原始定义。你写的 WDL 脚本在提交给 Cromwell 后,Cromwell 会把它展开、合并依赖,然后写入日志。
看下面这个例子。它看起来有点乱,但核心信息都在。
workflow my_workflow {
input {
String sample_name
File fastq_r1
File fastq_r2
}
call align {
input:
reads_r1 = fastq_r1,
reads_r2 = fastq_r2
}
output {
File aligned_bam = align.aligned_bam
}
}
task align {
input {
File reads_r1
File reads_r2
}
command {
bwa mem -t 4 ${reads_r1} ${reads_r2} > aligned.sam
}
output {
File aligned_bam = "aligned.sam"
}
runtime {
docker: "biocontainers/bwa:v0.7.17_cv1"
memory: "8GB"
cpu: 4
}
}
嗯,这里要注意了。日志里的 WDL 描述块,和你写的源文件可能不完全一样。Cromwell 在解析时,会对变量名做标准化,比如把 sample_name 变成 sample_name,但也会展开一些宏或者默认值。你乍一看会觉得格式不对,其实它没丢信息。
有一次我排查一个奇怪的错误,明明 WDL 里写了 String sample_name,但 Cromwell 跑起来后日志里显示 sample_name = null。我当时也纳闷,后来才发现是输入 JSON 文件里没传这个参数。所以,WDL 描述块不只是给你看的,更是 Cromwell 自己用来做参数校验的。
3.3 元数据块解析
元数据块,是 Cromwell 的“驾驶舱仪表盘”。它告诉你每个 Workflow 和 Task 的实时状态。
我截取一段元数据块的核心内容给你看看。
status: Running
start: 2024-03-15T10:32:17.123Z
end: null
attempt: 1
preemptible: true
runtimeAttributes:
memory: "8GB"
cpu: 4
这里的信息量其实很大。我一条一条说。
- status:状态字段。常见状态有
Submitted、Running、Done、Failed、Aborted。注意,如果状态长时间停留在Running,可能意味着任务挂住了。 - start 和 end:时间戳。格式是标准的 ISO 8601。差值就是任务执行时长。如果
end为null,说明任务还没结束。 - attempt:重试次数。Cromwell 默认最多重试 2 次。如果你看到
attempt: 3及以上,可能不是单纯的重试了,而是 Cromwell 在自动执行scatter或sub-workflow里的任务。 - preemptible:是否允许抢占式实例。云上跑任务时这个字段特别重要。如果设了
true但 Cromwell 还是执行了非抢占实例,排查方向就变了。 - runtimeAttributes:运行时属性。这里写明了 Cromwell 给这个任务分配的资源和镜像。
preemptible: true 看了半天,以为 Cromwell 会自己选择机器类型。结果发现它只是记录了这个配置,真正决定使用什么机器的是后端调度器。所以,元数据块的信息需要和后端日志对照着看,不能单独信它。
3.4 执行记录块解析
执行记录块,是日志里最核心、也最长的部分。它详细记录了 Cromwell 是如何执行每个 Task 的。包括实际运行的命令、标准输出、标准错误、退出码,以及 Cromwell 内部的一些调用轨迹。
command: bwa mem -t 4 /reference/hg38.fa /input/sample_R1.fastq.gz /input/sample_R2.fastq.gz
stdout (last 10 lines):
[M::bwa_idx_load_from_disk] read 0 ALT contigs
[M::process] read 100000 sequences (10000000 bp)...
stderr (first 5 lines):
[W::bwa_idx] fail to locate the index files
exit_code: 1
call_caching:
hit: false
effective: false
result: cache miss (input hash mismatch)
这块信息最实用。我经常用它做三件事。
- 看命令:先检查
command里实际运行的命令,和你预期的一不一样。有时候 WDL 里拼接路径出错,日志里就会显示奇怪的路径。比如/home/user//reference/hg38.fa(多了一个斜杠)。 - 看退出码:
exit_code: 1表示失败。但有时候退出码是0任务也失败了?这种情况常见于工具本身不返回错误码,但产生了空文件。所以退出码要结合 stdout 和 stderr 一起看。 - 看 call_caching:这个字段挺关键的。如果你的任务用了 cache,但日志里显示
hit: false,说明 Cromwell 没有命中缓存。至于为什么没命中,result字段会给出原因,比如 input hash mismatch(输入文件哈希值不一样)。这是排查重复运行任务的利器。
我在项目中遇到过最典型的一个情况:某次批量分析,同样的样本跑了两遍。我一开始以为是调度出 Bug 了。后来查日志发现 call_caching.hit: false,原因是输入文件的路径虽然一样,但时间戳变了,导致 Cromwell 认为输入不一样。所以,如果你用缓存,建议在 WDL 里显式控制 cache 的策略。
command、stdout、stderr、exit_code 这四个字段拼在一起,基本就能在本地重现 Cromwell 的行为。
3.5 日志结构图:一张图看懂
为了帮你更直观地理解这三块之间的关系,我画了一张结构图。它展示了日志文件的“三明治”结构,以及每一块的入口关键词。
这张图你看明白了吗?日志自上而下,从定义到执行。WDL 描述块在最上面,它是“蓝图”。执行记录块在最下面,它是“施工日志”。中间层元数据块则是“监理报告”。
排查问题时,你先去最下面看“施工日志”,找出具体报错信息。如果没有,再回头看“监理报告”里的状态和时间戳。最后才回到“蓝图”检查是不是定义错了。
3.6 本章小结
日志结构听起来简单,但你能吃透它,后面所有分析工具和方法才能落地。我见过很多同事,拿着一个复杂的日志文件,上来就 grep “error” 找输出。但有时候异常根本不在 error 里,而是在 WDL 描述块里某个变量名字写错了。
所以我的建议是:先看结构,再看内容。每次打开 Cromwell 日志,默念“三明治”:WDL 描述、元数据、执行记录。然后根据你要解决的问题,按顺序往下走。
嗯,等你把这三块结构都摸熟了,后面几章再讲日志级别分类、过滤工具、可视化面板,你就能融会贯通了。千万别跳过基础去追高级技巧——地基不稳,楼再高也白搭。