系统出事时,你要看得见
原创 · 约 23 分钟阅读 · 阅读 --

系统出事时,你要看得见

作者: 字与码


古董级程序员,大厂出来后一直在创业公司,现在仍在一线写代码、做产品、和 AI 编程工具较劲。更完整的更新会放在微信公众号「字与码」:工作经历、新技术观察、AI 开发实践,以及这些年踩过的坑,都会陆续写在那里。

《AI时代的程序员修养》前面讲了测试。测试回答的是:在已知输入下,系统是不是按预期工作。

但线上系统更麻烦。真实用户的输入不一定在测试里出现,第三方接口不一定稳定,队列可能积压,数据库可能慢,某个新版本可能只在一小部分请求上出错。这个时候,代码写得再漂亮也没用,你必须能看见系统正在发生什么。

第 10 篇讲可观测性。日志、指标、Trace、审计记录和业务埋点,不是上线以后补几行 print。它们是系统设计的一部分,也是 AI 生成代码必须满足的工程边界。

AI 写代码时最容易漏掉“眼睛”

让 AI 写一个接口,它通常会把主流程写出来:

async def create_report(request: ReportRequest) -> ReportResponse:
    report = await save_report_request(request)
    await enqueue_report_job(report.id)
    return ReportResponse(report_id=report.id, status="queued")

这段代码看起来可以跑,但如果线上用户说“我点了生成报告,一直没有结果”,你马上会遇到一串问题:

  • 请求有没有进来?
  • 数据库有没有写成功?
  • 队列有没有投递成功?
  • worker 有没有消费?
  • 第三方接口有没有超时?
  • 失败后有没有重试?
  • 重试是不是重复扣费或重复写结果?
  • 这个用户是不是命中了某个特殊参数?
  • 只有这个用户慢,还是所有用户都慢?

如果系统没有可观测性,这些问题只能靠猜。

AI 生成代码最常见的问题,是把“功能能跑”当成“系统可交付”。真正可交付的服务,必须在出问题时留下足够的证据。

日志、指标、Trace 各看一层

很多人把可观测性理解成日志。日志当然重要,但只靠日志不够。

一个服务至少需要三类视角:

日志 Logs       发生了什么事
指标 Metrics   整体状态变成什么样
链路 Trace      一次请求经过了哪些步骤

日志适合回答“这一次发生了什么”。例如某个 report_id 创建成功、某个第三方接口返回 429、某次重试失败。

指标适合回答“系统整体怎么样”。例如每分钟请求数、错误率、P95 延迟、队列积压、数据库连接池使用率。

Trace 适合回答“一次请求慢在哪里”。例如 API 花了 80ms,数据库写入 20ms,队列投递 10ms,worker 等待 4 分钟,第三方调用 15 秒。

这三者缺一块,排障都会变成半盲。

日志不是字符串,是事件

AI 很容易写出这样的日志:

logger.info(f"create report success: {report.id}")

这比没有日志好,但还不够。它只是给人看的字符串,机器很难稳定分析。

生产系统更应该写结构化日志:

logger.info(
    "report_request_created",
    extra={
        "event": "report_request_created",
        "report_id": report.id,
        "user_id": request.user_id,
        "tool_id": request.tool_id,
        "request_id": context.request_id,
        "trace_id": context.trace_id,
        "status": "queued",
    },
)

关键区别在于字段。

字段让日志可以被查询、聚合和关联。你可以查某个 report_id 的完整过程,可以看某个 tool_id 最近失败率,可以把 API 日志和 worker 日志按 trace_id 串起来。

结构化日志里最重要的字段通常有这些:

timestamp       事件时间
level           日志级别
event           稳定事件名
service         服务名
env             环境
request_id      单次入口请求 ID
trace_id        分布式链路 ID
span_id         当前步骤 ID
user_id         用户 ID,注意脱敏和合规
resource_id     业务对象 ID,例如 report_id / order_id
status          成功、失败、跳过、重试
error_code      稳定错误码
duration_ms     当前步骤耗时

这里有一个很实际的判断标准:日志不是给当时写代码的人看的,而是给三个月后值班的人看的。

日志级别要有纪律

AI 生成代码经常乱用日志级别。正常分支打 error,大循环里打 info,异常又被吞掉,只留一句“failed”。

日志级别应该有相对固定的含义:

DEBUG  开发和临时诊断,生产默认不打开或采样
INFO   正常生命周期事件,例如请求创建、任务完成、状态变更
WARN   可恢复异常,例如重试、降级、限流、第三方临时失败
ERROR  当前操作失败,需要被统计和告警
FATAL  进程无法继续工作,通常很少用

这里最容易犯错的是 INFO。如果一个循环要处理几十万个对象,每个对象都打一条 INFO,日志系统会被拖慢,数据库也可能被写垮。被跳过的正常分支,通常应该是计数器或采样日志,而不是逐条刷屏。

给 AI 的要求要写清楚:

不要在高频循环里逐条写 INFO 日志。
对正常 skip 分支使用 metrics 计数。
DEBUG 日志允许采样,队列满时可以丢弃摘要。
ERROR 日志必须包含 error_code、trace_id、resource_id 和异常类型。

这不是吹毛求疵。日志本身也会成为系统负载。

指标看趋势,不看故事

日志告诉你某次请求发生了什么。指标告诉你系统是不是正在变坏。

服务最常用的是 RED 指标:

Rate      请求速率
Errors    错误率
Duration  延迟

如果是资源层,还会看 USE 指标:

Utilization  使用率,例如 CPU、连接池、磁盘
Saturation   饱和度,例如队列积压、线程等待
Errors       错误数

假设有一个报告生成服务,最少应该有这些指标:

http_requests_total{route, method, status}
http_request_duration_seconds_bucket{route, method}
report_requests_total{tool_id, status}
report_jobs_enqueued_total{queue}
report_jobs_processed_total{queue, status}
report_job_duration_seconds_bucket{queue}
report_queue_depth{queue}
third_party_calls_total{provider, status}
third_party_call_duration_seconds_bucket{provider}
db_pool_in_use{database}

注意,指标标签不能乱加。

routestatusprovidertool_id 这种低到中等基数的字段适合做标签。user_idrequest_idreport_id 通常不适合做指标标签,因为基数太高,会把时序数据库打爆。

这也是 AI 生成代码需要被检查的地方。它可能会觉得“字段越多越好”,但指标系统最怕无限基数。

直方图比平均值诚实

延迟指标不要只看平均值。

平均 200ms 的接口,可能是大多数请求 50ms,少数请求 10 秒。用户感受到的是尾部延迟,不是平均值。

工程里更常看 P50、P95、P99:

P50   一半请求低于这个耗时
P95   95% 请求低于这个耗时
P99   99% 请求低于这个耗时

如果报告接口 P50 是 120ms,P95 是 2s,P99 是 15s,说明少数请求很慢。这个时候应该顺着 trace 查慢请求,而不是盯着平均值说“看起来还行”。

让 AI 做指标时,要明确:

为外部接口、数据库查询、队列任务和整体请求增加 duration histogram。
不要只记录平均耗时。
告警以错误率、P95/P99、队列积压和饱和度为主。

Trace 是一次请求的时间线

Trace 的核心是把一次请求拆成多个 span。

一个报告生成链路可能长这样:

trace_id=abc

API /reports
  ├─ validate_request                 3ms
  ├─ insert report_request           18ms
  ├─ enqueue report_job               9ms
  └─ return queued                    1ms

worker report_job
  ├─ load report_request             12ms
  ├─ call provider A               2400ms
  ├─ normalize result                35ms
  ├─ save report_result              22ms
  └─ publish notification             8ms

如果没有 Trace,你只能在 API 日志、队列日志、worker 日志之间来回搜。Trace 把这些步骤放到一条时间线上。

这里最关键的是上下文传播。入口请求生成 trace_id,API 写数据库、发消息、调用下游时,都要把这个 ID 传下去。队列任务被 worker 消费时,worker 要继续使用同一个 trace,而不是重新生成一个孤立 trace。

HTTP 里通常通过 header 传:

traceparent: 00-<trace_id>-<span_id>-01

队列里可以放到 message metadata:

{
  "job_id": "job_123",
  "report_id": "rpt_456",
  "trace_id": "abc",
  "parent_span_id": "def"
}

如果让 AI 写异步任务,这一点必须写进需求。否则 API 和 worker 会各打一套日志,出了问题只能靠业务 ID 硬拼。

审计记录不是日志的替代品

还有一类数据经常被混在日志里:审计记录。

日志面向工程排障,审计记录面向业务事实。两者不能互相替代。

比如用户修改权限,日志可以记录:

event=permission_update_saved trace_id=...

审计记录应该记录更稳定的业务事实:

{
  "actor_id": "user_1",
  "action": "permission.grant",
  "target_type": "project",
  "target_id": "project_9",
  "before": {"role": "viewer"},
  "after": {"role": "editor"},
  "reason": "owner_action",
  "created_at": "2026-07-03T08:00:00Z"
}

审计记录要可追溯、可展示、可长期保存。日志可以按保留期清理,审计记录通常不能随便丢。

AI 生成后台管理、权限、扣费、状态变更代码时,必须问一句:这个动作需不需要审计?

业务埋点回答“用户怎么用”

日志和 Trace 主要帮助工程师看系统。业务埋点帮助产品和运营看用户行为。

同一个报告生成服务,可以埋这些事件:

report_create_clicked
report_create_submitted
report_create_succeeded
report_create_failed
report_result_viewed
report_result_exported

每个事件都要有稳定 schema:

{
  "event": "report_create_submitted",
  "user_id": "user_1",
  "session_id": "sess_1",
  "tool_id": "tool_x",
  "source": "search_result",
  "input_size_bucket": "small",
  "created_at": "2026-07-03T08:00:00Z"
}

埋点最怕两件事。

一是事件名随手起。今天叫 click_report,明天叫 report_click,后天叫 submit_report_request,最后没人敢用。

二是把所有字段都塞进去。敏感信息、长文本、原始参数、用户输入,如果未经处理就进入埋点系统,后面会非常麻烦。

让 AI 写埋点时,应该先给事件字典,而不是让它自由发挥。

可观测性也有成本

可观测性不是越多越好。

日志太多会增加存储成本,指标标签太多会撑爆时序库,Trace 全量采样会很贵,业务埋点太细会让分析变得混乱。

实际系统里常用这些策略:

日志:ERROR 全量,INFO 控制数量,DEBUG 默认关闭或采样。
指标:核心路径全量聚合,避免高基数字段。
Trace:低流量服务可全量,高流量服务按比例采样,错误请求全量保留。
审计:关键业务动作全量保存,字段稳定。
埋点:围绕漏斗和关键路径设计,不随手埋。

如果某个服务每天只有几千次请求,简单日志加少量指标可能足够。如果每天有几千万次请求,不控制采样和标签,观测系统会比业务系统先倒。

这是 AI 不会天然替你判断的地方。它可以生成接入代码,但成本边界要由人指定。

一个接口的观测验收清单

如果让 AI 写一个生产接口,我会把可观测性要求放进验收清单:

1. 每个入口请求生成或继承 request_id 和 trace_id。
2. 成功、失败、重试、跳过都有稳定 event 名。
3. ERROR 日志包含 error_code、异常类型、trace_id、业务对象 ID。
4. 高频循环不逐条写 INFO。
5. 请求数、错误数、延迟直方图有 metrics。
6. 外部调用、数据库查询、队列投递有独立 span。
7. 异步任务继承上游 trace context。
8. 权限、扣费、状态变更写审计记录。
9. 用户关键行为写业务埋点,事件名和字段来自事件字典。
10. 指标标签没有 user_id、request_id 这类高基数字段。

这份清单比“加日志”有效得多。

给 AI 的提示词可以这样写

可观测性要在实现前写进 prompt:

请实现报告生成接口和异步 worker。

除了业务逻辑,还必须满足以下可观测性要求:
- 使用结构化日志,不要拼接不可查询的长字符串。
- 每个入口请求都有 request_id 和 trace_id。
- API、数据库写入、队列投递、worker 消费、第三方调用分别创建 trace span。
- 队列消息必须携带 trace_id 和 parent_span_id。
- 暴露 RED 指标:请求数、错误数、延迟直方图。
- 暴露队列指标:入队数、消费数、失败数、队列积压、任务耗时。
- ERROR 日志必须包含 error_code、trace_id、report_id、异常类型。
- 高频循环禁止逐条 INFO;正常 skip 用 counter,DEBUG 可采样。
- 用户发起生成、查看结果、导出结果需要写业务埋点。
- 创建、取消、扣费、权限变化需要写审计记录。

实现完成后,请列出可以用哪些查询定位:
1. 某个 report_id 卡在哪里。
2. 最近 15 分钟错误率是否升高。
3. 某个 provider 是否变慢。
4. 队列是否积压。
5. 某个用户的关键操作路径。

这段提示词的目的不是让 AI 多写几行样板代码,而是让它知道:系统上线以后要能被理解。

真正的工程质量,是出事时不慌

平时看一个系统,大家容易看功能、界面、性能。

出事时,真正救命的是证据链。

请求从哪里来,经过哪些模块,在哪一步变慢,哪个依赖失败,影响了多少用户,是否重复执行,是否产生业务损失,是否需要补偿。

这些问题靠日志、指标、Trace、审计记录和埋点一起回答。

AI 会让代码生成更快,也会让“看起来能跑”的服务更多。程序员的责任不是阻止 AI 写代码,而是给它设下工程边界:能测试,能观测,能排障,能交接。

系统迟早会出事。区别只在于,出事时你是打开控制台开始定位,还是坐在黑盒前面猜。