Node.js Logging Best Practices: Structured JSON with Pino and Winston
Use a mature logger like Pino or Winston to output structured JSON to stdout, include unique transaction IDs for every request, and let your container orchestrator handle log routing to aggregation platforms.
The goldbergyoni/nodebestpractices repository defines production-grade standards for Node.js applications. According to the source code analysis, robust logging requires three tightly-coupled concepts: smart logging with mature libraries, smart aggregation via stdout/stderr, and smart visualization through centralized dashboards.
The Three Pillars of Production-Grade Logging
The sections/production/smartlogging.md file outlines a three-step approach that separates log generation from routing and analysis.
Smart Logging with Mature Libraries
Smart logging starts with selecting a production-ready logger instead of console.log. The sections/errorhandling/usematurelogger.md file explicitly recommends Pino for high-performance scenarios and Winston for flexibility. Both libraries support structured JSON output, configurable log levels (debug, info, warn, error), and plugin ecosystems.
Structured logs must include contextual metadata such as user IDs, request paths, and a unique transaction ID generated at the entry point of each request. This correlation identifier propagates through all downstream log statements, enabling distributed tracing without a full APM suite.
Smart Aggregation via stdout/stderr
Smart aggregation adheres to the 12-Factor App methodology by writing logs exclusively to stdout and stderr. The sections/production/logrouting.md file emphasizes that application code must never contain file paths or network endpoints for log shipping. Instead, the execution environment—whether Docker, Kubernetes, or AWS Lambda—captures the stream and forwards it to aggregation platforms like Elastic Stack, Splunk, or CloudWatch.
This separation of concerns allows operators to change log destinations without modifying application code or redeploying services.
Smart Visualization
Smart visualization leverages dashboards such as Kibana, Grafana, or Splunk to transform raw JSON logs into actionable metrics. Teams can calculate error rates, latency percentiles, and resource utilization trends by querying structured fields rather than parsing raw text.
Implementing Pino for High-Performance Logging
Pino prioritizes speed and minimal overhead. The following implementation demonstrates structured JSON output, error serialization, and transaction ID propagation as recommended in sections/errorhandling/usematurelogger.md.
// logger.js – central logger configuration
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
timestamp: pino.stdTimeFunctions.isoTime,
base: null, // remove default pid and hostname
serializers: {
err: pino.stdSerializers.err // proper error stack serialization
}
});
module.exports = logger;
// app.js – request handling with transaction IDs
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const logger = require('./logger');
const app = express();
// Middleware to generate and attach transaction ID
app.use((req, res, next) => {
req.id = uuidv4();
logger.info({
txnId: req.id,
method: req.method,
url: req.originalUrl
}, 'incoming request');
next();
});
app.get('/users/:id', async (req, res) => {
try {
// Business logic here
logger.info({
txnId: req.id,
userId: req.params.id
}, 'user fetched');
res.json({ id: req.params.id });
} catch (err) {
logger.error({
err,
txnId: req.id
}, 'failed to fetch user');
res.status(500).send('internal error');
}
});
app.listen(3000);
All log lines output pure JSON to stdout, include the txnId for correlation, and utilize Pino's standard serializers for error objects.
Configuring Winston for Flexible Logging
Winston excels when you need complex transport configurations or custom formatting. The following setup adheres to the stdout-only principle while demonstrating JSON structuring as defined in sections/errorhandling/usematurelogger.md.
// winstonLogger.js
const { createLogger, format, transports } = require('winston');
const { combine, timestamp, json, errors } = format;
const logger = createLogger({
level: 'info',
format: combine(
timestamp(),
errors({ stack: true }), // capture stack traces
json()
),
defaultMeta: { service: 'my-node-app' },
transports: [
new transports.Console() // stdout only
]
});
module.exports = logger;
// route.js
const logger = require('./winstonLogger');
const { v4: uuidv4 } = require('uuid');
app.post('/order', (req, res) => {
const txnId = uuidv4();
logger.info({
txnId,
payload: req.body
}, 'create order request');
// Process order...
logger.info({
txnId,
orderId: 123
}, 'order created');
res.status(201).json({ orderId: 123 });
});
Externalizing Log Routing with Container Orchestration
Following the guidance in sections/production/logrouting.md, application code must never specify log file paths or network endpoints. Instead, configure the execution environment to capture stdout and stderr.
The following Docker daemon configuration demonstrates routing logs directly to Splunk without application changes:
// daemon.json (Docker daemon configuration)
{
"log-driver": "splunk",
"log-opts": {
"splunk-token": "<YOUR_TOKEN>",
"splunk-url": "https://splunk.example.com:8088",
"splunk-index": "node-logs",
"splunk-sourcetype": "json"
}
}
When running on Kubernetes, the container runtime collects stdout streams and forwards them to your cluster's logging stack. This approach satisfies the 12-Factor App methodology and allows operators to change log destinations independently of development cycles.
Key Files in the Repository
The goldbergyoni/nodebestpractices repository contains authoritative guidance in these specific locations:
sections/errorhandling/usematurelogger.md– Explains why to choose a mature logger (Pino/Winston) and provides implementation snippets.sections/production/smartlogging.md– Describes the three-step "smart logging" approach covering structured logs, aggregation, and visualization.sections/production/logrouting.md– Argues for keeping log routing out of application code and using stdout/stderr with container orchestration.
Summary
- Choose mature loggers like Pino or Winston instead of
console.logto gain structured JSON output, log levels, and performance optimizations. - Include transaction IDs in every log line to enable distributed tracing and correlation across microservices.
- Write exclusively to stdout/stderr and externalize routing to container orchestrators, following the 12-Factor App principles documented in
sections/production/logrouting.md. - Leverage visualization tools like Kibana or Grafana to transform structured logs into actionable metrics and alerts.
Frequently Asked Questions
What is the difference between Pino and Winston for Node.js logging?
Pino prioritizes performance and minimal overhead, making it ideal for high-throughput services. It uses a child logger pattern for context and enforces JSON output by default. Winston offers greater flexibility with custom transports, multiple output destinations, and complex formatting pipelines, which suits applications requiring sophisticated log routing logic within the process.
Why should I avoid using console.log in production Node.js applications?
console.log writes to stdout synchronously without log levels, structured formatting, or error serialization. It lacks the ability to redact sensitive fields or rotate streams, and it blocks the event loop under high load. Mature loggers like Pino and Winston provide asynchronous, non-blocking writes, configurable levels, and JSON structuring that downstream aggregation systems require.
How do transaction IDs improve log analysis in distributed systems?
A transaction ID (or correlation ID) is a unique identifier generated at the entry point of a request and propagated through all downstream services. Including this ID in every log line allows operators to filter and join logs across multiple services and time windows, effectively reconstructing the complete request lifecycle without expensive full-text searches or complex heuristics.
What is the 12-Factor App approach to log routing?
The 12-Factor methodology treats logs as event streams that applications write to stdout/stderr, never to files or remote sockets. The execution environment (Docker, Kubernetes, or systemd) captures these streams and handles routing, rotation, and shipping to aggregation platforms. This separation of concerns allows developers to focus on log content while operators control destination infrastructure without code changes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →