Source code for rheojax.logging.handlers

"""
RheoJAX Log Handlers.

Custom handlers for console, file, and memory-based logging.
"""

import logging
import sys
from logging.handlers import MemoryHandler, RotatingFileHandler
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from rheojax.logging.config import LogConfig


[docs] class RheoJAXStreamHandler(logging.StreamHandler): """Enhanced stream handler with flush control. Provides immediate flushing for interactive use and buffered output for batch processing. """
[docs] def __init__(self, stream=None, immediate_flush: bool = True) -> None: """Initialize the handler. Args: stream: Output stream (default: sys.stderr) immediate_flush: Flush after each log message """ super().__init__(stream or sys.stderr) self.immediate_flush = immediate_flush
[docs] def emit(self, record: logging.LogRecord) -> None: """Emit a log record. Args: record: LogRecord to emit. """ try: super().emit(record) if self.immediate_flush: self.flush() except Exception: self.handleError(record)
[docs] class RheoJAXRotatingFileHandler(RotatingFileHandler): """Enhanced rotating file handler with UTF-8 encoding. Automatically handles log rotation and maintains backup files. """
[docs] def __init__( self, filename: Path | str, max_bytes: int = 10_000_000, backup_count: int = 5, encoding: str = "utf-8", ) -> None: """Initialize the rotating file handler. Args: filename: Path to log file. max_bytes: Maximum file size before rotation (default 10MB). backup_count: Number of backup files to keep (default 5). encoding: File encoding (default UTF-8). """ # Ensure parent directory exists Path(filename).parent.mkdir(parents=True, exist_ok=True) super().__init__( filename=str(filename), maxBytes=max_bytes, backupCount=backup_count, encoding=encoding, )
[docs] class RheoJAXMemoryHandler(MemoryHandler): """Memory handler for buffered logging. Useful for batch operations where you want to collect logs and flush them periodically or at the end of an operation. """
[docs] def __init__( self, capacity: int = 1000, flush_level: int = logging.ERROR, target: logging.Handler | None = None, ) -> None: """Initialize the memory handler. Args: capacity: Number of log records to buffer. flush_level: Level that triggers immediate flush. target: Target handler to flush to. """ super().__init__(capacity=capacity, flushLevel=flush_level, target=target)
[docs] def shouldFlush(self, record: logging.LogRecord) -> bool: """Check if buffer should be flushed. Extends stdlib behavior: when no target is set, caps the buffer at capacity by dropping the oldest records to prevent unbounded memory growth. With a target, delegates to stdlib MemoryHandler. Args: record: Current log record. Returns: True if buffer should be flushed. """ if self.target is None and len(self.buffer) >= self.capacity: self.buffer = self.buffer[-(self.capacity - 1) :] return False return super().shouldFlush(record)
class NullHandler(logging.NullHandler): """Null handler that discards all log records. Useful for library mode where the user hasn't configured logging. """ pass def create_handlers(config: "LogConfig") -> list[logging.Handler]: """Create handlers based on configuration. Args: config: LogConfig instance. Returns: List of configured handlers. """ handlers: list[logging.Handler] = [] # Console handler if config.console: console_handler = RheoJAXStreamHandler(stream=sys.stderr, immediate_flush=True) handlers.append(console_handler) # File handler if config.file: file_handler = RheoJAXRotatingFileHandler( filename=config.file, max_bytes=config.file_max_bytes, backup_count=config.file_backup_count, ) handlers.append(file_handler) # If no handlers configured, add a null handler if not handlers: handlers.append(NullHandler()) return handlers