日志中间件
要点
- Hono 内置的
logger()中间件提供开箱即用的请求日志 - 默认输出请求方法、路径、状态码和耗时,适合开发阶段调试
- 生产环境通常需要结构化日志,
logger()支持自定义输出函数 - 日志中间件应放在中间件栈的最外层,确保被拦截的请求也有记录
- 可以结合 request-id 中间件,在日志里携带追踪 ID
- 常见的生产日志方案:
pino、winston、JSON 格式输出
1. 默认用法
logger() 是 Hono 内置的日志中间件,直接挂载就能用:
// src/index.ts
import { Hono } from 'hono'
import { logger } from 'hono/logger'
const app = new Hono()
app.use('*', logger())
app.get('/', (c) => c.text('Hello'))
app.get('/users', (c) => c.json([{ id: 1, name: 'Alice' }]))
app.get('/error', () => { throw new Error('oops') })
export default app请求时控制台输出:
// output.txt
--> GET / 200 1ms
--> GET /users 200 2ms
--> GET /error 500 3ms每条日志包含:方向箭头、HTTP 方法、路径、状态码、耗时。--> 表示响应完成。如果是请求到达,还会打印 <-- GET /users。
logger() 默认使用 console.log() 输出。在开发阶段足够用,但在生产环境有几个局限:
- 输出是纯文本,不便解析
- 没有时间戳
- 没有请求 ID,无法关联上下游日志
- 无法按日志级别过滤
2. 自定义日志输出
logger() 接收一个 PrintFunc 函数,可以替换默认的输出行为:
// src/middleware/logger.ts
import { logger } from 'hono/logger'
import type { Log } from 'hono/logger'
type PrintFunc = (
str: string,
...rest: string[]
) => void
const customLogger: PrintFunc = (str, ...rest) => {
// str 是格式化后的日志字符串
// rest 是额外的参数
console.log(`[CUSTOM] ${str}`, ...rest)
}
// src/index.ts
import { Hono } from 'hono'
import { customLogger } from './middleware/logger'
const app = new Hono()
app.use('*', logger(customLogger))
export default app不过 PrintFunc 接收的是已经格式化好的字符串,不方便做结构化输出。更彻底的做法是写一个自定义的日志中间件。
3. 自定义日志中间件
如果需要对日志格式有完全的控制,可以自己写一个日志中间件:
// src/middleware/custom-logger.ts
import { createMiddleware } from 'hono/factory'
export const customLogger = createMiddleware(async (c, next) => {
const start = Date.now()
const requestId = c.get('requestId') as string | undefined
await next()
const duration = Date.now() - start
const status = c.res.status
// JSON 格式输出,便于日志采集工具解析
console.log(JSON.stringify({
time: new Date().toISOString(),
requestId: requestId ?? '-',
method: c.req.method,
path: c.req.path,
status,
duration: `${duration}ms`,
userAgent: c.req.header('user-agent') ?? '-',
}))
})// src/index.ts
import { Hono } from 'hono'
import { requestId } from 'hono/request-id'
import { customLogger } from './middleware/custom-logger'
const app = new Hono()
// requestId 在 customLogger 之前,这样日志可以携带 requestId
app.use('*', requestId())
app.use('*', customLogger)
app.get('/', (c) => c.text('Hello'))
export default app输出:
{
"time": "2026-06-20T10:00:00.000Z",
"requestId": "abc123",
"method": "GET",
"path": "/",
"status": 200,
"duration": "2ms",
"userAgent": "Mozilla/5.0"
}JSON 格式日志可以被 jq、fluentd、loki 等工具直接解析,比纯文本格式更适合生产环境。
4. 接入 pino
pino 是 Node.js 生态里性能较好的 JSON 日志库。接入方式:
// src/middleware/pino-logger.ts
import { createMiddleware } from 'hono/factory'
import pino from 'pino'
const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
})
export const pinoLogger = createMiddleware(async (c, next) => {
const start = Date.now()
const requestId = c.get('requestId') as string | undefined
await next()
const duration = Date.now() - start
logger.info({
requestId: requestId ?? '-',
method: c.req.method,
path: c.req.path,
status: c.res.status,
duration,
userAgent: c.req.header('user-agent') ?? '-',
}, 'request completed')
})pino 的优势:
- 输出固定 JSON 格式,字段类型精确(
duration是数字,不是字符串) - 支持日志级别(
trace、debug、info、warn、error) - 性能经过优化,高吞吐场景下比
console.log快一个数量级 - 配合
pino-pretty可以在开发阶段格式化输出
5. 错误日志
日志中间件通常只记录请求完成的信息。如果路由抛出异常,中间件的后段不会执行(错误冒泡到了 app.onError()),日志可能缺失。
两种处理方式:
5.1 在 onError 里记录
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
app.onError((err, c) => {
console.error(JSON.stringify({
time: new Date().toISOString(),
method: c.req.method,
path: c.req.path,
error: err.message,
stack: err.stack,
}))
return c.json({ error: 'Internal Server Error' }, 500)
})5.2 在中间件里 try-catch
// src/middleware/custom-logger.ts
import { createMiddleware } from 'hono/factory'
export const customLogger = createMiddleware(async (c, next) => {
const start = Date.now()
try {
await next()
} catch (err) {
// 记录错误日志
console.error(JSON.stringify({
time: new Date().toISOString(),
method: c.req.method,
path: c.req.path,
error: (err as Error).message,
}))
// 继续抛给 app.onError() 处理
throw err
}
const duration = Date.now() - start
console.log(JSON.stringify({
time: new Date().toISOString(),
method: c.req.method,
path: c.req.path,
status: c.res.status,
duration: `${duration}ms`,
}))
})第一种方式更简洁,错误处理集中在 onError;第二种方式让日志中间件自己处理错误日志,职责更内聚。两种方式可以并存——onError 负责返回统一格式的错误响应,日志中间件负责记录日志。
6. 日志级别
生产环境通常需要按级别过滤日志,避免信息过载:
// src/middleware/pino-logger.ts
import { createMiddleware } from 'hono/factory'
import pino from 'pino'
const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
})
export const pinoLogger = createMiddleware(async (c, next) => {
const start = Date.now()
// 请求到达时记录 debug 级别
logger.debug({
method: c.req.method,
path: c.req.path,
}, 'request started')
await next()
const duration = Date.now() - start
const status = c.res.status
// 根据状态码选择日志级别
const logLevel = status >= 500
? 'error'
: status >= 400
? 'warn'
: 'info'
logger[logLevel]({
method: c.req.method,
path: c.req.path,
status,
duration,
}, 'request completed')
})通过环境变量 LOG_LEVEL=debug 可以在开发阶段看到更详细的日志,生产环境设置为 info 或 warn 减少输出量。
7. 敏感信息脱敏
日志里不应该出现密码、token、身份证号等敏感信息。如果请求体包含这些字段,需要在记录前脱敏:
// src/middleware/custom-logger.ts
import { createMiddleware } from 'hono/factory'
const SENSITIVE_KEYS = ['password', 'token', 'secret', 'authorization']
function redact(obj: Record<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(obj)) {
if (SENSITIVE_KEYS.includes(key.toLowerCase())) {
result[key] = '[REDACTED]'
} else if (typeof value === 'object' && value !== null) {
result[key] = redact(value as Record<string, unknown>)
} else {
result[key] = value
}
}
return result
}
export const customLogger = createMiddleware(async (c, next) => {
const start = Date.now()
await next()
const duration = Date.now() - start
const headers = redact(Object.fromEntries(c.req.raw.headers))
console.log(JSON.stringify({
time: new Date().toISOString(),
method: c.req.method,
path: c.req.path,
status: c.res.status,
duration: `${duration}ms`,
headers,
}))
})pino 内置了 pino-std-serializers 和 redact 功能,可以直接配置脱敏路径,不需要手动实现:
const logger = pino({
redact: {
paths: [
'req.headers.authorization',
'req.headers.cookie',
'body.password',
],
censor: '[REDACTED]',
},
})8. 日志采集
生产环境的日志通常需要采集到集中式日志系统。常见方案:
- stdout + 日志采集器:应用输出到 stdout,由
fluentd、loki、vector等工具采集 - 文件日志 + logrotate:输出到文件,配合日志轮转和采集
- 直接推送到日志服务:使用日志服务的 SDK 直接写入(阿里云 SLS、AWS CloudWatch 等)
对于 stdout 方案,JSON 格式日志是最友好的输出方式。采集器可以按行解析 JSON,提取字段建立索引。
Hono 跑在 Cloudflare Workers、Deno Deploy 等边缘运行时,stdout 不可用。这种情况下通常使用运行时的日志 API,或者直接把日志写到远程服务。
总结
日志中间件是 Hono 中间件栈的第一层,负责记录每个请求的到达、处理和完成。
这一节涉及到的几个层次:
- 内置 logger():开发阶段够用,输出纯文本,生产环境需要替换
- 自定义日志中间件:JSON 格式输出,便于日志采集工具解析
- 接入 pino:生产环境推荐,支持日志级别、高性能 JSON 输出
- 错误日志:在
onError或中间件 try-catch 里记录 - 日志级别:根据状态码动态选择级别,通过环境变量控制详细程度
- 敏感信息脱敏:日志里不应出现密码、token 等字段
- 日志采集:stdout + 采集器是最常见的生产方案
下一篇看 CORS 中间件——跨域配置、preflight 处理、credentials 模式。