Nuxt3 Best Practices 01: Logging
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
fromNodeMiddlewareinstead ofdefineEventHandlerto ensure thatpino-httpinjects thelogvariable (pino.Loggerinstance) into thereqobject.
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:
- Manually retrieving
loggerfromevent.node.req.log. - Passing
loggerexplicitly 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
defineWrappedEventHandlerinstead ofdefineEventHandler.
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