This repository has been archived on 2026-02-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
shared/log_config/setup.py
giles 9bc9f64dce 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>
2026-02-11 13:55:20 +00:00

67 lines
2.0 KiB
Python

"""
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)