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:
3
log_config/__init__.py
Normal file
3
log_config/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .setup import configure_logging, get_logger
|
||||
|
||||
__all__ = ["configure_logging", "get_logger"]
|
||||
66
log_config/setup.py
Normal file
66
log_config/setup.py
Normal 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)
|
||||
Reference in New Issue
Block a user