开发者被要求把越来越多的问题“左移”:更早发现、更早验证、更早修复。现在,可观测性也开始进入开发阶段。它不只是线上平台团队的工作,而是代码作者在提交代码时就应该留下的诊断入口。
这里的重点不是给每个函数都加日志,而是让开发者能够回答几个实际问题:这段代码什么时候执行?输入和结果是什么?它为什么变慢或失败?出现问题时,是否能把一次请求串回完整的调用路径?
先观察关键路径,而不是给所有代码加日志
“可观测性左移”最容易被误解成“日志越多越好”。实际情况通常相反:无目的的日志会增加噪声、成本和隐私风险,反而让排查变慢。
可以从一条关键路径开始,例如:
- 一个 HTTP 请求从入口到数据库的耗时;
- 一次支付、订单或任务状态变化;
- 一个经常失败的外部 API 调用;
- 一段最近发生性能回退的代码。
针对这些路径,优先记录结构化事件:事件名称、耗时、结果、错误类型,以及用于串联同一次请求的标识。业务参数应经过筛选或脱敏,不要把完整请求体和访问令牌写入日志。
把诊断信息放进代码的自然位置
观察代码时,开发者通常需要三类信号:
- 日志:说明发生了什么,适合记录状态变化和明确错误。
- 指标:说明发生了多少次、花了多久,适合发现趋势和异常。
- 追踪:说明一次请求经过了哪些步骤,适合分析跨服务调用。
不必一开始就引入完整的平台。可以先用标准库在本地建立一个最小实践:为关键操作记录结构化日志和耗时。下面的 Python 示例可以直接运行,也可以改造成项目中的装饰器。
运行方式:保存为 observe_example.py,然后执行 python observe_example.py。示例使用标准库,不依赖外部服务。
import json
import logging
import time
import uuid
from contextlib import contextmanager
logging.basicConfig(level=logging.INFO, format="%(message)s")
@contextmanager
def observe(operation: str, request_id: str):
started = time.perf_counter()
try:
yield
except Exception as exc:
elapsed_ms = round((time.perf_counter() - started) * 1000, 2)
logging.error(json.dumps({
"event": "operation_failed",
"operation": operation,
"request_id": request_id,
"duration_ms": elapsed_ms,
"error_type": type(exc).__name__,
}))
raise
else:
elapsed_ms = round((time.perf_counter() - started) * 1000, 2)
logging.info(json.dumps({
"event": "operation_succeeded",
"operation": operation,
"request_id": request_id,
"duration_ms": elapsed_ms,
}))
def load_profile(user_id: str) -> dict:
time.sleep(0.03)
return {"user_id": user_id, "tier": "standard"}
request_id = str(uuid.uuid4())
with observe("load_profile", request_id):
profile = load_profile("user-42")
print(profile)
这个例子有几个值得保留的边界:日志是结构化 JSON,耗时使用单调时钟计算,成功和失败事件使用不同的事件名,并且没有记录完整的用户资料。接入日志平台后,还可以按 operation、error_type 或 request_id 查询。
在开发阶段验证“能不能观察”
可观测性不是代码部署以后才检查的属性。提交前可以问:
- 失败路径是否产生了足够的上下文?
- 耗时是否能定位到具体操作?
- 日志中的请求标识是否能贯穿调用链?
- 是否可能泄露密码、令牌、个人信息或完整支付数据?
- 高频循环是否会产生无法承受的日志量?
可以把这些问题变成轻量的测试或代码检查。例如,针对外部 API 客户端,测试超时和异常时是否带有操作名称与请求标识:
def test_external_call_error_contains_context(caplog):
request_id = "req-test-1"
try:
with observe("payment_provider", request_id):
raise TimeoutError("provider timeout")
except TimeoutError:
pass
assert "payment_provider" in caplog.text
assert request_id in caplog.text
assert "TimeoutError" in caplog.text
实际项目中,日志断言应配合团队现有的测试框架和日志采集方式调整。这个测试的目的不是锁定日志文本,而是保护诊断契约:关键失败发生时,必须留下能行动的信息。
从最小实践逐步接入平台
当本地信号稳定后,再决定是否接入指标、追踪和集中式日志。一个务实的推进顺序是:
- 为少数关键操作定义事件名和字段。
- 为失败、超时和慢请求增加耗时与错误信息。
- 在请求入口生成或接收请求标识,并向下游传递。
- 为高价值路径增加计数和延迟指标。
- 当跨服务排查成为主要成本时,再引入分布式追踪。
每一步都应有明确用途。例如,日志用于回答“这次发生了什么”,指标用于回答“问题规模多大”,追踪用于回答“时间花在了哪一段”。如果某个信号没有对应的排查问题,就应该重新评估它是否值得保留。
还需要控制三个风险:
- 隐私风险:敏感字段采用白名单和脱敏策略。
- 成本风险:限制高频事件的采样率、字段数量和保留时间。
- 性能风险:避免在热路径中进行昂贵的序列化或同步网络上报。
一份适合提交前使用的清单
可观测性左移的目标,不是让每位开发者维护一套复杂平台,而是让代码在交付时具备基本的自解释能力。提交一个新功能或修复问题前,可以检查:
- 关键成功和失败路径是否可区分;
- 慢操作是否有耗时信息;
- 错误是否包含操作名称和关联标识;
- 记录内容是否经过隐私审查;
- 信号是否足够低噪声,能够支持一个具体的排查动作。
从一条最常出问题的路径开始,通常比一次性改造整个系统更容易获得反馈。好的可观测性代码不会把实现淹没在日志里,而是在真正需要排查时,准确告诉你应该看哪里。