| import logging |
| import platform |
| import re |
| import sys |
| from typing import Dict, Optional |
|
|
| |
| COLORS = { |
| "DEBUG": "\033[34m", |
| "INFO": "\033[32m", |
| "WARNING": "\033[33m", |
| "ERROR": "\033[31m", |
| "CRITICAL": "\033[1;31m", |
| } |
|
|
|
|
| |
| if platform.system() == "Windows": |
| import ctypes |
|
|
| kernel32 = ctypes.windll.kernel32 |
| kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) |
|
|
|
|
| class ColoredFormatter(logging.Formatter): |
| """ |
| 自定义的日志格式化器,添加颜色支持 |
| """ |
|
|
| def format(self, record): |
| |
| color = COLORS.get(record.levelname, "") |
| |
| record.levelname = f"{color}{record.levelname}\033[0m" |
| |
| record.fileloc = f"[{record.filename}:{record.lineno}]" |
| return super().format(record) |
|
|
|
|
| class AccessLogFormatter(logging.Formatter): |
| """ |
| Custom access log formatter that redacts API keys in URLs |
| """ |
|
|
| |
| API_KEY_PATTERNS = [ |
| r"\bAIza[0-9A-Za-z_-]{35}", |
| r"\bsk-[0-9A-Za-z_-]{20,}", |
| ] |
|
|
| def __init__(self, *args, **kwargs): |
| super().__init__(*args, **kwargs) |
| |
| self.compiled_patterns = [ |
| re.compile(pattern) for pattern in self.API_KEY_PATTERNS |
| ] |
|
|
| def format(self, record): |
| |
| formatted_msg = super().format(record) |
|
|
| |
| return self._redact_api_keys_in_message(formatted_msg) |
|
|
| def _redact_api_keys_in_message(self, message: str) -> str: |
| """ |
| Replace API keys in log message with redacted versions |
| """ |
| try: |
| for pattern in self.compiled_patterns: |
|
|
| def replace_key(match): |
| key = match.group(0) |
| return redact_key_for_logging(key) |
|
|
| message = pattern.sub(replace_key, message) |
|
|
| return message |
| except Exception as e: |
| |
| import logging |
|
|
| logger = logging.getLogger(__name__) |
| logger.error(f"Error redacting API keys in access log: {e}") |
| return "[LOG_REDACTION_ERROR]" |
|
|
|
|
| def redact_key_for_logging(key: str) -> str: |
| """ |
| Redacts API key for secure logging by showing only first and last 6 characters. |
| |
| Args: |
| key: API key to redact |
| |
| Returns: |
| str: Redacted key in format "first6...last6" or descriptive placeholder for edge cases |
| """ |
| if not key: |
| return key |
|
|
| if len(key) <= 12: |
| return f"{key[:3]}...{key[-3:]}" |
| else: |
| return f"{key[:6]}...{key[-6:]}" |
|
|
|
|
| |
| FORMATTER = ColoredFormatter( |
| "%(asctime)s | %(levelname)-17s | %(fileloc)-30s | %(message)s" |
| ) |
|
|
| |
| LOG_LEVELS = { |
| "debug": logging.DEBUG, |
| "info": logging.INFO, |
| "warning": logging.WARNING, |
| "error": logging.ERROR, |
| "critical": logging.CRITICAL, |
| } |
|
|
|
|
| class Logger: |
| def __init__(self): |
| pass |
|
|
| _loggers: Dict[str, logging.Logger] = {} |
|
|
| @staticmethod |
| def setup_logger(name: str) -> logging.Logger: |
| """ |
| 设置并获取logger |
| :param name: logger名称 |
| :return: logger实例 |
| """ |
| |
| from app.config.config import settings |
|
|
| |
| log_level_str = settings.LOG_LEVEL.lower() |
| level = LOG_LEVELS.get(log_level_str, logging.INFO) |
|
|
| if name in Logger._loggers: |
| |
| existing_logger = Logger._loggers[name] |
| if existing_logger.level != level: |
| existing_logger.setLevel(level) |
| return existing_logger |
|
|
| logger = logging.getLogger(name) |
| logger.setLevel(level) |
| logger.propagate = False |
|
|
| |
| console_handler = logging.StreamHandler(sys.stdout) |
| console_handler.setFormatter(FORMATTER) |
| logger.addHandler(console_handler) |
|
|
| Logger._loggers[name] = logger |
| return logger |
|
|
| @staticmethod |
| def get_logger(name: str) -> Optional[logging.Logger]: |
| """ |
| 获取已存在的logger |
| :param name: logger名称 |
| :return: logger实例或None |
| """ |
| return Logger._loggers.get(name) |
|
|
| @staticmethod |
| def update_log_levels(log_level: str): |
| """ |
| 根据当前的全局配置更新所有已创建 logger 的日志级别。 |
| """ |
| log_level_str = log_level.lower() |
| new_level = LOG_LEVELS.get(log_level_str, logging.INFO) |
|
|
| updated_count = 0 |
| for logger_name, logger_instance in Logger._loggers.items(): |
| if logger_instance.level != new_level: |
| logger_instance.setLevel(new_level) |
| |
| |
| updated_count += 1 |
|
|
|
|
| |
| def get_openai_logger(): |
| return Logger.setup_logger("openai") |
|
|
|
|
| def get_gemini_logger(): |
| return Logger.setup_logger("gemini") |
|
|
|
|
| def get_chat_logger(): |
| return Logger.setup_logger("chat") |
|
|
|
|
| def get_model_logger(): |
| return Logger.setup_logger("model") |
|
|
|
|
| def get_security_logger(): |
| return Logger.setup_logger("security") |
|
|
|
|
| def get_key_manager_logger(): |
| return Logger.setup_logger("key_manager") |
|
|
|
|
| def get_main_logger(): |
| return Logger.setup_logger("main") |
|
|
|
|
| def get_embeddings_logger(): |
| return Logger.setup_logger("embeddings") |
|
|
|
|
| def get_request_logger(): |
| return Logger.setup_logger("request") |
|
|
|
|
| def get_retry_logger(): |
| return Logger.setup_logger("retry") |
|
|
|
|
| def get_image_create_logger(): |
| return Logger.setup_logger("image_create") |
|
|
|
|
| def get_exceptions_logger(): |
| return Logger.setup_logger("exceptions") |
|
|
|
|
| def get_application_logger(): |
| return Logger.setup_logger("application") |
|
|
|
|
| def get_initialization_logger(): |
| return Logger.setup_logger("initialization") |
|
|
|
|
| def get_middleware_logger(): |
| return Logger.setup_logger("middleware") |
|
|
|
|
| def get_routes_logger(): |
| return Logger.setup_logger("routes") |
|
|
|
|
| def get_config_routes_logger(): |
| return Logger.setup_logger("config_routes") |
|
|
|
|
| def get_config_logger(): |
| return Logger.setup_logger("config") |
|
|
|
|
| def get_database_logger(): |
| return Logger.setup_logger("database") |
|
|
|
|
| def get_log_routes_logger(): |
| return Logger.setup_logger("log_routes") |
|
|
|
|
| def get_stats_logger(): |
| return Logger.setup_logger("stats") |
|
|
|
|
| def get_update_logger(): |
| return Logger.setup_logger("update_service") |
|
|
|
|
| def get_scheduler_routes(): |
| return Logger.setup_logger("scheduler_routes") |
|
|
|
|
| def get_message_converter_logger(): |
| return Logger.setup_logger("message_converter") |
|
|
|
|
| def get_api_client_logger(): |
| return Logger.setup_logger("api_client") |
|
|
|
|
| def get_openai_compatible_logger(): |
| return Logger.setup_logger("openai_compatible") |
|
|
|
|
| def get_error_log_logger(): |
| return Logger.setup_logger("error_log") |
|
|
|
|
| def get_request_log_logger(): |
| return Logger.setup_logger("request_log") |
|
|
|
|
| def get_files_logger(): |
| return Logger.setup_logger("files") |
|
|
|
|
| def get_vertex_express_logger(): |
| return Logger.setup_logger("vertex_express") |
|
|
|
|
| def get_gemini_embedding_logger(): |
| return Logger.setup_logger("gemini_embedding") |
|
|
|
|
| def setup_access_logging(): |
| """ |
| Configure uvicorn access logging with API key redaction |
| |
| This function sets up a custom access log formatter that automatically |
| redacts API keys in HTTP access logs. It works by: |
| |
| 1. Intercepting uvicorn's access log messages |
| 2. Using regex patterns to find API keys in URLs |
| 3. Replacing them with redacted versions (first6...last6) |
| |
| Supported API key formats: |
| - Google/Gemini API keys: AIza[35 chars] |
| - OpenAI API keys: sk-[48 chars] |
| - General sk- prefixed keys: sk-[20+ chars] |
| |
| Usage: |
| - Automatically called in main.py when running with uvicorn |
| - For production deployment with gunicorn, ensure this is called in startup |
| """ |
| |
| access_logger = logging.getLogger("uvicorn.access") |
|
|
| |
| for handler in access_logger.handlers[:]: |
| access_logger.removeHandler(handler) |
|
|
| |
| handler = logging.StreamHandler(sys.stdout) |
| access_formatter = AccessLogFormatter("%(asctime)s | %(levelname)-8s | %(message)s") |
| handler.setFormatter(access_formatter) |
|
|
| |
| access_logger.addHandler(handler) |
| access_logger.setLevel(logging.INFO) |
| access_logger.propagate = False |
|
|
| return access_logger |
|
|