Source code for dash_evals.utils.logging
"""Logging utilities for the dash-evals runner.
Provides file and console logging for tracing runner execution.
"""
import logging
import sys
from contextlib import contextmanager
from datetime import datetime, timezone
from pathlib import Path
from typing import TextIO
[docs]
class TeeStream:
"""A stream that writes to both the original stream and a file."""
[docs]
def __init__(self, original: TextIO, log_file: TextIO):
self.original = original
self.log_file = log_file
[docs]
def write(self, text: str) -> int:
"""Write to both streams."""
self.original.write(text)
# Strip ANSI codes for cleaner log file
clean_text = _strip_ansi(text)
self.log_file.write(clean_text)
return len(text)
[docs]
def flush(self) -> None:
"""Flush both streams."""
self.original.flush()
self.log_file.flush()
[docs]
def fileno(self) -> int:
"""Return the file descriptor of the original stream."""
return self.original.fileno()
[docs]
def isatty(self) -> bool:
"""Return whether the original stream is a tty."""
return self.original.isatty()
def _strip_ansi(text: str) -> str:
"""Remove ANSI escape codes from text."""
import re
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
return ansi_escape.sub("", text)
[docs]
@contextmanager
def capture_output(log_file_path: Path):
"""Context manager to capture stdout/stderr to a log file.
Args:
log_file_path: Path to the log file to append output to.
Yields:
None - stdout/stderr are captured during the context.
"""
# Open log file in append mode
log_file = open(log_file_path, "a", encoding="utf-8")
# Save original streams
original_stdout = sys.stdout
original_stderr = sys.stderr
try:
# Replace with tee streams
sys.stdout = TeeStream(original_stdout, log_file) # type: ignore
sys.stderr = TeeStream(original_stderr, log_file) # type: ignore
yield
finally:
# Restore original streams
sys.stdout = original_stdout
sys.stderr = original_stderr
log_file.close()
[docs]
def setup_logging(log_dir: Path, name: str = "dash_evals") -> tuple[logging.Logger, Path]:
"""Configure logging to both console and file.
Args:
log_dir: Directory to write log files
name: Logger name (default: dash_evals)
Returns:
Tuple of (configured logger instance, log file path)
"""
# Create logger
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
logger.propagate = False # Prevent duplicate output to root logger
# Clear any existing handlers (avoid duplicates)
logger.handlers.clear()
# Console handler (INFO level)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_format = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s",
datefmt="%H:%M:%S",
)
console_handler.setFormatter(console_format)
logger.addHandler(console_handler)
# File handler (DEBUG level - more verbose)
log_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d_%H-%M-%S")
log_file = log_dir / f"runner_{timestamp}.log"
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_format = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
file_handler.setFormatter(file_format)
logger.addHandler(file_handler)
logger.info(f"📝 Runner log: {log_file}")
return logger, log_file