Files
prompts/docs/skills/python-logging/SKILL.md
T
2026-07-08 21:03:38 -05:00

9.4 KiB

name, description, x-personal-mcp
name description x-personal-mcp
python-logging Design, review, or refactor Python logging. Use when choosing logger names, levels, handlers, library/application boundaries, basicConfig, dictConfig, structured logs, or operational logging defaults.
id version tags capabilities
python-logging 1.0.0
logging
python
observability
resource://skills/python-logging/document

Python Logging

Use this skill to produce idiomatic Python logging guidance or a small logging setup for an application, library, CLI, worker, or web service.

Load references only when needed:

Python logging references
Python logging overview, library guidance, handlers, and dictConfig schema
JSON file logging pattern
Queue-backed rotating JSON file pattern for local machine-readable logs
Network logging minimal example
Minimal network logging example with a receiver and queue-backed client
HTTPX logging handler example
HTTP JSON logging example with httpx and a queue-backed client

When to Use

  • A project mixes print, root logger calls, scattered basicConfig, or ad hoc handlers.
  • You need to choose logging levels, destinations, formatter fields, or logger names.
  • You need a clear boundary between library logging and application logging configuration.
  • You need a centralized logging setup, including a logging.config.dictConfig section.
  • You are tuning framework or third-party loggers such as uvicorn, sqlalchemy, or HTTP clients.

Inputs To Collect

  1. Runtime type: script, library, CLI, web app, worker, service, or notebook.
  2. Audience: humans in a terminal, operators in files, machines in JSON, or test assertions.
  3. Destinations: stdout/stderr, file, rotating file, queue, syslog, external collector, or none for libraries.
  4. Default level and verbosity controls: INFO, DEBUG, CLI flag, environment variable, or config file.
  5. Operational constraints: async event loop, multiprocessing, container logs, sensitive data, or high-volume paths.

If missing, assume:

  • application code, not a reusable library
  • stdout console logging
  • human-readable formatter
  • root level INFO
  • no file logging unless requested

Procedure

  1. Classify the project boundary first: application code configures logging; library code emits logs and avoids configuring handlers.
  2. In modules, create loggers with logger = logging.getLogger(__name__) so logger names follow the package hierarchy.
  3. Use level semantics consistently: DEBUG for diagnosis, INFO for normal milestones, WARNING for notable recoverable conditions, ERROR for failed operations, and CRITICAL for process-threatening failures.
  4. Prefer parameterized logging calls such as logger.info("Processed %s items", count) so message formatting is deferred until the record is emitted.
  5. Configure handlers and formatters once during application startup.
  6. Keep third-party logger overrides explicit and narrow. Tune noisy loggers by name instead of muting broad logger hierarchies.
  7. Smoke-check output at expected levels and destinations, including one suppressed DEBUG message and one exception path if errors are logged.

Best Practices

  • Do not name a module logging.py; it shadows the standard library package.
  • Do not call basicConfig or attach handlers in every module.
  • Do not log to the root logger from libraries. Use named loggers and, only if needed, attach logging.NullHandler() to the library's top-level logger.
  • Do not create loggers per request, user, file, or connection. Use contextual fields, adapters, or filters instead.
  • Use logger.exception(...) only inside an exception handler when the traceback is useful.
  • For async or high-throughput code, avoid slow network or file handlers on the hot path; consider QueueHandler and a listener.
  • Avoid custom levels unless there is a strong interoperability reason.

Examples

logging.basicConfig

import logging

logging.basicConfig(level=logging.INFO, format="%(message)s")

logging.info("Hello, world!")
import logging

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
logging.info("Hello, world!")

logging.config.dictConfig

import logging
import logging.config

logging.config.dictConfig(
    {
        "version": 1,
        "disable_existing_loggers": False,
        "formatters": {
            "console": {
                "format": "%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s",
                "datefmt": "%Y-%m-%dT%H:%M:%S",
            }
        },
        "handlers": {
            "console": {
                "class": "logging.StreamHandler",
                "formatter": "console",
            }
        },
        "root": {"level": "INFO", "handlers": ["console"]},
    }
)

logging.info("Hello world")

Config Composition

Example function suitable for merging dicts for dictConfig

from collections.abc import Iterable
from collections.abc import Mapping
from collections.abc import Sequence
from copy import copy
from functools import reduce


BASE = ...
CONSOLE = ...
JSON_FILE = ...
RICH = ...


def merge(a: Mapping, b: Mapping) -> Mapping:
    """Recursively merge config dicts"""
    a = dict(a)
    for k, v in b.items():
        match a.get(k), v:
            case Mapping() as inner, Mapping():
                a[k] = merge(inner, v)
            case Sequence() as inner, Iterable():
                new = list(copy(inner))
                a[k] = new + [sub_v for sub_v in v if sub_v not in new]
            case _:
                a[k] = v
    return a


def configure_logging(
    base_config: dict | None = None,
    *,
    enable_console: bool = True,
    enable_file: bool = True,
    enable_rich: bool = True,
) -> dict:
    """Configure logging using the merged configuration."""
    configs = [base_config or BASE]
    if enable_console:
        configs.append(CONSOLE)
    if enable_file:
        configs.append(JSON_FILE)
    if enable_rich:
        configs.append(RICH)
    final_config = dict(reduce(merge, configs))
    logging.config.dictConfig(final_config)
    return final_config

Using dictConfig

Use logging.config.dictConfig when configuration should be centralized, data-driven, or richer than basicConfig.

  1. Define one LOGGING dictionary in a startup-oriented module such as logging_config.py.
  2. Include version: 1 and usually set disable_existing_loggers: False so existing named loggers are not silently disabled.
  3. Define formatters, then handlers, then logger routing with root and optional named loggers.
  4. Call logging.config.dictConfig(LOGGING) once during application startup.
  5. Keep application logging calls unchanged when adding new destinations or formats.

Application Usage

Concrete examples of how logging should be configured and used.

!!! warning "It's important to avoid the obvious name of logging.py to avoid weird clashes with IDEs and python internals."

=== "dictConfig"

```python title="logging_config.py"
import logging.config

LOGGING = ...

def configure_logging() -> None:
    logging.config.dictConfig(LOGGING)
```

=== "basicConfig"

```python title="logging_config.py"
import logging.config

LOGGING = ...

def configure_logging() -> None:
    logging.basicConfig(**LOGGING)
```
import logging

logger = logging.getLogger(__name__)


def run(count: int) -> None:
    logger.info("Processing %s items", count)
from app import run
from logging_config import configure_logging

configure_logging()
run(5)

Branching Guidance

  • If the code is a tiny script: use basicConfig once near the entry point and module loggers elsewhere.
  • If the code is a library: remove handlers and configuration calls; document logger names and optionally add NullHandler at the package root.
  • If structured logs are required: keep the same logger and handler topology, but switch formatter output to JSON or a structured formatter.
  • If console and file output are needed: add one file or rotating-file handler and attach it centrally. For a queue-backed JSON file setup, use the JSON file logging pattern.
  • If multiple processes write to one file: use a queue/listener or process-safe collection path rather than opening the same file independently in each process.
  • If logs must cross a network: send records to a receiver or collector from a queue-backed handler, keep the receiver responsible for final destinations, and avoid exposing unauthenticated logging ports.
  • If a framework logger is noisy: add a named logger override with a level and leave unrelated logger propagation alone.

Completion Checks

  1. Modules use logging.getLogger(__name__).
  2. Application startup configures logging once.
  3. Libraries do not configure application handlers.
  4. Levels match the severity semantics in this skill.
  5. Logs include enough context to identify source, severity, and event without leaking secrets.
  6. Expected destinations receive messages and suppressed levels stay quiet.
  7. No source file or package is named logging.py.