While console.log() is great for quick debugging during development, it has major drawbacks in a production environment:
- It's synchronous and can slow down your application.
- It's unstructured, making it hard to search and analyze.
- It lacks log levels (e.g., info, warn, error), so you can't filter by severity.
Professional applications use dedicated logging libraries like Winston or Pino to solve these problems. We'll focus on Pino because it's known for its extremely high performance.
What is Structured Logging?
Structured logging is the practice of writing logs in a consistent, machine-readable format, typically JSON.
Unstructured Log (bad): User 123 logged in successfully from IP 192.168.1.1
Structured Log (good):
JSON
{"level":"info","time":1678886400000,"pid":12345,"hostname":"server-a","msg":"User logged in successfully","userId":123,"ip":"192.168.1.1"}
The benefits are immense: you can easily filter, search, and aggregate logs using tools like Elasticsearch or Datadog. For example, you could quickly find all error logs related to a specific userId.
1. Setting Up Pino
First, install pino:
Bash
npm install pino pino-http pino-pretty # pino-http is middleware for Express # pino-pretty makes logs human-readable in development
2. Creating a Logger Instance
Create a logger.js file to configure your logger.
JavaScript
// logger.js
const pino = require('pino');
// Configure the logger
const logger = pino({
// In development, use pino-pretty for nice, human-readable output.
// In production, we'd want to write raw JSON.
transport: process.env.NODE_ENV === 'development' ? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:dd-mm-yyyy HH:MM:ss',
ignore: 'pid,hostname',
},
} : undefined,
});
module.exports = logger;
3. Using the Logger and Middleware
Now, you can import your logger instance anywhere in your application. The pino-http middleware makes it easy to automatically log every incoming request and outgoing response.
JavaScript
const express = require('express');
const logger = require('./logger'); // Import our configured logger
const pinoHttp = require('pino-http');
const app = express();
// Create and use the pino-http middleware
const httpLogger = pinoHttp({ logger });
app.use(httpLogger);
// --- Routes ---
app.get('/', (req, res) => {
// The middleware automatically logs the request and response.
// We can also add custom log messages.
logger.info('Handling request for the root path');
res.send('Hello, world!');
});
app.get('/user/:id', (req, res) => {
const userId = req.params.id;
// Add context to your logs!
logger.info({ userId }, `Fetching data for user`);
if (userId === '0') {
// Log errors with the error level
logger.error({ userId }, 'Invalid user ID provided');
return res.status(400).send('Invalid user ID');
}
res.send(`Data for user ${userId}`);
});
app.listen(3000, () => {
logger.info('Server is up and running on port 3000');
});
When you run this in development, pino-pretty will give you nicely formatted output. If you set NODE_ENV=production, it will output raw JSON, which is perfect for shipping to a log management service. This practice is fundamental to building observable and maintainable production systems.