Bot detection isn't perfect
But usually it's good enough to stop 80% of the worst actors with only 20% of the effort of doing it yourself.
Using the Pino logging library to add structured logging to Next.js. How to set up logging to JSON for Next.js.
Next.js is a powerful framework for building modern web applications, but it doesn't ship with a robust logging solution out of the box. The first place you’ll start is probably console.log
. It works consistently across client and server and can easily print most JavaScript objects (docs). You can also use other console functionality such as timers.
However, If you've ever found yourself digging through console logs in production, struggling to pinpoint errors, or wishing you had more insight into how users interact with your app, you know the pain.
At Arcjet where we're building a developer-first approach to security, we already have structured logging for our Go backend. Piped into Datadog, it’s easy to search for a specific user or trace ID because each log line has various attributes attached, including the user ID. We wanted to do the same with our web app, so we spent some time switching Next.js to structured JSON logging.
In this post, we'll walk you through implementing structured logging in Next.js.
When searching for a structured logging solution for Next.js, you might stumble upon next-logger. This library conveniently patches the logging mechanisms used by Next.js, allowing you to continue using console.log
without any code changes. It automatically formats your logs as JSON and adds useful metadata like hostname and timestamp.
As of the v5 release, next-logger uses the Next.js Instrumentation hooks to replace Next.js's built-in logger with their custom one. In instrumentation.ts
:
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await require('pino')
await require('next-logger')
}
}
And then in next.config.js
you can enable it:
const nextConfig = {
// [...]
experimental: {
instrumentationHook: true,
},
}
This works well if you are running your own Next.js server or are using serverless functions with the Node runtime. For other runtimes and for a more custom implementation, we explored other options.
The next library I found was pino, which is a fast logger for JS. It supports log levels, different output formats, and has a nice pretty-print plugin for local logging with colorized output.
Pino is a high-performance logging library for Node.js that offers the flexibility and customization we needed at Arcjet. It supports log levels, different output formats, and has a handy pretty-print plugin for development environments. It even allows you to redact fields by JSON path.
While Pino doesn't have out-of-the-box support for Next.js, it's easy to integrate with a few tweaks. Here's how:
npm install pino pino-pretty
In your next.config.js
file, add pino and its pretty-print plugin to the serverComponentsExternalPackages
array:
experimental: {
serverComponentsExternalPackages: ["pino", "pino-pretty"],
},
To streamline logging across your codebase, create a utility file (e.g., lib/logger.ts
) that configures Pino with the desired output format and log level:
import pino, { Logger } from "pino";
export const logger: Logger =
process.env["NODE_ENV"] === "production"
? // JSON in production
pino({ level: "warn" })
: // Pretty print in development
pino({
transport: {
target: "pino-pretty",
options: {
colorize: true,
},
},
level: "debug",
});
Then in each file where you want to log, import logger
and create a child logger. This allows you to set attributes which appear on every log line, which we use to identify the module:
import { logger } from "@/lib/logger";
const log = logger.child({ module: "totoro" });
Then we can use the logger:
log.debug("called");
And if we want to include additional attributes then they come first:
log.debug({ "magic": "hats" }, "a log line");
Pino has a lot more options, but this will get you started.
Pino is known for its high performance which is important for production environments where you want minimal overhead for your logging. It also gives you fine-grained control so you can output JSON in production to make it easy to search on fields, but pretty-print in development.
This has been working well for us in production so we now have consistent JSON logging for both frontend (Next.js) and backend (Go). Let us know if you’ve found other improvements we should consider!
But usually it's good enough to stop 80% of the worst actors with only 20% of the effort of doing it yourself.
How to protect GraphQL backends using Arcjet. Implementing rate limiting and bot protection for Yoga + Next.js.
Learn how to improve the security of your self-hosted Next.js applications. This guide covers Docker container image best practices, secret management, vulnerability mitigation, and more, so your Next.js projects are better protected from threats.
Get the full posts by email every week.