220 lines
6.5 KiB
Python
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
|