log
Swift Code Chronicles

Nuxt3 Best Practices 01: Logging

Published on January 31, 2025
Updated on January 31, 2025
31 min read
Vue

1. Introduction

In Nuxt 3, the default logging mechanism provided by Nuxt Kit’s useLogger is sufficient for basic logging needs. However, it does not support generating a unique request ID for each HTTP request, making log tracking difficult.

This article introduces how to generate a unique ID for each HTTP request in log outputs, allowing for better log analysis and debugging.

2. Implementation

2.1 Install pino

We will use pino for log management. Install it using the following command:

npm install pino pino-http pino-pretty

2.2 Generate a Unique Request ID

First, we need to generate a unique ID for each HTTP request. Create a file named 01.tracing-id.ts in the server/middleware/ directory. The prefix 01 ensures that this middleware executes first.

Note: In Nuxt 3, middleware execution order is determined by filename alphabetical/numerical order.

// 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();
});

Here, we use Math.random() to generate a 10-character random string and store it in the request headers to ensure each request has a unique x-tracing-id.

2.3 Configure pino

Next, create 02.log-register.ts in the server/middleware/ directory to register the pino-http logging middleware.

// 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,
  }),
);

Note: We use fromNodeMiddleware instead of defineEventHandler to ensure that pino-http injects the log variable (pino.Logger instance) into the req object.

This configuration ensures that pino logs include reqId for easy log correlation.

2.4 Using Logging in API Handlers

With the setup complete, we can now use pino for logging in API handlers.

// server/api/test.ts
export default defineEventHandler((event) => {
  const logger = event.node.req.log;
  logger.info('Hello, world!');
  return { message: 'Hello, world!' };
});

When accessing this API, the logs will output:

[2025-01-31 09:52:11.217 +0900] INFO: [reqId:b0v9ldkiv1] - Hello, world!

3. Custom Logging Utility

While pino solves log formatting issues, using logger in API handlers has some drawbacks:

  1. Manually retrieving logger from event.node.req.log.
  2. Passing logger explicitly when calling utility functions that require logging.

Ideally, we should be able to call logger.info('xxx'); anywhere without manually retrieving logger.

To achieve this, we will create a custom logging utility.

3.1 Using AsyncLocalStorage

We can use AsyncLocalStorage to store logger as a global variable, ensuring it remains accessible throughout the request lifecycle.

Create async-storage.ts in server/utils/:

// 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 provides a Thread Local Storage mechanism, ensuring data is not mixed between different requests.

3.2 Creating a Global Logger

Create logger.ts in server/utils/:

// 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),
};

This utility retrieves logger from asyncLocalStorage. If unavailable, it defaults to console.log.

3.3 Wrapping defineEventHandler

Create handler.ts in server/utils/ to wrap defineEventHandler, ensuring logger is available throughout each request lifecycle.

// 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`);
      }
    });
  });

This wrapper function uses asyncLocalStorage.run to initiate an async context, storing logger for use within the request lifecycle.

3.4 Using logger in API Handlers

// server/api/test.ts
export default defineWrappedEventHandler(() => {
  logger.info('Hello, world!');
  return { message: 'Hello, world!' };
});

Note: We use defineWrappedEventHandler instead of defineEventHandler.

Now, logger can be used directly in APIs and utility functions without manual retrieval or passing:

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

With this setup, we have implemented an elegant logging management solution that ensures traceability and maintainability!


Next article: Nuxt3 Best Practices 02: Request Validation

About

A personal blog sharing technical insights, experiences and thoughts

Quick Links

Contact

  • Email: hushukang_blog@proton.me
  • GitHub

© 2025 Swift Code Chronicles. All rights reserved