Introduction: The Black Box Problem
Your application crashed in production at 3 AM. Users are complaining. You SSH into the server and... nothing. No clue what happened, when it happened, or why.
This is what happens without proper logging.
Logging is your application's flight recorder. When something goes wrong (and it will), logs are your only way to understand what happened.
In this guide, you'll learn:
- What logging is and why it's critical
- Log levels and when to use them
- How to implement logging in Python and Node.js
- Structured logging for production
- Best practices and common mistakes
- What NOT to log (security)
What is Logging?
Logging is the practice of recording events, errors, and information about your application's behavior.
The Simple Definition
Logging = Writing down what your application is doing, so you can understand it later
Think of It Like This:
Without Logs:
User reports: "The app crashed"
You: "When? What were you doing? What error?"
User: "I don't remember"
You: 🤷 (Can't fix what you can't see)
With Logs:
User: "The app crashed"
You: *checks logs*
[2024-01-14 03:23:45] ERROR: Database connection timeout after 30s
[2024-01-14 03:23:45] ERROR: Failed to process order #12345
You: ✅ "Database issue, I know how to fix this"
Log Levels Explained
Log levels categorize the importance/severity of log messages.
The Standard Hierarchy
DEBUG → Detailed diagnostic information
INFO → General informational messages
WARNING → Something unexpected but handled
ERROR → Error occurred but app continues
CRITICAL → Serious error, app may crash
When to Use Each Level
1. DEBUG - Development Details
Use for: Detailed diagnostic information useful during development
logger.debug(f"User {user_id} attempting login")
logger.debug(f"SQL Query: {query}")
logger.debug(f"Cache hit: {cache_key}")
logger.debug(f"Function arguments: {args}, {kwargs}")
When: Only in development. Turn OFF in production (too verbose).
2. INFO - Normal Operations
Use for: Confirming things are working as expected
logger.info(f"User {user_id} logged in successfully")
logger.info(f"Order {order_id} created")
logger.info(f"Server started on port 8080")
logger.info(f"Cronjob completed: 500 emails sent")
When: Production. Track normal business operations.
3. WARNING - Something's Off
Use for: Unexpected situations that don't prevent operation
logger.warning(f"API response took 5s (expected <1s)")
logger.warning(f"Disk usage at 85%")
logger.warning(f"Deprecated function called: {func_name}")
logger.warning(f"User tried invalid action: {action}")
When: Things that might become problems. Set up alerts.
4. ERROR - Something Failed
Use for: Errors that prevent a specific operation but app continues
logger.error(f"Failed to send email to {email}", exc_info=True)
logger.error(f"Payment processing failed: {error}")
logger.error(f"Database query failed: {query}")
logger.error(f"File not found: {filepath}")
When: An operation failed. Always log the exception.
5. CRITICAL - Everything's on Fire 🔥
Use for: Severe errors that might crash the application
logger.critical("Database connection pool exhausted")
logger.critical("Out of memory")
logger.critical("Cannot connect to primary database")
logger.critical("Security breach detected")
When: System-level failures. Immediate action required.
Quick Decision Tree
Is it useful for debugging? → DEBUG
Is it a normal operation? → INFO
Is it unexpected but handled? → WARNING
Did something fail? → ERROR
Is the system in danger? → CRITICAL
Implementation: Python
Basic Logging
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Use it
logger.debug("This won't show (level is INFO)")
logger.info("Application started")
logger.warning("Cache miss for key: user_123")
logger.error("Failed to connect to database")
logger.critical("System out of memory")
Output:
2024-01-14 10:30:45,123 - __main__ - INFO - Application started
2024-01-14 10:30:46,456 - __main__ - WARNING - Cache miss for key: user_123
2024-01-14 10:30:47,789 - __main__ - ERROR - Failed to connect to database
2024-01-14 10:30:48,012 - __main__ - CRITICAL - System out of memory
Production-Ready Setup
import logging
import logging.handlers
import sys
from pathlib import Path
def setup_logging(log_level=logging.INFO):
"""Configure logging for production"""
# Create logs directory
log_dir = Path("logs")
log_dir.mkdir(exist_ok=True)
# Create logger
logger = logging.getLogger()
logger.setLevel(log_level)
# Format
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Console handler (stdout)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# File handler (rotating)
file_handler = logging.handlers.RotatingFileHandler(
log_dir / "app.log",
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Error file handler (errors only)
error_handler = logging.handlers.RotatingFileHandler(
log_dir / "error.log",
maxBytes=10 * 1024 * 1024,
backupCount=5
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(formatter)
logger.addHandler(error_handler)
return logger
# Initialize
logger = setup_logging()
# Use in your application
def process_order(order_id):
logger.info(f"Processing order {order_id}")
try:
# Process order
result = do_something(order_id)
logger.info(f"Order {order_id} processed successfully")
return result
except Exception as e:
logger.error(f"Failed to process order {order_id}: {e}", exc_info=True)
raise
What this does:
- ✅ Logs to console (for Docker/CloudWatch)
- ✅ Logs to rotating files (10MB max, keeps 5 backups)
- ✅ Separate error log file
- ✅ Includes timestamps and log levels
- ✅ Automatically includes stack traces for errors
Structured Logging (JSON)
Why JSON? Easy to parse, search, and analyze with tools like ELK, Splunk, DataDog.
import logging
import json
from datetime import datetime
class JSONFormatter(logging.Formatter):
"""Custom formatter that outputs JSON"""
def format(self, record):
log_data = {
'timestamp': datetime.utcnow().isoformat(),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
'module': record.module,
'function': record.funcName,
'line': record.lineno
}
# Add exception info if present
if record.exc_info:
log_data['exception'] = self.formatException(record.exc_info)
# Add extra fields (if any)
if hasattr(record, 'user_id'):
log_data['user_id'] = record.user_id
if hasattr(record, 'request_id'):
log_data['request_id'] = record.request_id
return json.dumps(log_data)
# Configure
logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Use with extra context
logger.info("User login", extra={'user_id': 123, 'request_id': 'abc-123'})
Output:
{
"timestamp": "2024-01-14T10:30:45.123456",
"level": "INFO",
"logger": "__main__",
"message": "User login",
"module": "app",
"function": "login",
"line": 42,
"user_id": 123,
"request_id": "abc-123"
}
Benefits:
- ✅ Easy to query:
logs where user_id=123 - ✅ Structured data for analytics
- ✅ Works with log aggregation tools
Using python-json-logger (Easier)
from pythonjsonlogger import jsonlogger
import logging
logger = logging.getLogger()
handler = logging.StreamHandler()
# JSON formatter
formatter = jsonlogger.JsonFormatter(
'%(timestamp)s %(level)s %(name)s %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Use it
logger.info("User logged in", extra={
'user_id': 123,
'ip_address': '192.168.1.1',
'user_agent': 'Mozilla/5.0'
})
Implementation: Node.js
Basic Logging with Winston (Industry Standard)
const winston = require('winston');
// Create logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
// Console output
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
// File output
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'logs/combined.log'
})
]
});
// Use it
logger.debug('Debugging info');
logger.info('User logged in', { userId: 123 });
logger.warn('Slow response time', { duration: 5000 });
logger.error('Database error', { error: err.message });
Production Setup with Winston
const winston = require('winston');
const path = require('path');
// Different formats for different environments
const isDevelopment = process.env.NODE_ENV !== 'production';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
// Format
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()
),
// Default metadata
defaultMeta: {
service: 'my-app',
environment: process.env.NODE_ENV
},
transports: [
// Console (always)
new winston.transports.Console({
format: isDevelopment
? winston.format.combine(
winston.format.colorize(),
winston.format.printf(({ level, message, timestamp, ...meta }) => {
return `${timestamp} [${level}]: ${message} ${
Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
}`;
})
)
: winston.format.json()
}),
// File - All logs
new winston.transports.File({
filename: path.join('logs', 'combined.log'),
maxsize: 10485760, // 10MB
maxFiles: 5
}),
// File - Errors only
new winston.transports.File({
filename: path.join('logs', 'error.log'),
level: 'error',
maxsize: 10485760,
maxFiles: 5
})
],
// Handle exceptions and rejections
exceptionHandlers: [
new winston.transports.File({
filename: path.join('logs', 'exceptions.log')
})
],
rejectionHandlers: [
new winston.transports.File({
filename: path.join('logs', 'rejections.log')
})
]
});
module.exports = logger;
Usage in Express:
const express = require('express');
const logger = require('./logger');
const app = express();
// Log all requests
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
userAgent: req.get('user-agent'),
ip: req.ip
});
});
next();
});
// Your routes
app.get('/api/users/:id', async (req, res) => {
try {
logger.info('Fetching user', { userId: req.params.id });
const user = await getUserById(req.params.id);
logger.info('User fetched successfully', {
userId: req.params.id,
username: user.username
});
res.json(user);
} catch (error) {
logger.error('Failed to fetch user', {
userId: req.params.id,
error: error.message,
stack: error.stack
});
res.status(500).json({ error: 'Internal server error' });
}
});
// Global error handler
app.use((err, req, res, next) => {
logger.error('Unhandled error', {
error: err.message,
stack: err.stack,
url: req.url,
method: req.method
});
res.status(500).json({ error: 'Something went wrong' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
logger.info('Server started', { port: PORT });
});
Simple Console Logging (Quick Start)
// Don't use console.log in production!
// But if you must keep it simple:
const log = {
debug: (msg, ...args) => console.log(`[DEBUG] ${msg}`, ...args),
info: (msg, ...args) => console.log(`[INFO] ${msg}`, ...args),
warn: (msg, ...args) => console.warn(`[WARN] ${msg}`, ...args),
error: (msg, ...args) => console.error(`[ERROR] ${msg}`, ...args)
};
log.info('User logged in', { userId: 123 });
log.error('Database error', new Error('Connection failed'));
⚠️ Not recommended for production - use Winston or Pino instead.
What to Log (and What NOT to Log)
✅ DO Log
1. Application Lifecycle
logger.info("Application started")
logger.info("Database connection established")
logger.info("Server listening on port 8080")
logger.info("Graceful shutdown initiated")
2. Business Operations
logger.info(f"Order {order_id} created by user {user_id}")
logger.info(f"Payment processed: ${amount}")
logger.info(f"Email sent to {email}")
3. Errors and Exceptions
try:
result = risky_operation()
except Exception as e:
logger.error(f"Operation failed: {e}", exc_info=True)
# exc_info=True includes the stack trace
4. Performance Metrics
start = time.time()
result = slow_operation()
duration = time.time() - start
if duration > 1.0:
logger.warning(f"Slow operation: {duration:.2f}s")
5. Security Events
logger.warning(f"Failed login attempt for user {username} from {ip}")
logger.error(f"Invalid API key used: {api_key[:8]}...")
logger.critical("Potential SQL injection detected")
6. External Service Calls
logger.info(f"Calling external API: {api_url}")
logger.error(f"External API failed: {api_url} - {status_code}")
❌ DO NOT Log
1. Passwords (NEVER)
# ❌ NEVER DO THIS
logger.info(f"User login: {username} with password {password}")
# ✅ DO THIS
logger.info(f"User login: {username}")
2. API Keys / Secrets
# ❌ BAD
logger.debug(f"Stripe key: {stripe_key}")
# ✅ GOOD (if you must log it)
logger.debug(f"Stripe key: {stripe_key[:8]}...") # First 8 chars only
3. Credit Card Numbers
# ❌ BAD
logger.info(f"Processing card {card_number}")
# ✅ GOOD
logger.info(f"Processing card ending in {card_number[-4:]}")
4. Personal Identifiable Information (PII)
# ❌ BAD
logger.info(f"User data: {email}, {ssn}, {address}")
# ✅ GOOD
logger.info(f"User data updated", extra={'user_id': user_id})
5. Full Request/Response Bodies (in production)
# ❌ BAD (may contain sensitive data)
logger.debug(f"Request body: {request.body}")
# ✅ GOOD
logger.debug(f"Request received", extra={
'endpoint': request.path,
'method': request.method,
'content_length': len(request.body)
})
Best Practices
1. Use Structured Logging
// ❌ BAD - Hard to parse
logger.info(`User 123 logged in from 192.168.1.1`);
// ✅ GOOD - Structured, searchable
logger.info('User logged in', {
userId: 123,
ipAddress: '192.168.1.1',
timestamp: new Date().toISOString()
});
Why? You can search: "Show me all logs where userId=123"
2. Include Context
# ❌ BAD - No context
logger.error("Database error")
# ✅ GOOD - Full context
logger.error(
"Database connection failed",
extra={
'database': 'postgresql',
'host': 'db.example.com',
'port': 5432,
'timeout': 30,
'retry_count': 3
},
exc_info=True
)
3. Use Correlation IDs
// Generate unique ID for each request
app.use((req, res, next) => {
req.requestId = crypto.randomUUID();
next();
});
// Include in all logs
app.get('/api/users', async (req, res) => {
logger.info('Fetching users', { requestId: req.requestId });
try {
const users = await db.getUsers();
logger.info('Users fetched', {
requestId: req.requestId,
count: users.length
});
res.json(users);
} catch (error) {
logger.error('Failed to fetch users', {
requestId: req.requestId,
error: error.message
});
res.status(500).json({ error: 'Server error' });
}
});
Benefit: Track a single request across multiple log entries.
4. Log at the Right Level
# Development
logger.setLevel(logging.DEBUG) # See everything
# Staging
logger.setLevel(logging.INFO) # Normal operations + errors
# Production
logger.setLevel(logging.WARNING) # Only warnings and errors
Set via environment variable:
import os
log_level = os.getenv('LOG_LEVEL', 'INFO')
logger.setLevel(getattr(logging, log_level.upper()))
5. Rotate Log Files
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler(
'app.log',
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5 # Keep 5 old files
)
# Creates: app.log, app.log.1, app.log.2, app.log.3, app.log.4, app.log.5
Why? Prevent logs from filling up disk space.
6. Don't Log in Loops (Unless Necessary)
# ❌ BAD - Creates 1 million log entries
for user in users: # 1 million users
logger.info(f"Processing user {user.id}")
process_user(user)
# ✅ GOOD - Log summary
logger.info(f"Starting batch process for {len(users)} users")
for user in users:
process_user(user)
logger.info(f"Batch process completed")
# ✅ GOOD - Log errors only
for user in users:
try:
process_user(user)
except Exception as e:
logger.error(f"Failed to process user {user.id}: {e}")
7. Use Appropriate Formats for Environments
// Development: Human-readable
const devFormat = winston.format.combine(
winston.format.colorize(),
winston.format.printf(info => `${info.level}: ${info.message}`)
);
// Production: JSON for parsing
const prodFormat = winston.format.json();
const logger = winston.createLogger({
format: process.env.NODE_ENV === 'production' ? prodFormat : devFormat,
transports: [new winston.transports.Console()]
});
Common Mistakes
Mistake #1: Logging Too Much in Production
# ❌ BAD - DEBUG in production
logger.setLevel(logging.DEBUG)
# Result: Gigabytes of logs, costs $$$, slows down app
# ✅ GOOD
logger.setLevel(logging.WARNING) # Only warnings and errors
Mistake #2: Not Logging Enough Context
# ❌ BAD
logger.error("Error occurred")
# What error? Where? Why?
# ✅ GOOD
logger.error(
"Failed to process payment",
extra={
'user_id': user_id,
'order_id': order_id,
'amount': amount,
'payment_method': payment_method,
'error_code': error.code
},
exc_info=True
)
Mistake #3: Using print() Instead of Logger
# ❌ BAD
print("User logged in")
print("Error:", error)
# Problems:
# - No log levels
# - No timestamps
# - Can't control output
# - Can't send to different destinations
# ✅ GOOD
logger.info("User logged in")
logger.error("Error occurred", exc_info=True)
Mistake #4: Logging Synchronously in High-Traffic Apps
// ❌ Can slow down requests
logger.info('Request processed'); // Blocks until written to file
// ✅ Use async transports
const logger = winston.createLogger({
transports: [
new winston.transports.File({
filename: 'app.log',
options: { flags: 'a' } // Append mode
})
]
});
Better: Send logs to a queue (Redis, Kafka) and process asynchronously.
Mistake #5: Not Including Stack Traces for Errors
# ❌ BAD
try:
risky_operation()
except Exception as e:
logger.error(f"Error: {e}")
# Where did it happen? What was the call stack?
# ✅ GOOD
try:
risky_operation()
except Exception as e:
logger.error(f"Error: {e}", exc_info=True)
# Includes full stack trace
Log Aggregation Tools
For production, send logs to centralized systems:
Popular Tools
1. ELK Stack (Elasticsearch, Logstash, Kibana)
from logstash_formatter import LogstashFormatterV1
handler = logging.StreamHandler()
handler.setFormatter(LogstashFormatterV1())
logger.addHandler(handler)
2. CloudWatch (AWS)
import watchtower
logger.addHandler(watchtower.CloudWatchLogHandler(
log_group='my-app',
stream_name='production'
))
3. Datadog
const logger = winston.createLogger({
transports: [
new winston.transports.Http({
host: 'http-intake.logs.datadoghq.com',
path: '/v1/input',
ssl: true
})
]
});
4. Sentry (for errors)
const Sentry = require('@sentry/node');
Sentry.init({ dsn: 'your-dsn' });
try {
riskyOperation();
} catch (error) {
Sentry.captureException(error);
logger.error('Operation failed', { error });
}
Quick Reference
Python Logging Cheat Sheet
import logging
# Setup
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
# Usage
logger.debug('Debug message')
logger.info('Info message')
logger.warning('Warning message')
logger.error('Error message', exc_info=True)
logger.critical('Critical message')
# With context
logger.info('User action', extra={'user_id': 123})
# Format
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
Node.js Winston Cheat Sheet
const winston = require('winston');
// Setup
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'app.log' })
]
});
// Usage
logger.debug('Debug message');
logger.info('Info message', { userId: 123 });
logger.warn('Warning message');
logger.error('Error message', { error: err.message });
Conclusion
Key Takeaways
- Always log - It's your application's black box recorder
- Use log levels correctly - DEBUG for development, INFO for production operations, ERROR for failures
- Structure your logs - JSON format for production
- Add context - User IDs, request IDs, timestamps
- Never log secrets - Passwords, API keys, credit cards
- Rotate log files - Prevent disk space issues
- Use proper tools - Winston (Node.js), logging module (Python)
- Centralize in production - ELK, CloudWatch, Datadog
The Golden Rules
✅ Log enough to debug issues
❌ Don't log so much you can't find anything
✅ Log errors with full context
❌ Never log sensitive data
✅ Use structured logging (JSON)
❌ Don't use print() or console.log() in production
Next Steps
- Implement logging in your current project
- Add structured logging (JSON format)
- Set up log rotation to prevent disk issues
- Review your logs - Are you logging the right things?
- Set up alerting on ERROR and CRITICAL logs
- Consider log aggregation for production (ELK, Datadog)
Questions?
Want to know more about:
- Setting up ELK stack?
- Distributed tracing with correlation IDs?
- Log monitoring and alerting?
- Performance impact of logging?
Drop a comment below! 🚀
Remember: Good logging is the difference between fixing a bug in 5 minutes and debugging for 5 hours. Invest time in setting it up properly!
Top comments (0)