Nuxt3 最佳实践01: 日志输出
1. 简介
在 Nuxt 3 中,默认使用 Nuxt Kit 提供的 useLogger 进行日志输出,虽然基本功能足够,但它不支持为每个 HTTP 请求生成唯一的请求 ID,不利于日志追踪。
本篇文章将介绍如何在日志输出时,为每个 HTTP 请求生成唯一 ID,以便更好地进行日志分析与调试。
2. 具体实现
2.1 安装 pino
我们将使用 pino 进行日志管理,安装方式如下:
npm install pino pino-http pino-pretty
2.2 生成唯一请求 ID
首先,需要为每个 HTTP 请求生成一个唯一 ID。在 server/middleware/ 目录下创建 01.tracing-id.ts,文件名前缀 01 用于确保此中间件最先执行。
注意: 在 Nuxt 3 中,中间件的执行顺序由文件名的字母/数字顺序决定。
// server/middleware/01.tracing-id.ts
export default fromNodeMiddleware((req, _res, next) => {
const tracingId = req.headers['x-tracing-id'] || Math.random().toString(36).slice(2, 12);
req.headers['x-tracing-id'] = tracingId;
next();
});
这里使用 Math.random() 生成一个长度为 10 的随机字符串,并将其存入请求头中,确保每个请求都有唯一的 x-tracing-id。
2.3 配置 pino
接下来,在 server/middleware/ 目录下创建 02.log-register.ts,用于注册 pino-http 日志中间件。
// server/middleware/02.log-register.ts
import logger from 'pino-http';
export default fromNodeMiddleware(
logger({
level: 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: process.env.NODE_ENV !== 'production',
translateTime: 'SYS:standard',
ignore: 'req,res,responseTime,pid,hostname',
messageFormat: '[reqId:{req.id}] - {msg}',
},
},
genReqId: (req) => req.headers['x-tracing-id'] || req.id,
autoLogging: false,
}),
);
注意: 这里使用
fromNodeMiddleware而非defineEventHandler,确保pino-http在req对象中注入log变量(pino.Logger实例)。
此配置保证 pino 在日志输出时包含 reqId,方便日志关联。
2.4 在 API 里使用日志
完成以上配置后,即可在 API 处理函数中使用 pino 进行日志记录。
// server/api/test.ts
export default defineEventHandler((event) => {
const logger = event.node.req.log;
logger.info('Hello, world!');
return { message: 'Hello, world!' };
});
访问该 API 时,日志输出如下:
[2025-01-31 09:52:11.217 +0900] INFO: [reqId:b0v9ldkiv1] - Hello, world!
3. 自定义日志工具
虽然 pino 解决了日志格式化的问题,但在 API 代码中使用 logger 仍有不便之处:
- 需要手动从
event.node.req.log获取logger。 - 在调用其他工具函数时,若工具函数需要日志功能,还需手动传递
logger。
而我们想要实现的效果是无论在哪里输出日志,只需要执行 logger.info('xxx'); ,无须从别处获取 logger。
为了解决这些问题,我们需要自定义一个日志工具。
3.1 使用 AsyncLocalStorage
我们可以利用 AsyncLocalStorage 让 logger 作为全局变量存储,确保在整个请求生命周期内都能访问它。
在 server/utils/ 目录下创建 async-storage.ts:
// server/utils/async-storage.ts
import { AsyncLocalStorage } from 'async_hooks';
import type pino from 'pino';
type StorageT = { logger: pino.Logger };
export const asyncLocalStorage = new AsyncLocalStorage<StorageT>();
AsyncLocalStorage 提供了一种 线程局部存储(Thread Local Storage) 机制,确保数据不会在不同请求之间混淆。
我们可以将每次Http Request看作是一个异步调用。只要我们把 logger 作为一个变量存到 AsyncLocalStorage 里面,那么在这个异步调用的生命周期里的任何地方都可以访问到这个 logger。
3.2 创建全局 logger
在 server/utils/ 目录下创建 logger.ts:
// server/utils/logger.ts
const getLogger = () => asyncLocalStorage.getStore()?.logger ?? console;
export const logger = {
info: (msg: string) => getLogger().info(msg),
error: (msg: string) => getLogger().error(msg),
warn: (msg: string) => getLogger().warn(msg),
debug: (msg: string) => getLogger().debug(msg),
};
我们把自定义的日志工具放在这里,每次输出日志的时候,都从 asyncLocalStorage 里获取 logger 变量。如果获取不到的话则使用 console.log 来输出。
3.3 封装 defineEventHandler
在 server/utils/ 目录下创建 handler.ts,用于封装 defineEventHandler,确保 logger 在每个请求生命周期内可用。
// server/utils/handler.ts
import type { EventHandler, EventHandlerRequest } from 'h3';
export const defineWrappedEventHandler = <T extends EventHandlerRequest, D>(
handler: EventHandler<T, D>,
): EventHandler<T, D> =>
defineEventHandler<T>(async (event) => {
const logger = event.node.req.log;
const startTime = Date.now();
logger.info(`Request received: ${event.node.req.url}`);
return asyncLocalStorage.run({ logger }, async () => {
try {
return await handler(event);
} catch (err) {
logger.error(err);
throw err;
} finally {
logger.info(`Request completed in ${Date.now() - startTime}ms`);
}
});
});
我们定义一个EventHandler的封装函数。在这个函数里,我们使用 asyncLocalStorage.run 来开启一个异步调用,并且把 logger 放到这个异步调用的存储里。
3.4 API 里使用 logger
// server/api/test.ts
export default defineWrappedEventHandler(() => {
logger.info('Hello, world!');
return { message: 'Hello, world!' };
});
注意:这里使用的是
defineWrappedEventHandler而不是defineEventHandler。
现在,在 API 或工具函数中均可直接使用 logger,无需手动获取或传递:
// server/util/some-util.ts
export const someUtil = () => {
logger.info('use some util');
};
// server/api/test.ts
export default defineWrappedEventHandler(() => {
someUtil();
return { message: 'Hello, world!' };
});
这样,我们就成功实现了优雅的日志管理方案,并确保了日志的可追踪性与可维护性!