日志与分布式追踪
指导 Agent 设计结构化日志、传播 Trace / Span 上下文,并在多服务场景下把请求、错误与业务标识关联起来,缩短定位时间。
SKILL 应约定日志字段(时间、级别、服务名、trace_id、span_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 关联两者 ]
关联标识: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/orders、db.query),勿把用户 id 或完整 URL 拼进 Span 名以免拖垮追踪 UI 与存储。 -
异步:在
asyncio/ 线程池 / 回调边界显式传递 context;避免「 fire-and-forget 任务丢失 parent」导致孤儿 Span。
日志聚合配置与安全边界
- 必选或强约定:
timestamp、level、service.name(或等价)、trace_id、span_id(若可得)、message或事件码。 - 错误类:增加
error.type、error.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] # 高基数