21.07-Trace追踪
要点
- Trace 追踪用于追踪跨多个服务的请求链路
- 一个 trace 包含多个 span,每个 span 代表一个操作
- Cloudflare Workers 支持 OpenTelemetry 标准
- 对于 AI 网关,trace 可以追踪:HTTP 请求 → 缓存查询 → LLM 调用 → 响应
内容
1. Trace 和 Span 的概念
Trace:一次完整请求的链路追踪。例如用户调用 /v1/chat/completions,整个请求从开始到结束就是一个 trace。
Span:trace 中的一个操作单元。例如:
- HTTP 请求处理(父 span)
- 鉴权中间件(子 span)
- 缓存查询(子 span)
- LLM API 调用(子 span)
- 响应序列化(子 span)
每个 span 记录:
operationName:操作名称(如llm.call.openai)startTime:开始时间duration:持续时间tags:标签(如model=gpt-4、tokens=1500)logs:事件日志(如错误信息)
2. 为什么需要 Trace
日志可以告诉你「这个请求花了 2500ms」,但不能告诉你:
- 这 2500ms 里,哪一步最慢?
- 缓存命中率是多少?
- LLM API 调用了多少次?每次耗时多少?
Trace 可以可视化整个请求链路:
HTTP Request (2500ms)
├── Auth Middleware (5ms)
├── Cache Lookup (10ms) [HIT]
├── LLM Call (2450ms)
│ ├── OpenAI API (2400ms)
│ └── Response Parsing (50ms)
└── Response Serialization (35ms)
一眼就能看出:LLM API 调用占了 96% 的时间。
3. Cloudflare Workers 的 Trace 支持
Workers 原生支持 OpenTelemetry(OTel),可以通过 observability.traces 配置启用:
// wrangler.jsonc
{
"observability": {
"traces": {
"enabled": true
}
}
}启用后,Workers 会自动生成 trace 并发送到 Cloudflare Dashboard。
3.1 手动创建 Span
自动 trace 只记录粗粒度的信息(HTTP 请求、fetch 调用)。要记录业务逻辑,需要手动创建 span:
// src/lib/trace.ts
import { trace } from '@opentelemetry/api'
const tracer = trace.getTracer('ai-gateway')
export async function withSpan<T>(
name: string,
fn: () => Promise<T>,
attributes?: Record<string, string | number>
): Promise<T> {
return tracer.startActiveSpan(name, async (span) => {
try {
if (attributes) {
for (const [key, value] of Object.entries(attributes)) {
span.setAttribute(key, value)
}
}
const result = await fn()
span.setStatus({ code: 0 }) // OK
return result
} catch (err) {
span.setStatus({ code: 2, message: err.message }) // ERROR
span.recordException(err)
throw err
} finally {
span.end()
}
})
}// src/routes/chat.ts
import { withSpan } from '../lib/trace'
app.post('/v1/chat/completions', async (c) => {
const requestId = c.get('requestId')
// 1. 鉴权
const user = await withSpan('auth.verify', async () => {
return verifyApiKey(c.req.header('Authorization'))
})
// 2. 缓存查询
const cacheKey = generateCacheKey(body)
const cached = await withSpan('cache.lookup', async () => {
return await c.env.CACHE.get(cacheKey)
}, { 'cache.key': cacheKey })
if (cached) {
return c.json(cached)
}
// 3. LLM 调用
const result = await withSpan('llm.call', async () => {
return await callLLM(body, c.env)
}, {
'llm.model': body.model,
'llm.provider': 'openai',
})
// 4. 缓存写入
await withSpan('cache.write', async () => {
await c.env.CACHE.put(cacheKey, JSON.stringify(result))
})
return c.json(result)
})4. 安装 OpenTelemetry SDK
Workers 需要安装 OpenTelemetry SDK:
// terminal
npm install @opentelemetry/api @opentelemetry/sdk-trace-base// src/index.ts
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-base'
import { trace } from '@opentelemetry/api'
// 初始化 tracer provider
const provider = new NodeTracerProvider()
provider.register()
// 创建 tracer
export const tracer = trace.getTracer('ai-gateway')
// ... Hono app注意:Workers 环境下,OpenTelemetry 的配置可能和 Node.js 有些差异。具体参考 Cloudflare 的文档。
5. 实战:AI 网关的完整 Trace
// src/routes/chat.ts
import { tracer } from '../lib/trace'
import { log } from '../lib/logger'
app.post('/v1/chat/completions', async (c) => {
const requestId = c.get('requestId')
const userId = c.get('userId')
const body = await c.req.json()
// 创建根 span
const rootSpan = tracer.startSpan('http.post./v1/chat/completions')
rootSpan.setAttribute('request.id', requestId)
rootSpan.setAttribute('user.id', userId)
try {
// 1. 鉴权
const authSpan = tracer.startSpan('auth.verify', { childOf: rootSpan })
try {
const user = await verifyApiKey(c.req.header('Authorization'))
authSpan.setAttribute('user.id', user.id)
authSpan.setStatus({ code: 0 })
} catch (err) {
authSpan.setStatus({ code: 2, message: err.message })
throw err
} finally {
authSpan.end()
}
// 2. 缓存查询
const cacheSpan = tracer.startSpan('cache.lookup', { childOf: rootSpan })
try {
const cacheKey = generateCacheKey(body)
const cached = await c.env.CACHE.get(cacheKey)
cacheSpan.setAttribute('cache.key', cacheKey)
cacheSpan.setAttribute('cache.hit', cached ? 'true' : 'false')
cacheSpan.setStatus({ code: 0 })
if (cached) {
return c.json(JSON.parse(cached))
}
} catch (err) {
cacheSpan.setStatus({ code: 2, message: err.message })
} finally {
cacheSpan.end()
}
// 3. LLM 调用
const llmSpan = tracer.startSpan('llm.call', { childOf: rootSpan })
let result
try {
llmSpan.setAttribute('llm.model', body.model)
llmSpan.setAttribute('llm.provider', 'openai')
result = await callLLM(body, c.env)
llmSpan.setAttribute('llm.tokens.prompt', result.usage.prompt_tokens)
llmSpan.setAttribute('llm.tokens.completion', result.usage.completion_tokens)
llmSpan.setAttribute('llm.tokens.total', result.usage.total_tokens)
llmSpan.setStatus({ code: 0 })
} catch (err) {
llmSpan.setStatus({ code: 2, message: err.message })
llmSpan.recordException(err)
throw err
} finally {
llmSpan.end()
}
// 4. 缓存写入
const cacheWriteSpan = tracer.startSpan('cache.write', { childOf: rootSpan })
try {
const cacheKey = generateCacheKey(body)
await c.env.CACHE.put(cacheKey, JSON.stringify(result), {
expirationTtl: 3600,
})
cacheWriteSpan.setAttribute('cache.key', cacheKey)
cacheWriteSpan.setAttribute('cache.ttl', 3600)
cacheWriteSpan.setStatus({ code: 0 })
} catch (err) {
cacheWriteSpan.setStatus({ code: 2, message: err.message })
} finally {
cacheWriteSpan.end()
}
rootSpan.setStatus({ code: 0 })
return c.json(result)
} catch (err) {
rootSpan.setStatus({ code: 2, message: err.message })
rootSpan.recordException(err)
throw err
} finally {
rootSpan.end()
}
})6. 用装饰器简化 Trace
手动管理 span 很繁琐,可以用装饰器简化:
// src/lib/trace-decorator.ts
import { tracer } from './trace'
export function traced(operationName?: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value
const name = operationName || `${target.constructor.name}.${propertyKey}`
descriptor.value = async function (...args: any[]) {
const span = tracer.startSpan(name)
try {
const result = await original.apply(this, args)
span.setStatus({ code: 0 })
return result
} catch (err) {
span.setStatus({ code: 2, message: err.message })
span.recordException(err)
throw err
} finally {
span.end()
}
}
}
}// src/lib/llm.ts
export class LLMService {
@traced('llm.call.openai')
async callOpenAI(body: ChatRequest): Promise<ChatResponse> {
// ...
}
@traced('cache.lookup')
async lookupCache(key: string): Promise<string | null> {
// ...
}
}7. Trace 和日志的结合
Trace 和日志是互补的:
- Trace:可视化请求链路,快速定位慢操作
- 日志:记录详细信息,深入排查问题
两者通过 requestId(或 traceId)关联:
// src/routes/chat.ts
import { trace } from '@opentelemetry/api'
app.post('/v1/chat/completions', async (c) => {
const span = tracer.startSpan('http.post./v1/chat/completions')
const traceId = span.spanContext().traceId
// 把 traceId 存到上下文,所有日志都带上
c.set('traceId', traceId)
try {
log.info('llm.call.start', {
requestId: c.get('requestId'),
traceId,
model: body.model,
})
const result = await callLLM(body, c.env)
log.info('llm.call.success', {
requestId: c.get('requestId'),
traceId,
model: body.model,
tokens: result.usage.total_tokens,
})
return c.json(result)
} catch (err) {
log.error('llm.call.failed', {
requestId: c.get('requestId'),
traceId,
model: body.model,
error: err.message,
})
throw err
} finally {
span.end()
}
})这样在 Dashboard 里:
- 看到 trace 显示「LLM 调用花了 2400ms」
- 点击 span,看到关联的日志:「
llm.call.successmodel=gpt-4 tokens=1500」 - 结合两者判断:这 2400ms 是正常的 LLM 响应时间,还是有什么异常
8. 第三方 Trace 平台
Cloudflare Dashboard 的 trace 功能比较基础,复杂场景可以接入第三方平台:
| 平台 | 特点 | 价格 |
|---|---|---|
| Honeycomb | 专为 trace 设计,查询强大 | 按数据量计费 |
| Datadog | 全功能 APM,和日志/指标集成 | 按主机计费 |
| Jaeger | 开源,可以自建 | 免费(需要自己部署) |
| New Relic | 全功能 APM | 按数据量计费 |
接入方式通常是通过 OpenTelemetry 导出器:
// src/index.ts
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
const exporter = new OTLPTraceExporter({
url: 'https://api.honeycomb.io/v1/traces',
headers: {
'x-honeycomb-team': env.HONEYCOMB_API_KEY,
},
})
provider.addSpanProcessor(new BatchSpanProcessor(exporter))9. 小结
Trace 追踪的关键点:
- 概念:Trace 是一次请求的完整链路,Span 是其中的一个操作
- 用途:快速定位慢操作、分析请求链路、排查跨服务问题
- Workers 支持:通过
observability.traces启用,支持 OpenTelemetry - 手动 Span:用
tracer.startSpan()记录业务逻辑 - 和日志结合:通过
traceId关联 trace 和日志
对于中小项目,Workers Observability 自带的 trace 功能基本够用。复杂场景可以接入 Honeycomb、Datadog 等平台。
下一节讲 Metrics 指标,看看怎么收集和查询自定义指标。