"""Centralized, production-ready logging configuration for the ALwrity backend."""
from __future__ import annotations
import asyncio
import json
import logging
import os
import sys
from typing import Dict, Optional, Tuple
from loguru import logger
_LOGGING_CONFIGURED = False
DEFAULT_LOG_OVERRIDES: Dict[str, str] = {
"sqlalchemy": "ERROR",
"sqlalchemy.engine": "ERROR",
"sqlalchemy.pool": "ERROR",
"uvicorn.access": "WARNING",
"watchfiles": "WARNING",
"httpx": "WARNING",
"urllib3": "WARNING",
"apscheduler": "INFO",
}
VIDEO_SERVICE_NAMES = {
"video_generation_service",
"services.story_writer.video_generation_service",
"services.llm_providers.main_video_generation",
}
class InterceptHandler(logging.Handler):
"""Forward standard-library logging records into Loguru sinks."""
def emit(self, record: logging.LogRecord) -> None:
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
frame, depth = logging.currentframe(), 2
while frame and frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
stdlib_extra = {
key: value
for key, value in record.__dict__.items()
if key
not in {
"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"
}
}
log = logger.bind(stdlib_logger=record.name, **stdlib_extra)
log.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
def _env_bool(name: str, default: bool = False) -> bool:
value = os.getenv(name)
if value is None:
return default
return value.strip().lower() in {"1", "true", "yes", "on"}
def _parse_level_overrides() -> Dict[str, str]:
overrides = dict(DEFAULT_LOG_OVERRIDES)
raw_overrides = os.getenv("ALWRITY_LOG_LEVEL_OVERRIDES", "").strip()
if not raw_overrides:
return overrides
for pair in raw_overrides.split(","):
pair = pair.strip()
if not pair or "=" not in pair:
continue
logger_name, level = pair.split("=", 1)
logger_name = logger_name.strip()
level = level.strip().upper()
if logger_name and level:
overrides[logger_name] = level
return overrides
def _resolve_log_level(level_name: str, default: int = logging.INFO) -> Tuple[int, bool]:
try:
return logging._checkLevel(level_name), True
except (TypeError, ValueError):
return default, False
def _apply_logger_overrides(verbose_mode: bool) -> None:
root_level = logging.DEBUG if verbose_mode else logging.INFO
logging.getLogger().setLevel(root_level)
for logger_name, level_name in _parse_level_overrides().items():
level_no, valid = _resolve_log_level(level_name)
if not valid:
logger.warning(
"Invalid log level override '{}' for logger '{}'; defaulting to INFO",
level_name,
logger_name,
)
logging.getLogger(logger_name).setLevel(level_no)
def _serialize_record(record: Dict) -> str:
payload = {
"time": record["time"].isoformat(),
"level": record["level"].name,
"name": record["name"],
"function": record["function"],
"line": record["line"],
"message": record["message"],
"extra": record.get("extra", {}),
}
if record.get("exception"):
payload["exception"] = str(record["exception"])
return json.dumps(payload, default=str)
def _base_log_format(verbose_mode: bool) -> str:
if verbose_mode:
return (
"{time:YYYY-MM-DD HH:mm:ss.SSS} | "
"{level: <8} | "
"{name}:{function}:{line} | "
"rid={extra[request_id]} jid={extra[job_id]} uid={extra[user_id]} | "
"{message}"
)
return (
"{time:HH:mm:ss} | "
"{level: <8} | "
"{name}:{line} | "
"{message}"
)
def _patch_record(record: Dict) -> Dict:
extra = record.setdefault("extra", {})
extra.setdefault("request_id", "-")
extra.setdefault("job_id", "-")
extra.setdefault("user_id", "-")
return record
def _video_generation_filter(record: Dict) -> bool:
message = record.get("message", "")
name = record.get("name", "")
service_name = record.get("extra", {}).get("service")
return (
"[StoryVideoGeneration]" in message
or "[video_gen]" in message
or service_name in VIDEO_SERVICE_NAMES
or any(service in name for service in VIDEO_SERVICE_NAMES)
)
def _configure_loguru_sinks(verbose_mode: bool) -> None:
logger.remove()
logger.configure(patcher=_patch_record)
log_json = _env_bool("ALWRITY_LOG_JSON", default=False)
console_format = _serialize_record if log_json else _base_log_format(verbose_mode)
logger.add(
sys.stdout,
level="DEBUG" if verbose_mode else "WARNING",
format=console_format,
backtrace=True,
diagnose=verbose_mode,
enqueue=True,
)
enable_video_focus = _env_bool("ALWRITY_ENABLE_VIDEO_LOG_FOCUS", default=not verbose_mode)
if enable_video_focus and not verbose_mode:
logger.add(
sys.stdout,
level="INFO",
format=console_format,
filter=_video_generation_filter,
enqueue=True,
)
log_file = os.getenv("ALWRITY_LOG_FILE", "").strip()
if log_file:
logger.add(
log_file,
level="DEBUG" if verbose_mode else "INFO",
format=console_format,
rotation=os.getenv("ALWRITY_LOG_ROTATION", "50 MB"),
retention=os.getenv("ALWRITY_LOG_RETENTION", "14 days"),
enqueue=True,
backtrace=True,
diagnose=verbose_mode,
)
def _configure_stdlib_intercept(verbose_mode: bool) -> None:
intercept_handler = InterceptHandler()
root_logger = logging.getLogger()
root_logger.handlers = [intercept_handler]
root_logger.setLevel(logging.DEBUG if verbose_mode else logging.INFO)
for name in ("uvicorn", "uvicorn.error", "uvicorn.access", "gunicorn", "gunicorn.error"):
target_logger = logging.getLogger(name)
target_logger.handlers = [intercept_handler]
target_logger.propagate = False
logging.captureWarnings(True)
def _register_exception_hooks() -> None:
def _excepthook(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logger.opt(exception=(exc_type, exc_value, exc_traceback)).critical("Uncaught exception")
def _async_exception_handler(loop, context):
exc = context.get("exception")
if exc:
logger.opt(exception=exc).error("Unhandled asyncio exception")
else:
logger.error("Unhandled asyncio exception: {}", context.get("message", context))
sys.excepthook = _excepthook
try:
loop = asyncio.get_running_loop()
loop.set_exception_handler(_async_exception_handler)
except RuntimeError:
pass
def configure_logging(*, verbose_mode: Optional[bool] = None, force: bool = False, bootstrap_source: str = "unknown") -> bool:
"""Configure Loguru + stdlib logging in one place.
Environment variables:
- ALWRITY_VERBOSE=true|false
- ALWRITY_LOG_LEVEL_OVERRIDES="sqlalchemy=ERROR,uvicorn.access=WARNING"
- ALWRITY_ENABLE_VIDEO_LOG_FOCUS=true|false
- ALWRITY_LOG_JSON=true|false
- ALWRITY_LOG_FILE=/path/to/backend.log
- ALWRITY_LOG_ROTATION=50 MB
- ALWRITY_LOG_RETENTION=14 days
"""
global _LOGGING_CONFIGURED
if _LOGGING_CONFIGURED and not force:
return os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
if verbose_mode is None:
verbose_mode = _env_bool("ALWRITY_VERBOSE", default=False)
os.environ["ALWRITY_VERBOSE"] = "true" if verbose_mode else "false"
_configure_loguru_sinks(verbose_mode)
_configure_stdlib_intercept(verbose_mode)
_apply_logger_overrides(verbose_mode)
_register_exception_hooks()
logger.bind(source=bootstrap_source).info(
"Logging configured (verbose={}, source={})",
verbose_mode,
bootstrap_source,
)
_LOGGING_CONFIGURED = True
return verbose_mode
def bind_logger_context(*, request_id: Optional[str] = None, job_id: Optional[str] = None, user_id: Optional[str] = None):
"""Return a context-bound logger for request/job/user correlation."""
return logger.bind(
request_id=request_id or "-",
job_id=job_id or "-",
user_id=user_id or "-",
)
def setup_clean_logging() -> bool:
"""Backward-compatible wrapper for existing imports."""
return configure_logging(bootstrap_source="setup_clean_logging")
def get_uvicorn_log_level() -> str:
"""Get uvicorn log level based on verbose mode."""
verbose_mode = _env_bool("ALWRITY_VERBOSE", default=False)
return "debug" if verbose_mode else "warning"