logging
best practices
logging strategies
developer tips
software development

Logging best practices

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Effective logging gives you visibility into application behavior without requiring a debugger or manual reproduction. Poor logging either floods storage with noise or provides no useful information when incidents occur. The goal is structured, leveled, contextual log output that helps you diagnose problems quickly in production.

Use Log Levels Consistently

Log levels categorize message importance. Define team conventions for what each level means and enforce them in code review.

LevelWhen to useExample
DEBUGInternal state useful during development"Cache miss for key=user:42"
INFONormal operational events"Server started on port 8080"
WARNINGUnexpected but recoverable situations"Retry attempt 2/3 for payment API"
ERRORFailed operations that need investigation"Database connection refused after 3 retries"
CRITICALSystem-wide failures requiring immediate action"Out of disk space on /data volume"
python
1import logging
2
3logger = logging.getLogger(__name__)
4
5logger.debug("Processing batch of %d items", len(items))
6logger.info("User %s logged in from %s", user_id, ip_address)
7logger.warning("API response slow: %dms", response_time)
8logger.error("Payment failed for order %s: %s", order_id, error)

Use Structured Logging

Plain text logs are hard to search and aggregate. Structured logging (JSON format) makes logs machine-parseable for tools like ELK, Datadog, and CloudWatch.

python
1import json
2import logging
3
4class JsonFormatter(logging.Formatter):
5    def format(self, record):
6        return json.dumps({
7            "timestamp": self.formatTime(record),
8            "level": record.levelname,
9            "logger": record.name,
10            "message": record.getMessage(),
11            "module": record.module,
12            "line": record.lineno,
13        })
14
15handler = logging.StreamHandler()
16handler.setFormatter(JsonFormatter())
17logger = logging.getLogger("app")
18logger.addHandler(handler)
19logger.setLevel(logging.INFO)
20
21logger.info("Order processed", extra={"order_id": "ORD-123", "amount": 49.99})

In Go, use slog (standard library since Go 1.21):

go
1import "log/slog"
2
3logger := slog.Default()
4logger.Info("order processed",
5    slog.String("order_id", "ORD-123"),
6    slog.Float64("amount", 49.99),
7)

Include Context in Every Log Line

A log message without context is useless during incident response. Always include identifiers that let you trace the event.

Essential context fields:

  • Request ID or trace ID — correlate logs across services
  • User or tenant ID — identify who is affected
  • Operation name — what the code was trying to do
  • Duration — how long the operation took
python
1logger.info(
2    "Request completed",
3    extra={
4        "request_id": request_id,
5        "user_id": user.id,
6        "method": "POST",
7        "path": "/api/orders",
8        "status": 201,
9        "duration_ms": elapsed,
10    }
11)

Do Not Log Sensitive Data

Never log passwords, API keys, tokens, credit card numbers, or personal health information. Sanitize or redact before logging.

python
1def safe_log_request(request):
2    headers = dict(request.headers)
3    if "Authorization" in headers:
4        headers["Authorization"] = "[REDACTED]"
5    logger.info("Incoming request", extra={"headers": headers})

Set Appropriate Log Retention and Rotation

Logs consume disk space. Configure rotation to prevent disk exhaustion.

python
1from logging.handlers import RotatingFileHandler
2
3handler = RotatingFileHandler(
4    "app.log",
5    maxBytes=10 * 1024 * 1024,  # 10 MB per file
6    backupCount=5,               # Keep 5 rotated files
7)
8logger.addHandler(handler)

For Kubernetes, log to stdout and let the container runtime handle rotation. Use a centralized log aggregator (ELK, Loki, Datadog) for search and retention policies.

Avoid Common Anti-Patterns

python
1# BAD: String formatting before level check (wasted CPU)
2logger.debug("Result: " + json.dumps(large_object))
3
4# GOOD: Lazy formatting — only evaluated if DEBUG is enabled
5logger.debug("Result: %s", large_object)
6
7# BAD: Catching and logging without context
8try:
9    process()
10except Exception:
11    logger.error("Something went wrong")  # Useless
12
13# GOOD: Include exception info
14try:
15    process()
16except Exception:
17    logger.exception("Failed to process order %s", order_id)

Common Pitfalls

  • Logging at DEBUG level in production and flooding storage with gigabytes of noise per hour.
  • Using print() instead of a logging framework, losing level filtering, rotation, and structured output.
  • Logging inside tight loops without rate limiting, which can degrade application performance.
  • Including sensitive data (passwords, tokens, PII) in log messages that end up in shared log aggregators.
  • Not including correlation IDs, making it impossible to trace a request across multiple services.

Summary

  • Use consistent log levels with team-agreed definitions for each level.
  • Use structured (JSON) logging for machine-parseable output.
  • Include request IDs, user IDs, and operation context in every log line.
  • Never log sensitive data — redact tokens, passwords, and PII.
  • Configure log rotation and retention to prevent disk exhaustion.
  • Use lazy formatting (logger.info("msg %s", val)) to avoid unnecessary string construction.

Course illustration
Course illustration

All Rights Reserved.