21.02-请求日志
要点
- 每个请求都需要记录基础信息:method、path、status、duration
- 使用中间件统一处理,避免在每个路由里重复写日志代码
- requestId 是串联跨服务日志的关键,必须在请求入口生成
- 请求日志要结构化(JSON),方便后续查询和过滤
内容
1. 请求日志要记录什么
每个 HTTP 请求至少应该记录这几个字段:
| 字段 | 含义 | 为什么需要 |
|---|---|---|
| method | GET/POST/PUT/DELETE | 区分请求类型 |
| path | 请求路径 | 定位是哪个接口 |
| status | 响应状态码 | 判断成功/失败 |
| duration | 耗时(毫秒) | 性能监控 |
| requestId | 请求唯一 ID | 串联同一请求的多条日志 |
| userId / apiKeyId | 用户标识 | 定位是哪个用户 |
可选但推荐的字段:
ip:客户端 IP(用于安全审计)userAgent:客户端类型(浏览器/移动端/脚本)referer:来源页面(分析流量来源)contentLength:请求/响应体大小(排查大包问题)
2. 用中间件统一记录
不要在每个路由里手写日志,用 Hono 中间件统一处理:
// src/middleware/request-logger.ts
import { Context, Next } from 'hono'
import { log } from '../lib/logger'
export async function requestLogger(c: Context, next: Next) {
const startedAt = Date.now()
const requestId = crypto.randomUUID()
// 存储到请求上下文,后续路由可以读取
c.set('requestId', requestId)
c.set('startedAt', startedAt)
// 响应头也带上 requestId,方便前端排查
c.header('X-Request-Id', requestId)
await next()
// 请求完成后记录日志
log.info('http.request', {
requestId,
method: c.req.method,
path: c.req.path,
status: c.res.status,
duration: Date.now() - startedAt,
userId: c.get('userId'), // 如果鉴权中间件已经设置
})
}然后在入口文件里注册:
// src/index.ts
import { Hono } from 'hono'
import { requestLogger } from './middleware/request-logger'
const app = new Hono()
// 所有请求都经过这个中间件
app.use('*', requestLogger)
app.get('/health', (c) => c.json({ status: 'ok' }))
app.post('/v1/chat/completions', async (c) => {
// 这里可以直接读取 requestId
const requestId = c.get('requestId')
// ...
})
export default app3. requestId 的作用
requestId 是排查问题的关键。一个用户请求可能触发多条日志:
http.request requestId=abc123 method=POST path=/v1/chat/completions
llm.call requestId=abc123 model=gpt-4 tokens=1500
cache.miss requestId=abc123 key=user:123:preferences
llm.success requestId=abc123 duration=2300ms
http.request requestId=abc123 status=200 duration=2500ms
当你看到用户反馈「刚才那个请求很慢」,可以:
- 从前端拿到
X-Request-Id响应头 - 在日志平台搜索
requestId=abc123 - 看到完整的请求链路:HTTP 请求 → LLM 调用 → 缓存查询 → 响应
如果没有 requestId,你只能靠时间戳猜「这几条日志是不是同一个请求」,在高并发场景下几乎不可能。
3.1 requestId 的生成时机
必须在请求入口生成,而不是在路由里。原因:
- 中间件在路由之前执行,可以确保所有日志都带上 requestId
- 如果路由里抛出异常,全局错误处理器也能读取到 requestId
- 响应头里返回 requestId,前端可以记录下来
错误做法:
// ❌ 在路由里生成,太晚了
app.post('/v1/chat/completions', async (c) => {
const requestId = crypto.randomUUID() // 中间件的日志还没有这个 ID
// ...
})正确做法:
// ✅ 在中间件里生成,所有后续代码都能读取
app.use('*', async (c, next) => {
c.set('requestId', crypto.randomUUID())
await next()
})4. 敏感信息脱敏
请求日志里不要记录敏感信息:
- ❌ 密码、token、API key
- ❌ 完整的信用卡号
- ❌ 个人隐私信息(手机号、身份证号)
如果需要在日志里记录请求体,先做脱敏:
// src/lib/sanitize.ts
export function sanitizeBody(body: Record<string, unknown>): Record<string, unknown> {
const sensitiveFields = ['password', 'token', 'apiKey', 'creditCard']
const sanitized = { ...body }
for (const field of sensitiveFields) {
if (field in sanitized) {
sanitized[field] = '***'
}
}
return sanitized
}// src/middleware/request-logger.ts
import { sanitizeBody } from '../lib/sanitize'
export async function requestLogger(c: Context, next: Next) {
// ...
await next()
// 只记录非敏感请求体
if (c.req.method === 'POST' || c.req.method === 'PUT') {
const body = await c.req.json().catch(() => null)
if (body) {
log.info('http.request.body', {
requestId: c.get('requestId'),
body: sanitizeBody(body),
})
}
}
}5. 日志级别选择
不同场景使用不同级别:
| 场景 | 级别 | 示例 |
|---|---|---|
| 正常请求 | info | 200/201/204 响应 |
| 客户端错误 | warn | 400/401/403/404 |
| 服务端错误 | error | 500/502/503 |
| 慢请求 | warn | duration > 1000ms |
// src/middleware/request-logger.ts
export async function requestLogger(c: Context, next: Next) {
// ...
await next()
const status = c.res.status
const duration = Date.now() - startedAt
let level: 'info' | 'warn' | 'error' = 'info'
if (status >= 500) {
level = 'error'
} else if (status >= 400 || duration > 1000) {
level = 'warn'
}
log[level]('http.request', {
requestId: c.get('requestId'),
method: c.req.method,
path: c.req.path,
status,
duration,
})
}6. 性能影响
日志本身也有开销,需要注意:
- JSON.stringify 是同步的:大请求体会阻塞事件循环
- I/O 操作要异步:如果日志写入文件/数据库,用
await或后台任务 - 采样率控制:高流量场景下不需要记录每个请求
// 采样率控制
export async function requestLogger(c: Context, next: Next) {
const shouldLog = Math.random() < 0.1 // 只记录 10% 的请求
if (!shouldLog) {
await next()
return
}
// ... 记录日志
}Workers Observability 的 head_sampling_rate 也是这个原理,只不过是在平台层面做的。
7. 小结
请求日志是可观测性的基础。三个关键点:
- 统一中间件:不要在每个路由里手写日志
- requestId:在请求入口生成,响应头返回,所有日志都带上
- 结构化:JSON 格式,方便查询和过滤
下一节讲错误日志,看看怎么捕获和记录异常。