Skip to main content

Command Palette

Search for a command to run...

Why Your Logs Are Useless: 5 Logging Best Practices

Learn: Why Your Logs Are Useless: 5 Logging Best Practices

Updated
10 min readView as Markdown
T

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:

FieldPurposeExample
timestampWhen it happened (ISO 8601)2024-01-15T03:15:24Z
levelSeverity (ERROR, WARN, INFO, DEBUG)ERROR
serviceWhich microservicepayment-gateway
trace_idDistributed tracing correlationa1b2c3d4-e5f6-7890
user_idWho was affectedusr_12345
actionWhat was being attemptedprocess_payment
duration_msPerformance tracking1247
error_codeMachine-readable errorCARD_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.

LevelWhen to UseExample
ERRORSomething failed that requires immediate attentionPayment processing failed, database connection lost
WARNSomething unexpected but handledAPI rate limit approaching, deprecated feature used
INFOSignificant business eventsUser registered, order completed, service started
DEBUGDetailed diagnostic informationFunction entry/exit, variable values, query parameters
TRACEUltra-verbose debuggingLoop 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?

LevelCharacteristicsImpact
Level 0: ChaosInconsistent logging, mostly print statements, no structureDebugging takes hours, frequent outages
Level 1: BasicConsistent log levels, some structure, basic timestampsCan find issues eventually, slow debugging
Level 2: StructuredJSON logs, correlation IDs, proper levelsEfficient debugging, good observability
Level 3: ContextualRich context, distributed tracing, actionable errorsProactive issue detection, fast resolution
Level 4: IntelligentAutomated analysis, anomaly detection, predictive alertsIssues 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