pino icon indicating copy to clipboard operation
pino copied to clipboard

Per-request logger for nextjs

Open cbix opened this issue 6 months ago • 2 comments

I'm trying to do something similar to #1936, i.e. provide a global logger that injects a request ID field for log correlation. This request ID is read from a header, which since nextjs 15 is only possible using the async headers function:

export const getRequestId = async () => {
  const clientHeaders = await headers();
  const requestId = clientHeaders.get('x-request-id');
  if (requestId === null) {
    throw new Error('Request ID not present');
  }
  return requestId;
};

I believe the author of #1936 was using an older nextjs version with a synchronous headers. Using a mixin in pino wouldn't work since that needs to be synchronous, so my current (admittedly hacky) solution is using the hooks option:

export const logger = pino(
  {
    hooks: {
      async logMethod(args, method) {
        return method.apply(this.child({ request_id: await getRequestId() }), args);
      },
    },
  },
);

This technically works, however likely for good reason this is also not recommended:

Hook functions must be synchronous functions.

The only "proper" way I see would be to await a new logger every time it's used:

// inside some API handler or server component
const logger = await getRequestLogger();
// [...]
logger.debug(...);

This feels a bit cumbersome given that pino has several extension points that are better suited for this, none of which seem to work for this simple problem. I'm starting to believe it's a con of nextjs itself to not provide some kind of request context in its architecture.

What's the best way to inject request-specific information from nextjs in pino? Is there a way to inject asynchronously returned values while logging?

cbix avatar May 08 '25 19:05 cbix

My personal recommendation is to ask the maintainers of the framework you are using.

jsumners avatar May 08 '25 22:05 jsumners

Thanks, I added my use case to this discussion: https://github.com/vercel/next.js/discussions/77534

cbix avatar May 16 '25 19:05 cbix

Have you looked into using AsyncLocalStorage? See this article

ethrob avatar Sep 01 '25 15:09 ethrob

Have you looked into using AsyncLocalStorage? See this article

We make use of Nextjs' workUnit, which headers() use under water

import {
    throwForMissingRequestStore,
    workUnitAsyncStorage,
} from 'next/dist/server/app-render/work-unit-async-storage.external';
// @ts-expect-error
import defaultConfig from 'next-logger/lib/defaultPinoConfig.js';
import type { LoggerOptions } from 'pino';

// Copy of next-logger, can't easily plug into their pino config
export const pinoConfig: LoggerOptions = {
    ...defaultConfig,
    mixin() {
        try {
            // Reference for the workUnitStore implementation: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/request/headers.ts
            const workUnitStore = workUnitAsyncStorage.getStore();

            if (!workUnitStore || workUnitStore.type !== 'request') {
                throwForMissingRequestStore('logger');
            }

            return {
                url: workUnitStore.url,
                referer: workUnitStore.headers.get('referrer'),
            };
        } catch {
            return {};
        }
    },
};

Netail avatar Oct 16 '25 12:10 Netail