Last updated
October 6, 2025
3 min read

Logging standards for clean, parseable logs

Victor Ferraz
Fullstack Developer
Summarize with ChatGPT
Table of Contents

    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.

    Table of Contents
      Is your project stuck on a tough build?
      We turn complexity into a plan
      Talk to us