{ } Programming

Logging in code: levels, context and what never to record

How to write useful logs in your code: severity levels, messages with context, structured JSON logs and data never to record, with Python examples.

When a program runs on your computer you can watch it, stop it and inspect it with the debugger. When it runs on a server, at three in the morning, serving hundreds of users, the only window onto what is happening is the logs. A good logging system turns an hours-long investigation into a search of minutes; a bad one produces gigabytes of text in which the useful information cannot be found.

Logs and prints are not the same thing

Many people start with print() calls scattered through the code. It works for an experiment, but in a real application its limits show immediately: you cannot filter by importance, it does not say when or where the message was written, and it cannot be routed to a file or a central service without changing the code. Logging libraries, such as the logging module in Python's standard library, solve these problems by separating what is recorded from where and how it is recorded.

import logging

logger = logging.getLogger(__name__)

def import_customers(path):
    logger.info("Starting import from %s", path)
    ...

Using getLogger(__name__) gives each module a logger with its own name, so the logs immediately show which part of the code a message comes from.

Severity levels

Every message has a level. Using them consistently is what makes logs filterable:

Level When to use it Example
DEBUG Details useful only during development or an investigation Intermediate values of a calculation
INFO Normal, meaningful events Service started, import completed
WARNING Something unexpected, but the program continues Retry after a timeout
ERROR An operation failed Unable to save an order
CRITICAL The system cannot keep running Database unreachable at startup

In production you usually record from INFO upwards; DEBUG is switched on temporarily when needed. A frequent mistake is logging everything as ERROR: after a few days nobody looks at errors any more, because they are noise.

Messages with context

A message like "Error while saving" is almost useless. Saving what? For whom? A good log message answers the questions you will ask yourself when you read it:

logger.error(
    "Order save failed: order=%s customer=%s amount=%.2f",
    order.id, order.customer_id, order.total,
)

Include the identifiers that let you link the message to the data: order ID, user ID, request ID. In a web application it is very useful to assign each request a unique identifier and include it in every message about it: that way you can reconstruct the entire path of a single request among tens of thousands of lines.

Note the form too: with the logging module it is preferable to pass parameters separately instead of building the string in advance. Formatting only happens if the message is actually recorded.

Logging exceptions

Inside an except block, logger.exception() records the message at ERROR level and automatically adds the full traceback:

try:
    send_invoice(order)
except SendError:
    logger.exception("Invoice sending failed for order %s", order.id)
    raise

Beware of a harmful habit: logging the same exception at every level as it propagates. The result is the same error repeated five times, which confuses the reader. Log the error once, at the point where it is handled.

Structured logs

Free-text logs are convenient to read by eye but hard to analyse with automated tools. Structured logs record each event as a set of fields, usually in JSON format:

{"ts": "...", "level": "ERROR", "event": "order_save_failed",
 "order": 4821, "customer": 77, "duration_ms": 1530}

With a format like this, a log collection system can filter all events for one customer, count errors by type or compute average times. There are dedicated libraries, such as structlog for Python, but the standard module can also produce JSON with a custom formatter.

What never to record

Logs get copied, retained, sent to external services and read by many people. That is why some information must never end up in them:

  • passwords, access tokens, API keys;
  • full payment card numbers and security codes;
  • the full content of private documents or messages;
  • personal data not needed for the investigation, also to respect the data minimisation principle set out in the GDPR.

A typical case is logging the entire body of an HTTP request for convenience: along with the useful data, credentials end up in the logs too. It is better to record only the fields you need, or mask the sensitive ones.

Configuration separate from code

The code should only emit messages; where they go, in what format and from what level is a configuration choice. In development you may want coloured terminal output at DEBUG level, in production JSON at INFO level sent to a central system. Keeping this configuration in a single place, at application startup, avoids having to edit dozens of files.

In short

  • Use a logging library instead of print, with one logger per module.
  • Apply levels consistently: not everything is an error.
  • Write messages with context and identifiers, such as the request or order ID.
  • Log exceptions only once, with the traceback, and consider structured JSON logs.
  • Never record passwords, tokens, payment data or unnecessary personal data.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. With degrees in computer science and psychology, he combines technical analysis with an understanding of the human factor.

Read also