日志的价值不在于数量,而在于它能否回答问题。发生了什么、处于哪个阶段、花了多久、最终结果是什么,这四项往往比一长段“正在执行”更有用。
记录阶段,而不只是记录异常
一次处理可能包含读取、解析、转换和写入。如果只在最终失败时记录错误,就很难判断问题从哪里开始。
import logging
import time
log = logging.getLogger(__name__)
def run_job(job_id, operation):
started = time.perf_counter()
log.info("job_started id=%s", job_id)
try:
result = operation()
except Exception:
log.exception("job_failed id=%s elapsed_ms=%.1f",
job_id, (time.perf_counter() - started) * 1000)
raise
log.info("job_finished id=%s elapsed_ms=%.1f",
job_id, (time.perf_counter() - started) * 1000)
return result用于计算耗时的计时器应关注时间间隔。展示事件发生时间则使用统一的时区和时间格式。两者用途不同,不需要混为一谈。
让同一次操作有共同标识
多个任务并发时,日志会交错输出。为一次操作分配共同标识,能把相关事件重新串起来。这个标识应该是内部生成的随机值或任务编号,而不是邮箱、手机号或访问凭据。
给日志设置边界
不应直接输出完整配置、请求头或表单内容。调试时需要的信息,通常可以缩减为字段名称、数据类型、长度或经过脱敏的摘要。
- 密码、令牌与私钥不写入日志。
- 异常堆栈保留定位信息,但检查异常消息中是否混入输入内容。
- 为长期运行的程序设置轮转和保留期限。
- 调试级别只在确有需要时开启。
根据问题选择粒度
怀疑某一步慢,就记录各阶段耗时;怀疑输入不符合要求,就记录校验失败的字段;怀疑任务重复执行,就记录开始、结束与任务标识。
一份好的排错记录应当能让另一个人复核结论,同时不需要接触原始敏感数据。
先提出明确的问题,再决定记录什么,通常比“把所有东西都打印出来”更快找到答案。