Rename shared/logging/ to shared/log_config/ to avoid stdlib shadow

shared/logging/ shadows Python's stdlib logging module, causing a
circular import when any code does `import logging`. This breaks
both the entrypoint Redis flush and Hypercorn app loading.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-11 13:55:20 +00:00
parent 4dd25526b9
commit 9bc9f64dce
3 changed files with 1 additions and 1 deletions

3
log_config/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .setup import configure_logging, get_logger
__all__ = ["configure_logging", "get_logger"]

66
log_config/setup.py Normal file
View File

@@ -0,0 +1,66 @@
"""
Structured JSON logging for all Rose Ash apps.
Call configure_logging(app_name) once at app startup.
Use get_logger(name) anywhere to get a logger that outputs JSON to stdout.
"""
from __future__ import annotations
import json
import logging
import sys
from datetime import datetime, timezone
class JSONFormatter(logging.Formatter):
"""Format log records as single-line JSON objects."""
def __init__(self, app_name: str = ""):
super().__init__()
self.app_name = app_name
def format(self, record: logging.LogRecord) -> str:
entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"level": record.levelname,
"app": self.app_name,
"logger": record.name,
"message": record.getMessage(),
}
# Include extra fields if set on the record
for key in ("event_type", "user_id", "request_id", "duration_ms"):
val = getattr(record, key, None)
if val is not None:
entry[key] = val
if record.exc_info and record.exc_info[0] is not None:
entry["exception"] = self.formatException(record.exc_info)
return json.dumps(entry, default=str)
_configured = False
def configure_logging(app_name: str, level: int = logging.INFO) -> None:
"""Set up structured JSON logging to stdout. Safe to call multiple times."""
global _configured
if _configured:
return
_configured = True
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JSONFormatter(app_name=app_name))
root = logging.getLogger()
root.setLevel(level)
root.addHandler(handler)
# Quiet down noisy libraries
for name in ("httpx", "httpcore", "asyncio", "sqlalchemy.engine"):
logging.getLogger(name).setLevel(logging.WARNING)
def get_logger(name: str) -> logging.Logger:
"""Get a named logger. Uses the structured JSON format once configure_logging is called."""
return logging.getLogger(name)