DEV Community

Theo Gravity
Theo Gravity

Posted on

Custom logging in Next.js

Next.js does not have a way to use a custom logger for server-side uncaught exceptions and rejections.

While you can use a library like next-logger to help, you will be limited to using Pino only. This doesn't help if you want to use another logging library or even ship the logs to a cloud provider like DataDog.

Instead, you can use the LogLayer logging library to capture these exceptions and ship them off to your favorite logging library like Pino and DataDog at the same time.

Check the LogLayer site for supported loggers and cloud providers.

Install

This guide assumes you already have Next.js set up.

First, install the required packages. You can use any transport you prefer - we'll use Pino in this example:

npm i loglayer @loglayer/transport-pino pino serialize-error
Enter fullscreen mode Exit fullscreen mode

Setup

You will need to create an instrumentation file in the root of your project.

// instrumentation.ts
import { LogLayer, type ILogLayer } from 'loglayer';
import { PinoTransport } from "@loglayer/transport-pino";
import pino from "pino";
import { serializeError } from "serialize-error";

/**
 * Strip ANSI codes from a string, which is something Next.js likes to inject.
 */
function stripAnsiCodes(str: string): string {
  return str.replace(
    /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
    "",
  );
}

/**
 * Create a console method that logs to LogLayer
 */
function createConsoleMethod(log: ILogLayer, method: "error" | "info" | "warn" | "debug" | "log") {
  let mappedMethod = method;

  if (method === "log") {
    mappedMethod = "info";
  }

  return (...args: unknown[]) => {
    const data: Record<string, unknown> = {};
    let hasData = false;
    let error: Error | null = null;
    const messages: string[] = [];

    for (const arg of args) {
      if (arg instanceof Error) {
        error = arg;
        continue;
      }

      if (typeof arg === "object" && arg !== null) {
        Object.assign(data, arg);
        hasData = true;
        continue;
      }

      if (typeof arg === "string") {
        messages.push(arg);
      }
    }

    let finalMessage = stripAnsiCodes(messages.join(" ")).trim();

    // next.js uses an "x" for the error message when it's an error object
    if (finalMessage === "" && error) {
      finalMessage = error?.message || "";
    }

    if (error && hasData && messages.length > 0) {
      log.withError(error).withMetadata(data)[mappedMethod](finalMessage);
    } else if (error && messages.length > 0) {
      log.withError(error)[mappedMethod](finalMessage);
    } else if (hasData && messages.length > 0) {
      log.withMetadata(data)[mappedMethod](finalMessage);
    } else if (error && hasData && messages.length === 0) {
      log.withError(error).withMetadata(data)[mappedMethod]("");
    } else if (error && messages.length === 0) {
      log.errorOnly(error);
    } else if (hasData && messages.length === 0) {
      log.metadataOnly(data);
    } else {
      log[mappedMethod](finalMessage);
    }
  };
}

export async function register() {
  const logger = new LogLayer({
    errorSerializer: serializeError,
    transport: [
      new PinoTransport({
        logger: pino(),
      }),
    ]
  })

  if (process.env.NEXT_RUNTIME === "nodejs") {
    console.error = createConsoleMethod(logger, "error");
    console.log = createConsoleMethod(logger, "log");
    console.info = createConsoleMethod(logger, "info");
    console.warn = createConsoleMethod(logger, "warn");
    console.debug = createConsoleMethod(logger, "debug");
  }
}
Enter fullscreen mode Exit fullscreen mode

Test

If you threw an error from page.tsx that is uncaught, you should see this in the terminal:

{"err":{"type":"Object","message":"test","stack":"Error: test\n    at Page (webpack-internal:///(rsc)/./src/app/page.tsx:12:11)","digest":"699232626","name":"Error"},"msg":"test"}
Enter fullscreen mode Exit fullscreen mode

For more information

Top comments (0)