""" AWOOOI Structured Logging ========================= structlog configuration for production-grade logging Features: - JSON output in production - Pretty console output in development - Request ID propagation - Async-safe """ import logging import re import sys from typing import Any import structlog from structlog.types import Processor from src.core.config import settings _TELEGRAM_BOT_URL_RE = re.compile(r"(api\.telegram\.org/bot)[^/\s]+") def _redact_sensitive_log_text(text: str) -> str: """遮蔽可能出現在第三方 logger 訊息中的敏感 URL。""" return _TELEGRAM_BOT_URL_RE.sub(r"\1", text) class SensitiveURLRedactionFilter(logging.Filter): """標準 logging filter:避免 httpx 等第三方 logger 把 token URL 打進 log。""" def filter(self, record: logging.LogRecord) -> bool: record.msg = _redact_sensitive_log_text(str(record.msg)) if isinstance(record.args, tuple): record.args = tuple(_redact_sensitive_log_text(str(arg)) for arg in record.args) elif isinstance(record.args, dict): record.args = { key: _redact_sensitive_log_text(str(value)) for key, value in record.args.items() } return True def setup_logging() -> None: """Configure structlog for the application""" # Shared processors for all environments shared_processors: list[Processor] = [ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, structlog.processors.StackInfoRenderer(), structlog.processors.TimeStamper(fmt="iso"), structlog.processors.CallsiteParameterAdder( parameters=[ structlog.processors.CallsiteParameter.PATHNAME, structlog.processors.CallsiteParameter.LINENO, ] ), ] if settings.ENVIRONMENT == "dev": # Development: Pretty console output processors: list[Processor] = [ *shared_processors, structlog.processors.ExceptionPrettyPrinter(), structlog.dev.ConsoleRenderer(colors=True), ] else: # Production: JSON output for log aggregation processors = [ *shared_processors, structlog.processors.format_exc_info, structlog.processors.JSONRenderer(), ] structlog.configure( processors=processors, wrapper_class=structlog.make_filtering_bound_logger( logging.getLevelName(settings.LOG_LEVEL) ), context_class=dict, logger_factory=structlog.PrintLoggerFactory(), cache_logger_on_first_use=True, ) # Configure standard library logging to use structlog logging.basicConfig( format="%(message)s", stream=sys.stdout, level=logging.getLevelName(settings.LOG_LEVEL), ) redaction_filter = SensitiveURLRedactionFilter() root_logger = logging.getLogger() root_logger.addFilter(redaction_filter) for handler in root_logger.handlers: handler.addFilter(redaction_filter) # httpx INFO 會輸出完整 request URL;Telegram Bot API URL 內含 token。 logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.WARNING) def get_logger(name: str | None = None, **initial_context: Any) -> structlog.BoundLogger: """Get a configured logger instance""" logger = structlog.get_logger(name) if initial_context: logger = logger.bind(**initial_context) return logger