diff --git a/.gitignore b/.gitignore index 972a926..f75c506 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ config.toml docker-compose.yml kohakuhub.conf lint-reports/ +/logs # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/config-example.toml b/config-example.toml index 4b08eca..6859908 100644 --- a/config-example.toml +++ b/config-example.toml @@ -73,3 +73,7 @@ download_keep_sessions_days = 30 # Keep sessions from last N days debug_log_payloads = false # Log commit payloads (development only) # Site identification site_name = "KohakuHub" # Customizable site name (e.g., "MyCompany Hub") +# Log Settings +log_level = "INFO" # Low level logs will be filted (avaiable options: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL" ) +log_format = "terminal" # Output logs to "file" or "terminal" (maybe sql in future) +log_dir = "logs/" # Path to log file (if log_format is "file") \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 652b3eb..e796d46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "fastapi", "fsspec", "httpx", + "loguru", "numpy", "peewee", "psycopg2-binary", diff --git a/scripts/generate_docker_compose.py b/scripts/generate_docker_compose.py index 3ac14e7..0ce7906 100644 --- a/scripts/generate_docker_compose.py +++ b/scripts/generate_docker_compose.py @@ -360,6 +360,9 @@ def generate_hub_api_service(config: dict) -> str: - KOHAKU_HUB_LFS_KEEP_VERSIONS=5 - KOHAKU_HUB_LFS_AUTO_GC=true - KOHAKU_HUB_AUTO_MIGRATE=true # Auto-confirm database migrations (required for Docker) + - KOHAKU_HUB_LOG_LEVEL=INFO + - KOHAKU_HUB_LOG_FORMAT=terminal + - KOHAKU_HUB_LOG_DIR=logs/ ## ===== Auth & SMTP Configuration ===== - KOHAKU_HUB_REQUIRE_EMAIL_VERIFICATION=false diff --git a/src/kohakuhub/config.py b/src/kohakuhub/config.py index 4b748ef..5a8c67c 100644 --- a/src/kohakuhub/config.py +++ b/src/kohakuhub/config.py @@ -149,6 +149,10 @@ class AppConfig(BaseModel): ] # Site identification site_name: str = "KohakuHub" # Configurable site name (e.g., "MyCompany Hub") + # Log settings + log_level: str = "INFO" # DEBUG, INFO, WARNING, ERROR, CRITICAL + log_format: str = "file" # Output logs to "file" or "terminal" (maybe sql in future) + log_dir: str = "logs/" # Path to log file (if log_format is "file") class Config(BaseModel): @@ -423,6 +427,12 @@ def load_config(path: str = None) -> Config: app_env["debug_log_payloads"] = ( os.environ["KOHAKU_HUB_DEBUG_LOG_PAYLOADS"].lower() == "true" ) + if "KOHAKU_HUB_LOG_LEVEL" in os.environ: + app_env["log_level"] = os.environ["KOHAKU_HUB_LOG_LEVEL"] + if "KOHAKU_HUB_LOG_FORMAT" in os.environ: + app_env["log_format"] = os.environ["KOHAKU_HUB_LOG_FORMAT"] + if "KOHAKU_HUB_LOG_DIR" in os.environ: + app_env["log_dir"] = os.environ["KOHAKU_HUB_LOG_DIR"] if app_env: config_from_env["app"] = app_env diff --git a/src/kohakuhub/logger.py b/src/kohakuhub/logger.py index 59037c8..36638e4 100644 --- a/src/kohakuhub/logger.py +++ b/src/kohakuhub/logger.py @@ -1,63 +1,56 @@ -"""Custom logging module for KohakuHub with colored output and formatted tracebacks.""" +"""Loguru-based logging implementation for KohakuHub.""" import os import sys +import logging + import traceback as tb -from datetime import datetime +from loguru import logger from enum import Enum from typing import Optional - -class Color: - """ANSI color codes for terminal output.""" - - # Text colors - RESET = "\033[0m" - BLACK = "\033[30m" - RED = "\033[31m" - GREEN = "\033[32m" - YELLOW = "\033[33m" - BLUE = "\033[34m" - MAGENTA = "\033[35m" - CYAN = "\033[36m" - WHITE = "\033[37m" - - # Bright colors - BRIGHT_BLACK = "\033[90m" - BRIGHT_RED = "\033[91m" - BRIGHT_GREEN = "\033[92m" - BRIGHT_YELLOW = "\033[93m" - BRIGHT_BLUE = "\033[94m" - BRIGHT_MAGENTA = "\033[95m" - BRIGHT_CYAN = "\033[96m" - BRIGHT_WHITE = "\033[97m" - - # Background colors - BG_BLACK = "\033[40m" - BG_RED = "\033[41m" - BG_GREEN = "\033[42m" - BG_YELLOW = "\033[43m" - BG_BLUE = "\033[44m" - - # Text styles - BOLD = "\033[1m" - DIM = "\033[2m" - UNDERLINE = "\033[4m" +from kohakuhub.config import cfg class LogLevel(Enum): - """Log levels with associated colors.""" + """Log levels mapping to loguru levels.""" + DEBUG = "DEBUG" + INFO = "INFO" + SUCCESS = "SUCCESS" + WARNING = "WARNING" + ERROR = "ERROR" + CRITICAL = "CRITICAL" + TRACE = "TRACE" - DEBUG = ("DEBUG", Color.BRIGHT_BLACK) - INFO = ("INFO", Color.BRIGHT_CYAN) - SUCCESS = ("SUCCESS", Color.BRIGHT_GREEN) - WARNING = ("WARNING", Color.BRIGHT_YELLOW) - ERROR = ("ERROR", Color.BRIGHT_RED) - CRITICAL = ("CRITICAL", Color.BG_RED + Color.WHITE + Color.BOLD) + +class InterceptHandler(logging.Handler): + """ + Logger Interceptor:Redirects standard library logs to Loguru. + """ + + def emit(self, record: logging.LogRecord) -> None: + # Get Level Name and API name from LogRecord + try: + level = logger.level(record.levelname).name + api_name = record.name.upper() + if "UVICORN" in api_name: + api_name = "UVICORN" + except ValueError: + level = record.levelno + + frame, depth = logging.currentframe(), 2 + while frame.f_code.co_filename == logging.__file__: + frame = frame.f_back + depth += 1 + + logger.bind(api_name=api_name).opt(depth=depth, exception=record.exc_info).log( + level, + record.getMessage() + ) class Logger: - """Custom logger with API name prefix and colored output.""" + """Loguru-based logger""" def __init__(self, api_name: str = "APP"): """Initialize logger with API name. @@ -66,74 +59,49 @@ class Logger: api_name: Name of the API/module (e.g., "AUTH", "FILE", "LFS") """ self.api_name = api_name.upper() - - def _get_timestamp(self) -> str: - """Get current timestamp in HH:MM:SS format.""" - return datetime.now().strftime("%H:%M:%S") - - def _format_message(self, level: LogLevel, message: str) -> str: - """Format log message with colors and structure. - - Format: [LEVEL][API-NAME][Worker:PID][HH:MM:SS] message - - Args: - level: Log level - message: Message to log - - Returns: - Formatted colored string - """ - level_name, level_color = level.value - timestamp = self._get_timestamp() - worker_id = os.getpid() # Process ID identifies the worker - - # Build formatted message - parts = [ - f"{level_color}[{level_name}]{Color.RESET}", - f"{Color.BRIGHT_MAGENTA}[{self.api_name}]{Color.RESET}", - f"{Color.BLUE}[W:{worker_id}]{Color.RESET}", - f"{Color.BRIGHT_BLACK}[{timestamp}]{Color.RESET}", - message, - ] - - return " ".join(parts) + self._logger = logger.bind(api_name=api_name) def _log(self, level: LogLevel, message: str): """Internal log method.""" - formatted = self._format_message(level, message) - print( - formatted, - file=( - sys.stderr - if level in [LogLevel.ERROR, LogLevel.CRITICAL] - else sys.stdout - ), - ) + match level: + case LogLevel.DEBUG: + self._logger.debug(message) + case LogLevel.INFO: + self._logger.info(message) + case LogLevel.SUCCESS: + self._logger.success(message) + case LogLevel.WARNING: + self._logger.warning(message) + case LogLevel.ERROR: + self._logger.error(message) + case LogLevel.CRITICAL: + self._logger.critical(message) + case LogLevel.TRACE: + self._logger.trace(message) + case _: + self._logger.log(level, message) def debug(self, message: str): - """Log debug message.""" self._log(LogLevel.DEBUG, message) def info(self, message: str): - """Log info message.""" self._log(LogLevel.INFO, message) def success(self, message: str): - """Log success message.""" self._log(LogLevel.SUCCESS, message) def warning(self, message: str): - """Log warning message.""" self._log(LogLevel.WARNING, message) def error(self, message: str): - """Log error message.""" self._log(LogLevel.ERROR, message) def critical(self, message: str): - """Log critical error message.""" self._log(LogLevel.CRITICAL, message) + def trace(self, message: str): + self._log(LogLevel.TRACE, message) + def exception(self, message: str, exc: Optional[Exception] = None): """Log exception with formatted traceback. @@ -168,9 +136,9 @@ class Logger: frames = tb.extract_tb(exc_tb) # Print header - print(f"\n{Color.BRIGHT_RED}{'═' * 100}{Color.RESET}", file=sys.stderr) - print(f"{Color.BRIGHT_RED}{Color.BOLD}TRACEBACK{Color.RESET}", file=sys.stderr) - print(f"{Color.BRIGHT_RED}{'═' * 100}{Color.RESET}\n", file=sys.stderr) + self.trace(f"{'=' * 50}") + self.trace("TRACEBACK") + self.trace(f"{'=' * 50}") # Print stack frames as tables for i, frame in enumerate(frames, 1): @@ -179,7 +147,7 @@ class Logger: # Print final error table self._print_error_table(exc_type, exc_value, frames[-1] if frames else None) - print(f"{Color.BRIGHT_RED}{'═' * 100}{Color.RESET}\n", file=sys.stderr) + self.trace(f"{'=' * 50}") def _print_frame_table(self, index: int, frame: tb.FrameSummary, is_last: bool): """Print single stack frame as a table. @@ -189,39 +157,27 @@ class Logger: frame: Frame summary is_last: Whether this is the last frame (error location) """ - color = Color.BRIGHT_RED if is_last else Color.BRIGHT_BLACK + # loguru only renders colors in format + # color = "ff0000" if is_last else "09D0EF" # Table header - print( - f"{color}┌─ Frame #{index} {' (ERROR HERE)' if is_last else ''}", - file=sys.stderr, - ) + self.trace(f"┌─ Frame #{index} {' (ERROR HERE)' if is_last else ''}") # File - print( - f"{color}│ {Color.CYAN}File:{Color.RESET} {frame.filename}", file=sys.stderr - ) + self.trace(f"│ File: {frame.filename}") # Line number - print( - f"{color}│ {Color.YELLOW}Line:{Color.RESET} {frame.lineno}", file=sys.stderr - ) + self.trace(f"│ Line: {frame.lineno}") # Function/Code if frame.name: - print( - f"{color}│ {Color.GREEN}In:{Color.RESET} {frame.name}()", - file=sys.stderr, - ) + self.trace(f"│ In:{frame.name}()") if frame.line: # Show the actual code line - print( - f"{color}│ {Color.BRIGHT_WHITE}Code:{Color.RESET} {frame.line.strip()}", - file=sys.stderr, - ) + self.trace(f"│ Code: {frame.line.strip()}") - print(f"{color}└{'─' * 99}{Color.RESET}\n", file=sys.stderr) + self.trace(f"└{'─' * 99}") def _print_error_table( self, exc_type, exc_value, last_frame: Optional[tb.FrameSummary] @@ -234,46 +190,83 @@ class Logger: last_frame: Last stack frame (error location) """ # Table header - print( - f"{Color.BG_RED}{Color.WHITE}{Color.BOLD} EXCEPTION DETAILS {Color.RESET}", - file=sys.stderr, - ) - print(f"{Color.BRIGHT_RED}┌{'─' * 99}", file=sys.stderr) + self.trace(" EXCEPTION DETAILS ") + + self.trace(f"┌{'─' * 99}") # Error type - print( - f"{Color.BRIGHT_RED}│ {Color.BOLD}Type:{Color.RESET} {Color.BRIGHT_RED}{exc_type.__name__}{Color.RESET}", - file=sys.stderr, - ) + self.trace(f"│ Type: {exc_type.__name__}") # Error message - print( - f"{Color.BRIGHT_RED}│ {Color.BOLD}Message:{Color.RESET} {Color.WHITE}{str(exc_value)}{Color.RESET}", - file=sys.stderr, - ) + self.trace(f"│ Message: {str(exc_value)}") if last_frame: # Error location - print( - f"{Color.BRIGHT_RED}│ {Color.BOLD}Location:{Color.RESET} {Color.CYAN}{last_frame.filename}{Color.RESET}:{Color.YELLOW}{last_frame.lineno}{Color.RESET}", - file=sys.stderr, - ) + self.trace(f"│ Location: {last_frame.filename}:{last_frame.lineno}") if last_frame.line: # Code that caused error - print( - f"{Color.BRIGHT_RED}│ {Color.BOLD}Code:{Color.RESET} {Color.BRIGHT_WHITE}{last_frame.line.strip()}{Color.RESET}", - file=sys.stderr, - ) + self.trace(f"│ Code: {last_frame.line.strip()}") - print(f"{Color.BRIGHT_RED}└{'─' * 99}{Color.RESET}", file=sys.stderr) + self.trace(f"└{'─' * 99}") class LoggerFactory: - """Factory to create loggers with different API names.""" + """Factory to create loguru loggers.""" _loggers = {} + @classmethod + def init_logger_settings(cls): + + log_dir = cfg.app.log_dir + if not os.path.exists(log_dir): + os.mkdir(log_dir) + log_path = os.path.join(log_dir, "kohakuhub.log") + log_level = cfg.app.log_level.upper() + log_format = cfg.app.log_format.lower() + # Configure loguru format to match existing style + + # Remove default handler + logger.remove() + + """ About Color codes for loguru. + Actually, it also support 8bit , Hex , RGB color codes. + See https://loguru.readthedocs.io/en/stable/api/logger.html#color + """ + logger.level("DEBUG", color="") + logger.level("INFO", color="") + logger.level("SUCCESS", color="") + logger.level("WARNING", color="") + logger.level("ERROR", color="") + logger.level("CRITICAL", color="") + logger.level("TRACE", color="") + + """Add Defualt Terminal logger""" + logger.add( + sys.stderr, + format="[{level}][{extra[api_name]}][W:{process}][{time:HH:mm:ss}] {message}", + level=log_level, + colorize=True + ) + + """Add File logger""" + if log_format == "file": + logger.add( + log_path, + format="[{level}][{extra[api_name]}][W:{process}][{time:HH:mm:ss}] {message}", + level=log_level, + rotation="2 MB" + ) + + logger_name_list = [name for name in logging.root.manager.loggerDict] + for logger_name in logger_name_list: + _logger = logging.getLogger(logger_name) + _logger.setLevel(logging.INFO) + _logger.handlers = [] + if '.' not in logger_name: + _logger.addHandler(InterceptHandler()) + @classmethod def get_logger(cls, api_name: str) -> Logger: """Get or create logger for API name. @@ -289,22 +282,16 @@ class LoggerFactory: return cls._loggers[api_name] -# Convenience function for getting logger +def init_logger_settings(): + """Initialize logger settings.""" + LoggerFactory.init_logger_settings() + + def get_logger(api_name: str) -> Logger: """Get logger for specific API. - Usage: - logger = get_logger("AUTH") - logger.info("User logged in") - logger.error("Login failed") - - try: - # ... code ... - except Exception as e: - logger.exception("Failed to process request", e) - Args: - api_name: Name of the API/module (e.g., "AUTH", "FILE", "LFS") + api_name: Name of the API/module Returns: Logger instance @@ -312,6 +299,7 @@ def get_logger(api_name: str) -> Logger: return LoggerFactory.get_logger(api_name) +init_logger_settings() # Pre-create common loggers logger_auth = get_logger("AUTH") logger_file = get_logger("FILE") diff --git a/src/kohakuhub/old_logger.py b/src/kohakuhub/old_logger.py new file mode 100644 index 0000000..59037c8 --- /dev/null +++ b/src/kohakuhub/old_logger.py @@ -0,0 +1,323 @@ +"""Custom logging module for KohakuHub with colored output and formatted tracebacks.""" + +import os +import sys +import traceback as tb +from datetime import datetime +from enum import Enum +from typing import Optional + + +class Color: + """ANSI color codes for terminal output.""" + + # Text colors + RESET = "\033[0m" + BLACK = "\033[30m" + RED = "\033[31m" + GREEN = "\033[32m" + YELLOW = "\033[33m" + BLUE = "\033[34m" + MAGENTA = "\033[35m" + CYAN = "\033[36m" + WHITE = "\033[37m" + + # Bright colors + BRIGHT_BLACK = "\033[90m" + BRIGHT_RED = "\033[91m" + BRIGHT_GREEN = "\033[92m" + BRIGHT_YELLOW = "\033[93m" + BRIGHT_BLUE = "\033[94m" + BRIGHT_MAGENTA = "\033[95m" + BRIGHT_CYAN = "\033[96m" + BRIGHT_WHITE = "\033[97m" + + # Background colors + BG_BLACK = "\033[40m" + BG_RED = "\033[41m" + BG_GREEN = "\033[42m" + BG_YELLOW = "\033[43m" + BG_BLUE = "\033[44m" + + # Text styles + BOLD = "\033[1m" + DIM = "\033[2m" + UNDERLINE = "\033[4m" + + +class LogLevel(Enum): + """Log levels with associated colors.""" + + DEBUG = ("DEBUG", Color.BRIGHT_BLACK) + INFO = ("INFO", Color.BRIGHT_CYAN) + SUCCESS = ("SUCCESS", Color.BRIGHT_GREEN) + WARNING = ("WARNING", Color.BRIGHT_YELLOW) + ERROR = ("ERROR", Color.BRIGHT_RED) + CRITICAL = ("CRITICAL", Color.BG_RED + Color.WHITE + Color.BOLD) + + +class Logger: + """Custom logger with API name prefix and colored output.""" + + def __init__(self, api_name: str = "APP"): + """Initialize logger with API name. + + Args: + api_name: Name of the API/module (e.g., "AUTH", "FILE", "LFS") + """ + self.api_name = api_name.upper() + + def _get_timestamp(self) -> str: + """Get current timestamp in HH:MM:SS format.""" + return datetime.now().strftime("%H:%M:%S") + + def _format_message(self, level: LogLevel, message: str) -> str: + """Format log message with colors and structure. + + Format: [LEVEL][API-NAME][Worker:PID][HH:MM:SS] message + + Args: + level: Log level + message: Message to log + + Returns: + Formatted colored string + """ + level_name, level_color = level.value + timestamp = self._get_timestamp() + worker_id = os.getpid() # Process ID identifies the worker + + # Build formatted message + parts = [ + f"{level_color}[{level_name}]{Color.RESET}", + f"{Color.BRIGHT_MAGENTA}[{self.api_name}]{Color.RESET}", + f"{Color.BLUE}[W:{worker_id}]{Color.RESET}", + f"{Color.BRIGHT_BLACK}[{timestamp}]{Color.RESET}", + message, + ] + + return " ".join(parts) + + def _log(self, level: LogLevel, message: str): + """Internal log method.""" + formatted = self._format_message(level, message) + print( + formatted, + file=( + sys.stderr + if level in [LogLevel.ERROR, LogLevel.CRITICAL] + else sys.stdout + ), + ) + + def debug(self, message: str): + """Log debug message.""" + self._log(LogLevel.DEBUG, message) + + def info(self, message: str): + """Log info message.""" + self._log(LogLevel.INFO, message) + + def success(self, message: str): + """Log success message.""" + self._log(LogLevel.SUCCESS, message) + + def warning(self, message: str): + """Log warning message.""" + self._log(LogLevel.WARNING, message) + + def error(self, message: str): + """Log error message.""" + self._log(LogLevel.ERROR, message) + + def critical(self, message: str): + """Log critical error message.""" + self._log(LogLevel.CRITICAL, message) + + def exception(self, message: str, exc: Optional[Exception] = None): + """Log exception with formatted traceback. + + Args: + message: Error message + exc: Exception object (if None, uses sys.exc_info()) + """ + self.error(message) + self._print_formatted_traceback(exc) + + def _print_formatted_traceback(self, exc: Optional[Exception] = None): + """Print formatted traceback as tables. + + Format: + 1. Stack trace table for each frame + 2. Final error table with actual error details + + Args: + exc: Exception object (if None, uses sys.exc_info()) + """ + if exc is None: + exc_type, exc_value, exc_tb = sys.exc_info() + else: + exc_type = type(exc) + exc_value = exc + exc_tb = exc.__traceback__ + + if exc_tb is None: + return + + # Extract traceback frames + frames = tb.extract_tb(exc_tb) + + # Print header + print(f"\n{Color.BRIGHT_RED}{'═' * 100}{Color.RESET}", file=sys.stderr) + print(f"{Color.BRIGHT_RED}{Color.BOLD}TRACEBACK{Color.RESET}", file=sys.stderr) + print(f"{Color.BRIGHT_RED}{'═' * 100}{Color.RESET}\n", file=sys.stderr) + + # Print stack frames as tables + for i, frame in enumerate(frames, 1): + self._print_frame_table(i, frame, is_last=(i == len(frames))) + + # Print final error table + self._print_error_table(exc_type, exc_value, frames[-1] if frames else None) + + print(f"{Color.BRIGHT_RED}{'═' * 100}{Color.RESET}\n", file=sys.stderr) + + def _print_frame_table(self, index: int, frame: tb.FrameSummary, is_last: bool): + """Print single stack frame as a table. + + Args: + index: Frame index + frame: Frame summary + is_last: Whether this is the last frame (error location) + """ + color = Color.BRIGHT_RED if is_last else Color.BRIGHT_BLACK + + # Table header + print( + f"{color}┌─ Frame #{index} {' (ERROR HERE)' if is_last else ''}", + file=sys.stderr, + ) + + # File + print( + f"{color}│ {Color.CYAN}File:{Color.RESET} {frame.filename}", file=sys.stderr + ) + + # Line number + print( + f"{color}│ {Color.YELLOW}Line:{Color.RESET} {frame.lineno}", file=sys.stderr + ) + + # Function/Code + if frame.name: + print( + f"{color}│ {Color.GREEN}In:{Color.RESET} {frame.name}()", + file=sys.stderr, + ) + + if frame.line: + # Show the actual code line + print( + f"{color}│ {Color.BRIGHT_WHITE}Code:{Color.RESET} {frame.line.strip()}", + file=sys.stderr, + ) + + print(f"{color}└{'─' * 99}{Color.RESET}\n", file=sys.stderr) + + def _print_error_table( + self, exc_type, exc_value, last_frame: Optional[tb.FrameSummary] + ): + """Print final error details as a table. + + Args: + exc_type: Exception type + exc_value: Exception value + last_frame: Last stack frame (error location) + """ + # Table header + print( + f"{Color.BG_RED}{Color.WHITE}{Color.BOLD} EXCEPTION DETAILS {Color.RESET}", + file=sys.stderr, + ) + print(f"{Color.BRIGHT_RED}┌{'─' * 99}", file=sys.stderr) + + # Error type + print( + f"{Color.BRIGHT_RED}│ {Color.BOLD}Type:{Color.RESET} {Color.BRIGHT_RED}{exc_type.__name__}{Color.RESET}", + file=sys.stderr, + ) + + # Error message + print( + f"{Color.BRIGHT_RED}│ {Color.BOLD}Message:{Color.RESET} {Color.WHITE}{str(exc_value)}{Color.RESET}", + file=sys.stderr, + ) + + if last_frame: + # Error location + print( + f"{Color.BRIGHT_RED}│ {Color.BOLD}Location:{Color.RESET} {Color.CYAN}{last_frame.filename}{Color.RESET}:{Color.YELLOW}{last_frame.lineno}{Color.RESET}", + file=sys.stderr, + ) + + if last_frame.line: + # Code that caused error + print( + f"{Color.BRIGHT_RED}│ {Color.BOLD}Code:{Color.RESET} {Color.BRIGHT_WHITE}{last_frame.line.strip()}{Color.RESET}", + file=sys.stderr, + ) + + print(f"{Color.BRIGHT_RED}└{'─' * 99}{Color.RESET}", file=sys.stderr) + + +class LoggerFactory: + """Factory to create loggers with different API names.""" + + _loggers = {} + + @classmethod + def get_logger(cls, api_name: str) -> Logger: + """Get or create logger for API name. + + Args: + api_name: Name of the API/module + + Returns: + Logger instance + """ + if api_name not in cls._loggers: + cls._loggers[api_name] = Logger(api_name) + return cls._loggers[api_name] + + +# Convenience function for getting logger +def get_logger(api_name: str) -> Logger: + """Get logger for specific API. + + Usage: + logger = get_logger("AUTH") + logger.info("User logged in") + logger.error("Login failed") + + try: + # ... code ... + except Exception as e: + logger.exception("Failed to process request", e) + + Args: + api_name: Name of the API/module (e.g., "AUTH", "FILE", "LFS") + + Returns: + Logger instance + """ + return LoggerFactory.get_logger(api_name) + + +# Pre-create common loggers +logger_auth = get_logger("AUTH") +logger_file = get_logger("FILE") +logger_lfs = get_logger("LFS") +logger_repo = get_logger("REPO") +logger_org = get_logger("ORG") +logger_settings = get_logger("SETTINGS") +logger_api = get_logger("API") +logger_db = get_logger("DB")