Files
ALwrity/backend/logging_config.py

177 lines
6.3 KiB
Python

"""
Logging configuration for ALwrity backend.
Provides clean logging setup for end users vs developers.
"""
import logging
import os
import sys
from loguru import logger
_LOGGING_CONFIGURED = False
class LoguruInterceptHandler(logging.Handler):
"""Forward stdlib logging records to Loguru."""
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
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
def configure_logging(mode: str = "default", verbose: bool | None = None, app_name: str = "alwrity") -> bool:
"""Configure Loguru and stdlib logging into one shared pipeline."""
global _LOGGING_CONFIGURED
if verbose is None:
verbose_mode = mode == "verbose" or os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
else:
verbose_mode = verbose
if _LOGGING_CONFIGURED:
return verbose_mode
logger.remove()
if not verbose_mode:
# Suppress verbose logging for end users - be more aggressive
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)
# Suppress service initialization logs
logging.getLogger('services').setLevel(logging.WARNING)
logging.getLogger('api').setLevel(logging.WARNING)
logging.getLogger('models').setLevel(logging.WARNING)
# Suppress specific noisy loggers
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',
'onboarding_data_service',
'enhanced_linguistic_analyzer',
'persona_quality_improver',
'logging_middleware',
'exa_service',
'step3_research_service',
'sitemap_service',
'linkedin_image_generator',
'linkedin_prompt_generator',
'linkedin_image_storage',
'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)
# Configure loguru to be less verbose (only show warnings and errors)
def warning_only_filter(record):
return record["level"].name in ["WARNING", "ERROR", "CRITICAL"]
logger.add(
sys.stdout.write,
level="WARNING",
format=f"{app_name} | {{time:HH:mm:ss}} | {{level: <8}} | {{name}}:{{function}}:{{line}} - {{message}}\n",
filter=warning_only_filter
)
# Add a focused sink to surface Story Video Generation INFO logs in console
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=f"{app_name} | {{time:HH:mm:ss}} | {{level: <8}} | {{name}}:{{function}}:{{line}} - {{message}}\n",
filter=video_generation_filter
)
else:
# In verbose mode, show all log levels with detailed formatting
logger.add(
sys.stdout.write,
level="DEBUG",
format=f"{app_name} | {{time:HH:mm:ss}} | {{level: <8}} | {{name}}:{{function}}:{{line}} - {{message}}\n"
)
intercept_handler = LoguruInterceptHandler()
root_logger = logging.getLogger()
root_logger.handlers = [intercept_handler]
root_logger.setLevel(logging.DEBUG if verbose_mode else logging.WARNING)
logging.captureWarnings(True)
warnings_logger = logging.getLogger("py.warnings")
warnings_logger.handlers = [intercept_handler]
warnings_logger.propagate = True
for existing_logger in logging.root.manager.loggerDict.values():
if isinstance(existing_logger, logging.Logger):
existing_logger.handlers = []
existing_logger.propagate = True
_LOGGING_CONFIGURED = True
return verbose_mode
def setup_clean_logging():
"""Backward-compatible wrapper for existing startup files."""
return configure_logging(mode="default")
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"