Logging

Why Not Just Print?

Log Levels

Level When to use
DEBUG Detailed diagnostic information useful during development
INFO Confirmation that things are working as expected
WARNING Something unexpected happened, but the program continues
ERROR A serious problem; the program could not do something
CRITICAL The program cannot continue

Python's logging Module

import logging

logging.basicConfig(level=logging.DEBUG)

logging.debug("starting up")
logging.info("processing file: birds.csv")
logging.warning("species column has 3 missing values")
logging.error("cannot connect to database: connection refused")
DEBUG:root:starting up
INFO:root:processing file: birds.csv
WARNING:root:species column has 3 missing values
ERROR:root:cannot connect to database: connection refused

Loggers, Handlers, and Formatters

import logging

logger = logging.getLogger("birdcount")
logger.setLevel(logging.DEBUG)

handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
    "%(asctime)s %(name)s %(levelname)s %(message)s"
))
logger.addHandler(handler)

logger.info("loaded %d records", 4821)
logger.warning("skipped %d records with missing species", 3)
2026-03-29 14:02:11,304 birdcount INFO loaded 4821 records
2026-03-29 14:02:11,305 birdcount WARNING skipped 3 records with missing species

Rotating Log Files

import logging
from logging.handlers import RotatingFileHandler

# MAX_BYTES: rotate after 1 MiB; keep 3 old files
MAX_BYTES = 1_048_576
BACKUP_COUNT = 3

handler = RotatingFileHandler("app.log", maxBytes=MAX_BYTES, backupCount=BACKUP_COUNT)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))

logger = logging.getLogger("birdcount")
logger.setLevel(logging.INFO)
logger.addHandler(handler)

Structured (JSON) Logging

import logging
from pythonjsonlogger import jsonlogger

handler = logging.StreamHandler()
handler.setFormatter(jsonlogger.JsonFormatter(
    "%(asctime)s %(name)s %(levelname)s %(message)s"
))

logger = logging.getLogger("birdcount")
logger.setLevel(logging.INFO)
logger.addHandler(handler)

logger.info("loaded records", extra={"count": 4821, "filename": "birds.csv"})
{"asctime": "2026-03-29 14:02:11,304", "name": "birdcount", "levelname": "INFO", "message": "loaded records", "count": 4821, "filename": "birds.csv"}
cat app.log | jq 'select(.levelname == "ERROR")'

Where Logs Come From

journalctl -u birdcount.service --since "1 hour ago"

What Not to Log

import logging

logger = logging.getLogger("birdcount")

def authenticate(username, password):
    # safe: log the username but never the password
    logger.info("authentication attempt for user: %s", username)
    # ... authentication logic ...

Exercise: Adding Logging

  1. Take the bird server from the HTTP chapter and replace all print statements with appropriate logging calls. Use INFO for normal requests and ERROR for failed ones.

  2. Add a RotatingFileHandler so that the server logs to birdserver.log and rotates after 512 KiB with two backup files.

  3. Use jq to extract all requests that returned a 404 status code from the log.