refactor(logging): replace structlog with stdlib logging compat layer
- 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
This commit is contained in:
85
backend/app/utils/json_formatter.py
Normal file
85
backend/app/utils/json_formatter.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user