日志与分布式追踪

指导 Agent 设计结构化日志、传播 Trace / Span 上下文,并在多服务场景下把请求、错误与业务标识关联起来,缩短定位时间。

SKILL 应约定日志字段(时间、级别、服务名、trace_idspan_id、用户或租户脱敏标识)、采样策略与敏感信息过滤规则,避免在日志中落密钥或完整 PII。

分布式追踪部分需说明如何在网关或边车注入上下文、如何在异步消息中传递 trace 载体,以及日志平台与 APM 的查询入口(按 trace_id 串联日志与链路)。

对排障场景,可要求 Agent 先根据时间与错误指纹缩小范围,再沿 trace 拉取上下游 Span,最后对照代码路径与配置变更时间线。

  • 硬性项:关键路径必须带 request_id / trace_id;错误日志含可行动信息而非仅堆栈摘要。
  • 与指标对齐:高基数 label、全量 debug 日志等反模式需在 SKILL 中显式禁止或限流。
  • 与 on-call 手册衔接:给出典型查询模板(如按 service + status + trace_id)。

分布式上下文主流程(skill-flow-block)

  [ 入口:网关 / LB / 边车 ]
                    │
                    ▼
         [ 提取或生成 trace 上下文(W3C traceparent / vendor 头)]
                    │
                    ▼
    [ 当前服务:创建 root 或 child Span;绑定到异步边界(task / queue)]
                    │
           ┌────────┴────────┐
           ▼                 ▼
  [ 结构化日志:同一条记录写 trace_id + span_id + 业务关联键 ]   [ 出口调用:注入下游头或消息属性 ]
           │                 │
           └────────┬────────┘
                    ▼
         [ 存储:日志索引 + 追踪后端;用 trace_id 关联两者 ]
Agent 评审时核对三件事:上下文是否在进入点创建/继承、是否在异步与消息中延续、日志字段是否与 Span 使用同一套 ID 语义(避免混用「随机 request id」却未写入 trace)。

关联标识:trace、span 与 correlation / request id

  • Trace ID:标识一次分布式请求在多个服务间的整条链路;W3C Trace Context 中为 32 位十六进制(见下方校验器)。同一 trace 下可有多个 Span。
  • Span ID:标识链路中的一个工作单元(通常 16 位十六进制);父子 Span 表达调用树与时间区间。
  • Correlation / request id:团队自定义的业务或网关层关联键,可与 trace 并存:用于客服工单、幂等键或旧系统对齐。SKILL 应写明二者如何映射(例如网关生成 request id 并写入 traceparent 或日志字段),避免同名不同义。
  • 消息队列与批处理:在消息头或 envelope 中携带 W3C 载体或等价序列化上下文;消费者启动新 Span 时以 producer Span 为 parent,保持 trace id 不变。

结构化日志 JSON 格式标准(必要字段列表与示例):

// 结构化日志 JSON 格式标准
// 必要字段(所有服务统一)
{
  "timestamp": "2024-03-15T14:02:33.421Z",   // ISO 8601 UTC
  "level": "ERROR",                            // ERROR|WARN|INFO|DEBUG
  "service": {
    "name": "payment-svc",
    "version": "2.4.1",
    "instance": "payment-svc-7d9f8b-xk2p9"   // pod/instance id
  },
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "message": "Database connection pool exhausted",

  // 错误类必加字段
  "error": {
    "type": "PoolExhaustedException",
    "message": "Connection pool exhausted after 30000ms",
    "stack_fingerprint": "sha256:a3b4c5..."  // 完整栈存 trace,此处只存指纹
  },

  // 业务上下文(脱敏)
  "request": {
    "id": "req-8f4a2d1c",                    // request_id(非用户id)
    "method": "POST",
    "path": "/api/v1/payments"               // 不含查询参数(可能含敏感数据)
  },

  // 可选:高频访问字段
  "user_id_hash": "sha256:f8a3...",          // 哈希,非明文
  "tenant_id": "tenant-123"                  // 用于多租户路由
}

// 日志级别使用规范:
// ERROR: 需要立即关注的生产问题(服务不可用、数据丢失风险)
// WARN:  可能有问题但服务仍可运行(降级、重试成功、接近阈值)
// INFO:  关键业务事件(请求完成、状态变更、启动/停止)
// DEBUG: 详细排障信息(仅开发/预发使用,生产默认关闭)

OpenTelemetry 集成代码

Node.js instrumentation(使用 @opentelemetry/sdk-node):

// Node.js OpenTelemetry 初始化(在 app 入口最先执行)
// instrumentation.js
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';

const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'payment-svc',
    [SemanticResourceAttributes.SERVICE_VERSION]: process.env.APP_VERSION,
  }),
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://otel-collector:4318/v1/traces',
  }),
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-http': { enabled: true },
      '@opentelemetry/instrumentation-pg': { enabled: true },
    }),
  ],
});
sdk.start();

// 手动创建 Span 并传递 trace context
import { trace, context } from '@opentelemetry/api';

async function processPayment(req, paymentData) {
  const tracer = trace.getTracer('payment-svc');
  const span = tracer.startSpan('payment.process', {
    attributes: {
      'payment.amount': paymentData.amount,
      'payment.currency': paymentData.currency,
      'http.method': req.method,
    }
  });

  return context.with(trace.setSpan(context.active(), span), async () => {
    try {
      const result = await chargeCard(paymentData);
      span.setStatus({ code: SpanStatusCode.OK });
      return result;
    } catch (err) {
      span.recordException(err);
      span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
      throw err;
    } finally {
      span.end();
    }
  });
}

Python OpenTelemetry instrumentation:

# Python OpenTelemetry 集成(FastAPI 示例)
# requirements: opentelemetry-sdk opentelemetry-instrumentation-fastapi

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
import structlog

# 初始化 Tracer
provider = TracerProvider(
    resource=Resource.create({
        "service.name": "payment-svc",
        "service.version": os.getenv("APP_VERSION", "unknown"),
    })
)
provider.add_span_processor(BatchSpanProcessor(
    OTLPSpanExporter(endpoint="http://otel-collector:4318/v1/traces")
))
trace.set_tracer_provider(provider)

# 自动 instrumentation FastAPI
app = FastAPI()
FastAPIInstrumentor.instrument_app(app)

# 结构化日志 + trace_id 关联
def get_logger():
    span = trace.get_current_span()
    ctx = span.get_span_context()
    return structlog.get_logger().bind(
        trace_id=format(ctx.trace_id, '032x') if ctx.is_valid else None,
        span_id=format(ctx.span_id, '016x') if ctx.is_valid else None,
    )

@app.post("/payments")
async def create_payment(payment: PaymentRequest):
    log = get_logger()
    tracer = trace.get_tracer("payment-svc")
    with tracer.start_as_current_span("payment.create") as span:
        span.set_attribute("payment.amount", payment.amount)
        log.info("payment.processing", amount=payment.amount)
        # ... 业务逻辑

Trace ID 在 HTTP 请求中传播(traceparent 注入):

// Trace Context 在下游 HTTP 请求中传播(Node.js fetch)
import { propagation, context } from '@opentelemetry/api';
import { W3CTraceContextPropagator } from '@opentelemetry/core';

// 出口调用:自动注入 traceparent 头
async function callDownstreamService(path: string, body: unknown) {
  const headers: Record = {
    'Content-Type': 'application/json',
  };
  // 将当前 trace context 注入到 HTTP 头
  propagation.inject(context.active(), headers);
  // 注入后 headers 包含:
  // traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

  return fetch(`http://order-svc${path}`, {
    method: 'POST',
    headers,
    body: JSON.stringify(body),
  });
}

// 入口:从请求头提取 trace context(通常由 auto-instrumentation 处理)
function extractTraceContext(req: Request) {
  const ctx = propagation.extract(context.active(), req.headers);
  // 后续操作在此 ctx 中运行即自动关联到父 span
  return ctx;
}
  • 命名:使用低基数、可检索的静态片段(如 HTTP GET /api/ordersdb.query),勿把用户 id 或完整 URL 拼进 Span 名以免拖垮追踪 UI 与存储。
  • 异步:asyncio / 线程池 / 回调边界显式传递 context;避免「 fire-and-forget 任务丢失 parent」导致孤儿 Span。

日志聚合配置与安全边界

  • 必选或强约定:timestamplevelservice.name(或等价)、trace_idspan_id(若可得)、message 或事件码。
  • 错误类:增加 error.typeerror.message(脱敏后)、可选 error.stack_fingerprint;避免整段密钥或 token 进日志。
  • 采样:全量 debug 仅限预发或短时排障;生产默认按级别 + 错误全采 + 按比例/尾采样,并在 SKILL 中写清。

Loki 日志聚合索引策略配置:

# Loki 索引策略(promtail 配置)
# 关键原则:标签基数要低(否则 Loki 性能急剧下降)
scrape_configs:
  - job_name: kubernetes-pods
    kubernetes_sd_configs:
      - role: pod
    pipeline_stages:
      # 解析 JSON 结构化日志
      - json:
          expressions:
            level: level
            trace_id: trace_id
            service: service.name
      # 只将低基数字段设为 Loki 标签(可用于索引)
      - labels:
          level:        # ERROR/WARN/INFO/DEBUG(低基数)
          service:      # 服务名(低基数)
          # 注意:不要把 trace_id 设为标签!(高基数,查询时用 |= 过滤)
      # 过滤敏感字段
      - replace:
          expression: '("password"\s*:\s*)"[^"]*"'
          replace: '$1"[REDACTED]"'

# 查询示例:按 trace_id 关联日志
# {service="payment-svc"} |= "4bf92f3577b34da6a3ce929d0e0e4736"

Trace ID / traceparent 格式校验器

粘贴 32 位十六进制的 trace id,或完整的 traceparent 头值(00-<trace>-<parent-span>-<flags>)。校验规则对齐 W3C Trace Context:长度与字符集、禁止全零 trace id / parent id。解析仅在浏览器本地完成。


              

trace id 须恰好 32 个十六进制字符且非全 0;traceparent 中 parent id(第三段)须 16 个十六进制字符且非全 0;版本与 flags 各 2 位十六进制。

---
name: logging-tracing
description: 结构化日志与分布式追踪上下文规范
model: claude-sonnet-4-5
---

# 结构化日志必要字段
required_fields:
  - timestamp(ISO 8601 UTC)
  - level(ERROR|WARN|INFO|DEBUG)
  - service.name + service.version
  - trace_id(32位hex,若在追踪链路中)
  - span_id(16位hex,若可得)
  - message 或事件码

# 日志级别规范
log_levels:
  ERROR: 需要立即关注(服务不可用/数据丢失风险)
  WARN:  可能有问题但仍运行(降级/重试成功/接近阈值)
  INFO:  关键业务事件(请求完成/状态变更/启动停止)
  DEBUG: 详细排障信息(仅开发/预发,生产默认关闭)

# 安全约束
security:
  forbidden_in_logs: [password, token, credit_card, ssn, api_key]
  pii_handling: hash_or_truncate(user_id 哈希,email 仅保留域名)
  stack_trace: fingerprint_only(完整栈存追踪后端)

# Loki 索引策略
loki_labels: [level, service]  # 低基数标签
loki_forbidden_labels: [trace_id, user_id, request_id]  # 高基数

返回技能库 更多技能入口