TradingAgents/tradingagents/utils/logging_config.py

220 lines
6.5 KiB
Python

"""
Dual-Output Logging Configuration.
This module provides logging configuration that outputs to both:
1. Terminal (console) with Rich formatting
2. Rotating log files (5MB rotation, 3 backups)
Features:
- Terminal logging at INFO level by default
- File logging at DEBUG level by default
- Automatic log rotation at 5MB
- API key sanitization in log messages
- Log file creation in TRADINGAGENTS_RESULTS_DIR or ./logs
Usage:
from tradingagents.utils.logging_config import setup_dual_logger
logger = setup_dual_logger(
name="tradingagents",
log_file="./logs/tradingagents.log"
)
logger.info("This goes to both terminal and file")
logger.debug("This only goes to file")
# API keys are automatically sanitized
logger.error("Error with key sk-1234567890") # Logged as [REDACTED-API-KEY]
"""
import logging
import os
import re
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Optional
try:
from rich.logging import RichHandler
RICH_AVAILABLE = True
except ImportError:
RICH_AVAILABLE = False
# API key patterns to sanitize
API_KEY_PATTERNS = [
(re.compile(r'sk-[a-zA-Z0-9\-_]+'), '[REDACTED-API-KEY]'), # OpenAI keys
(re.compile(r'sk-or-v\d+-[a-zA-Z0-9\-_]+'), '[REDACTED-API-KEY]'), # OpenRouter keys
(re.compile(r'sk-ant-[a-zA-Z0-9\-_]+'), '[REDACTED-API-KEY]'), # Anthropic keys
(re.compile(r'sk-proj-[a-zA-Z0-9\-_]+'), '[REDACTED-API-KEY]'), # OpenAI project keys
(re.compile(r'Bearer\s+[A-Za-z0-9+/\-_.=]+'), 'Bearer [REDACTED-TOKEN]'), # Bearer tokens (incl. Base64)
]
class SanitizingFilter(logging.Filter):
"""
Logging filter that sanitizes API keys and sensitive data from log messages.
"""
def filter(self, record):
"""
Sanitize the log record message.
Args:
record: LogRecord to sanitize
Returns:
bool: Always True (we modify in place, don't filter out)
"""
if record.msg:
record.msg = sanitize_log_message(str(record.msg))
# Also sanitize args if present
if record.args:
try:
sanitized_args = tuple(
sanitize_log_message(str(arg)) if isinstance(arg, str) else arg
for arg in record.args
)
record.args = sanitized_args
except (TypeError, ValueError):
# If args aren't iterable or conversion fails, leave as-is
pass
return True
def sanitize_log_message(message: Optional[str]) -> str:
"""
Remove API keys and sensitive data from log messages.
Sanitizes the following patterns:
- OpenAI API keys (sk-*)
- OpenRouter API keys (sk-or-*)
- Anthropic API keys (sk-ant-*)
- Bearer tokens
- Other common API key patterns
Args:
message: The log message to sanitize
Returns:
str: Sanitized message with API keys replaced with [REDACTED-API-KEY]
Example:
>>> sanitize_log_message("Error with key sk-1234567890")
'Error with key [REDACTED-API-KEY]'
"""
if message is None:
return ""
if not isinstance(message, str):
message = str(message)
# Escape newlines/carriage returns to prevent log injection (CWE-117)
sanitized = message.replace('\r\n', '\\r\\n').replace('\n', '\\n').replace('\r', '\\r')
for pattern, replacement in API_KEY_PATTERNS:
sanitized = pattern.sub(replacement, sanitized)
return sanitized
def setup_dual_logger(
name: str = "tradingagents",
log_file: Optional[str] = None,
console_level: int = logging.INFO,
file_level: int = logging.DEBUG,
) -> logging.Logger:
"""
Setup a logger with dual output: terminal (Rich) + rotating file.
Creates a logger that outputs to:
1. Terminal with Rich formatting (if available) or standard StreamHandler
2. Rotating file handler (5MB max size, 3 backups)
Both handlers automatically sanitize API keys and sensitive data.
Args:
name: Logger name (default: "tradingagents")
log_file: Path to log file (default: logs/tradingagents.log in results dir)
console_level: Log level for terminal output (default: INFO)
file_level: Log level for file output (default: DEBUG)
Returns:
logging.Logger: Configured logger instance
Example:
>>> logger = setup_dual_logger("my_module", "./logs/app.log")
>>> logger.info("Terminal and file")
>>> logger.debug("File only")
"""
# Create logger
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG) # Capture all levels, handlers will filter
# Clear existing handlers to prevent duplicates
logger.handlers.clear()
# Create sanitizing filter
sanitize_filter = SanitizingFilter()
# ===== Terminal Handler =====
if RICH_AVAILABLE:
# Use Rich handler for beautiful terminal output
console_handler = RichHandler(
rich_tracebacks=True,
show_time=True,
show_path=False,
)
else:
# Fall back to standard stream handler
console_handler = logging.StreamHandler()
console_handler.setLevel(console_level)
console_handler.addFilter(sanitize_filter)
# Console format (simpler for terminal)
console_formatter = logging.Formatter(
'%(message)s'
)
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
# ===== File Handler =====
# Determine log file path
if log_file is None:
# Use TRADINGAGENTS_RESULTS_DIR or default to ./logs
results_dir = os.getenv("TRADINGAGENTS_RESULTS_DIR", "./results")
log_dir = Path(results_dir) / "logs"
log_file = str(log_dir / "tradingagents.log")
# Create log directory if it doesn't exist
log_path = Path(log_file)
log_path.parent.mkdir(parents=True, exist_ok=True)
# Create rotating file handler
# 5MB max size, 3 backup files
file_handler = RotatingFileHandler(
filename=str(log_path),
maxBytes=5 * 1024 * 1024, # 5MB
backupCount=3,
encoding='utf-8',
)
file_handler.setLevel(file_level)
file_handler.addFilter(sanitize_filter)
# File format (more detailed)
file_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
# Prevent propagation to root logger
logger.propagate = False
return logger