Why Logging Matters in Node.js
Every developer has experienced the frustration of debugging production issues with inadequate logs. As Node.js applications grow in complexity, basic console.log statements become insufficient for effective debugging and monitoring. This guide covers essential logging practices and tools that transform debugging from guesswork into systematic problem-solving.
Our /services/web-development/ team has implemented logging infrastructure for hundreds of production applications, and we've distilled these best practices into this comprehensive guide.
What You'll Learn
- How to choose the right logging library for your project
- Implementing structured JSON logging for machine readability
- Best practices for log levels, timestamps, and metadata
- Security considerations for sensitive data
- Performance optimization strategies
Choosing Your Logging Library
Your choice of logging library affects how you'll debug issues and monitor your application. Each library has different strengths that suit specific use cases.
Winston: The Versatile Powerhouse
Winston offers many configuration options and can send logs to multiple destinations at once--files, databases, and external services. With plugins for most logging destinations, Winston prioritizes flexibility over speed. It's ideal for complex applications with varied logging requirements.
Pino: Performance-First Logging
Pino focuses on performance above all else. It moves formatting and processing to separate processes to minimize impact on your application. Benchmarks show Pino handling 10,000+ logs per second with minimal overhead. Use Pino when speed matters most, such as high-throughput APIs.
Bunyan: JSON-Native Logging
Bunyan uses a JSON-first approach where each log entry is a JSON object with a consistent structure, making automated analysis easier. Its serializers convert common objects like Error instances to JSON-friendly formats, and it includes CLI tools for reading and formatting logs during development.
Morgan: HTTP Request Logging
Morgan specializes in HTTP request logging for Express and Next.js applications. It works as middleware with preset formats for access logs like common, combined, and dev. Morgan works best alongside general loggers like Winston or Pino for complete observability. When building /services/web-development/ solutions with Express or Next.js, integrating Morgan provides valuable insights into request patterns and performance bottlenecks.
| Library | GitHub Stars | Key Strength | Best For |
|---|---|---|---|
| Winston | 20k+ | Highly configurable | Complex applications |
| Pino | 12k+ | Performance-focused | High-throughput systems |
| Bunyan | 7k+ | JSON logging | Microservices |
| Morgan | 9k+ | HTTP request logging | Express.js apps |
| debug | 10k+ | Lightweight debugging | Small projects |
| log4js | 5k+ | Familiar Java API | Teams with Java background |
Implementing Effective Log Levels
Log levels categorize the severity and importance of log entries, enabling developers to quickly identify and respond to issues based on urgency and impact.
The Standard Hierarchy
| Level | Description | Use Case |
|---|---|---|
| DEBUG | Detailed information for diagnosing problems | Variable values, function entry/exit |
| INFO | Confirmation that things work as expected | Request completed, user logged in |
| WARN | Something unexpected happened | Rate limit approaching, retry initiated |
| ERROR | Function couldn't perform its task | Database connection failed, API timeout |
| FATAL | Critical errors requiring immediate attention | Server startup failed, data corruption |
Code Example: Log Levels
logger.debug('Processing request for user %s', userId);
logger.info('User %s logged in successfully', userId);
logger.warn('Rate limit approaching for user %s', userId);
logger.error('Database connection failed', { attempt: 3 });
logger.fatal('Unable to start server - shutting down');
Structured Logging: Machine-Readable Records
Structured logging organizes log data into a consistent, machine-readable format, making it easier for automated systems to process and understand logs.
The Power of JSON Logging
Instead of plain text logs like:
[2025-01-04 10:30:00] Error: Database connection failed for user_12345
Structured logging produces:
{
"level": "error",
"timestamp": "2025-01-04T10:30:00.000Z",
"service": "api-gateway",
"message": "Database connection failed",
"context": {
"userId": "user_12345",
"requestId": "req_abc123",
"database": "primary",
"attempt": 3
},
"error": {
"name": "ConnectionError",
"message": "Connection timeout after 30000ms"
}
}
Benefits of Structured Logging
- Searchable: Query logs by any field
- Consistent: Same structure across all entries
- Scalable: Easy to aggregate and analyze
- Integration-ready: Works with log aggregation tools
Essential Metadata Fields
Always include these fields for effective debugging:
timestamp: ISO 8601 formatted timelevel: Log severity levelservice: Name of the service/moduleenvironment: production, staging, developmentmessage: Human-readable descriptioncontext: Request ID, user ID, operation detailserror: Error name, message, and stack trace
1const winston = require('winston');2 3const logger = winston.createLogger({4 level: 'info',5 format: winston.format.combine(6 winston.format.timestamp(),7 winston.format.json()8 ),9 transports: [10 new winston.transports.File({ filename: 'error.log', level: 'error' }),11 new winston.transports.File({ filename: 'combined.log' })12 ]13});14 15// Add console output in development16if (process.env.NODE_ENV !== 'production') {17 logger.add(new winston.transports.Console({18 format: winston.format.simple()19 }));20}21 22// Use it in your code23logger.info('Server started on port 3000');24logger.error('Database connection failed', { reason: 'timeout', attempt: 3 });1const pino = require('pino');2 3const logger = pino({4 level: 'info',5 timestamp: pino.stdTimeFunctions.isoTime,6 formatters: {7 level: (label) => ({ level: label })8 },9 base: {10 service: 'api-gateway',11 environment: process.env.NODE_ENV12 }13});14 15// Production: log to file16const transport = pino.transport({17 targets: [18 { target: 'pino/file', options: { destination: 'app.log' } }19 ]20});21 22// Use it in your code23logger.info({ userId: 'user_12345' }, 'User logged in');24logger.error({ err: new Error('timeout') }, 'Database connection failed');HTTP Request Logging with Morgan
Morgan is essential middleware for logging HTTP requests in Express and Next.js API routes.
Integrating Morgan with Winston/Pino
const morgan = require('morgan');
const logger = require('./logger');
// Log HTTP requests with Morgan, sending to main logger
app.use(morgan('combined', {
stream: { write: (message) => logger.info(message.trim()) }
}));
Morgan Format Options
| Format | Description |
|---|---|
combined | Standard Apache combined log format |
common | Standard Apache common log format |
dev | Concise colored output for development |
short | Shorter than default |
tiny | Minimal output |
Morgan works alongside general-purpose loggers to provide complete observability for your web applications.
Security: Protecting Sensitive Data in Logs
Logging sensitive data can expose it to unauthorized individuals who have access to the logs. This is a critical security concern that must be addressed.
Our team specializes in implementing comprehensive security practices for Node.js applications. Contact our /services/ai-automation/ team if you need help implementing secure logging infrastructure.
Data That Must Never Appear in Logs
- Authentication credentials: Passwords, API keys, tokens
- Financial data: Credit card numbers, bank accounts
- Session data: Cookies, JWT tokens, session IDs
- Personal information: PII, email addresses, phone numbers
- Internal system details: Internal IPs, infrastructure info
Implementing Log Redaction
function redactSensitive(obj) {
const result = { ...obj };
const sensitiveKeys = ['password', 'token', 'secret', 'apiKey', 'cardNumber', 'ssn'];
for (const key of Object.keys(result)) {
if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk))) {
result[key] = '[REDACTED]';
}
}
return result;
}
// Use with Winston
logger.info('User login attempt', redactSensitive({ userId, email, password: 'secret123' }));
Best Practices for Secure Logging
- Implement redaction at the logging layer
- Validate logs before storage
- Limit log access based on role
- Encrypt logs at rest and in transit
- Implement log retention policies
Performance Optimization for Logging
Logging can impact application performance if not configured properly. Here are strategies to minimize overhead.
Minimizing Logging Overhead
- Use async transports: All production loggers support non-blocking writes
- Configure appropriate log levels: Debug logs add overhead
- Consider sampling: For high-volume debug scenarios
- Separate log streams: High-frequency logs to dedicated files
- Choose performance-focused libraries: Pino for high-throughput
Optimized Logging Pattern
// Avoid: Expensive operations in log statements
logger.debug('Processing data: %s', JSON.stringify(largeObject));
// Better: Check level first, then log
if (logger.isLevelEnabled('debug')) {
logger.debug('Processing data', { data: largeObject });
}
// Best: Use structured logging efficiently
logger.debug('Processing data', {
dataId: largeObject.id,
dataCount: largeObject.items.length
});
When Debug Logging Becomes Expensive
For high-traffic applications:
- Use sampling: Log 1 in 1000 debug statements
- Separate debug files: Don't mix with production logs
- Enable on-demand: Use feature flags for verbose logging
Log Centralization and Aggregation
In distributed systems, logs from multiple services need to be collected and analyzed together for effective debugging and monitoring.
The Log Aggregation Pipeline
Application Logs → Log Shipper → Storage → Visualization
↓ ↓ ↓ ↓
Winston/Pino Fluentd/Loki S3/GCS Kibana
Logstash Elasticsearch Grafana
Cloud-Native Logging Solutions
| Service | Best For |
|---|---|
| AWS CloudWatch | AWS-based applications |
| Google Cloud Logging | GCP ecosystems |
| Azure Monitor | Azure deployments |
| Datadog | Full observability platform |
| New Relic | APM and logging combined |
Key Metrics to Track
Monitor these metrics derived from your logs:
- Error rate trends: Percentage of errors over time
- Response time distributions: P50, P95, P99 latencies
- Failed operation counts: By type and service
- User-impacting incidents: Correlated with user reports
Implement these practices for production-ready logging
Choose the Right Library
Winston for flexibility, Pino for performance, or Morgan for HTTP requests. Match the tool to your requirements.
Use Structured JSON Logging
Machine-readable logs enable powerful queries, filtering, and integration with monitoring tools.
Implement Log Levels
Use DEBUG, INFO, WARN, ERROR, FATAL consistently. Enable dynamic level changes for debugging.
Include Essential Context
Timestamps, service name, request IDs, and user context transform vague logs into actionable data.
Never Log Sensitive Data
Implement redaction at the logging layer. PII, passwords, and tokens must never appear in logs.
Centralize Your Logs
Aggregate logs from all services for distributed debugging and proactive monitoring.