Skip to content
intermediate Phase 13 · Python Debugging & Profiling

Logging & Structured Logs

Implement logging with the logging module, structured logs, and log levels.

1h
0 problems
Topic Progress 0%

Levels, Loggers, and Propagation

Levels, Loggers, and Propagation

The logging module uses a numeric severity scale — DEBUG 10, INFO 20, WARNING 30, ERROR 40, CRITICAL 50 — and a dot-separated logger hierarchy: app.api is a child of app, which is a child of the root logger.

import logging
log = logging.getLogger(__name__)   # "package.module", not the root

def handle(req):
    log.debug("payload: %s", req)   # %s stays lazy — no work if DEBUG is off
    if req.get("bad"):
        log.error("rejected %s", req["id"])

A logger with NOTSET level inherits from its nearest ancestor. Messages also propagate up to ancestor handlers unless propagate = False. Effective level comes from the root logger by default, which is WARNING until you configure it.

Handlers, Formatters, and Rotation

Handlers, Formatters, and Rotation

Loggers decide whether to record; handlers decide where it goes:

import logging
from logging.handlers import RotatingFileHandler

root = logging.getLogger()
root.setLevel(logging.INFO)

fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")

console = logging.StreamHandler()
console.setFormatter(fmt)

fileh = RotatingFileHandler("app.log", maxBytes=5_000_000, backupCount=3,
                            encoding="utf-8")
fileh.setFormatter(fmt)
root.addHandler(fileh)

maxBytes + backupCount bound disk usage: the active file rolls to app.log.1, and the oldest backup is deleted. Libraries should only call logging.getLogger(...) and never install handlers — configuration belongs to the application entry point via basicConfig or dictConfig.

Structured Logs and Common Mistakes

Structured Logs and Common Mistakes

Human lines are fine for local dev; production wants machine-parseable events. Emit one JSON object per record with python-json-logger or structlog, adding context fields through extra=:

log.info("checkout.done", extra={"order_id": oid, "ms": elapsed})

Common failure modes:

  • print() bypasses levels, formatting, and streams — it disappears when stdout is redirected
  • logging.basicConfig() is a no-op if handlers already exist, so "my INFO lines vanished" often means the root logger already had a WARNING handler
  • f-strings in log calls format eagerly even when the level is disabled — prefer %s args
  • Missing encoding="utf-8" on FileHandler mangles non-ASCII under some locales

Quiz

1. What does logger = logging.getLogger("app.db") inherit when its own level is NOTSET?

Question 1 options

2. Why should libraries avoid calling logging.basicConfig()?

Question 2 options

3. Why prefer log.debug("val: %s", x) over log.debug(f"val: {x}")?

Question 3 options

Flashcards

Question

Effective level

Answer

A logger’s own level, or the nearest ancestor’s if NOTSET — root defaults to WARNING.

Question

Propagation

Answer

Records bubble to ancestor loggers’ handlers; disable with propagate=False.

Question

RotatingFileHandler

Answer

Rolls the file at maxBytes, keeping backupCount generations to cap disk use.

Question

Structured logging

Answer

One JSON object per event with stable fields (order_id, ms) for querying in production.

Revision Notes

Key Takeaways

  • 1. getLogger(__name__) per module; applications configure handlers once at entry point
  • 2. Levels control filtering, handlers control destinations, propagation shares records up the tree
  • 3. Use %-style lazy args and JSON output for anything shipped to production

Interview Tips

  • • Explain the logger → handler → formatter pipeline and where each configuration decision belongs
  • • Describe how you would correlate a request across services with structured fields

Cheat Sheet

levels 10/20/30/40/50; getLogger(name); %s lazy args; RotatingFileHandler(maxBytes, backupCount); JSON via extra/structlog; root default WARNING.

Continue Learning