log
码中赤兔

Nuxt3 最佳实践01: 日志输出

发布于 2025年1月31日
更新于 2025年1月31日
16 分钟阅读
Vue

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-httpreq 对象中注入 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 仍有不便之处:

  1. 需要手动从 event.node.req.log 获取 logger
  2. 在调用其他工具函数时,若工具函数需要日志功能,还需手动传递 logger

而我们想要实现的效果是无论在哪里输出日志,只需要执行 logger.info('xxx'); ,无须从别处获取 logger

为了解决这些问题,我们需要自定义一个日志工具。

3.1 使用 AsyncLocalStorage

我们可以利用 AsyncLocalStoragelogger 作为全局变量存储,确保在整个请求生命周期内都能访问它。

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!' };
});

这样,我们就成功实现了优雅的日志管理方案,并确保了日志的可追踪性与可维护性!


下一篇文章:Nuxt3 最佳实践02: Request 请求体验证

关于

分享技术见解、经验和思考的个人博客

联系方式

  • Email: hushukang_blog@proton.me
  • GitHub

© 2025 码中赤兔. 版权所有