Node.js Pino Logging Tutorial
Set up structured, high-performance logging in Node.js with Pino, including child loggers, redaction, and pretty-printing for development.
What you'll learn
- ✓Why structured logging matters
- ✓How Pino achieves low overhead
- ✓Child loggers and request context
- ✓Redacting sensitive fields
- ✓Shipping logs to aggregators
Prerequisites
- •Basic Node.js and npm familiarity
What and Why
Pino is a JSON logger for Node.js designed to be one of the fastest in the ecosystem. Instead of formatting strings on the hot path, it writes structured JSON to stdout and lets a separate process handle formatting, shipping, or storage. That separation is what keeps Pino so cheap to call inside tight loops.
Why bother with structured logging? Because plain text is hard to query. When you log user_id as a JSON field, you can grep, filter, and aggregate across millions of lines in any log platform. When you log it inside a sentence, you cannot. Pino gives you that structure with almost no syntactic cost.
Mental Model
Think of Pino as two layers. The first layer is the in-process logger that produces one JSON object per log line as fast as possible. The second layer is the transport, which can be the terminal, a file, an HTTP endpoint, or a worker thread that forwards lines elsewhere. Your application code only touches the first layer.
Child loggers are how you attach persistent context. A child logger inherits its parent’s bindings plus any you add, so every request can log under a reqId without manually threading it through every function.
Hands-on Example
A small Express app that logs each request with a correlation id.
import express from 'express';
import pino from 'pino';
import { randomUUID } from 'node:crypto';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
redact: ['req.headers.authorization', 'password'],
});
const app = express();
app.use((req, res, next) => {
req.log = logger.child({ reqId: randomUUID(), path: req.path });
req.log.info('request received');
res.on('finish', () => {
req.log.info({ status: res.statusCode }, 'request completed');
});
next();
});
app.get('/hello', (req, res) => {
req.log.debug('building hello response');
res.json({ ok: true });
});
app.listen(3000, () => logger.info('listening on 3000'));
In development, pipe the output through pino-pretty for human-friendly colored logs: node app.js | pino-pretty. In production, leave it as raw JSON and let your collector parse it.
App code -> pino.info({...})
|
v
JSON line to stdout
|
+----+-----+
| |
v v
pino-pretty log shipper -> Loki / ELK / Datadog Common Pitfalls
Logging large objects on every request can dominate CPU. Pino is fast, but serializing a 200KB payload is never free. Log identifiers, not entire bodies.
Forgetting to redact secrets is the classic mistake. Authorization headers, tokens, and passwords end up in your aggregator and trigger compliance incidents. Configure redact once, at the root logger.
Using console.log alongside Pino splits your log stream into structured and unstructured halves. Pick one and remove the other from production code paths.
Setting log level to debug in production fills disks fast and slows the event loop. Make level configurable through an environment variable and default to info.
Practical Tips
Create one root logger per process and derive child loggers from it. Avoid creating new root loggers in modules, which can cause inconsistent bindings.
Add a service and env field at the root so every line is identifiable when multiple services share a log destination. Build a child logger per request and pass it down explicitly rather than reading from globals.
Use pino.stdSerializers for req, res, and err so common objects are flattened into stable shapes. Custom serializers handle the rest.
For very high throughput, move transports into worker threads with pino.transport. Your main loop stays free, and slow downstreams cannot block request handling.
Wrap-up
Pino gives you structured, fast logging with almost no ceremony. Start with a root logger, add request-scoped child loggers, redact secrets, and pipe to pino-pretty in development. With those four habits your logs become a queryable dataset rather than a wall of text, and production debugging gets dramatically less painful.
Related articles
- Node.js Node.js Async Iterators Tutorial
Master async iterators in Node.js for streaming files, paginated APIs, and backpressure-aware data processing.
- Node.js Debugging Node.js Memory Leaks
Find and fix memory leaks in Node.js using heap snapshots, sampling, and a few reliable patterns to avoid leaks.
- Node.js Node.js Process vs Thread vs Cluster
Understand when to reach for child processes, worker threads, or the cluster module to scale Node.js workloads.
- DevOps DevOps Observability Stack Overview
A tour of the modern observability stack: metrics, logs, traces, and events. Learn how the pillars fit together and how to choose tooling without drowning in dashboards.