关于 logging-observability
面向生产系统开发者的可观测性技能,涵盖结构化日志、分布式追踪、指标和告警。内容包括 OpenTelemetry 设置、关联 ID 传播及避免敏感信息泄露等实践。
主要功能
- 01JSON 结构化日志
- 02关联 ID 与追踪传播
- 03OpenTelemetry 配置
- 04日志级别与脱敏策略
- 05指标、仪表板与告警模式
适用场景
- 01为服务配置结构化日志
- 02跨服务请求追踪故障
- 03设置生产环境告警阈值
- 04检查日志中的敏感数据风险
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill logging-observability安装后在 Claude Code 中输入/logging-observability确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.claude-plugin/plugin.jsonreferences/alerting-patterns.mdopentelemetry-setup.md详细介绍
日志记录与可观测性
面向生产系统的结构化日志记录、分布式追踪和指标。涵盖从日志格式到告警路由的完整可观测性技术栈。
适用场景
在以下情况下启用:“结构化日志记录”、“分布式追踪”、“OpenTelemetry”、“OTel”、“关联 ID”、“日志级别”、“Grafana 仪表板”、“告警阈值”、“SLI SLO”、“Prometheus 指标”、“PagerDuty 集成”、“可观测性技术栈”、“Winston 配置”、“Pino 日志记录器”、“日志聚合”、“Datadog”、“Honeycomb”
**不适用于:**性能分析(CPU/内存火焰图)| 负载测试 | 数据库查询优化 | 安全审计
决策树:各级别应记录什么
flowchart TD
E[Event Occurs] --> Q1{Does it represent\na system failure?}
Q1 -->|Yes| Q2{Is it recoverable\nwithout human?}
Q2 -->|No| FATAL[FATAL: Service cannot\ncontinue — trigger pager]
Q2 -->|Yes| ERROR[ERROR: Operation failed,\nwill retry or degrade]
Q1 -->|No| Q3{Is it unexpected\nbut not failing?}
Q3 -->|Yes| WARN[WARN: Unusual condition,\ncircuit breaker open,\ndeprecation used]
Q3 -->|No| Q4{Is it a meaningful\nbusiness event?}
Q4 -->|Yes| INFO[INFO: User action,\npayment processed,\nservice started]
Q4 -->|No| Q5{Needed to debug\na specific issue?}
Q5 -->|Yes| DEBUG[DEBUG: DB queries,\ncache hits/misses,\nfunction inputs]
Q5 -->|No| TRACE[TRACE: Fine-grained\nloop iterations,\nOTel spans]
经验法则:生产环境运行 INFO 及以上级别。仅通过动态配置按服务启用 DEBUG,切勿在生产环境中始终启用。
核心模式
结构化日志格式(JSON)
每一行日志都必须可解析。字符串拼接不算日志。
使用 Pino 的 Node.js:
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
base: {
service: 'payment-service',
version: process.env.SERVICE_VERSION,
env: process.env.NODE_ENV,
},
redact: {
paths: ['req.headers.authorization', 'body.password', 'body.cardNumber', '*.ssn'],
censor: '[REDACTED]',
},
});
// Good: structured fields
logger.info({ orderId, userId, amountCents }, 'Payment processed');
// Bad: string interpolation
logger.info(`Payment processed for user ${userId} order ${orderId}`);
使用 structlog 的 Python:
import structlog
log = structlog.get_logger()
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
]
)
# Good: key-value pairs
log.info("payment_processed", order_id=order_id, user_id=user_id, amount_cents=amount)
关联 ID
每个请求都需要一个贯穿所有下游调用的追踪 ID。这是在不使用 OTel 时实现分布式追踪的最低可行方案。
// Express middleware
import { randomUUID } from 'crypto';
import { AsyncLocalStorage } from 'async_hooks';
const requestContext = new AsyncLocalStorage<{ traceId: string; spanId: string }>();
export function correlationMiddleware(req, res, next) {
const traceId = req.headers['x-trace-id'] ?? randomUUID();
const spanId = randomUUID().slice(0, 8);
requestContext.run({ traceId, spanId }, () => {
res.setHeader('x-trace-id', traceId);
next();
});
}
// Logger that auto-includes context
export function getLogger(name: string) {
return {
info: (msg: string, fields?: object) => {
const ctx = requestContext.getStore();
logger.info({ ...ctx, ...fields, logger: name }, msg);
},
// ... error, warn, debug
};
}
OpenTelemetry 配置
完整的 OTel 收集器配置、各语言的 SDK 初始化以及 Span 属性约定,请参阅 references/opentelemetry-setup.md。
Node.js 最小化引导配置:
// Must be first import in entrypoint
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
serviceName: 'payment-service',
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
分布式追踪传播
sequenceDiagram
participant C as Client
participant GW as API Gateway
participant SVC as Payment Service
participant DB as Database
C->>GW: POST /checkout<br/>(no trace header)
Note over GW: Generate trace-id: abc123<br/>span-id: 0001
GW->>SVC: POST /payment<br/>traceparent: 00-abc123-0001-01
Note over SVC: Inherit trace-id: abc123<br/>New span-id: 0002
SVC->>DB: INSERT payment<br/>traceparent: 00-abc123-0002-01
Note over DB: Inherit trace-id: abc123<br/>New span-id: 0003
DB-->>SVC: OK (span 0003 ends)
SVC-->>GW: 200 OK (span 0002 ends)
GW-->>C: 200 OK (span 0001 ends)<br/>x-trace-id: abc123
W3C traceparent 标头格式:00-{traceId}-{spanId}-{flags}。每次下游 HTTP 调用都必须传播此标头。
参考文件
| 文件 | 内容 |
|---|---|
references/opentelemetry-setup.md | 各语言的 OTel SDK 初始化、收集器 YAML 配置、Span 属性、上下文传播 |
references/alerting-patterns.md | SLI/SLO 定义、告警路由、PagerDuty 严重性映射、防止告警疲劳 |
反模式(行家鉴别点)
反模式 1:在生产环境中记录 PII 或机密信息
新手想法:“我直接记录完整请求正文来调试这个身份验证问题。”
问题所在:违反 GDPR/CCPA 最高可被处以全球营收 4% 的罚款。日志中的机密信息会传播到日志聚合器、S3 导出数据、审计跟踪等位置,而这些位置的访问控制各不相同。一次 console.log(req.body) 就可能在你的 Datadog 仪表板上暴露数千名用户的密码。
检测特征:搜索日志中作为字段值(而非键名)出现的 password、ssn、cardNumber、authorization。如果出现任何此类内容,就说明存在 PII 泄露。
修复方法 — 允许列表:
// Never log what you don't explicitly approve
const SAFE_BODY_FIELDS = ['orderId', 'productId', 'quantity', 'currency'];
logger.info({
body: pick(req.body, SAFE_BODY_FIELDS), // only known-safe fields
path: req.path,
method: req.method,
}, 'Request received');
修复方法 — 在日志记录器配置中进行脱敏:
// Pino's redact runs before any transport
const logger = pino({
redact: {
paths: [
'req.headers.authorization',
'req.headers.cookie',
'body.password',
'body.*.password', // nested objects too
'body.cardNumber',
'body.ssn',
'*.token',
'*.secret',
],
censor: '[REDACTED]',
},
});
行家鉴别点:专家会在初始化日志记录器时设置脱敏,而不是留下提醒注释。脱敏必须通过结构化方式实现,不能临时处理。
反模式 2:使用非结构化字符串日志,而非结构化 JSON
新手想法:logger.info('User ' + userId + ' purchased ' + productId + ' for $' + amount)
问题所在:你无法在任何日志聚合器中筛选、汇总或针对字符串插值数据设置告警。Grafana 查询 amount > 1000 时,amount 必须是数值字段,而不能嵌在句子里。字符串日志只能写入,无法在大规模场景下查询——你可以阅读它们,却无法查询它们。
影响:你每天的 1000 万行日志将变得无法搜索。发生事故时,工程师只能逐个检索字符串,而不是筛选结构化字段,导致平均恢复时间(MTTR)翻倍。
修复方法:
// Bad: string log — amount is buried in text
logger.info(`User ${userId} purchased ${productId} for $${amount}`);
// Good: structured — every field is queryable
logger.info({ userId, productId, amountDollars: amount / 100 }, 'purchase_completed');
统一事件命名:使用 snake_case 动词-名词事件名称(payment_processed、user_signed_up、order_failed)作为消息字符串,为仪表板和告警建立稳定的词汇体系。
行家鉴别点:日志消息字符串用于供人快速浏览日志尾部。所有可查询数据都应放在结构化字段中。输出形状为 {} 的日志记录器,胜过输出可读字符串的日志记录器。
反模式 3:记录日志后抛出(重复日志条目)
新手的想法:先记录错误,然后重新抛出,让调用方也知道发生了错误。
// BAD: log-and-throw
async function processPayment(orderId: string) {
try {
return await chargeCard(orderId);
} catch (err) {
logger.error({ err, orderId }, 'Payment failed'); // Logged here
throw err; // And the caller logs it again
}
}
async function handleCheckout(req, res) {
try {
await processPayment(req.body.orderId);
} catch (err) {
logger.error({ err }, 'Checkout failed'); // Same error logged twice
res.status(500).json({ error: 'Checkout failed' });
}
}
为什么不对:根据调用深度,每个错误会在日志中出现 2 到 5 次。根据错误数量触发告警会变得不可靠。事件复盘也会令人困惑——工程师会以为发生了多次故障。日志量还会产生费用(Datadog 按摄取的 GB 数收费)。
修复方法——仅在处理错误的边界处记录日志:
// Good: log only where you decide what to do with the error
async function processPayment(orderId: string) {
// No try-catch: let errors propagate naturally
return await chargeCard(orderId);
}
async function handleCheckout(req, res) {
try {
await processPayment(req.body.orderId);
res.json({ success: true });
} catch (err) {
// One log, at the boundary where we're deciding to return 500
logger.error({ err, orderId: req.body.orderId }, 'checkout_failed');
res.status(500).json({ error: 'Checkout failed' });
}
}
规则:在处理错误的地方记录日志,不要在传递错误的地方记录。错误对象中的调用栈已经能说明错误的起源。
辨别标志:如果单个请求的多个错误日志行中出现相同的 traceId,且次数超过两次,就说明某处存在记录日志后抛出的调用链。
质量检查清单
[ ] All log lines are JSON (no string concatenation)
[ ] Log level strategy documented: what goes at each level
[ ] PII/secrets redacted at logger config level, not call site
[ ] Correlation IDs propagated on all outbound HTTP calls
[ ] OTel SDK initialized before any other imports
[ ] Error logs include the error object (not just message)
[ ] No log-and-throw patterns in error handling
[ ] DEBUG logs use conditional guards or sampling
[ ] SLI/SLO defined for each critical user journey
[ ] Alert routing: notify vs page threshold documented
[ ] Runbook linked from every paging alert
[ ] Log retention policy set (cost vs compliance)
输出产物
- Logger 配置 — Pino/Winston/structlog 设置及脱敏规则
- OTel 引导文件 — SDK 初始化及自动插桩
- 关联中间件 — AsyncLocalStorage 请求上下文
- Prometheus 指标模块 — Counter/histogram/gauge 定义
- Grafana 仪表板 JSON — 四个黄金信号面板
- Alertmanager 规则 YAML — 基于 SLO 的告警定义