mcpField
L
logging-observability

logging-observability

github.com/curiositech云与 DevOps257141 次安装1 天前更新
代码仓库

关于 logging-observability

面向生产系统开发者的可观测性技能,涵盖结构化日志、分布式追踪、指标和告警。内容包括 OpenTelemetry 设置、关联 ID 传播及避免敏感信息泄露等实践。

主要功能

  1. 01JSON 结构化日志
  2. 02关联 ID 与追踪传播
  3. 03OpenTelemetry 配置
  4. 04日志级别与脱敏策略
  5. 05指标、仪表板与告警模式

适用场景

  1. 01为服务配置结构化日志
  2. 02跨服务请求追踪故障
  3. 03设置生产环境告警阈值
  4. 04检查日志中的敏感数据风险

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/curiositech/some_claude_skills --skill logging-observability

安装后在 Claude Code 中输入/logging-observability确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.claude-plugin/
plugin.json
references/
alerting-patterns.md
opentelemetry-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.mdSLI/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)

输出产物

  1. Logger 配置 — Pino/Winston/structlog 设置及脱敏规则
  2. OTel 引导文件 — SDK 初始化及自动插桩
  3. 关联中间件 — AsyncLocalStorage 请求上下文
  4. Prometheus 指标模块 — Counter/histogram/gauge 定义
  5. Grafana 仪表板 JSON — 四个黄金信号面板
  6. Alertmanager 规则 YAML — 基于 SLO 的告警定义

评论

支持 Markdown