- Remove structlog dependency from backend/pyproject.toml - Add app.utils.logging_compat shim for keyword-arg logging API - Add app.utils.json_formatter for JSON log output with extra fields - Update all backend modules to use logging_compat.get_logger() - Update docstrings in log_sanitizer.py and json_formatter.py - Update test comment in test_async_utils.py - Record 406 failing tests in Docs/Tasks.md for tracking
86 lines
2.3 KiB
Python
86 lines
2.3 KiB
Python
"""JSON formatter for stdlib logging that preserves extra fields.
|
||
|
||
A single logging.Formatter subclass that serialises any keyword arguments
|
||
passed via ``extra=`` into the JSON output alongside the standard record
|
||
attributes.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
from datetime import datetime, timezone
|
||
from typing import Any
|
||
|
||
# Attributes that belong to the standard LogRecord and should NOT be
|
||
# treated as user-supplied extra fields.
|
||
_STD_RECORD_ATTRS: frozenset[str] = frozenset(
|
||
{
|
||
"name",
|
||
"msg",
|
||
"args",
|
||
"levelname",
|
||
"levelno",
|
||
"pathname",
|
||
"filename",
|
||
"module",
|
||
"exc_info",
|
||
"exc_text",
|
||
"stack_info",
|
||
"lineno",
|
||
"funcName",
|
||
"created",
|
||
"msecs",
|
||
"relativeCreated",
|
||
"thread",
|
||
"threadName",
|
||
"processName",
|
||
"process",
|
||
"message",
|
||
"asctime",
|
||
"taskName",
|
||
}
|
||
)
|
||
|
||
|
||
class JSONFormatter(logging.Formatter):
|
||
"""Format log records as JSON lines, including extra fields.
|
||
|
||
Usage::
|
||
|
||
handler = logging.StreamHandler()
|
||
handler.setFormatter(JSONFormatter())
|
||
logging.getLogger().addHandler(handler)
|
||
|
||
Output keys:
|
||
- ``event`` – the log message
|
||
- ``level`` – lower-cased level name
|
||
- ``timestamp`` – ISO-8601 UTC timestamp
|
||
- ``logger`` – logger name
|
||
- any ``extra`` fields supplied by the caller
|
||
"""
|
||
|
||
def format(self, record: logging.LogRecord) -> str:
|
||
"""Return a JSON string for *record*."""
|
||
log_dict: dict[str, Any] = {
|
||
"event": record.getMessage(),
|
||
"level": record.levelname.lower(),
|
||
"timestamp": (
|
||
datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat()
|
||
),
|
||
"logger": record.name,
|
||
}
|
||
|
||
# Merge any extra fields attached to the record.
|
||
for key, value in record.__dict__.items():
|
||
if key not in _STD_RECORD_ATTRS:
|
||
log_dict[key] = value
|
||
|
||
# Include exception info when present.
|
||
if record.exc_info and not record.exc_text:
|
||
record.exc_text = self.formatException(record.exc_info)
|
||
if record.exc_text:
|
||
log_dict["exception"] = record.exc_text
|
||
|
||
return json.dumps(log_dict, default=str)
|