Logging Strategy: Structure Logs for Production
Learn: Logging Strategy: Structure Logs for Production
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
Logging Strategy: Structure Logs for Production - Winston vs Pino
Problem
Unstructured logging in production environments creates critical challenges:
- Debugging Difficulty: Plain text logs are hard to parse and correlate across services
- Performance Impact: Synchronous logging blocks event loops; poor buffering causes memory issues
- Scalability Issues: Centralized log aggregation systems struggle with unstructured data
- Compliance Gaps: Audit trails lack proper context and traceability
- Operational Blind Spots: No structured metadata for filtering, alerting, or analytics
Modern applications need structured, JSON-based logging with minimal performance overhead.
Solution
Implement structured logging using either Winston (feature-rich, flexible) or Pino (ultra-fast, minimal overhead). Both serialize logs to JSON, enabling:
- Structured Metadata: Consistent key-value pairs for filtering and analysis
- Performance: Asynchronous, non-blocking operations
- Correlation: Request IDs and trace context across services
- Integration: Native support for log aggregation platforms (ELK, Datadog, CloudWatch)
- Levels & Filtering: Environment-aware log verbosity
Code Examples
1. Winston Setup (Feature-Rich Approach)
// logger-winston.js
const winston = require('winston');
const path = require('path');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
defaultMeta: { service: 'api-service', environment: process.env.NODE_ENV },
transports: [
// Error logs
new winston.transports.File({
filename: path.join('logs', 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5,
}),
// Combined logs
new winston.transports.File({
filename: path.join('logs', 'combined.log'),
maxsize: 5242880,
maxFiles: 10,
}),
],
});
// Console output in development
if (process.env.NODE_ENV !== 'production') {
logger.add(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.printf(
({ timestamp, level, message, ...meta }) =>
`${timestamp} [${level}]: ${message} ${
Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
}`
)
),
})
);
}
module.exports = logger;
2. Pino Setup (Performance-Optimized)
// logger-pino.js
const pino = require('pino');
const path = require('path');
const transport = pino.transport({
targets: [
{
level: 'info',
target: 'pino/file',
options: { destination: path.join('logs', 'combined.log') },
},
{
level: 'error',
target: 'pino/file',
options: { destination: path.join('logs', 'error.log') },
},
...(process.env.NODE_ENV !== 'production'
? [
{
level: 'debug',
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
},
]
: []),
],
});
const logger = pino(
{
level: process.env.LOG_LEVEL || 'info',
base: {
service: 'api-service',
environment: process.env.NODE_ENV,
},
timestamp: pino.stdTimeFunctions.isoTime,
},
transport
);
module.exports = logger;
3. Express Middleware Integration (Winston)
// middleware-winston.js
const logger = require('./logger-winston');
const expressWinston = require('express-winston');
const requestLogger = expressWinston.logger({
transports: logger.transports,
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
meta: true,
msg: 'HTTP {{req.method}} {{req.url}}',
expressFormat: true,
colorize: false,
ignoreRoute: (req) => req.url === '/health',
});
const errorLogger = expressWinston.errorLogger({
transports: logger.transports,
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
});
module.exports = { requestLogger, errorLogger };
4. Express Middleware Integration (Pino)
// middleware-pino.js
const logger = require('./logger-pino');
const pinoHttp = require('pino-http');
const httpLogger = pinoHttp(
{
logger,
customLogLevel: (req, res, err) => {
if (res.statusCode >= 500 || err) return 'error';
if (res.statusCode >= 400) return 'warn';
return 'info';
},
customSuccessMessage: (req, res) =>
`${req.method} ${req.url} - ${res.statusCode}`,
customErrorMessage: (req, res, err) =>
`${req.method} ${req.url} - ${res.statusCode} - ${err.message}`,
skip: (req) => req.url === '/health',
},
logger
);
module.exports = { httpLogger };
5. Application Usage (Winston)
// app-winston.js
const express = require('express');
const logger = require('./logger-winston');
const { requestLogger, errorLogger } = require('./middleware-winston');
const app = express();
app.use(requestLogger);
app.get('/api/users/:id', (req, res) => {
const userId = req.params.id;
logger.info('Fetching user', {
userId,
requestId: req.id,
timestamp: new Date().toISOString(),
});
try {
if (!userId || isNaN(userId)) {
logger.warn('Invalid user ID provided', {
userId,
requestId: req.id,
source: 'validation',
});
return res.status(400).json({ error: 'Invalid user ID' });
}
const user = { id: userId, name: 'John Doe', email: 'john@example.com' };
logger.info('User fetched successfully', {
userId,
requestId: req.id,
duration: '45ms',
});
res.json(user);
} catch (error) {
logger.error('Failed to fetch user', {
userId,
requestId: req.id,
error: error.message,
stack: error.stack,
});
res.status(500).json({ error: 'Internal server error' });
}
});
app.use(errorLogger);
app.listen(3000, () => {
logger.info('Server started', { port: 3000, environment: process.env.NODE_ENV });
});
6. Application Usage (Pino)
// app-pino.js
const express = require('express');
const logger = require('./logger-pino');
const { httpLogger } = require('./middleware-pino');
const app = express();
app.use(httpLogger);
app.get('/api/users/:id', (req, res) => {
const userId = req.params.id;
const childLogger = req.log.child({ userId, requestId: req.id });
childLogger.info('Fetching user');
try {
if (!userId || isNaN(userId)) {
childLogger.warn({ userId }, 'Invalid user ID provided');
return res.status(400).json({ error: 'Invalid user ID' });
}
const user = { id: userId, name: 'John Doe', email: 'john@example.com' };
childLogger.info({ duration: '45ms' }, 'User fetched successfully');
res.json(user);
} catch (error) {
childLogger.error(
{ error: error.message, stack: error.stack },
'Failed to fetch user'
);
res.status(500).json({ error: 'Internal server error' });
}
});
app.listen(3000, () => {
logger.info({ port: 3000 }, 'Server started');
});
7. Correlation & Request Tracing
// middleware-correlation.js
const { v4: uuidv4 } = require('uuid');
const correlationMiddleware = (req, res, next) => {
req.id = req.headers['x-request-id'] || uuidv4();
req.traceId = req.headers['x-trace-id'] || uuidv4();
res.setHeader('x-request-id', req.id);
res.setHeader('x-trace-id', req.traceId);
next();
};
module.exports = correlationMiddleware;
8. Structured Error Logging
// error-handler.js
const logger = require('./logger-winston'); // or pino
class AppError extends Error {
constructor(message, statusCode, context = {}) {
super(message);
this.statusCode = statusCode;
this.context = context;
this.timestamp = new Date().toISOString();
}
}
const errorHandler = (err, req, res, next) => {
const errorLog = {
message: err.message,
statusCode: err.statusCode || 500,
requestId: req.id,
traceId: req.traceId,
method: req.method,
url: req.url,
userAgent: req.get('user-agent'),
ip: req.ip,
context: err.context || {},
stack: err.stack,
};
if (err.statusCode >= 500) {
logger.error(errorLog);
} else {
logger.warn(errorLog);
}
res.status(err.statusCode || 500).json({
error: err.message,
requestId: req.id,
});
};
module.exports = { AppError, errorHandler };
Comparison Matrix
| Feature | Winston | Pino |
| Performance | Good | Excellent (10x faster) |
| Bundle Size | ~1.2MB | ~200KB |
| Learning Curve | Moderate | Gentle |
| Customization | Extensive | Limited but sufficient |
| Ecosystem | Large (many transports) | Growing |
| Child Loggers | Yes | Yes (optimized) |
| JSON Output | Yes | Native |
| Production Ready | Yes | Yes |
Best Practices
- Always use structured metadata - Include request IDs, user IDs, timestamps
- Async logging - Never block event loop; use transports with buffering
- Log levels appropriately - Error, warn, info, debug, trace
- Correlation IDs - Track requests across microservices
- Sensitive data - Never log passwords, tokens, or PII
- Log rotation - Implement file rotation to manage disk space
- Centralized aggregation - Send logs to ELK, Datadog, or CloudWatch
- Performance monitoring - Track logging overhead in production
Choose Pino for high-throughput APIs; choose Winston for complex, multi-transport scenarios.