Logging standards for clean, parseable logs

Victor Ferraz
October 6, 2025

Great logs make debugging faster, on-call calmer, and audits painless. We standardized how we log across projects so messages are consistent, traceable, machine-friendly, and safe. Use this playbook during implementation and PR reviews.

1) Timestamps: always ISO-8601

All log timestamps are ISO-8601. This is enforced by our logging configuration, so no manual formatting is needed.

Keeps time unambiguous and sortable, so logs line up correctly across services and tools.

2) Make logs traceable with correlation IDs

Every related message should carry a correlation or trace ID so you can follow a request or job end-to-end. Our config attaches this automatically. For app-level support, use a request or async scope correlation ID. Recommended library for Django projects: django-guid.

If a code path spans multiple services or tasks, check that the correlation ID flows through all log statements.

3) Name your logger instances clearly

Use unique, descriptive logger names that reflect the module or script emitting messages.

  • In normal modules:
LOGGER = logging.getLogger(__name__)
  • Define as a module-level “global”. Treat the variable like a constant.

  • In support or one-off scripts, use an explicit, readable name:
from support.helpers.logs import get_logger
LOGGER = get_logger("data_export_monthly")
  • The helper handles date-stamped log files.

4) Log format: machine-parseable (JSON-like)

Write messages in a structure that machines can parse and humans can scan.

Pattern: a short action sentence plus a single structured payload object.

LOGGER.info(
  "Docket status updated. %(payload)s",
  {"payload": {
      "docket_id": str(docket.id),
      "status_id": str(status.id),
      "firm_id": str(firm.id),
      "update_date": datetime_instance.isoformat(),
  }},
)

Notes:

  • Use str() for UUIDs and .isoformat() for datetimes.

  • Keep the payload flat and typed. Do not log raw objects (like a document or a class instance).

5) Message consistency and context

Each log line should be unique in text and context across the project. Prefer clear, action-oriented phrasing:

  • ✅ “Official deadline dates calculated.” with a single, meaningful payload

  • ❌ “Calculating…” then “Calculated.” two lines with little value

6) Choose the right level

  • DEBUG - log at this level about anything that happens in the program. This is mostly used during debugging, and I’d advocate trimming down the number of debug statements before entering the production stage, so that only the most meaningful entries are left, and can be activated during troubleshooting.
  • INFO - log at this level all actions that are user-driven, or system-specific, and all notable events that are not considered an error;
  • WARNING - log at this level all events that could potentially become an error. For instance, if one database call took more than a predefined time, or if an in-memory cache is near capacity. This will allow proper automated alerting, and during troubleshooting, will allow us to better understand how the system was behaving before the failure.
  • ERROR - log every error condition at this level. That can be API calls that return errors or internal error conditions.
  • CRITICAL - too bad, it’s doomsday. Use this very scarcely; this shouldn’t happen a lot in a real program. Usually, logging at this level signifies the end of the program. For instance, if a network daemon can’t bind a network socket, logging at this level and exiting is the only sensible thing to do.

7) Avoid excessive or redundant logging

Prefer one clear, contextual line over many trivial ones. If a line does not help you debug or alert, delete it.

8) Never log personal or identifiable data

Strict prohibition. Do not log names, emails, addresses, or anything that can identify a user. Always log by IDs. If you need to trace who or what, include internal IDs in the payload.

Example: “good” vs “bad”

Good

LOGGER.info(
  "Payment captured. %(payload)s",
  {"payload": {
      "payment_id": str(payment.id),
      "order_id": str(order.id),
      "amount_cents": payment.amount_cents,
      "captured_at": timezone.now().isoformat(),
  }},
)

Bad

# Leaks PII, unstructured, hard to trace or search
LOGGER.info("Payment for John Smith captured!")

PR review checklist

  • Timestamp is ISO-8601 using config

  • Correlation ID is present along the request or job path

  • LOGGER is module level and uniquely named for scripts

  • Messages follow “action + payload” and payload is JSON-like, with strings for UUIDs and .isoformat() for datetimes

  • Messages are unique, concise, and useful

  • Levels are being correctly used (DEBUG, INFO, WARNING, ERROR, …)

  • No personal or identifiable data, IDs only

Quick starter for Django

import logging
from uuid import uuid4
from django.utils import timezone

LOGGER = logging.getLogger(__name__)  # module-level constant

def run():
    batch_id = str(uuid4())
    LOGGER.info("Batch started. %(payload)s", {"payload": {
        "batch_id": batch_id,
        "started_at": timezone.now().isoformat(),
    }})
    # ...work...
    LOGGER.info("Batch finished. %(payload)s", {"payload": {
        "batch_id": batch_id,
        "duration_ms": 842,
        "result": "ok",
    }})

For correlation IDs in web requests, add middleware such as django-guid to attach a request-scoped ID and include it in all emitted records.

Use this setup to trace a run with one batch_id, keep lines concise and consistent, and avoid PII while staying easy to parse across services.