用 Claude Code 设计日志与监控:生产可观测性实战
用Claude Code落地结构化日志、OpenTelemetry、告警、健康检查和安全事故交接。
先定义运行契约
如果只对 Claude Code 说“加日志”,它通常会增加一些 console.log。本地调试时这看起来有用,但生产故障中远远不够。真正的日志与监控基线要能回答:哪个用户动作触发了请求,哪个 requestId 贯穿了调用,哪个服务或依赖变慢,哪些信息可以安全交给下一位值班工程师。
OpenTelemetry Observability primer 把可观测性定义为从系统外部理解系统,并能回答未知问题的能力。What is OpenTelemetry? 也强调,OpenTelemetry 是供应商中立的计装层,不是监控后台本身。本文把这个原则转换成 Claude Code 提示词、可复制代码、仪表盘复查、告警规则和事故交接模板。
Masa 在一个小型结账 API 上试过,最初的提示词只是“让日志更详细”。Claude Code 为了保留上下文,建议在支付失败时记录整个 request body。它没有包含卡号,但可能泄露邮箱、优惠码、地址和客户备注。后来把禁止字段写在前面,只允许 requestId、traceparent 和必要业务字段,审查重点就从“代码像不像”变成了“能不能处理真实生产数据”。
| 信号 | 回答的问题 | 给 Claude Code 的约束 |
|---|---|---|
| 日志 | 发生了什么 | JSON、固定字段、PII 脱敏 |
| 指标 | 严重到什么程度 | rate、p95、错误率 |
| Trace | 时间花在哪里 | 传递 traceparent,命名 span |
| 健康检查 | 依赖是否可用 | 每个依赖返回状态和 latency |
安全提示词与 CLAUDE.md
Claude Code 的 memory 文档说明,CLAUDE.md 会在会话开始时提供项目、个人或组织级指令。日志监控中,应该把日志级别、字段名、禁止字段、测试命令、仪表盘名称和事故交接格式写进去。站内可继续参考 CLAUDE.md 最佳实践,权限边界则要看官方 Claude Code permissions 和 hooks。
Claude Code task:
- Add observability to the checkout API only.
- Keep all changes inside src/checkout and tests/checkout.
- Use structured JSON logs with requestId and traceparent.
- Never log passwords, tokens, cookies, email, phone, address,
raw prompt text, or full request/response bodies.
- Add tests proving redaction and requestId propagation.
- Add a /healthz report with database and cache latency.
- Add alert rules for 5xx rate, p95 latency, and redaction failure.
- Show a diff summary and remaining manual checks at the end.
这个提示词故意很窄。它不让 Claude Code 发明整套平台,不让它改无关路由,也不要求粘贴生产原始日志。它要求的是可审查、可回滚、可验证的运维差异。
结构化日志与请求关联 ID
OWASP Logging Cheat Sheet 把日志当成安全能力:要做代码审查、测试、访问控制和失败验证。Claude Code 的任务也应该包含脱敏测试、字段 schema 和日志失败时的行为,而不是只添加文字消息。
下面是无依赖的 JSON logger。保存为 structured-logger.mjs,用 Node.js 18 以上执行 node structured-logger.mjs。
import { randomUUID } from "node:crypto";
const rank = { debug: 10, info: 20, warn: 30, error: 40 };
const current = process.env.LOG_LEVEL || "info";
const threshold = rank[current] ?? rank.info;
const secretKeys = [
"password",
"token",
"authorization",
"cookie",
"set-cookie",
"apikey",
];
function cleanText(value) {
return String(value).replace(/[\r\n\t]/g, " ").slice(0, 500);
}
function redact(value) {
if (Array.isArray(value)) return value.map(redact);
if (!value || typeof value !== "object") return value;
return Object.fromEntries(
Object.entries(value).map(([key, item]) => {
if (secretKeys.includes(key.toLowerCase())) {
return [key, "[REDACTED]"];
}
return [key, redact(item)];
}),
);
}
export function log(level, message, fields = {}) {
if ((rank[level] ?? 99) < threshold) return;
const entry = {
ts: new Date().toISOString(),
level,
service: process.env.SERVICE_NAME || "checkout-api",
env: process.env.NODE_ENV || "development",
requestId: fields.requestId || randomUUID(),
msg: cleanText(message),
...redact(fields),
};
process.stdout.write(`${JSON.stringify(entry)}\n`);
}
log("info", "payment accepted", {
requestId: "req_demo_001",
userId: "user_123",
amount: 4980,
token: "sk_live_should_not_leak",
});
Web 应用中,还要把 request ID 写回响应头,并保存在异步上下文里。W3C Trace Context 说明了 traceparent 不存在时如何创建,存在时如何继续传递。建议把 x-request-id 作为应用层关联 ID,把 traceparent 作为分布式 trace 载体,不要混用。
import { AsyncLocalStorage } from "node:async_hooks";
import { randomUUID } from "node:crypto";
import type { Request, Response, NextFunction } from "express";
import { log } from "./structured-logger";
type RequestContext = {
requestId: string;
traceparent?: string;
userId?: string;
};
const storage = new AsyncLocalStorage<RequestContext>();
export function getRequestContext() {
return storage.getStore();
}
export function requestContext(
req: Request,
res: Response,
next: NextFunction,
) {
const started = performance.now();
const user = (req as Request & { user?: { id?: string } }).user;
const requestId =
req.get("x-request-id") ||
req.get("cf-ray") ||
randomUUID();
const context = {
requestId,
traceparent: req.get("traceparent"),
userId: user?.id,
};
res.setHeader("x-request-id", requestId);
storage.run(context, () => {
res.on("finish", () => {
const durationMs = Math.round(performance.now() - started);
const level = res.statusCode >= 500
? "error"
: res.statusCode >= 400
? "warn"
: "info";
log(level, "http request completed", {
requestId,
method: req.method,
path: req.path,
statusCode: res.statusCode,
durationMs,
});
});
next();
});
}
日志级别要保持朴素:debug 用于本地临时细节,info 记录重要正常事件,warn 表示可恢复风险,error 表示需要调查。还要要求 Claude Code 不要因为日志失败改变业务流程。
OpenTelemetry 基础
OpenTelemetry 用来统一应用发出的 trace、metrics 和 logs,然后交给团队已有的后端。Node.js 设置请以官方 JavaScript Node.js guide 和 JavaScript exporters guide 为准。
npm install @opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-proto \
@opentelemetry/exporter-metrics-otlp-proto \
@opentelemetry/sdk-metrics
const opentelemetry = require("@opentelemetry/sdk-node");
const {
getNodeAutoInstrumentations,
} = require("@opentelemetry/auto-instrumentations-node");
const {
OTLPTraceExporter,
} = require("@opentelemetry/exporter-trace-otlp-proto");
const {
OTLPMetricExporter,
} = require("@opentelemetry/exporter-metrics-otlp-proto");
const {
PeriodicExportingMetricReader,
} = require("@opentelemetry/sdk-metrics");
process.env.OTEL_SERVICE_NAME ||= "checkout-api";
const endpoint =
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||
"http://localhost:4318";
const sdk = new opentelemetry.NodeSDK({
traceExporter: new OTLPTraceExporter({
url: `${endpoint}/v1/traces`,
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: `${endpoint}/v1/metrics`,
}),
exportIntervalMillis: 30000,
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
process.on("SIGTERM", () => {
sdk.shutdown().finally(() => process.exit(0));
});
flowchart LR
A["用户操作"] --> B["应用"]
B --> C["结构化日志"]
B --> D["指标"]
B --> E["Trace span"]
C --> F["日志平台"]
D --> G["告警规则"]
E --> H["Trace 后端"]
F --> I["事故交接"]
G --> I
H --> I
健康检查与告警
好的健康检查不只是 200 OK。它应该分别检查数据库、缓存、队列和关键外部 API,并返回每个依赖的 latency。不要返回连接字符串、token 或内部拓扑细节。
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error("timeout")), ms);
});
}
export async function buildHealthReport(checks) {
const started = Date.now();
const results = {};
for (const [name, check] of Object.entries(checks)) {
const before = Date.now();
try {
await Promise.race([check(), timeout(800)]);
results[name] = {
status: "ok",
latencyMs: Date.now() - before,
};
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
results[name] = {
status: "fail",
latencyMs: Date.now() - before,
reason: message.slice(0, 120),
};
}
}
const failed = Object.values(results)
.filter((item) => item.status === "fail")
.length;
return {
status: failed ? "degraded" : "ok",
uptimeSec: Math.round(process.uptime()),
totalLatencyMs: Date.now() - started,
checks: results,
};
}
告警最好基于时间窗口内的比例和分位点,不要因为单条 error 日志就叫醒人。下面是 Prometheus 风格的例子。
groups:
- name: checkout-api
rules:
- alert: CheckoutHigh5xxRate
expr: |
sum(rate(http_requests_total{
service="checkout-api",
status_code=~"5.."
}[5m]))
/
sum(rate(http_requests_total{
service="checkout-api"
}[5m])) > 0.02
for: 10m
labels:
severity: page
annotations:
summary: "Checkout 5xx rate is above 2%"
- alert: CheckoutP95LatencyHigh
expr: |
histogram_quantile(
0.95,
sum by (le) (
rate(http_request_duration_seconds_bucket{
service="checkout-api"
}[5m])
)
) > 1.5
for: 15m
labels:
severity: ticket
annotations:
summary: "Checkout p95 latency is above 1.5s"
三个具体场景
第一个场景是电商结账 API。保留 orderId、requestId、paymentProvider 和 amount,但不要记录卡信息、邮箱、地址和访问 token。告警要区分 5xx 率、支付失败率和支付服务商 p95 延迟。
第二个场景是 SaaS 管理后台。登录、权限变更、成员邀请、套餐变更应该进入审计日志;邀请邮件正文和个人备注不应该进入日志。要求 Claude Code 把审计日志和应用日志分开,并添加 RBAC 测试。
第三个场景是媒体站或博客 CMS。需要追踪发布、CTA 点击、咨询表单成功、图片生成失败和翻译缺失。只看 PV 不会改善收入。把 cta_click 和 generate_lead 分开,并结合 Claude Code 分析实现 复查仪表盘。
如果你的系统是微服务,也要阅读 Claude Code 微服务设计。service.name、路由名和环境标签不一致时,OpenTelemetry 数据会很难搜索。
失败模式与事故交接
常见失败包括:为了上下文记录整个 request body;日志消息全是自由文本;requestId、correlationId、traceId、transactionId 同时存在却没有主键;/healthz 不检查依赖却永远返回 ok;告警建好了但没人知道该看哪个 dashboard。
Claude Code 还有一个额外风险:生产原始日志会变成 prompt 输入。请求它帮忙排查事故前,先脱敏日志、汇总指标,只分享短 request ID 和 trace ID。安全调查流程可以配合 Claude Code permissions guide。
{
"incident_id": "INC-2026-06-02-001",
"severity": "SEV2",
"owner": "oncall-api",
"customer_impact": "Checkout errors for some card payments",
"first_seen": "2026-06-02T09:15:00+09:00",
"request_ids": ["req_7f3a", "req_8b21"],
"trace_ids": ["7bba9f33312b3dbb8b2c2c62bb7abe2d"],
"dashboards": ["Checkout API overview"],
"current_hypothesis": "Payment provider latency spike",
"actions_taken": ["Disabled checkout_v2 feature flag"],
"next_checks": ["Compare p95 latency by region"],
"do_not_do": ["Do not paste raw customer data into prompts"]
}
仪表盘复查、CTA 与验证结果
每周复查仪表盘:前五类错误、p95 延迟、日志量增长、误报、脱敏失败和未关闭事故。每月选一个真实事故回看,确认新值班人员能否从日志、指标和 trace 更快找到原因。
个人可以先用 免费 Claude Code cheatsheet 固定日常检查。需要可复用提示词和设置材料时看 产品页。团队要把日志标准、CLAUDE.md、权限、CI 检查和事故流程一起落地,可以从 Claude Code 培训与咨询 开始。
按本文流程试过后,最有效的是先写禁止字段。structured-logger.mjs 会把 token 变成 [REDACTED],也会把换行压成一行。模拟缓存失败时,健康检查变成 degraded;事故交接里只放 requestId 和 traceId,审查对话明显更短。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code Permission Receipt Pattern:记录权限、证据和回滚方式
Claude Code 权限 receipt:记录允许动作、需要批准的边界、验证命令、回滚说明,以及 Gumroad 和咨询 CTA 检查。
Claude Code/Codex 安全 Agent Harness 实战:权限、验证与回滚
用权限策略、执行计划、验证脚本和回滚日志,为 Claude Code 与 Codex 搭建更安全的 AI Agent 工作流。
Claude Code 子代理实战指南:安全委派并行文章与代码工作
用 Claude Code 子代理安全拆分文章和代码工作:委派规则、提示词模板、失败模式与检查清单。