logging skill updates

This commit is contained in:
John Lancaster
2026-07-08 21:03:38 -05:00
parent a3ca1a65c2
commit 963805c551
2 changed files with 326 additions and 38 deletions
+150 -38
View File
@@ -17,9 +17,18 @@ x-personal-mcp:
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 overview, library guidance, handlers, and dictConfig schema: [Python logging references](./references/python-logging-docs.md)
- Minimal network logging example with a receiver and queue-backed client: [Network logging minimal example](./references/network-logging-minimal-example.md)
- HTTP JSON logging example with `httpx` and a queue-backed client: [HTTPX logging handler example](./references/httpx-logging-handler-example.md)
[Python logging references](./references/python-logging-docs.md)
: Python logging overview, library guidance, handlers, and dictConfig schema
[JSON file logging pattern](./references/json-file-logging.md)
: Queue-backed rotating JSON file pattern for local machine-readable logs
[Network logging minimal example](./references/network-logging-minimal-example.md)
: Minimal network logging example with a receiver and queue-backed client
[HTTPX logging handler example](./references/httpx-logging-handler-example.md)
: HTTP JSON logging example with `httpx` and a queue-backed client
## When to Use
@@ -50,7 +59,7 @@ If missing, assume:
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. For small scripts, `basicConfig` can be enough; for applications, prefer a centralized configuration function.
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.
@@ -64,6 +73,111 @@ If missing, assume:
- 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`
```python title="Bare minimum"
import logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
logging.info("Hello, world!")
```
```python title="With a little formatting"
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`
```python title="Minimal dictConfig example"
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`
```python title="composed config"
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`.
@@ -74,45 +188,35 @@ Use `logging.config.dictConfig` when configuration should be centralized, data-d
4. Call `logging.config.dictConfig(LOGGING)` once during application startup.
5. Keep application logging calls unchanged when adding new destinations or formats.
### Minimal dictConfig Baseline
## Application Usage
```python title="logging_config.py"
import logging.config
Concrete examples of how logging should be configured and used.
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"console": {
"format": "%(asctime)s.%(msecs)03d %(levelname)s %(name)s %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "console",
"stream": "ext://sys.stdout",
}
},
"root": {
"level": "INFO",
"handlers": ["console"],
},
}
!!! warning "It's important to avoid the obvious name of `logging.py` to avoid weird clashes with IDEs and python internals."
=== "dictConfig"
def configure_logging() -> None:
logging.config.dictConfig(LOGGING)
```
```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)
```
```python title="app.py"
from .logging_config import configure_logging
configure_logging()
```
```python title="feature.py"
import logging
logger = logging.getLogger(__name__)
@@ -122,12 +226,20 @@ def run(count: int) -> None:
logger.info("Processing %s items", count)
```
```python title="main.py"
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.
- 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](./references/json-file-logging.md).
- 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.