日志字段先定准

日志字段最常见的问题,是大家都在写日志,却没人能靠日志复盘。线上接口报错时,日志里有“请求失败”;任务失败时,日志里有“处理异常”;用户投诉时,日志里有一堆 print。看起来信息很多,真正要定位时却缺关键字段:哪个用户、哪个请求、哪个版本、哪个下游、耗时多少、输入摘要是什么。
我见过一次 Python 服务排查,错误日志很密集,但每条只有异常堆栈,没有 requestId。值班同学只能按时间倒推,去网关日志、业务日志、数据库日志之间来回翻。最后定位到某个批量导入任务触发了下游限流,但这个结论花了很久。如果一开始日志字段就统一,复盘会快很多。
日志不是写给程序看的,是写给未来排查问题的人看的。Python 日志先定字段,说的是先把每条日志的基本身份、业务上下文和技术上下文定义清楚,再谈格式和采集平台。
message 不能承担所有信息
很多日志只有一个 message 字段,比如“create order failed”。开发者觉得自己写清楚了,但排查时还要知道订单号、用户、接口、错误类型、下游返回、耗时、重试次数。把这些都塞进 message,后续检索和聚合都会很难。
结构化日志的价值是让字段可查询。比如 request_iduser_idorder_idserviceroutestatuscost_mserror_type。你可以按 request_id 串链路,按 error_type 聚合,按 cost_ms 找慢请求,按 route 看某个接口。
message 仍然有用,但它应该负责补充人能读懂的描述,而不是承担全部上下文。一个好日志可以这样理解:字段负责机器检索,message 负责人类解释。
request_id 是最低成本的链路线索
如果只能先改一个字段,我会先加 request_id。没有 request_id,排查跨服务问题就像拼散落的纸片。入口收到请求时生成或透传 request_id,后续所有日志都带上它,下游调用也尽量透传。这样一次请求的路径就能串起来。
Python Web 框架里可以通过中间件处理。请求进来时从 header 读取 X-Request-Id,没有就生成一个;把它放到上下文变量里,日志 formatter 从上下文取。异步框架要注意上下文传播,不能用简单全局变量。
  1. from contextvars import ContextVar

  2. request_id_var = ContextVar("request_id", default="-")

  3. def get_request_id():
  4. return request_id_var.get()
python
这段代码很简单,真正要做的是让所有日志统一读取它,而不是每个业务函数手动传。手动传字段容易漏,中间件和日志封装更稳定。
业务字段不要越多越好
一听结构化日志,有人会把所有参数都打出来。这样会带来两个问题:日志太大,隐私风险太高。业务字段要选能帮助定位的,而不是把请求体原样塞进去。
比如订单接口,订单号、用户 id、渠道、支付方式、业务状态有价值;完整地址、手机号、身份证、备注不应该直接打印。导入任务可以记录文件 id、任务 id、行号范围、失败类型,不一定要把每行原始数据写进日志。
字段选择要结合排查路径。一个字段如果经常被用来定位问题,就应该标准化;一个字段只是偶尔好奇,不一定进每条日志。日志不是数据仓库,不能无限承载业务明细。
错误类型要稳定,不要只靠异常文本
异常文本经常变化,不能作为稳定聚合维度。比如下游限流、参数错误、权限失败、数据库超时,都应该有稳定的 error_type。这样你能看到“限流错误今天突然上升”,而不是面对一堆不同文本。
Python 里可以给业务异常定义 code,也可以在捕获外部异常时映射类型。不要所有异常都记成 Exception。同时要保留原始异常信息,方便深入排查。稳定类型用于聚合,原始堆栈用于定位细节。
错误日志还要区分可恢复和不可恢复。重试中的失败、最终失败、用户输入错误、系统依赖错误,处理方式不同。如果都打成 error,告警会被噪音淹没。可以把用户输入错误放 warning,把最终系统失败放 error,把可预期的业务拒绝放 info warning。
耗时字段要贴近动作
接口总耗时有用,但不足以定位慢在哪里。一次请求里可能查缓存、查数据库、调用下游、写消息。总耗时 2 秒,你需要知道是哪一步慢。关键动作应该单独记录 cost_ms。
不要只在函数外面打开始和结束日志。开始日志多了会产生噪音,结束日志如果包含状态和耗时,价值更高。比如 db_query_doneremote_call_donetask_step_done。每条日志有 step、cost_ms、status,排查时能直接看到卡点。
对于批处理任务,耗时还要带数量。处理 100 条花 1 秒和处理 10000 条花 1 秒不是一回事。记录 item_count、batch_size、success_count、fail_count,才能判断吞吐。
日志级别要服务告警
日志级别不是情绪标签。debug 用于开发细节,info 用于关键状态变化,warning 用于异常但可恢复,error 用于需要关注的失败。级别乱了,告警就会乱。
很多系统 error 太多,最后没人看。比如用户输错密码、参数校验失败、业务规则拒绝,如果每天大量出现,不应该全部打 error。真正需要 error 的,是系统无法按预期完成:依赖失败、数据不一致、任务最终失败、不可恢复异常。
同时也不要把严重问题降成 info。为了减少告警噪音而降级,只是把问题藏起来。正确做法是细分 error_type 和告警规则,而不是让日志级别失真。
本地调试和线上日志要分开设计
开发时喜欢看详细日志,线上需要可检索、低噪音、低风险。这两者不完全一样。本地可以打印更多上下文,线上要控制字段、脱敏、采样和级别。
对于高频日志,可以采样或只在异常时打印。比如每次循环处理一条数据都打 info,线上会很快淹没。可以改成每批打一次汇总,失败时打详情。日志不是越多越安全,过多日志会增加成本,也会让真正问题更难看见。
脱敏要在日志封装层做,而不是靠每个业务开发自觉。手机号、邮箱、身份证、token、地址、银行卡等字段要统一处理。不要等安全审计时才发现日志里存了敏感数据。
线上日志还要考虑保留周期。排查近期问题需要详细日志,审计可能需要更长周期,但高频字段不一定都要长期保存。不同日志可以分层:业务关键事件保留久一点,高频 debug 采样保留短一点,敏感字段尽量不落盘。日志成本不是小事,成本压力一上来,团队容易直接删日志;如果一开始就分层,后面会从容很多。
对于 Python 批处理任务,日志策略也要和 Web 请求不同。批处理更关心任务 id、批次、分片、当前步骤、处理数量和失败样本。每条数据都打日志会爆炸,完全不打又无法定位。可以每批输出汇总,失败记录少量样本,最终输出任务结果。这样既能看整体,也能追局部问题。
字段命名要统一,不要各写各的
结构化日志还有一个常见坑:字段含义相同,名字不同。一个服务写 request_id,另一个写 traceId,第三个写 rid;用户字段有人写 uid,有人写 user_id。检索时就要写一堆条件,聚合也容易漏。
字段命名最好有团队级规范。常见字段包括 request_id、user_id、tenant_id、route、method、status、cost_ms、error_type、task_id、attempt。规范不需要一开始覆盖所有场景,但核心字段要稳定。字段名一旦广泛使用,再改成本很高。
字段值也要统一。状态码用数字还是字符串,耗时单位是 ms 还是秒,错误类型用大写还是小写,都要约定。日志平台不会替你理解这些差异。统一不是形式主义,它直接影响检索和告警。
日志封装要让正确写法更省事
如果每次写日志都要手动传十几个字段,开发者一定会漏。正确做法应该更省事。比如中间件自动注入 request_id、route、user_id,日志封装自动带 service、env、version,业务只补充 order_id、task_id 这类局部字段。
Python 可以用 logging Formatter、Filter 或结构化日志库实现。关键是把公共字段从上下文拿出来,而不是让业务函数到处传。业务代码越简单,日志越一致。
同时要给开发者留扩展空间。不是所有字段都能提前规定,具体业务仍然需要额外上下文。封装应该支持 extra 字段,但要经过脱敏和类型处理,避免把复杂对象直接塞进日志。
上线前用一次事故来验收日志
日志字段是否够用,最好的验收方式是模拟一次事故。比如下游超时、任务失败、权限异常、数据不一致。看能不能只靠日志回答几个问题:影响哪个用户或任务,失败在哪一步,耗时多少,是否重试,错误类型是什么,关联请求是什么。
如果回答不了,就补字段。不要等真实事故发生才发现日志不够。日志设计是可验证的,不是凭感觉。
最后给一个判断标准:一条关键日志至少要能回答“谁、在哪、做什么、结果怎样、花了多久、和哪次请求有关”。Python 日志先定字段,不是为了格式漂亮,而是为了让排查从翻文本变成查证据。