Files
ALwrity/backend/logging_config.py
ajaysi 1a2ec68095 Enhance logging with exception handlers and context tracking
- Add InterceptHandler to route stdlib logging to Loguru
- Add _patch_record_context for request/job/user ID tracking
- Add _uncaught_exception_hook to capture top-level exceptions
- Add _asyncio_exception_handler for asyncio task exceptions
- Add _register_global_exception_handlers to register all hooks
- Add _configure_uvicorn_loggers for unified uvicorn logging
- Improve log format with contextual fields (req, job, user)
2026-03-22 10:59:46 +05:30

227 lines
7.2 KiB
Python

"""
Logging configuration for ALwrity backend.
Provides clean logging setup for end users vs developers.
"""
import asyncio
import logging
import os
import sys
from loguru import logger
class InterceptHandler(logging.Handler):
"""Route standard logging records into Loguru."""
def emit(self, record):
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
logger.bind(logger_name=record.name).opt(
depth=depth,
exception=record.exc_info,
).log(level, record.getMessage())
def _patch_record_context(record):
"""Ensure common context keys exist in every log record."""
extra = record["extra"]
extra.setdefault("request_id", "-")
extra.setdefault("job_id", "-")
extra.setdefault("user_id", "-")
def _uncaught_exception_hook(exc_type, exc_value, exc_traceback):
"""Capture any uncaught top-level exception with 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 reached sys.excepthook"
)
def _asyncio_exception_handler(loop, context):
"""Capture unhandled asyncio task/loop exceptions."""
exception = context.get("exception")
message = context.get("message", "Unhandled asyncio exception")
if exception:
logger.opt(exception=(type(exception), exception, exception.__traceback__)).critical(
"{}", message
)
else:
logger.critical("{} | context={}", message, context)
def _register_global_exception_handlers():
"""Register global hooks for uncaught runtime exceptions."""
sys.excepthook = _uncaught_exception_hook
try:
policy = asyncio.get_event_loop_policy()
class LoggingEventLoopPolicy(type(policy)):
def new_event_loop(self):
loop = super().new_event_loop()
loop.set_exception_handler(_asyncio_exception_handler)
return loop
asyncio.set_event_loop_policy(LoggingEventLoopPolicy())
except Exception:
logger.exception("Failed to install asyncio event loop policy")
try:
loop = asyncio.get_event_loop()
loop.set_exception_handler(_asyncio_exception_handler)
except Exception:
pass
def _configure_uvicorn_loggers(log_level):
"""Route uvicorn loggers through the same handlers and format as Loguru."""
intercept_handler = InterceptHandler()
for name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
uvicorn_logger = logging.getLogger(name)
uvicorn_logger.handlers = [intercept_handler]
uvicorn_logger.propagate = False
uvicorn_logger.setLevel(log_level)
def setup_clean_logging():
"""Set up clean logging for end users."""
verbose_mode = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
logger.remove()
logger.configure(patcher=_patch_record_context)
common_format = (
"{time:HH:mm:ss} | {level: <8} | req={extra[request_id]} "
"job={extra[job_id]} user={extra[user_id]} | "
"{name}:{function}:{line} - {message}\n{exception}"
)
if not verbose_mode:
logging.getLogger('sqlalchemy.engine').setLevel(logging.CRITICAL)
logging.getLogger('sqlalchemy.pool').setLevel(logging.CRITICAL)
logging.getLogger('sqlalchemy.dialects').setLevel(logging.CRITICAL)
logging.getLogger('sqlalchemy.orm').setLevel(logging.CRITICAL)
logging.getLogger('sqlalchemy').setLevel(logging.CRITICAL)
logging.getLogger('sqlalchemy.engine.Engine').setLevel(logging.CRITICAL)
logging.getLogger('services').setLevel(logging.WARNING)
logging.getLogger('api').setLevel(logging.WARNING)
logging.getLogger('models').setLevel(logging.WARNING)
noisy_loggers = [
'linkedin_persona_service',
'facebook_persona_service',
'core_persona_service',
'persona_analysis_service',
'ai_service_manager',
'ai_engine_service',
'website_analyzer',
'competitor_analyzer',
'keyword_researcher',
'content_gap_analyzer',
'onboarding_data_service',
'comprehensive_user_data',
'strategy_data',
'gap_analysis_data',
'phase1_steps',
'daily_schedule_generator',
'gsc_service',
'wordpress_oauth',
'data_filter',
'source_mapper',
'grounding_engine',
'blog_content_seo_analyzer',
'linkedin_service',
'citation_manager',
'content_analyzer',
'linkedin_prompt_generator',
'linkedin_image_storage',
'hallucination_detector',
'writing_assistant',
'enhanced_linguistic_analyzer',
'persona_quality_improver',
'logging_middleware',
'exa_service',
'step3_research_service',
'sitemap_service',
'linkedin_image_generator',
'router_manager',
'frontend_serving',
'database',
'user_business_info',
'auth_middleware',
'pricing_service',
'create_billing_tables'
]
for logger_name in noisy_loggers:
logging.getLogger(logger_name).setLevel(logging.WARNING)
def warning_only_filter(record):
return record["level"].name in ["WARNING", "ERROR", "CRITICAL"]
logger.add(
sys.stdout.write,
level="WARNING",
format=common_format,
filter=warning_only_filter,
backtrace=True,
diagnose=True,
)
def video_generation_filter(record):
msg = record.get("message", "")
name = record.get("name", "")
service = record.get("extra", {}).get("service")
return (
"[StoryVideoGeneration]" in msg
or "services.story_writer.video_generation_service" in name
or "[video_gen]" in msg
or service == "video_generation_service"
or "services.llm_providers.main_video_generation" in name
)
logger.add(
sys.stdout.write,
level="INFO",
format=common_format,
filter=video_generation_filter,
backtrace=True,
diagnose=True,
)
uvicorn_level = logging.WARNING
else:
logger.add(
sys.stdout.write,
level="DEBUG",
format=common_format,
backtrace=True,
diagnose=True,
)
uvicorn_level = logging.DEBUG
_configure_uvicorn_loggers(uvicorn_level)
_register_global_exception_handlers()
return verbose_mode
def get_uvicorn_log_level():
"""Get appropriate uvicorn log level based on verbose mode."""
verbose_mode = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
return "debug" if verbose_mode else "warning"