Hono 工程化实战 02:日志、Request ID 和可观测性
wxk1991 Lv5

Hono 工程化实战 02:日志、Request ID 和可观测性

API 上线以后,最怕的不是报错。

最怕的是报错了,但你不知道发生了什么。

用户说:

1
刚才点保存失败了

你去看日志,只有一句:

1
Error: failed

没有请求路径,没有用户,没有租户,没有 request id,没有耗时,没有错误栈。

这不是日志。

这是谜语。

Hono 项目要进生产,必须把日志和可观测性当成基础设施来做。

这篇文章讲 Hono API 里最实用的一套可观测性做法:Request ID、结构化日志、耗时统计、错误日志、业务事件日志。

技术栈快照时间:2026-06-23。Hono logger、request-id、timing middleware 会更新,生产使用前以官方文档为准。


一、先理解可观测性的目标

可观测性不是把日志打得越多越好。

它要解决三个问题:

1
2
3
发生了什么
影响了谁
为什么发生

对 API 来说,至少要能回答:

1
2
3
4
5
6
7
8
哪个请求失败了
状态码是多少
耗时是多少
哪个用户触发的
属于哪个租户
错误栈在哪里
有没有关联的后台任务
有没有调用第三方服务

如果日志不能帮助你定位问题,那它只是噪音。


二、每个请求都要有 Request ID

Request ID 是排查问题的线头。

一个请求进入系统时生成一个 ID,之后所有相关日志都带上它。

Hono 有内置 request-id middleware。

你也可以自己写一个简单版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Hono } from 'hono'

type Variables = {
requestId: string
}

const app = new Hono<{ Variables: Variables }>()

app.use('*', async (c, next) => {
const requestId =
c.req.header('X-Request-ID') ?? crypto.randomUUID()

c.set('requestId', requestId)
c.header('X-Request-ID', requestId)

await next()
})

这样前端收到错误时,可以把 request id 一起上报:

1
2
3
4
5
6
7
{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "服务器内部错误"
},
"requestId": "01JZ8Y8MQZ8C7N5F9W1T"
}

用户反馈问题时,你就能直接按 request id 查日志。


三、用结构化日志,不要拼字符串

不要这样写:

1
console.log(`user ${user.id} create post ${post.id}`)

更推荐结构化日志:

1
2
3
4
5
6
console.info('post_created', {
requestId: c.get('requestId'),
userId: user.id,
tenantId: tenant.id,
postId: post.id,
})

结构化日志的好处是:

1
2
3
4
可以按字段搜索
可以做统计
可以聚合告警
更容易接入日志平台

日志消息用稳定事件名:

1
2
3
4
5
request_completed
auth_failed
post_created
payment_webhook_received
job_failed

不要每次换一种写法。

稳定事件名会让排查和统计都轻很多。


四、写一个请求日志 middleware

Hono 内置 logger 很适合本地开发。

生产环境更推荐结构化请求日志。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.use('*', async (c, next) => {
const start = Date.now()
const requestId = c.get('requestId')

await next()

const durationMs = Date.now() - start

console.info('request_completed', {
requestId,
method: c.req.method,
path: new URL(c.req.url).pathname,
status: c.res.status,
durationMs,
userAgent: c.req.header('user-agent'),
})
})

这样每个请求都会有一条完成日志。

字段不要太多。

建议基础字段:

1
2
3
4
5
6
7
8
9
requestId
method
path
status
durationMs
userId
tenantId
ip
userAgent

如果还没鉴权,userId 可以为空。


五、错误日志要带上下文

Hono 的 app.onError() 是统一错误日志的好地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
app.onError((err, c) => {
const requestId = c.get('requestId')

console.error('unhandled_error', {
requestId,
method: c.req.method,
path: new URL(c.req.url).pathname,
message: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
})

return c.json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: '服务器内部错误',
},
requestId,
}, 500)
})

注意两点。

第一,错误栈可以进服务端日志,但不要返回给前端。

第二,日志里要有 request id。

否则你看到错误栈,也很难知道它对应的是哪个用户请求。


六、业务错误和系统错误分开

不是所有错误都应该打成 console.error

比如:

1
2
3
4
密码错误
参数缺失
无权限
资源不存在

这些是业务预期内的错误。

可以记录为 warn 或 info。

真正需要 error 的通常是:

1
2
3
4
5
数据库连接失败
第三方接口异常
未知异常
队列任务连续失败
数据状态不一致

可以定义一个 AppError:

1
2
3
4
5
6
7
8
9
class AppError extends Error {
constructor(
public code: string,
public status: number,
message: string
) {
super(message)
}
}

统一处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
app.onError((err, c) => {
const requestId = c.get('requestId')

if (err instanceof AppError) {
console.warn('app_error', {
requestId,
code: err.code,
status: err.status,
message: err.message,
})

return c.json({
error: {
code: err.code,
message: err.message,
},
requestId,
}, err.status)
}

console.error('unhandled_error', {
requestId,
message: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
})

return c.json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: '服务器内部错误',
},
requestId,
}, 500)
})

这样日志等级会更干净。

你不会被大量 404、401 淹没真正的系统错误。


七、耗时统计要分层

只知道请求总耗时还不够。

比如一个请求耗时 2 秒,你还想知道慢在哪里:

1
2
3
4
数据库查询慢
第三方 API 慢
对象存储慢
业务计算慢

可以写一个简单计时工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function measure<T>(
name: string,
fn: () => Promise<T>,
meta: Record<string, unknown> = {}
) {
const start = Date.now()

try {
return await fn()
} finally {
console.info('operation_timing', {
name,
durationMs: Date.now() - start,
...meta,
})
}
}

使用:

1
2
3
4
5
6
7
8
const post = await measure(
'db.posts.findById',
() => posts.findById(id),
{
requestId: c.get('requestId'),
postId: id,
}
)

Hono 也有 Server-Timing middleware,可以把部分耗时写到响应 header,方便浏览器 DevTools 查看。

生产里我更建议:

1
2
开发调试:Server-Timing
线上分析:结构化日志 + 指标系统

八、不要记录敏感信息

日志里不要出现:

1
2
3
4
5
6
7
8
9
密码
token
session cookie
身份证号
银行卡
完整手机号
完整邮箱
第三方 secret
Authorization header

请求日志尤其要小心。

不要把所有 header 和 body 直接打出来:

1
console.log(await c.req.json())

这很危险。

如果必须记录 body,也要做脱敏:

1
2
3
4
5
6
7
function redact(input: Record<string, unknown>) {
return {
...input,
password: input.password ? '[REDACTED]' : undefined,
token: input.token ? '[REDACTED]' : undefined,
}
}

更好的方式是只记录业务必要字段:

1
2
3
4
5
console.info('login_failed', {
requestId,
emailHash,
reason: 'invalid_password',
})

日志是长期留存的数据。

不要把它变成另一个泄露风险。


九、业务事件日志

除了请求日志,还应该有业务事件日志。

比如:

1
2
3
4
5
6
7
user_registered
user_logged_in
post_published
payment_succeeded
tenant_plan_changed
member_invited
api_key_created

业务事件日志的价值是:

1
2
3
4
排查用户行为
分析关键流程
做审计
做告警

示例:

1
2
3
4
5
6
7
console.info('tenant_plan_changed', {
requestId: c.get('requestId'),
tenantId: tenant.id,
actorUserId: user.id,
fromPlan: 'free',
toPlan: 'pro',
})

不要把业务事件混在普通请求日志里。

请求日志回答“请求发生了什么”。

业务事件回答“业务发生了什么”。


十、后台任务也要串起来

如果 API 创建了后台任务,日志要能串起来。

创建任务时:

1
2
3
4
5
6
await c.env.REPORT_QUEUE.send({
jobId,
requestId: c.get('requestId'),
tenantId: tenant.id,
userId: user.id,
})

消费者处理时:

1
2
3
4
5
console.info('job_started', {
jobId: message.body.jobId,
requestId: message.body.requestId,
tenantId: message.body.tenantId,
})

这样你就能从一个 API 请求追到后台任务。

否则用户说“我点了导出没反应”,你只能在请求日志和任务日志之间靠猜。


十一、告警不要太晚做

最基础的告警可以先围绕这些指标:

1
2
3
4
5
6
7
8
5xx 数量
核心接口错误率
核心接口 P95/P99 耗时
队列积压
后台任务失败率
支付 Webhook 失败
登录失败异常升高
数据库查询异常

不用一开始就上复杂平台。

但至少要保证:

1
系统坏了,你能比用户更早知道

这是生产系统的底线。


十二、我推荐的最小可观测性模板

一个 Hono API 上线前,至少做这几件事:

1
2
3
4
5
6
7
8
9
1. 每个请求生成 requestId
2. 响应 header 返回 X-Request-ID
3. 请求完成时打结构化日志
4. app.onError 统一记录未知错误
5. AppError 区分业务错误和系统错误
6. 核心业务动作打事件日志
7. 后台任务继承 requestId
8. 日志脱敏
9. 给核心错误率和耗时做告警

这套东西不复杂。

但它会显著改变你维护系统的手感。

没有可观测性的 API,出了问题像在黑盒里摸索。

有了 request id、结构化日志和关键事件,你至少知道从哪里开始查。

Hono 很轻,但生产运维不能轻飘飘。