feat: extract shared infrastructure from shared_lib
Phase 1-3 of decoupling plan: - Shared DB, models, infrastructure, browser, config, utils - Event infrastructure (domain_events outbox, bus, processor) - Structured logging - Generic container concept (container_type/container_id) - Alembic migrations for all schema changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
66
logging/setup.py
Normal file
66
logging/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