AI 电子伴侣
创建时间: 2026-04-18最后更新: 2026-04-18

1. Workers 的可观测性为什么「不一样」

传统服务器有 /var/log,有 pm2 /systemd /docker logs,出问题 SSH 上去看日志。Workers 没有这些——代码跑在全球几百个节点上,一次请求可能落到东京、法兰克福、圣保罗里的任意一个。

这带来两个现实:

  1. 你看不见单个节点。你要的是「全球汇总的视角」
  2. 日志不会自己持久化。Worker 跑完了,console.log 就飘走了,除非你主动收集

不过 Cloudflare 在这方面的工具已经比较全了,目前可用的:

工具用途
wrangler tail实时在终端里看日志,开发期最常用
Workers Observability(新功能)Dashboard 上自动存日志、trace、metrics
Logpush把日志推送到 R2/S3/第三方(Datadog、Axiom 等)
Analytics Engine超便宜的自定义指标写入
Tail Workers用一个 Worker 处理另一个 Worker 的日志(过滤、转发)
Sentry / 第三方 SDK业务级错误追踪

这一篇把这些工具过一遍,最后给 AI 网关补上完整的可观测性。

2. wrangler tail:开发期的救命稻草

最直接的调试方式:

terminal
01
# 跟踪生产 Worker 的实时日志
02
npx wrangler tail
03
04
# 跟踪特定 Worker
05
npx wrangler tail my-api-gateway
06
07
# 只看错误
08
npx wrangler tail --status=error
09
10
# 只看某个 IP 的请求
11
npx wrangler tail --ip-address=203.0.113.42

终端里会看到每次请求的 method/path/status、以及你在代码里 console.log 的东西。生产环境临时查问题,这个命令足以解决 80% 场景。

它的两个局限:

  • 必须开着终端:关了就丢
  • 历史不可回查:你看到的是「从这一刻起」的日志

要长期可见 + 可回查的日志,需要下一节的 Workers Observability。

3. Workers Observability:Cloudflare 自己的日志平台

wrangler.jsonc 里打开这一项,Cloudflare 就会自动把日志存进 Dashboard:

wrangler.jsonc
1
{
2
"observability": {
3
"logs": {
4
"enabled": true,
5
"head_sampling_rate": 1 // 采样率 1 = 全部记录
6
}
7
}
8
}

启用后:

  • Cloudflare Dashboard → Workers → 你的 Worker → Logs 可以看到完整历史
  • 默认保留 7 天(付费版可延长)
  • 支持按 status code / duration / path / 自定义字段过滤
  • Query Builder 可以像 SQL 一样查日志

中小项目用这个基本够了,不一定需要接第三方日志平台。

3.1 采样率怎么选

head_sampling_rate 是采样率,0~1 的浮点数,代表记录多大比例的请求。比如 0.1 就是只记录 10% 的请求——流量大了之后全量记录太贵,只采样一部分就够看趋势了:

  • 1:全量(开发环境、刚上线)
  • 0.1:10%(流量大后的默认推荐)
  • 0.01:1%(超大流量、只看趋势)

错误日志不受采样影响——Workers Observability 会额外自动记录所有 5xx,你不会因为采样而漏掉错误。

4. 结构化日志:console.log 怎么写

console.log('error happened') 这种日志字符串在 Dashboard 里很难过滤。改成 JSON 对象:

src/lib/logger.ts
01
export const log = {
02
info: (event: string, data?: Record<string, unknown>) => {
03
console.log(JSON.stringify({ level: 'info', event, ...data, ts: Date.now() }))
04
},
05
warn: (event: string, data?: Record<string, unknown>) => {
06
console.warn(JSON.stringify({ level: 'warn', event, ...data, ts: Date.now() }))
07
},
08
error: (event: string, data?: Record<string, unknown>) => {
09
console.error(JSON.stringify({ level: 'error', event, ...data, ts: Date.now() }))
10
},
11
}

Workers Observability 会自动识别 JSON 结构,你在 Query Builder 里可以直接按字段过滤:

src/routes/chat.ts
01
import { log } from '../lib/logger'
02
03
app.post('/chat', async (c) => {
04
const body = await c.req.json()
05
const startedAt = Date.now()
06
07
try {
08
const result = await callLLM(body)
09
log.info('llm.success', {
10
model: body.model,
11
userId: c.get('apiKeyId'),
12
tokens: result.usage.total_tokens,
13
duration: Date.now() - startedAt,
14
})
15
return c.json(result)
16
} catch (err) {
17
log.error('llm.failed', {
18
model: body.model,
19
userId: c.get('apiKeyId'),
20
message: (err as Error).message,
21
duration: Date.now() - startedAt,
22
})
23
throw err
24
}
25
})

之后 Dashboard 里可以查「最近 1 小时内 model=claude-opus-4-6 的所有失败请求」——这种查询在字符串日志里是做不到的。

4.1 一个好日志的字段清单

生产 Worker 建议每次请求都带上这几个字段:

字段含义
event业务事件名(如 llm.successqueue.retry
levelinfo / warn / error
userId / apiKeyId哪个用户
requestId请求级唯一 ID(方便跨日志串起来)
duration耗时(毫秒)
model / resource这次操作的对象
error.message / error.stack出错时的详细信息

requestId 可以用 crypto.randomUUID() 在请求入口生成,通过 c.set('requestId', ...) 在整个请求里共享。

5. 错误追踪:Sentry 之类

日志好用,但它不告诉你「错误的聚合情况」。业务级错误追踪(Sentry、Bugsnag、Axiom)解决的是这类问题:

  • 同一个错误在过去 1 小时发生了多少次?
  • 哪个版本部署后开始的?
  • 影响了多少独立用户?
  • 异常栈是什么?

5.1 给 Hono 接 Sentry

Sentry 官方提供了 Workers SDK:

terminal
1
npm install @sentry/cloudflare
src/index.ts
01
import * as Sentry from '@sentry/cloudflare'
02
import { Hono } from 'hono'
03
04
type Bindings = {
05
SENTRY_DSN: string
06
}
07
08
const app = new Hono<{ Bindings: Bindings }>()
09
10
app.onError((err, c) => {
11
// 让 Sentry 抓到
12
Sentry.captureException(err, {
13
tags: { path: c.req.path, method: c.req.method },
14
user: { id: c.get('userId') },
15
})
16
return c.json({ error: 'Internal Server Error' }, 500)
17
})
18
19
export default Sentry.withSentry(
20
(env: Bindings) => ({
21
dsn: env.SENTRY_DSN,
22
tracesSampleRate: 0.1,
23
}),
24
app
25
)

Sentry.withSentry() 包一层默认导出,它会自动捕获未处理的异常、附加 Workers 特有的运行环境信息。

5.2 不要把所有东西都扔给 Sentry

一个常见反模式:console.error 全部 captureException 一遍。Sentry 按事件数计费,几小时内你就会收到账单告警。

原则:Sentry 只收需要人类介入修复的异常。业务级失败(校验错误、用户权限不足)走普通日志就行。

6. Analytics Engine:便宜到奢侈的自定义指标

Cloudflare 的 Analytics Engine 是专门给 Workers 写指标的时序数据库。免费版每天 10 万次写入 / 1 万次读取,个人项目够用。

适用场景:

  • 每次 LLM 调用的 token 数、延迟、成本
  • 每次缓存命中/miss 的统计
  • 每类业务事件的 counter

6.1 写入

wrangler.jsonc
1
{
2
"analytics_engine_datasets": [
3
{ "binding": "ANALYTICS", "dataset": "ai_gateway_metrics" }
4
]
5
}
src/routes/chat.ts
01
type Bindings = { ANALYTICS: AnalyticsEngineDataset } // Workers 内置类型
02
03
app.post('/chat', async (c) => {
04
const result = await callLLM(body)
05
06
// writeDataPoint 的字段分三类:
07
// blobs — 字符串列(最多 20 个),用来过滤和 group by
08
// doubles — 数值列(最多 20 个),用来求和、求平均
09
// indexes — 采样索引(最多 1 个),高基数场景下的采样键
10
c.env.ANALYTICS.writeDataPoint({
11
blobs: [body.model, c.get('apiKeyId'), 'success'],
12
doubles: [result.usage.total_tokens, Date.now() - startedAt],
13
indexes: [c.get('apiKeyId')],
14
})
15
16
return c.json(result)
17
})

6.2 查询

Dashboard 里有 UI,或者用 SQL API:

query.sql
1
SELECT
2
blob1 AS model,
3
SUM(_sample_interval) AS total_requests,
4
SUM(double1) AS total_tokens,
5
AVG(double2) AS avg_duration_ms
6
FROM ai_gateway_metrics
7
WHERE timestamp > NOW() - INTERVAL '1' HOUR
8
GROUP BY blob1
9
ORDER BY total_requests DESC

对 AI 项目来说 Analytics Engine 很实用:一行代码埋点,一行 SQL 出报表。比自己在 KV 里攒数据再 cron 汇总简单多了。

7. Logpush:把日志送到外部平台

如果你团队已经有 Datadog、Axiom、Grafana Loki 之类的统一日志平台,可以用 Logpush 把 Workers 日志直接推过去:

terminal
1
npx wrangler logpush create \
2
--dataset workers_trace_events \
3
--destination "https://your-endpoint/path?token=xxx"

Logpush 支持的目的地:R2、S3、HTTP endpoint、Datadog、New Relic、Sumo Logic 等。配置好之后日志会被批量推送(通常每几分钟一批),而不是实时。

对绝大多数中小项目,Workers Observability 本身就够了,Logpush 主要给已经用了 Datadog 的大团队

8. 实战:AI 网关的完整可观测性

回到第 18 篇的 AI 网关,我们给它补全可观测性:

src/index.ts
01
import { Hono } from 'hono'
02
import * as Sentry from '@sentry/cloudflare'
03
import { log } from './lib/logger'
04
05
type Bindings = {
06
SENTRY_DSN: string
07
ANALYTICS: AnalyticsEngineDataset
08
OPENAI_API_KEY: string
09
}
10
11
const app = new Hono<{ Bindings: Bindings }>()
12
13
// 请求级 trace ID
14
app.use('*', async (c, next) => {
15
const requestId = crypto.randomUUID()
16
c.set('requestId', requestId)
17
c.header('X-Request-Id', requestId)
18
await next()
19
})
20
21
// 全局请求日志
22
app.use('*', async (c, next) => {
23
const startedAt = Date.now()
24
await next()
25
log.info('http.request', {
26
requestId: c.get('requestId'),
27
method: c.req.method,
28
path: c.req.path,
29
status: c.res.status,
30
duration: Date.now() - startedAt,
31
})
32
})
33
34
app.post('/v1/chat/completions', async (c) => {
35
const body = await c.req.json()
36
const startedAt = Date.now()
37
38
try {
39
const result = await callLLM(body, c.env.OPENAI_API_KEY)
40
41
// 结构化日志
42
log.info('llm.success', {
43
requestId: c.get('requestId'),
44
model: body.model,
45
userId: c.get('apiKeyId'),
46
tokens: result.usage.total_tokens,
47
duration: Date.now() - startedAt,
48
})
49
50
// Analytics Engine 埋点
51
c.env.ANALYTICS.writeDataPoint({
52
blobs: [body.model, c.get('apiKeyId'), 'success'],
53
doubles: [result.usage.total_tokens, Date.now() - startedAt],
54
indexes: [c.get('apiKeyId')],
55
})
56
57
return c.json(result)
58
} catch (err) {
59
log.error('llm.failed', {
60
requestId: c.get('requestId'),
61
model: body.model,
62
userId: c.get('apiKeyId'),
63
message: (err as Error).message,
64
})
65
66
// Analytics Engine 记录失败
67
c.env.ANALYTICS.writeDataPoint({
68
blobs: [body.model, c.get('apiKeyId'), 'error'],
69
doubles: [0, Date.now() - startedAt],
70
indexes: [c.get('apiKeyId')],
71
})
72
73
throw err // 让全局 onError 接住 + Sentry 抓
74
}
75
})
76
77
// 全局错误兜底
78
app.onError((err, c) => {
79
Sentry.captureException(err, {
80
tags: { path: c.req.path },
81
extra: { requestId: c.get('requestId') },
82
})
83
return c.json({ error: 'Internal Server Error' }, 500)
84
})
85
86
export default Sentry.withSentry(
87
(env: Bindings) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 0.1 }),
88
app as any
89
)

这套东西给你的可观测性:

  1. 每个请求都有 requestId(响应头里也返回),排查时可以串起所有日志
  2. 请求级日志(method/path/status/duration)自动化到中间件
  3. 业务级日志(llm.success / llm.failed)在关键分支手工写
  4. Analytics Engine 记录可查询的时序指标
  5. Sentry 捕获未处理异常,按聚合告警
  6. Workers Observability 在 Dashboard 里提供原生查询 UI

9. 小结

Workers 的日志默认不持久化——console.log 打完就丢了。加一行 observability.logs.enabled = true 就能在 Dashboard 里查历史日志。

根据团队规模选组合:

场景推荐组合
个人项目wrangler tail + Workers Observability
中小团队上面 + 结构化日志 + Analytics Engine
已上 Sentry 的团队上面 + Sentry SDK
已上 Datadog / Grafana 的团队上面 + Logpush 推送到现有平台

三个实用原则:日志要结构化(JSON 对象比纯字符串好查得多),每个请求带 requestId(方便串起跨服务日志),只把真正需要人修的异常扔给 Sentry(业务失败走普通日志)。