Insurance-RAG / utils /logger.py
DeltaVenom's picture
Update app code and initialize runtime databases
72bff80
"""
Centralized structured logging with rotation and request ID tracking.
"""
import os
import json
import logging
import threading
from logging.handlers import RotatingFileHandler
from typing import Optional
from datetime import datetime
from config import config
# Thread-local storage for request context
_request_context = threading.local()
class RequestContextFilter(logging.Filter):
"""Add request ID to log records."""
def filter(self, record):
record.request_id = getattr(_request_context, 'request_id', 'N/A')
record.user_ip = getattr(_request_context, 'user_ip', 'N/A')
return True
class JSONFormatter(logging.Formatter):
"""Format logs as JSON for structured logging."""
def format(self, record):
log_data = {
'timestamp': datetime.utcnow().isoformat(),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
'module': record.module,
'function': record.funcName,
'line': record.lineno,
'request_id': getattr(record, 'request_id', 'N/A'),
'user_ip': getattr(record, 'user_ip', 'N/A'),
}
# Add exception info if present
if record.exc_info:
log_data['exception'] = self.formatException(record.exc_info)
# Add extra fields
if hasattr(record, 'extra_data'):
log_data['extra'] = record.extra_data
return json.dumps(log_data)
def setup_logger(name: str, log_level: Optional[str] = None) -> logging.Logger:
"""
Create a logger with both file and console handlers.
Args:
name: Logger name (typically __name__)
log_level: Optional override for log level
Returns:
Configured logger instance
"""
logger = logging.getLogger(name)
# Avoid duplicate handlers
if logger.handlers:
return logger
# Set level
level = log_level or config.LOG_LEVEL
logger.setLevel(getattr(logging, level.upper()))
# Add request context filter
logger.addFilter(RequestContextFilter())
# Console handler (human-readable for development)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG if config.DEBUG else logging.INFO)
if config.ENVIRONMENT.value == "production":
# JSON format for production
console_handler.setFormatter(JSONFormatter())
else:
# Human-readable format for development
console_format = logging.Formatter(
'%(asctime)s - [%(request_id)s] - %(name)s - %(levelname)s - %(message)s'
)
console_handler.setFormatter(console_format)
logger.addHandler(console_handler)
# File handler with rotation
try:
log_dir = os.path.dirname(config.LOG_FILE_PATH)
if log_dir:
os.makedirs(log_dir, exist_ok=True)
file_handler = RotatingFileHandler(
config.LOG_FILE_PATH,
maxBytes=config.LOG_MAX_BYTES,
backupCount=config.LOG_BACKUP_COUNT
)
file_handler.setLevel(logging.DEBUG)
# Always use JSON format for file logs
file_handler.setFormatter(JSONFormatter())
logger.addHandler(file_handler)
except Exception as e:
logger.warning(f"Failed to setup file logging: {e}")
return logger
def set_request_context(request_id: str, user_ip: Optional[str] = None):
"""Set request context for the current thread."""
_request_context.request_id = request_id
_request_context.user_ip = user_ip or 'unknown'
def clear_request_context():
"""Clear request context for the current thread."""
if hasattr(_request_context, 'request_id'):
delattr(_request_context, 'request_id')
if hasattr(_request_context, 'user_ip'):
delattr(_request_context, 'user_ip')
def log_with_extra(logger: logging.Logger, level: str, message: str, **extra_data):
"""Log with extra structured data."""
log_method = getattr(logger, level.lower())
# Create a custom log record with extra data
if extra_data:
extra_record = {'extra_data': extra_data}
log_method(message, extra=extra_record)
else:
log_method(message)
# Create module-level loggers for common components
app_logger = setup_logger('app')
agent_logger = setup_logger('agents')
retrieval_logger = setup_logger('retrieval')
ingestion_logger = setup_logger('ingestion')
llm_logger = setup_logger('llm')
api_logger = setup_logger('api')