jackkuo's picture
add QA
79899c0
"""
BioLogger - A comprehensive logging utility for the bio RAG server.
This module provides a centralized logging system with correlation ID support,
structured logging, and configurable output handlers.
"""
import sys
import traceback
from pathlib import Path
from typing import Any, Optional
from asgi_correlation_id import correlation_id
from loguru import logger
class BioLogger:
"""
Enhanced logging utility with correlation ID support and structured logging.
This class provides a unified interface for logging with automatic
correlation ID binding and comprehensive error tracking.
"""
def __init__(self, log_dir: str = "logs", max_retention_days: int = 30):
"""
Initialize the BioLogger.
Args:
log_dir: Directory to store log files
max_retention_days: Maximum number of days to retain log files
"""
self.log_dir = Path(log_dir)
self.max_retention_days = max_retention_days
self._setup_logging()
def _setup_logging(self) -> None:
"""Configure loguru logger with handlers."""
# Remove default handler
logger.remove()
# Create log directory
self.log_dir.mkdir(exist_ok=True)
# Terminal handler
logger.add(
sys.stderr,
format=self._get_format_string(),
level="INFO",
colorize=True,
backtrace=True,
diagnose=True,
)
# File handlers
log_file = self.log_dir / "bio_rag_{time:YYYY-MM-DD}.log"
# Info level file handler
logger.add(
str(log_file),
format=self._get_format_string(),
level="INFO",
rotation="1 day",
retention=f"{self.max_retention_days} days",
compression="zip",
backtrace=True,
diagnose=True,
)
# Error level file handler
logger.add(
str(log_file),
format=self._get_format_string(),
level="ERROR",
rotation="1 day",
retention=f"{self.max_retention_days} days",
compression="zip",
backtrace=True,
diagnose=True,
)
def _get_format_string(self) -> str:
"""Get the log format string with correlation ID."""
return "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | [CID:{extra[correlation_id]}] | {name}:{function}:{line} | {message}"
def _get_correlation_id(self) -> str:
"""Get the current correlation ID or return SYSTEM."""
return correlation_id.get() or "SYSTEM"
def _bind_logger(self):
"""Bind logger with current correlation ID."""
return logger.bind(correlation_id=self._get_correlation_id())
def debug(self, message: str, **kwargs: Any) -> None:
"""
Log a debug message.
Args:
message: The message to log
**kwargs: Additional context data
"""
self._bind_logger().debug(message, **kwargs)
def info(self, message: str, **kwargs: Any) -> None:
"""
Log an info message.
Args:
message: The message to log
**kwargs: Additional context data
"""
self._bind_logger().info(message, **kwargs)
def warning(self, message: str, **kwargs: Any) -> None:
"""
Log a warning message.
Args:
message: The message to log
**kwargs: Additional context data
"""
self._bind_logger().warning(message, **kwargs)
def error(
self, message: str, exc_info: Optional[Exception] = None, **kwargs: Any
) -> None:
"""
Log an error message with optional exception information.
Args:
message: The error message
exc_info: Optional exception object for detailed error tracking
**kwargs: Additional context data
"""
if exc_info is not None:
error_details = self._format_exception_details(message, exc_info)
self._bind_logger().error(error_details, **kwargs)
else:
self._bind_logger().error(message, **kwargs)
def critical(
self, message: str, exc_info: Optional[Exception] = None, **kwargs: Any
) -> None:
"""
Log a critical error message.
Args:
message: The critical error message
exc_info: Optional exception object for detailed error tracking
**kwargs: Additional context data
"""
if exc_info is not None:
error_details = self._format_exception_details(message, exc_info)
self._bind_logger().critical(error_details, **kwargs)
else:
self._bind_logger().critical(message, **kwargs)
def _format_exception_details(self, message: str, exc_info: Exception) -> str:
"""
Format exception details for logging.
Args:
message: The base error message
exc_info: The exception object
Returns:
Formatted error details string
"""
exc_type = exc_info.__class__.__name__
exc_message = str(exc_info)
# Get stack trace
stack_trace = []
if exc_info.__traceback__:
tb_list = traceback.extract_tb(exc_info.__traceback__)
for tb in tb_list:
stack_trace.append(
f" File: {tb.filename}, "
f"Line: {tb.lineno}, "
f"Function: {tb.name}"
)
# Format error details
error_details = [
f"Error Message: {message}",
f"Exception Type: {exc_type}",
f"Exception Details: {exc_message}",
]
if stack_trace:
error_details.append("Stack Trace:")
error_details.extend(stack_trace)
return "\n".join(error_details)
def log_performance(self, operation: str, duration: float, **kwargs: Any) -> None:
"""
Log performance metrics.
Args:
operation: Name of the operation
duration: Duration in seconds
**kwargs: Additional performance metrics
"""
message = f"Performance: {operation} took {duration:.3f}s"
if kwargs:
metrics = ", ".join(f"{k}={v}" for k, v in kwargs.items())
message += f" | {metrics}"
self.info(message)
def log_api_call(
self, method: str, url: str, status_code: int, duration: float
) -> None:
"""
Log API call details.
Args:
method: HTTP method
url: API endpoint URL
status_code: HTTP status code
duration: Request duration in seconds
"""
level = "error" if status_code >= 400 else "info"
message = f"API Call: {method} {url} -> {status_code} ({duration:.3f}s)"
if level == "error":
self.error(message)
else:
self.info(message)
def log_database_operation(
self, operation: str, table: str, duration: float, **kwargs: Any
) -> None:
"""
Log database operation details.
Args:
operation: Database operation (SELECT, INSERT, etc.)
table: Table name
duration: Operation duration in seconds
**kwargs: Additional operation details
"""
message = f"Database: {operation} on {table} took {duration:.3f}s"
if kwargs:
details = ", ".join(f"{k}={v}" for k, v in kwargs.items())
message += f" | {details}"
self.info(message)
# Create singleton instance
bio_logger = BioLogger()