6.2 留下可检索的日志
前置知识:读懂错误与调用栈
预计时间:20 分钟
练习环境:Python 3.9+ 标准库;日志写入临时目录
完成标志:能写出可查询且不泄密的日志,并知道去哪里找它
终端关掉后,证据还要在
程序在无人查看终端时崩溃,排障仍需要知道何时失败、处理哪个请求、经过哪些步骤。一直开着终端不叫可观测性,顶多叫屏幕很敬业;真正需要的是可收集、可检索、可关联的事件记录。
日志、输出流和存储位置
日志是应用记录的事件。日志框架负责级别、字段、格式和路由,最终目的地则取决于部署:
- 本地程序可以写控制台或文件;
- systemd 服务可以由 journal 接收;
- 容器通常写
stdout/stderr,再由运行时或集群收集; - 集中式系统会把多台机器的记录发送到统一后端。
因此,“生产环境没人看 stdout”和“日志一定在 /var/log”都不成立。先读项目配置和部署说明。
# 文件日志
tail -n 100 application.log
less application.log
# systemd 服务
journalctl -u myapp.service --since '30 minutes ago'
# Kubernetes 容器
kubectl logs pod/myapp --since=30m一条有用的日志长什么样
{"timestamp":"2026-06-23T14:32:15.456Z","level":"ERROR","event":"job.failed","service":"catalog-api","request_id":"req-7f2a","item_id":"item-1024","message":"item unavailable"}至少考虑这些字段:
| 字段 | 原因 |
|---|---|
| 带时区时间戳 | 跨机器对齐时间;通常使用 UTC |
| 级别 | 按团队策略控制和筛选详细度 |
| 稳定事件名 | 查询不依赖自然语言措辞 |
| 服务、版本、环境 | 确认谁产生了记录 |
| 请求或关联 ID | 串起一次操作的多条记录 |
| 业务标识 | 定位对象,但必须最小化并符合数据政策 |
| 结果与异常 | 说明发生什么,必要时保留异常链 |
结构化日志不只等于“输出 JSON”。字段名和类型也要稳定,例如 duration_ms 始终写数字。
日志级别没有跨项目的绝对定义
常见框架提供以下级别,但具体含义应写进团队规范:
| 级别 | 一种实用约定 | 常见噪声 |
|---|---|---|
| TRACE | 短时间观察极细执行路径 | 在高频循环中常开 |
| DEBUG | 定向排障上下文 | 输出完整对象或请求正文 |
| INFO | 正常但值得保留的状态变化 | 每个函数入口都记录 |
| WARN | 异常条件出现,但操作仍可继续 | 每一次正常重试都告警 |
| ERROR | 当前操作失败或需明确处置 | 预期的用户输入错误全部升为 ERROR |
配置级别是阈值。例如某 logger 配为 INFO,通常会保留 INFO 及更严重事件,过滤 DEBUG 和 TRACE。不同框架还可能提供 FATAL、CRITICAL 或自定义级别。
“开发固定 DEBUG、生产固定 INFO”只是起点,不是规则。高流量服务可能要更克制;事故排查时可只把某个 logger 临时调细,并约定恢复时间。
动手:用 Python 标准库写文件日志
先建立隔离目录,再把下面内容保存为 worker.py;实验只用 Python 标准库,basicConfig 的 encoding 参数需要 Python 3.9+:
log_workdir=$(mktemp -d)
cd "$log_workdir"import logging
logging.basicConfig(
filename="application.log",
encoding="utf-8",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S%z",
)
logger = logging.getLogger("catalog.worker")
def process_item(item_id: str) -> None:
logger.info("job.started item_id=%s", item_id)
try:
raise RuntimeError("item unavailable")
except RuntimeError:
logger.exception("job.failed item_id=%s", item_id)
raise
process_item("item-1024")status=0
python3 worker.py || status=$?
printf 'status=%s\n' "$status"
tail -n 20 application.log
grep -F 'item_id=item-1024' application.log这个示例写的是带字段的文本日志,前面的 JSON 是另一种记录格式。它没有配置轮转;grep 只匹配带该 ID 的行,不会自动带出紧随其后的多行异常栈,完整证据仍需查看原文件。
logger.exception() 应在异常处理块中调用,它会附上当前异常栈。示例重新抛出异常,所以进程以非零状态结束。保存证据后清理:
cd
ls -ld -- "$log_workdir"
rm -r -- "$log_workdir"真实应用中要约定记录边界。若底层、服务层和入口层都“记录后重抛”,同一个失败会生成多条重复 ERROR。常见做法是在能加入足够业务上下文且确定处置结果的一层记录一次。
print() 和日志库各做什么
print() 或 System.out 适合:
- 命令行工具向用户输出正常结果;
- 教学示例展示一个值;
- 本地短时验证控制流。
它们不能单独提供统一的级别、上下文、字段、路由和保留策略。生产应用应通过日志框架产生可管理记录;框架仍可以把这些记录送到标准输出,供容器平台采集。
不能写进日志的内容
以下内容应按字段分类处理;密码、令牌和密钥默认不记录,必要的标识才考虑经审查的脱敏方案。散列或加密不会自动让敏感数据变得适合进入日志:
- 密码、访问令牌、API 密钥和加密密钥;
- Cookie、会话标识和数据库连接串;
- 不必要的个人信息、健康信息和客户数据;
- 完整请求正文、支付数据和源码;
- 未处理的用户输入中的换行或控制字符。
不要靠开发者每次手动记住。优先在日志封装、序列化器或采集管道中统一过滤,并用测试覆盖。
轮转、保留和权限
日志写入磁盘时至少要回答:
- 按大小还是时间切分?
- 保留多少份、多少天?
- 何时压缩和删除?
- 磁盘接近上限时谁告警?
- 哪些账号可读取或修改?
轮转负责切分文件,保留策略决定保存期限。日志既不能早于合规期限被删除,也不应无限期保存。发送到远端时要保护传输通道。
日志、指标和追踪
| 信号 | 擅长回答 |
|---|---|
| 日志 | 某个离散事件发生了什么 |
| 指标 | 错误率、延迟、吞吐量怎样变化 |
| 追踪 | 一次请求经过哪些组件,各花了多久 |
把 trace_id、span_id 或请求 ID 注入日志,可把事件串回一次操作。但一个自定义请求 ID 本身不等于完整追踪。
离开这一页前
- 运行 Python 示例,确认文件日志、异常栈和非零退出码;
- 写一条包含时间、级别、事件名、服务名和请求 ID 的结构化日志;
- 列出至少五类不得原样记录的数据;
- 解释容器为何常把日志写到标准输出;
- 说明轮转与保留策略的区别。
参考资料
- Python Logging HOWTO
- OWASP Logging Cheat Sheet
- Apache Log4j:日志级别
- Kubernetes:日志架构
- OpenTelemetry Logs specification
现场已经能被保存和关联。接下来要把错误安全地交给搜索工具或另一位开发者,并让对方真的复现出来。