跑一批本地 CLI / 定时脚本时,最烦的不是报错本身,而是:终端刷屏后现场没了,或者多进程一起写,日志串成一团,排错只能靠猜。下面这套做法只解决一件事:日志怎么分级、怎么落盘、排错时怎么一次对齐到某次运行。

先定四级,再谈写文件
级别只保留四个,数字越大越「重」:
| 级别 | 数值 | 适合写什么 |
|---|---|---|
debug |
10 | 入参、中间态、分支判断,本地排错开 |
info |
20 | 正常里程碑:开始、跳过、完成 |
warn |
30 | 还能继续,但结果可能打折 |
error |
40 | 失败、未捕获异常、需要人介入 |
过滤规则很直接:当前阈值阈值,低于阈值的调用直接 return,不打印也不写盘。默认开到 debug,本地脚本库通常宁可多记一点;上线或定时任务再收紧。
用配置改默认级别,别改业务代码
仓库根放一份 env.toml,例如:
toml
[logger]
level = "info"
读配置时把 warning 归一成 warn;缺文件、段写错、值非法,一律回落到 debug。单次运行也能在构造时传入 level=,盖过配置,方便「只这一次开全量」。
业务里怎么选级别
- 成功路径的关键节点:
info - 可恢复的降级(重试、跳过某步):
warn - 失败与堆栈:
error/exception - 只有你坐在机器前才需要看的细节:
debug
别把大段二进制、密钥、Cookie 打进日志;需要核对身份时只打脱敏后的短标识。
落盘约定:一天一个文件,一行一条现场
路径固定成项目根下的 logs/<YYYY-MM-DD>_run.log,追加不覆盖。同一天多次运行、多个脚本,都往同一个日文件里写,靠行内字段区分,而不是一天拆出几十个小文件。
项目根怎么找:从当前工作目录(或脚本路径)向上走,碰到带 pyproject.toml 的目录就停 。不靠 .git、不靠硬编码 ../..,换机器、换启动 cwd 也不容易写飞。
推荐用法是上下文管理器,保证异常路径也走同一套记录逻辑:
python
from core.logger import Logger
with Logger("sync-job") as log:
log.info("[OK] 开始同步")
log.debug(f"batch_size={batch_size}")
# ...
log.info("[OK] 同步完成")
标准行长什么样
每一行(毫秒时间 | 任务编号 | 级别 | 脚本相对路径 | 正文):
text
14:32:08.127 | 8F3K2A9B1C | INFO | cron/jobs/sync.py | [OK] 开始同步
- 时间 :
HH:mm:ss.SSS,对齐同秒内的先后 - 任务编号 :一次
Logger实例一个雪花 ID;同一次运行里所有行共享,grep 它就能抽出完整时间线 - 级别:大写,方便肉眼扫
- 脚本路径:相对项目根的 POSIX 路径,多脚本共写一日志时一眼知道是谁打的
终端和文件写同一行内容:print(..., flush=True) 同步刷屏,文件侧再追加。排错时「刚才终端闪过的那行」和磁盘上的行是同一格式。

多进程追加怎么不丢、不乱
多个进程往同一个日文件追加时,用跨进程排他锁(例如 portalocker)包住「打开 → 写入 → flush」。等锁设上限(实现里常见是数秒级);超时后仍尝试无锁追加一次,优先保证「终端已经打出的行」尽量落盘,而不是为了锁干净直接丢行。写完即释放,别把锁拖到整个任务结束。
还有一个容易忽略的细节:子进程 / 子任务如果已经按同一标准格式打过日志,父进程再包一层前缀会变成「日志套日志」。判断「看起来已经是标准行」时,只再打印到终端,不再二次写盘、不再套前缀。

排错:用任务编号对齐一次运行
出问题后不要从文件头开始读。先拿终端里任意一行的任务编号,再:
bash
rg "8F3K2A9B1C" logs/2026-09-03_run.log
同一编号下,按时间顺序就是那次运行的完整轨迹。需要按脚本收窄时,再叠加路径片段过滤。
异常要把堆栈写进同一次任务
python
try:
do_work()
except Exception as exc:
log.exception("同步失败", exc)
raise
exception 先打一条 error 说明,再把堆栈按行写出;续行带 | 前缀,和正文区分开,但仍挂在同一个任务编号下。事后 grep 编号,原因和栈在一起,不用再翻别的 dump。
用 with Logger(...) 时,未捕获异常会在退出上下文时再记一条;KeyboardInterrupt / SystemExit 不当作故障去刷「未捕获异常」------用户 Ctrl+C 是正常中止,CLI 侧通常只留一行 [SKIP] 已中断 并以退出码 130 结束即可。
落地时的几条硬约束
- 统一入口 :业务只走同一个
Logger,别再print一套、logging再一套。 - 状态词固定 :成功 / 跳过 / 失败用
[OK]/[SKIP]/[FAIL]/[ERROR],和级别配合,扫日志更快。 - 资源与日志无关但也要收尾:进程退出前关掉浏览器、子进程、文件句柄;日志只能告诉你「死在哪」,清不掉孤儿进程。
- 日志目录当运行时数据 :
logs/不要当源码提交;需要留证时按天拷走即可。
按这套约定,分级决定「记多少」,日文件 + 标准行决定「存在哪、长什么样」,任务编号决定「怎么一次捞齐现场」。下次脚本又挂在半路,先复制编号,再打开当天的 *_run.log。