Logging Debugging

When your codebase is tiny, print() and random try/except blocks "work". Once you have APIs, background workers, cron jobs, queues, and multiple servers, that approach falls apart. Learn professional-grade debugging + logging + error-handling systems used by real engineering teams.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

This lesson takes you from basic debugging → production-grade logging systems used by companies handling thousands of users, multiple environments, and distributed services.

🔥 1. Why Logging Beats print() in Real Systems

When you have thousands of users, multiple environments (dev/staging/prod), background jobs, schedulers, and queues — print() falls apart.

Rule: print() is only for quick experiments. Real services use logging everywhere.

⚙️ 2. Basic Logging Setup (Per-Module Loggers)

You should never use the root logger directly in big projects. Instead, use one logger per module:

Then in your entry point (e.g. main.py or app.py):

This gives you timestamp, log level, logger name (payments.service, users.api), and message. You can raise log level in dev: DEBUG, staging: INFO, prod: WARNING.

🧱 3. Logging Levels: A Contract for Your Team

Use logging levels consistently across your team:

When everyone uses the same level rules, you can alert only on ERROR/CRITICAL, filter out noisy DEBUG in production, and quickly see timeline of events with INFO logs.

🧩 4. Structured Logging (So Logs Are Actually Searchable)

For small scripts, string messages are fine. At scale, you need structured logs so tools like Datadog/Loki/ELK/CloudWatch can filter and aggregate.

Now your logging backend can filter by user_id, count logins from each IP, and group by event.

🧲 5. Handlers: Sending Logs to Multiple Destinations

A handler decides where logs go. Common ones:

Now dev uses console, ops can inspect app.log, and files don't grow forever. In a SaaS/microservice setup, you'd usually log to stdout and let Docker/Kubernetes send logs to a central system.

🧠 6. Error Handling Strategy (Don't Just "try/except Everything")

Good pattern: Catch specific exceptions, log with traceback, decide whether to handle or re-raise

Key ideas: Use domain-level exceptions (PaymentError), wrap low-level exceptions, pass exc_info=True to capture traceback, use raise ... from e to keep the error chain.

🧨 7. Logging Tracebacks Correctly

This logs message, full traceback, and error type.

Both approaches are valid. logger.exception() is just a shortcut for logger.error(..., exc_info=True) inside an except block.

🪤 8. Global Exception Hooks (Last-Resort Safety Net)

For CLI/worker processes, you can register a global handler:

Now any uncaught error in the main thread is logged at CRITICAL. You still let the process crash (which is often correct in prod). In frameworks: Django has middleware for logging unhandled errors, FastAPI lets you add global exception handlers, Celery workers log errors from tasks.

🧪 9. Debugging Strategy: Logs + Debugger + Assertions

✅ Use assertions in dev: assert total >= 0, "Total should never be negative"

In production, you can turn off certain assertions or keep only the critical ones.

🎯 10. Principles for "At Scale" Error Handling

Always log unexpected exceptions. Prefer fail loud and fast to silent corruption.

HTTP layer (FastAPI/Django view), Message queue consumer, CLI/worker entry point

DomainError → ValidationError, PaymentError, ExternalServiceError. This makes handling & logging more deliberate.

4. Keep logs human-readable AND machine-parseable

Too much DEBUG in prod = noisy. Too few logs in prod = blind.

Part 1 covered local logging and error handling. Now we move into real production engineering — the systems used by FastAPI/Django backends, microservices, SaaS platforms, and distributed workers.

🔥 1. Centralised Log Aggregation (The Real World Standard)

As soon as you have multiple servers, background workers, containers, or functions-as-a-service — you cannot inspect logs locally anymore. Real systems send logs to a central place.

✔ ELK Stack (Elasticsearch + Logstash + Kibana) — Most customizable, open-source, works at huge scale

✔ Loki + Grafana — Insanely fast, cheap, streams logs from Docker/Kubernetes

✔ AWS CloudWatch / GCP Logging / Azure Monitor — Great if you already host on those platforms

✔ Datadog / Sentry / NewRelic — Expensive, but world-class dashboards + alerts

⚙️ 2. Logging to STDOUT in Containers (Best Practice)

In Docker/Kubernetes, you never write log files inside the container. You output logs to STDOUT:

Kubernetes will automatically: ✔ capture your stdout, ✔ send it to your log system, ✔ attach metadata (pod, namespace, service). This makes logs fully searchable across the entire cluster.

🧠 3. The Importance of Request IDs / Correlation IDs

Imagine debugging: User logs in → Their request hits API A → API A calls API B → API B queries the database → A background worker processes a message → Something fails.

Without correlation IDs, logs look like a mess. Solution: Generate a unique ID per user request.

Pass the ID through HTTP headers, background jobs, microservice calls, and log contexts. Now you can filter your log system for request_id = "1f0cd133-f9ab-4bd9-a6cd-92d3a002a415" and see EVERYTHING that happened.

This alone can save hours per week in debugging.

🧩 4. Logging Context Automatically (ContextVars)

Python provides a way to attach values (like request IDs) to all logs inside async or threaded code.

Now every log line automatically includes request_id, user_id (if added), job_id, trace_id. This is essential in async web apps like FastAPI.

🚀 5. Distributed Tracing (How Big Systems Debug)

Used by Uber, Netflix, Stripe, Google-scale systems. Distributed tracing tracks: "This request flowed through: API → Worker → DB → Cache → Queue → Another Worker"

Tools: OpenTelemetry (industry standard), Jaeger, Zipkin, Datadog APM

Now logs + traces appear linked. This gives timings for each step, slow points, bottlenecks, failed spans, and dependency maps. This is how modern SaaS teams debug performance problems.

🧵 6. Logging in Async Python (Trickier Than It Looks)

Async apps (FastAPI, aiohttp) handle hundreds/thousands of concurrent tasks. Debugging them needs extra care.

Problem 1: Interleaved logs — Two tasks printing logs at the same time → scrambled output

Practice quiz

Why does the lesson say logging beats print() in real systems?

  • print() is being removed from Python
  • print() is slower than logging
  • logging supports severity levels, multiple destinations, filtering, and context
  • logging is the only way to show output

Answer: logging supports severity levels, multiple destinations, filtering, and context. logging adds levels, multiple handlers, per-module filtering, and structured context — print() does none of that.

What is the recommended way to create a logger in each module?

  • logging.getLogger(__name__)
  • logging.getLogger()
  • print
  • logging.root

Answer: logging.getLogger(__name__). Use logging.getLogger(__name__) so each module has its own named logger instead of the root logger.

Which log level order is correct, lowest to highest severity?

  • INFO, DEBUG, WARNING, ERROR, CRITICAL
  • DEBUG, WARNING, INFO, CRITICAL, ERROR
  • ERROR, CRITICAL, WARNING, INFO, DEBUG
  • DEBUG, INFO, WARNING, ERROR, CRITICAL

Answer: DEBUG, INFO, WARNING, ERROR, CRITICAL. The levels run DEBUG to CRITICAL: DEBUG, INFO, WARNING, ERROR, CRITICAL.

When should you use the CRITICAL level?

  • For internal variable details
  • When the system is not usable (e.g. database unreachable)
  • For normal events like a user registering
  • For slow but recoverable queries

Answer: When the system is not usable (e.g. database unreachable). CRITICAL is for when the system is unusable — DB unreachable, config missing.

What is the benefit of structured logging (extra={...} or dict messages)?

  • Log systems can filter and aggregate by fields like user_id and event
  • It makes logs shorter
  • It removes timestamps
  • It disables log levels

Answer: Log systems can filter and aggregate by fields like user_id and event. Structured logs let tools like Datadog/Loki/ELK filter, count, and group by fields.

Which handler rotates log files when they hit a size limit?

  • StreamHandler
  • FileHandler
  • RotatingFileHandler
  • SMTPHandler

Answer: RotatingFileHandler. RotatingFileHandler rotates at a size limit, preventing huge files in production.

What does logger.exception("msg") do that logger.error("msg") does not by default?

  • It runs faster
  • It logs the message with the full traceback (inside an except block)
  • It suppresses the error
  • It changes the log level to DEBUG

Answer: It logs the message with the full traceback (inside an except block). logger.exception() is a shortcut for logger.error(..., exc_info=True), logging the full traceback.

Why is bare 'except Exception: logger.error(...)' considered a bad pattern?

  • It is too slow
  • It only works in Python 2
  • It logs too much detail
  • It swallows context, hides the traceback, and can cause silent failures

Answer: It swallows context, hides the traceback, and can cause silent failures. Catching everything blindly hides what went wrong and the original traceback, leading to silent failures.

What is the purpose of a correlation ID (request ID)?

  • To encrypt log messages
  • To trace one request across APIs, workers, and services in the logs
  • To compress logs
  • To replace timestamps

Answer: To trace one request across APIs, workers, and services in the logs. A unique ID per request lets you filter the log system and see everything that request touched.

In containers (Docker/Kubernetes), where should you send logs?

  • To a file inside the container
  • To an email
  • To STDOUT, letting the platform capture and route them
  • To a local SQLite database

Answer: To STDOUT, letting the platform capture and route them. In containers you log to STDOUT and let Kubernetes/Docker capture and forward to a central system.