|
|
""" |
|
|
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.""" |
|
|
|
|
|
logger.remove() |
|
|
|
|
|
|
|
|
self.log_dir.mkdir(exist_ok=True) |
|
|
|
|
|
|
|
|
logger.add( |
|
|
sys.stderr, |
|
|
format=self._get_format_string(), |
|
|
level="INFO", |
|
|
colorize=True, |
|
|
backtrace=True, |
|
|
diagnose=True, |
|
|
) |
|
|
|
|
|
|
|
|
log_file = self.log_dir / "bio_rag_{time:YYYY-MM-DD}.log" |
|
|
|
|
|
|
|
|
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, |
|
|
) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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}" |
|
|
) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
bio_logger = BioLogger() |
|
|
|