Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this essential tutorial on logging best practices for production Python applications! ๐ If youโve ever struggled to debug issues in production or wondered why your app crashed at 3 AM, this guide is for you!
Logging is like having a flight recorder for your application โ๏ธ. It captures what happened, when it happened, and why things went wrong (or right!). Whether youโre building web APIs ๐, data pipelines ๐, or automation scripts ๐ค, mastering production logging will save you countless hours of debugging and help you sleep better at night! ๐ด
By the end of this tutorial, youโll know how to implement professional-grade logging that will make debugging a breeze and keep your production apps running smoothly! Letโs dive in! ๐โโ๏ธ
๐ Understanding Production Logging
๐ค What is Production Logging?
Production logging is like having security cameras ๐น throughout your application. Just as cameras record what happens in a building, logs record what happens in your code!
In Python terms, production logging means:
- โจ Capturing important events without impacting performance
- ๐ Providing enough detail to debug issues
- ๐ก๏ธ Protecting sensitive information
- ๐ Enabling monitoring and alerting
- ๐ Making problems easy to trace and fix
๐ก Why Production Logging Matters
Hereโs why professional developers prioritize logging:
- Debugging Without SSH ๐: Debug issues without accessing production servers
- Performance Monitoring ๐: Track response times and bottlenecks
- Security Auditing ๐ก๏ธ: Know who did what and when
- Business Intelligence ๐ผ: Understand user behavior and app usage
- Compliance ๐: Meet regulatory requirements
Real-world example: Imagine an e-commerce site ๐. Good logging helps you track orders, debug payment failures, monitor inventory updates, and understand why customers abandon their carts!
๐ง Basic Syntax and Usage
๐ Setting Up Python Logging
Letโs start with a production-ready logging setup:
import logging
import logging.handlers
import os
from datetime import datetime
# ๐จ Create a logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# ๐ Format for our logs
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# ๐พ File handler for persistent logs
file_handler = logging.handlers.RotatingFileHandler(
'app.log',
maxBytes=10485760, # 10MB
backupCount=5
)
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(formatter)
# ๐ฅ๏ธ Console handler for development
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(formatter)
# โ Add handlers to logger
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# ๐ฏ Let's test it!
logger.info("Application started! ๐")
logger.debug("Debug mode is active ๐")
๐ก Explanation: We set up rotating file logs (to prevent disk space issues) and console output for development. The formatter ensures consistent, readable log messages!
๐ฏ Logging Levels
Understanding logging levels is crucial:
# ๐ DEBUG - Detailed information for diagnosing problems
logger.debug(f"Processing user {user_id} with data: {data}")
# ๐ข INFO - General informational messages
logger.info(f"User {user_id} logged in successfully โ
")
# โ ๏ธ WARNING - Something unexpected but not critical
logger.warning(f"API rate limit approaching: {current_rate}/1000")
# โ ERROR - Something failed but app continues
logger.error(f"Failed to send email to {email}: {error}")
# ๐ฅ CRITICAL - Serious error, app might crash
logger.critical("Database connection lost! ๐จ")
๐ก Practical Examples
๐ Example 1: E-Commerce Order Processing
Letโs build a production-ready order processing system:
import logging
import json
from typing import Dict, Optional
from datetime import datetime
class OrderProcessor:
def __init__(self):
self.logger = logging.getLogger(f"{__name__}.OrderProcessor")
self.logger.info("OrderProcessor initialized ๐๏ธ")
def process_order(self, order_data: Dict) -> Optional[str]:
"""Process an order with comprehensive logging"""
order_id = order_data.get('id', 'unknown')
# ๐ฏ Log important business events
self.logger.info(
f"Processing order {order_id}",
extra={
'order_id': order_id,
'customer_id': order_data.get('customer_id'),
'total_amount': order_data.get('total'),
'items_count': len(order_data.get('items', []))
}
)
try:
# ๐ฆ Validate inventory
self._check_inventory(order_data['items'])
# ๐ณ Process payment
payment_result = self._process_payment(order_data)
# ๐ Create shipping
shipping_id = self._create_shipping(order_data)
# โ
Success!
self.logger.info(
f"Order {order_id} completed successfully! ๐",
extra={
'order_id': order_id,
'shipping_id': shipping_id,
'processing_time': datetime.now().isoformat()
}
)
return shipping_id
except InventoryError as e:
self.logger.warning(
f"Inventory issue for order {order_id}: {e}",
extra={'order_id': order_id, 'error_type': 'inventory'}
)
raise
except PaymentError as e:
self.logger.error(
f"Payment failed for order {order_id}: {e}",
extra={
'order_id': order_id,
'error_type': 'payment',
'payment_method': order_data.get('payment_method')
},
exc_info=True # ๐ Include stack trace
)
raise
except Exception as e:
self.logger.critical(
f"Unexpected error processing order {order_id}: {e}",
extra={'order_id': order_id},
exc_info=True
)
raise
def _check_inventory(self, items):
"""Check if items are in stock"""
self.logger.debug(f"Checking inventory for {len(items)} items ๐ฆ")
# Inventory logic here
def _process_payment(self, order_data):
"""Process payment securely"""
# โ ๏ธ Never log sensitive data!
self.logger.info(
"Processing payment",
extra={
'amount': order_data['total'],
'method': order_data['payment_method'],
# Don't log: credit card numbers, CVV, etc.
}
)
# Payment logic here
def _create_shipping(self, order_data):
"""Create shipping label"""
self.logger.debug("Creating shipping label ๐")
# Shipping logic here
return f"SHIP-{order_data['id']}"
# ๐ฎ Custom exceptions
class InventoryError(Exception):
pass
class PaymentError(Exception):
pass
๐ฏ Key Points: Notice how we use structured logging with extra
fields, include stack traces for errors, and never log sensitive data!
๐ฎ Example 2: API Performance Monitoring
Letโs create a logging decorator for API endpoints:
import time
import functools
import logging
from typing import Callable
class APILogger:
def __init__(self):
self.logger = logging.getLogger(f"{__name__}.API")
def log_endpoint(self, func: Callable) -> Callable:
"""Decorator to log API endpoint calls with performance metrics"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
# ๐ Start timing
start_time = time.time()
endpoint_name = func.__name__
# ๐ Log request
self.logger.info(
f"API call started: {endpoint_name}",
extra={
'endpoint': endpoint_name,
'method': kwargs.get('method', 'GET'),
'user_id': kwargs.get('user_id', 'anonymous')
}
)
try:
# ๐ฏ Execute the function
result = func(*args, **kwargs)
# ๐ Calculate duration
duration = time.time() - start_time
# โ
Log success
self.logger.info(
f"API call completed: {endpoint_name}",
extra={
'endpoint': endpoint_name,
'duration_ms': round(duration * 1000, 2),
'status': 'success',
'response_size': len(str(result))
}
)
# โ ๏ธ Warn if slow
if duration > 1.0:
self.logger.warning(
f"Slow API response: {endpoint_name} took {duration:.2f}s",
extra={
'endpoint': endpoint_name,
'duration_ms': round(duration * 1000, 2)
}
)
return result
except Exception as e:
# โ Log failure
duration = time.time() - start_time
self.logger.error(
f"API call failed: {endpoint_name}",
extra={
'endpoint': endpoint_name,
'duration_ms': round(duration * 1000, 2),
'status': 'error',
'error_type': type(e).__name__
},
exc_info=True
)
raise
return wrapper
# ๐ฎ Usage example
api_logger = APILogger()
class UserAPI:
@api_logger.log_endpoint
def get_user_profile(self, user_id: str, method='GET'):
"""Get user profile with automatic logging"""
# ๐ฏ Your API logic here
time.sleep(0.1) # Simulate work
return {'user_id': user_id, 'name': 'Alice', 'level': 42}
@api_logger.log_endpoint
def update_user_score(self, user_id: str, score: int, method='POST'):
"""Update user score with automatic logging"""
# ๐ฏ Your API logic here
if score < 0:
raise ValueError("Score cannot be negative! ๐ฑ")
return {'success': True, 'new_score': score}
# ๐ Test it!
api = UserAPI()
api.get_user_profile(user_id='123')
api.update_user_score(user_id='123', score=100)
๐ Advanced Concepts
๐งโโ๏ธ Structured Logging with JSON
For production apps, JSON logs are easier to parse and analyze:
import logging
import json
from pythonjsonlogger import jsonlogger
# ๐จ Setup JSON logging
logHandler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter()
logHandler.setFormatter(formatter)
logger = logging.getLogger()
logger.addHandler(logHandler)
logger.setLevel(logging.INFO)
# ๐ Log structured data
logger.info(
"User action",
extra={
"user_id": "123",
"action": "purchase",
"item_id": "ABC",
"amount": 29.99,
"timestamp": datetime.now().isoformat()
}
)
# Output: {"message": "User action", "user_id": "123", "action": "purchase", ...}
๐๏ธ Centralized Logging Configuration
Create a reusable logging configuration:
import logging.config
import os
def setup_logging(app_name: str, environment: str = 'production'):
"""Setup logging configuration for production apps"""
config = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'detailed': {
'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
},
'json': {
'()': 'pythonjsonlogger.jsonlogger.JsonFormatter',
'format': '%(asctime)s %(name)s %(levelname)s %(message)s'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'detailed' if environment == 'development' else 'json',
'stream': 'ext://sys.stdout'
},
'file': {
'class': 'logging.handlers.RotatingFileHandler',
'level': 'INFO',
'formatter': 'json',
'filename': f'/var/log/{app_name}/{app_name}.log',
'maxBytes': 10485760, # 10MB
'backupCount': 5
},
'error_file': {
'class': 'logging.handlers.RotatingFileHandler',
'level': 'ERROR',
'formatter': 'json',
'filename': f'/var/log/{app_name}/{app_name}_errors.log',
'maxBytes': 10485760, # 10MB
'backupCount': 5
}
},
'loggers': {
'': { # Root logger
'level': 'INFO',
'handlers': ['console', 'file', 'error_file']
},
'uvicorn': { # Example: Configure third-party loggers
'level': 'WARNING'
}
}
}
# ๐๏ธ Create log directory if needed
log_dir = f'/var/log/{app_name}'
os.makedirs(log_dir, exist_ok=True)
# ๐ฏ Apply configuration
logging.config.dictConfig(config)
# โจ Log startup
logger = logging.getLogger(__name__)
logger.info(
f"{app_name} logging initialized! ๐",
extra={
'app_name': app_name,
'environment': environment,
'log_directory': log_dir
}
)
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Logging Sensitive Data
# โ Wrong - Never log passwords or credit cards!
logger.info(f"User login: username={username}, password={password}")
logger.info(f"Payment: card_number={card_number}, cvv={cvv}")
# โ
Correct - Log safely!
logger.info(f"User login attempt: username={username}")
logger.info(f"Payment processed: last_four={card_number[-4:]}, amount={amount}")
# ๐ก๏ธ Even better - Use a sanitizer
def sanitize_sensitive_data(data: dict) -> dict:
"""Remove sensitive fields from log data"""
sensitive_fields = ['password', 'token', 'api_key', 'secret']
sanitized = data.copy()
for field in sensitive_fields:
if field in sanitized:
sanitized[field] = '***REDACTED***'
return sanitized
# Usage
logger.info("User data", extra=sanitize_sensitive_data(user_data))
๐คฏ Pitfall 2: Excessive Debug Logging
# โ Wrong - This will flood your logs!
for item in huge_list: # 1 million items
logger.debug(f"Processing item: {item}")
# โ
Correct - Log summaries and samples!
logger.debug(f"Processing {len(huge_list)} items")
if len(huge_list) > 0:
logger.debug(f"First item sample: {huge_list[0]}")
# ๐ Or use sampling
import random
if random.random() < 0.01: # Log 1% of items
logger.debug(f"Sample item: {item}")
๐ Pitfall 3: Synchronous Logging Blocking Performance
# โ Wrong - Blocks your app!
logger.info(f"Slow operation: {expensive_calculation()}")
# โ
Correct - Calculate first, then log!
result = expensive_calculation()
logger.info(f"Operation completed", extra={'result_size': len(result)})
# ๐ Even better - Use async logging
import asyncio
from concurrent.futures import ThreadPoolExecutor
class AsyncLogger:
def __init__(self):
self.executor = ThreadPoolExecutor(max_workers=1)
self.logger = logging.getLogger(__name__)
async def log_async(self, level, message, **kwargs):
"""Log without blocking the event loop"""
loop = asyncio.get_event_loop()
await loop.run_in_executor(
self.executor,
lambda: self.logger.log(level, message, **kwargs)
)
๐ ๏ธ Best Practices
- ๐ฏ Use Structured Logging: JSON format for easy parsing
- ๐ Include Context: Add request IDs, user IDs, etc.
- ๐ก๏ธ Protect Sensitive Data: Never log passwords, tokens, or PII
- ๐ Monitor Performance: Log response times and slow queries
- ๐ Rotate Log Files: Prevent disk space issues
- ๐ท๏ธ Use Log Levels Correctly: DEBUG for development, INFO for production
- ๐ Include Stack Traces: Use
exc_info=True
for exceptions - ๐ Log Business Events: Not just technical errors
- ๐ Async When Possible: Donโt block your application
- ๐ Follow Standards: Use consistent formats and fields
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Production-Ready API Logger
Create a comprehensive logging system for a REST API:
๐ Requirements:
- โ Log all API requests with timing
- ๐ก๏ธ Sanitize sensitive data automatically
- ๐ Track error rates and slow endpoints
- ๐ Implement request correlation IDs
- ๐ Add performance metrics
- ๐จ Support both JSON and human-readable formats
๐ Bonus Points:
- Add request/response body logging (with size limits)
- Implement log sampling for high-traffic endpoints
- Create alerts for critical errors
- Add distributed tracing support
๐ก Solution
๐ Click to see solution
import logging
import time
import uuid
import json
from typing import Dict, Any, Optional
from functools import wraps
from datetime import datetime
class ProductionAPILogger:
def __init__(self, app_name: str):
self.app_name = app_name
self.logger = self._setup_logger()
self.metrics = {'total_requests': 0, 'errors': 0}
def _setup_logger(self):
"""Setup production-ready logger"""
logger = logging.getLogger(self.app_name)
logger.setLevel(logging.INFO)
# ๐จ JSON formatter for production
json_formatter = logging.Formatter(
'{"time": "%(asctime)s", "app": "%(name)s", '
'"level": "%(levelname)s", "message": "%(message)s"}'
)
# ๐ Handlers
handler = logging.StreamHandler()
handler.setFormatter(json_formatter)
logger.addHandler(handler)
return logger
def _sanitize_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Remove sensitive information"""
if not isinstance(data, dict):
return data
sensitive_keys = {
'password', 'token', 'api_key', 'secret',
'credit_card', 'ssn', 'authorization'
}
sanitized = {}
for key, value in data.items():
if key.lower() in sensitive_keys:
sanitized[key] = '***REDACTED***'
elif isinstance(value, dict):
sanitized[key] = self._sanitize_data(value)
else:
sanitized[key] = value
return sanitized
def log_request(self, method='GET', path='/', user_id=None):
"""Decorator for logging API requests"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# ๐ฏ Generate correlation ID
correlation_id = str(uuid.uuid4())
start_time = time.time()
# ๐ Log request
request_data = {
'correlation_id': correlation_id,
'method': method,
'path': path,
'user_id': user_id or 'anonymous',
'timestamp': datetime.utcnow().isoformat()
}
self.logger.info(
f"API Request: {method} {path}",
extra=self._sanitize_data(request_data)
)
try:
# ๐ Execute function
result = func(*args, **kwargs)
# ๐ Calculate metrics
duration = (time.time() - start_time) * 1000
self.metrics['total_requests'] += 1
# โ
Log success
response_data = {
'correlation_id': correlation_id,
'duration_ms': round(duration, 2),
'status': 'success',
'path': path
}
self.logger.info(
f"API Response: {method} {path}",
extra=response_data
)
# โ ๏ธ Warn if slow
if duration > 1000:
self.logger.warning(
f"Slow endpoint detected: {path}",
extra={
'correlation_id': correlation_id,
'duration_ms': duration
}
)
return result
except Exception as e:
# โ Log error
duration = (time.time() - start_time) * 1000
self.metrics['errors'] += 1
error_data = {
'correlation_id': correlation_id,
'duration_ms': round(duration, 2),
'status': 'error',
'error_type': type(e).__name__,
'error_message': str(e),
'path': path
}
self.logger.error(
f"API Error: {method} {path}",
extra=error_data,
exc_info=True
)
# ๐จ Alert on critical errors
error_rate = self.metrics['errors'] / max(self.metrics['total_requests'], 1)
if error_rate > 0.1: # 10% error rate
self.logger.critical(
"High error rate detected!",
extra={
'error_rate': round(error_rate * 100, 2),
'total_errors': self.metrics['errors']
}
)
raise
return wrapper
return decorator
def get_metrics(self) -> Dict[str, Any]:
"""Get current metrics"""
return {
'total_requests': self.metrics['total_requests'],
'total_errors': self.metrics['errors'],
'error_rate': round(
self.metrics['errors'] / max(self.metrics['total_requests'], 1) * 100,
2
),
'timestamp': datetime.utcnow().isoformat()
}
# ๐ฎ Example usage
logger = ProductionAPILogger('MyAPI')
class UserAPI:
@logger.log_request(method='GET', path='/api/users/{id}')
def get_user(self, user_id: str):
"""Get user with automatic logging"""
# Simulate work
time.sleep(0.1)
return {'id': user_id, 'name': 'Alice', 'email': '[email protected]'}
@logger.log_request(method='POST', path='/api/users/{id}/score')
def update_score(self, user_id: str, score: int, api_key: str):
"""Update score with automatic sanitization"""
if score < 0:
raise ValueError("Invalid score!")
return {'success': True, 'new_score': score}
# ๐ Test it!
api = UserAPI()
api.get_user('123')
api.update_score('123', 100, api_key='secret-key-123')
# ๐ Check metrics
print(f"Metrics: {logger.get_metrics()}")
๐ Key Takeaways
Youโve mastered production logging! Hereโs what you can now do:
- โ Set up professional logging with proper levels and formatting ๐ช
- โ Protect sensitive data from appearing in logs ๐ก๏ธ
- โ Monitor performance with timing and metrics ๐
- โ Debug production issues without SSH access ๐
- โ Build scalable logging systems for any Python app! ๐
Remember: Good logging is like insurance - you hope you never need it, but when you do, youโll be incredibly grateful itโs there! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve leveled up your Python logging skills!
Hereโs what to do next:
- ๐ป Implement the logging system in your current project
- ๐๏ธ Set up centralized logging with ELK or CloudWatch
- ๐ Learn about distributed tracing with OpenTelemetry
- ๐ Share your logging best practices with your team!
Remember: Every production issue you quickly resolve with good logging is a victory. Keep logging smartly, and your future self will thank you! ๐
Happy logging! ๐๐โจ