Why Your Logs Are Useless: 5 Logging Best Practices
Learn: Why Your Logs Are Useless: 5 Logging Best Practices
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
Why Your Logs Are Useless: 5 Logging Best Practices for Real Observability
The 3 AM Wake-Up Call That Changed Everything
I'll never forget the Slack message that woke me at 3:17 AM: "Payment processing is down. Users can't checkout. Fix it NOW."
I stumbled to my laptop, pulled up our logging dashboard, and saw... nothing useful. Just thousands of lines of INFO: Request processed and DEBUG: Entering function. Our payment gateway was hemorrhaging money, and our logs were about as helpful as a chocolate teapot.
That night cost us $47,000 in lost revenue and taught me a brutal lesson: most logging is theater. We log because we're supposed to, not because it actually helps us debug production issues.
If you've ever stared at logs during an outage feeling completely lost, this article is for you. Let's fix your logging before it costs you a 3 AM panic attack.
The Logging Paradox: Too Much Noise, Not Enough Signal
Here's the dirty secret about modern application logging: we're drowning in data but starving for information.
The average microservices application generates millions of log entries per day. But when something breaks, you're frantically grep-ing through haystack after haystack, hoping to find that one needle that explains why your API latency just spiked to 30 seconds.
I've reviewed logging implementations at dozens of companies, and the pattern is always the same:
- 80% of logs are useless (generic info messages that provide no actionable insight)
- 15% are redundant (the same information logged multiple times)
- 5% are actually valuable (but impossible to find when you need them)
The problem isn't that we don't log enough. It's that we log badly.
The Five Logging Practices That Actually Matter
1. Structure Your Logs (JSON Is Your Friend)
The Problem: Unstructured logs are impossible to query efficiently.
Remember my 3 AM disaster? Here's what our logs looked like:
2024-01-15 03:15:23 INFO Payment processed for user John
2024-01-15 03:15:24 ERROR Something went wrong
2024-01-15 03:15:25 INFO Request completed
Good luck finding which payment failed, for which user, or why.
The Solution: Structured logging with consistent fields.
{
"timestamp": "2024-01-15T03:15:24Z",
"level": "ERROR",
"service": "payment-gateway",
"trace_id": "a1b2c3d4-e5f6-7890",
"user_id": "usr_12345",
"transaction_id": "txn_67890",
"amount": 99.99,
"currency": "USD",
"payment_provider": "stripe",
"error_code": "CARD_DECLINED",
"error_message": "Insufficient funds",
"duration_ms": 1247,
"metadata": {
"card_last4": "4242",
"retry_attempt": 1
}
}
Now I can instantly query: "Show me all failed Stripe transactions over $50 in the last hour."
Key Fields Every Log Should Have:
| Field | Purpose | Example |
timestamp | When it happened (ISO 8601) | 2024-01-15T03:15:24Z |
level | Severity (ERROR, WARN, INFO, DEBUG) | ERROR |
service | Which microservice | payment-gateway |
trace_id | Distributed tracing correlation | a1b2c3d4-e5f6-7890 |
user_id | Who was affected | usr_12345 |
action | What was being attempted | process_payment |
duration_ms | Performance tracking | 1247 |
error_code | Machine-readable error | CARD_DECLINED |
Implementation Example (Python):
import structlog
import logging
# Configure structured logging
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.JSONRenderer()
],
wrapper_class=structlog.stdlib.BoundLogger,
logger_factory=structlog.stdlib.LoggerFactory(),
)
logger = structlog.get_logger()
# Bad logging
logger.info("Payment processed")
# Good logging
logger.info(
"payment_processed",
user_id="usr_12345",
transaction_id="txn_67890",
amount=99.99,
currency="USD",
payment_provider="stripe",
duration_ms=1247
)
2. Log Levels Aren't Suggestions—Use Them Correctly
The Problem: Everything is logged as INFO or ERROR, making it impossible to filter signal from noise.
I once worked with a team that logged every single database query as ERROR. Their reasoning? "So we don't miss anything important." The result? They missed everything important because their error logs were 99% false positives.
The Solution: Use log levels with discipline.
| Level | When to Use | Example |
| ERROR | Something failed that requires immediate attention | Payment processing failed, database connection lost |
| WARN | Something unexpected but handled | API rate limit approaching, deprecated feature used |
| INFO | Significant business events | User registered, order completed, service started |
| DEBUG | Detailed diagnostic information | Function entry/exit, variable values, query parameters |
| TRACE | Ultra-verbose debugging | Loop iterations, every conditional branch |
The Golden Rule: If you wouldn't wake someone up at 3 AM for it, it's not an ERROR.
Real-World Example:
// Bad: Everything is an error
logger.error('User login attempt');
logger.error('Database query executed');
logger.error('Email sent');
// Good: Appropriate levels
logger.info('user_login_success', { user_id: 'usr_123', ip: '192.168.1.1' });
logger.debug('database_query', { query: 'SELECT * FROM users', duration_ms: 45 });
logger.info('email_sent', { to: 'user@example.com', template: 'welcome' });
// When things go wrong
logger.error('payment_failed', {
user_id: 'usr_123',
error_code: 'GATEWAY_TIMEOUT',
transaction_id: 'txn_456',
amount: 99.99
});
3. Context Is King: Correlation IDs and Distributed Tracing
The Problem: In microservices architectures, a single user request touches dozens of services. Without correlation, you're debugging blind.
Picture this: A user reports their checkout failed. You check the frontend logs—looks fine. Backend API logs—also fine. Payment service logs—fine. Database logs—fine. But the checkout did fail. Where?
The Solution: Trace IDs that follow requests across your entire stack.
# Generate trace ID at entry point (API Gateway/Load Balancer)
import uuid
from contextvars import ContextVar
trace_id_var = ContextVar('trace_id', default=None)
def middleware(request):
trace_id = request.headers.get('X-Trace-ID') or str(uuid.uuid4())
trace_id_var.set(trace_id)
# Add to all logs in this request context
logger = structlog.get_logger().bind(trace_id=trace_id)
# Pass to downstream services
response = call_downstream_service(
headers={'X-Trace-ID': trace_id}
)
return response
# Now every log in this request chain has the same trace_id
logger.info("order_created", order_id="ord_123", trace_id=trace_id)
What to Include in Context:
- Trace ID: Unique identifier for the entire request chain
- Span ID: Unique identifier for this specific operation
- Parent Span ID: Links to the calling operation
- User ID: Who initiated the action
- Session ID: Groups related user actions
- Request ID: Unique per HTTP request
4. Don't Log Secrets (Seriously, Stop It)
The Problem: Sensitive data in logs is a security nightmare and compliance violation.
I've seen production logs containing:
- Credit card numbers
- API keys and passwords
- Social security numbers
- Authentication tokens
- Personal health information
One company I consulted for had their entire AWS credentials exposed in CloudWatch logs. An attacker found them and racked up $80,000 in cryptocurrency mining charges in 48 hours.
The Solution: Sanitize, redact, and never log sensitive fields.
import re
SENSITIVE_PATTERNS = {
'credit_card': r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b',
'ssn': r'\b\d{3}-\d{2}-\d{4}\b',
'email': r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
'api_key': r'(api[_-]?key|apikey|api[_-]?secret)[\s:=]+[\'"]?([a-zA-Z0-9_\-]+)[\'"]?'
}
SENSITIVE_FIELDS = {
'password', 'secret', 'token', 'api_key',
'credit_card', 'ssn', 'cvv', 'pin'
}
def sanitize_log_data(data):
"""Recursively sanitize sensitive data from logs"""
if isinstance(data, dict):
return {
k: '***REDACTED***' if k.lower() in SENSITIVE_FIELDS
else sanitize_log_data(v)
for k, v in data.items()
}
elif isinstance(data, str):
for pattern_name, pattern in SENSITIVE_PATTERNS.items():
data = re.sub(pattern, f'***{pattern_name.upper()}_REDACTED***', data)
return data
elif isinstance(data, list):
return [sanitize_log_data(item) for item in data]
return data
# Usage
user_data = {
'user_id': 'usr_123',
'email': 'user@example.com',
'password': 'super_secret_123',
'credit_card': '4532-1234-5678-9010'
}
logger.info('user_registration', **sanitize_log_data(user_data))
# Output: user_id=usr_123, email=***EMAIL_REDACTED***,
# password=***REDACTED***, credit_card=***CREDIT_CARD_REDACTED***
PII Handling Checklist:
- ✅ Redact credit card numbers (show only last 4 digits)
- ✅ Hash or omit email addresses
- ✅ Never log passwords or tokens
- ✅ Mask SSN, phone numbers, addresses
- ✅ Use user IDs instead of names
- ✅ Implement automated scanning for sensitive data
- ✅ Regular audit of logs for compliance (GDPR, HIPAA, PCI-DSS)
5. Make Logs Actionable: Include What to Do Next
The Problem: Logs tell you what happened, but not why or how to fix it.
ERROR: Database connection failed
Great. Now what? Is the database down? Network issue? Wrong credentials? Connection pool exhausted?
The Solution: Logs should guide debugging and remediation.
{
"level": "ERROR",
"message": "Database connection failed",
"error_code": "DB_CONNECTION_TIMEOUT",
"error_type": "DatabaseConnectionError",
"database": "postgres-primary",
"host": "db.example.com",
"port": 5432,
"timeout_ms": 5000,
"connection_pool_size": 20,
"active_connections": 20,
"retry_attempt": 3,
"max_retries": 3,
"troubleshooting": {
"possible_causes": [
"Database server is down",
"Network connectivity issue",
"Connection pool exhausted",
"Firewall blocking connection"
],
"recommended_actions": [
"Check database server status: kubectl get pods -n database",
"Verify network connectivity: nc -zv db.example.com 5432",
"Review connection pool metrics in Grafana",
"Check recent deployments for configuration changes"
],
"runbook_url": "https://wiki.company.com/runbooks/database-connection-failures",
"alert_channel": "#database-alerts"
}
}
Actionable Logging Pattern:
class ActionableLogger:
def __init__(self, logger):
self.logger = logger
def log_error_with_context(self, error_code, message, **kwargs):
"""Log errors with troubleshooting context"""
error_context = {
'error_code': error_code,
'message': message,
**kwargs
}
# Add runbook links based on error code
runbooks = {
'DB_CONNECTION_TIMEOUT': 'https://wiki.company.com/db-timeout',
'API_RATE_LIMIT': 'https://wiki.company.com/rate-limits',
'PAYMENT_GATEWAY_ERROR': 'https://wiki.company.com/payment-issues'
}
if error_code in runbooks:
error_context['runbook_url'] = runbooks[error_code]
# Add suggested actions
actions = self._get_suggested_actions(error_code)
if actions:
error_context['suggested_actions'] = actions
self.logger.error(**error_context)
def _get_suggested_actions(self, error_code):
"""Return troubleshooting steps for common errors"""
actions_map = {
'DB_CONNECTION_TIMEOUT': [
'Check database server health',
'Verify connection pool configuration',
'Review recent schema changes'
],
'API_RATE_LIMIT': [
'Implement exponential backoff',
'Check if rate limit increase is needed',
'Review API usage patterns'
]
}
return actions_map.get(error_code, [])
# Usage
actionable_logger = ActionableLogger(logger)
actionable_logger.log_error_with_context(
error_code='DB_CONNECTION_TIMEOUT',
message='Failed to connect to primary database',
database='postgres-primary',
timeout_ms=5000,
retry_attempt=3
)
The Logging Maturity Model: Where Are You?
| Level | Characteristics | Impact |
| Level 0: Chaos | Inconsistent logging, mostly print statements, no structure | Debugging takes hours, frequent outages |
| Level 1: Basic | Consistent log levels, some structure, basic timestamps | Can find issues eventually, slow debugging |
| Level 2: Structured | JSON logs, correlation IDs, proper levels | Efficient debugging, good observability |
| Level 3: Contextual | Rich context, distributed tracing, actionable errors | Proactive issue detection, fast resolution |
| Level 4: Intelligent | Automated analysis, anomaly detection, predictive alerts | Issues prevented before users notice |
Most companies are stuck at Level 1. Getting to Level 2 solves 80% of debugging pain.
Quick Wins: Implement These Today
You don't need to overhaul your entire logging infrastructure overnight. Start with these high-impact changes:
Week 1: Structure Your Logs
- ✅ Switch to JSON logging format
- ✅ Add consistent timestamp format (ISO 8601)
- ✅ Include service name in every log
Week 2: Add Correlation
- ✅ Implement trace IDs for request tracking
- ✅ Pass trace IDs to all downstream services
- ✅ Add user_id to all user-initiated actions
Week 3: Clean Up Log Levels
- ✅ Audit current ERROR logs (are they really errors?)
- ✅ Move informational messages to INFO
- ✅ Reserve ERROR for actual failures
Week 4: Security Audit
- ✅ Scan logs for sensitive data
- ✅ Implement automatic PII redaction
- ✅ Document what should never be logged
Real-World Impact: The Numbers
After implementing these practices at my last company:
- Mean Time to Resolution (MTTR): Dropped from 47 minutes to 8 minutes
- False Positive Alerts: Reduced by 73%
- Log Storage Costs: Decreased by 40% (less noise = less data)
- Debugging Efficiency: Engineers spent 60% less time searching logs
- Compliance Violations: Zero PII leaks in production logs
The best part? We implemented these changes incrementally over 6 weeks without any downtime.
Common Logging Anti-Patterns to Avoid
❌ Logging in Loops Without Sampling
# Bad: Generates millions of logs
for item in large_dataset:
logger.info(f"Processing item {item}")
# Good: Sample or aggregate
logger.info(f"Processing batch", batch_size=len(large_dataset))
❌ Logging Entire Request/Response Bodies
# Bad: Logs potentially huge payloads
logger.info(f"API Response: {response.json()}")
# Good: Log summary
logger.info("api_response",
status_code=response.status_code,
response_size=len(response.content),
duration_ms=response.elapsed.total_seconds() * 1000
)
❌ Using String Concatenation
# Bad: Harder to query, poor performance
logger.info("User " + user_id + " completed order " + order_id)
# Good: Structured fields
logger.info("order_completed", user_id=user_id, order_id=order_id)
❌ Logging Without Context
# Bad: What failed? Why?
logger.error("Operation failed")
# Good: Rich context
logger.error("payment_processing_failed",
user_id=user_id,
transaction_id=txn_id,
error_code="GATEWAY_TIMEOUT",
payment_provider="stripe",
amount=99.99
)
Frequently Asked Questions
Q: Should I use a logging library or build my own?
A: Always use a battle-tested logging library. Don't reinvent the wheel.
For Python, use structlog or the built-in logging module with JSON formatters. For Node.js, use winston or pino. For Java, use Logback with logstash-logback-encoder. For Go, use zap or zerolog.
These libraries handle thread safety, performance optimization, and formatting edge cases that you'll inevitably get wrong if you build your own. I've seen teams waste months building custom logging solutions that performed worse and had more bugs than established libraries.
The only exception: if you have truly unique requirements that no existing library addresses. But be honest—you probably don't.
Q: How much logging is too much?
A: If your logs cost more than your compute, you're logging too much.
A good rule of thumb: **log at decision points, not execution