Skip to content

EzLogger

Loguru-based file logging handler.

Overview

The EzLogger class (alias: Logger) provides file logging with rotation, retention, and compression using loguru. It offers structured log formatting with timestamps, levels, and source location.

Class Reference

EzLogger

EzLogger(log_file: Path | str, level: str = 'INFO', rotation: str | None = None, retention: str | None = None, compression: str | None = None)

Bases: LoggingHandler

File logger handler with advanced formatting and session management.

This handler provides file-based logging with: - Structured log format - Session separators - HTML tag sanitization - Automatic file creation

Initialize the file logger handler.

PARAMETER DESCRIPTION
log_file

Path to the log file

TYPE: Path | str

level

The desired logging level

TYPE: str DEFAULT: 'INFO'

rotation

Rotation size (e.g., "10 MB") or time (e.g., "1 day")

TYPE: str | None DEFAULT: None

retention

Retention period (e.g., "7 days")

TYPE: str | None DEFAULT: None

compression

Compression format (e.g., "zip", "gz")

TYPE: str | None DEFAULT: None

RAISES DESCRIPTION
ValidationError

If the provided level is invalid

FileOperationError

If file operations fail

Source code in src/ezpl/handlers/file.py
def __init__(
    self,
    log_file: Path | str,
    level: str = "INFO",
    rotation: str | None = None,
    retention: str | None = None,
    compression: str | None = None,
) -> None:
    """
    Initialize the file logger handler.

    Args:
        log_file: Path to the log file
        level: The desired logging level
        rotation: Rotation size (e.g., "10 MB") or time (e.g., "1 day")
        retention: Retention period (e.g., "7 days")
        compression: Compression format (e.g., "zip", "gz")

    Raises:
        ValidationError: If the provided level is invalid
        FileOperationError: If file operations fail
    """
    if not LogLevel.is_valid_level(level):
        raise ValidationError(f"Invalid log level: {level}", "level", level)

    self._level = level.upper()
    self._level_manually_set = False
    self._log_file = Path(log_file)
    self._logger = logger.bind(task="logger")
    self._logger_id: int | None = None
    self._rotation = rotation
    self._retention = retention
    self._compression = compression

    # Validate and create parent directory
    try:
        self._log_file.parent.mkdir(parents=True, exist_ok=True)
    except (PermissionError, OSError) as e:
        raise FileOperationError(
            f"Cannot create log directory: {e}",
            str(self._log_file.parent),
            "create_directory",
        ) from e

    # Validate that the file can be created/written
    try:
        if not self._log_file.exists():
            self._log_file.touch()
        # Write test
        with open(self._log_file, "a", encoding="utf-8") as f:
            f.write("")
    except (PermissionError, OSError) as e:
        raise FileOperationError(
            f"Cannot write to log file: {e}", str(self._log_file), "write"
        ) from e

    self._initialize_logger()

Attributes

level property

level: str

Return the current logging level.

level_manually_set property

level_manually_set: bool

Return whether level was set manually at runtime.

rotation property

rotation: str | None

Return current rotation setting.

retention property

retention: str | None

Return current retention setting.

compression property

compression: str | None

Return current compression setting.

Functions

mark_level_as_configured

mark_level_as_configured() -> None

Mark the current level as coming from configuration (not manual set).

Source code in src/ezpl/handlers/file.py
def mark_level_as_configured(self) -> None:
    """Mark the current level as coming from configuration (not manual set)."""
    self._level_manually_set = False

set_level

set_level(level: str) -> None

Set the logging level.

PARAMETER DESCRIPTION
level

The desired logging level

TYPE: str

RAISES DESCRIPTION
ValidationError

If the provided level is invalid

LoggingError

If level update fails

Source code in src/ezpl/handlers/file.py
def set_level(self, level: str) -> None:
    """
    Set the logging level.

    Args:
        level: The desired logging level

    Raises:
        ValidationError: If the provided level is invalid
        LoggingError: If level update fails
    """
    if not LogLevel.is_valid_level(level):
        raise ValidationError(f"Invalid log level: {level}", "level", level)

    old_level = self._level
    try:
        self._level = level.upper()
        self._level_manually_set = True
        self._initialize_logger()
    except Exception as e:
        self._level = old_level  # Rollback to previous level on failure
        raise LoggingError(f"Failed to update log level: {e}", "file") from e

log

log(level: str, message: Any) -> None

Log a message with the specified level.

PARAMETER DESCRIPTION
level

The log level

TYPE: str

message

The message to log (any type, will be converted to string)

TYPE: Any

RAISES DESCRIPTION
ValidationError

If the level is invalid

LoggingError

If logging fails

Source code in src/ezpl/handlers/file.py
def log(self, level: str, message: Any) -> None:
    """
    Log a message with the specified level.

    Args:
        level: The log level
        message: The message to log (any type, will be converted to string)

    Raises:
        ValidationError: If the level is invalid
        LoggingError: If logging fails
    """
    if not LogLevel.is_valid_level(level):
        raise ValidationError(f"Invalid log level: {level}", "level", level)

    # Convert message to string robustly
    message = safe_str_convert(message)

    try:
        log_method = getattr(self._logger, level.lower())
        log_method(message)
    except Exception as e:
        raise LoggingError(f"Failed to log message: {e}", "file") from e

trace

trace(message: Any, *args, **kwargs) -> None

Log a trace message.

Source code in src/ezpl/handlers/file.py
def trace(self, message: Any, *args, **kwargs) -> None:
    """Log a trace message."""
    message = safe_str_convert(message)
    self._logger.trace(message, *args, **kwargs)

debug

debug(message: Any, *args, **kwargs) -> None

Log a debug message.

Source code in src/ezpl/handlers/file.py
def debug(self, message: Any, *args, **kwargs) -> None:
    """Log a debug message."""
    message = safe_str_convert(message)
    self._logger.debug(message, *args, **kwargs)

info

info(message: Any, *args, **kwargs) -> None

Log an info message.

Source code in src/ezpl/handlers/file.py
def info(self, message: Any, *args, **kwargs) -> None:
    """Log an info message."""
    message = safe_str_convert(message)
    self._logger.info(message, *args, **kwargs)

success

success(message: Any, *args, **kwargs) -> None

Log a success message.

Source code in src/ezpl/handlers/file.py
def success(self, message: Any, *args, **kwargs) -> None:
    """Log a success message."""
    message = safe_str_convert(message)
    self._logger.success(message, *args, **kwargs)

warning

warning(message: Any, *args, **kwargs) -> None

Log a warning message.

Source code in src/ezpl/handlers/file.py
def warning(self, message: Any, *args, **kwargs) -> None:
    """Log a warning message."""
    message = safe_str_convert(message)
    self._logger.warning(message, *args, **kwargs)

warn

warn(message: Any, *args, **kwargs) -> None

Alias for warning(). Log a warning message.

Source code in src/ezpl/handlers/file.py
def warn(self, message: Any, *args, **kwargs) -> None:
    """Alias for warning(). Log a warning message."""
    self.warning(message, *args, **kwargs)

error

error(message: Any, *args, **kwargs) -> None

Log an error message.

Source code in src/ezpl/handlers/file.py
def error(self, message: Any, *args, **kwargs) -> None:
    """Log an error message."""
    message = safe_str_convert(message)
    self._logger.error(message, *args, **kwargs)

critical

critical(message: Any, *args, **kwargs) -> None

Log a critical message.

Source code in src/ezpl/handlers/file.py
def critical(self, message: Any, *args, **kwargs) -> None:
    """Log a critical message."""
    message = safe_str_convert(message)
    self._logger.critical(message, *args, **kwargs)

exception

exception(message: Any, *args, **kwargs) -> None

Log an exception with traceback.

Source code in src/ezpl/handlers/file.py
def exception(self, message: Any, *args, **kwargs) -> None:
    """Log an exception with traceback."""
    message = safe_str_convert(message)
    self._logger.exception(message, *args, **kwargs)

bind

bind(**kwargs: Any) -> Any

Bind context variables to the logger.

Source code in src/ezpl/handlers/file.py
def bind(self, **kwargs: Any) -> Any:
    """Bind context variables to the logger."""
    return self._logger.bind(**kwargs)

opt

opt(**kwargs: Any) -> Any

Configure logger options.

Source code in src/ezpl/handlers/file.py
def opt(self, **kwargs: Any) -> Any:
    """Configure logger options."""
    return self._logger.opt(**kwargs)

patch

patch(patcher: Any) -> Any

Patch log records.

Source code in src/ezpl/handlers/file.py
def patch(self, patcher: Any) -> Any:
    """Patch log records."""
    return self._logger.patch(patcher)

get_loguru

get_loguru() -> Logger

Get the underlying Loguru logger instance for advanced usage.

Returns:

* loguru.Logger: The loguru logger instance

Raises:

* LoggingError: If the logger is not initialized
Source code in src/ezpl/handlers/file.py
def get_loguru(self) -> LoguruLogger:
    """
    Get the underlying Loguru logger instance for advanced usage.

    **Returns:**

        * loguru.Logger: The loguru logger instance

    **Raises:**

        * LoggingError: If the logger is not initialized
    """
    if not self._logger:
        raise LoggingError("File logger not initialized", "file")
    return self._logger  # type: ignore[return-value]

get_log_file

get_log_file() -> Path

Get the current log file path.

RETURNS DESCRIPTION
Path

Path to the log file

Source code in src/ezpl/handlers/file.py
def get_log_file(self) -> Path:
    """
    Get the current log file path.

    Returns:
        Path to the log file
    """
    return self._log_file

get_file_size

get_file_size() -> int

Get the current log file size in bytes.

RETURNS DESCRIPTION
int

File size in bytes, or 0 if file doesn't exist or error occurs

Source code in src/ezpl/handlers/file.py
def get_file_size(self) -> int:
    """
    Get the current log file size in bytes.

    Returns:
        File size in bytes, or 0 if file doesn't exist or error occurs
    """
    try:
        if self._log_file.exists():
            return self._log_file.stat().st_size
        return 0
    except Exception:
        return 0

close

close() -> None

Close the logger handler and release file handles.

This method removes the loguru handler to release file handles, which is especially important on Windows where files can remain locked.

Source code in src/ezpl/handlers/file.py
def close(self) -> None:
    """
    Close the logger handler and release file handles.

    This method removes the loguru handler to release file handles,
    which is especially important on Windows where files can remain locked.
    """
    try:
        # Remove existing handler if any
        logger_id: int | None = self._logger_id
        if logger_id is not None:
            # loguru.remove() synchronously flushes and closes the file handle
            self._logger.remove(logger_id)
            self._logger_id = None
    except Exception as e:
        raise LoggingError("Failed to close logger", "file") from e

add_separator

add_separator() -> None

Add a separator line to the log file for session distinction.

RAISES DESCRIPTION
FileOperationError

If writing to the log file fails

Source code in src/ezpl/handlers/file.py
def add_separator(self) -> None:
    """
    Add a separator line to the log file for session distinction.

    Raises:
        FileOperationError: If writing to the log file fails
    """
    try:
        current_time = datetime.now().strftime("%Y-%m-%d - %H:%M")
        separator = f"\n\n## ==> {current_time}\n## /////////////////////////////////////////////////////////////////\n"
        with open(self._log_file, "a", encoding="utf-8") as log_file:
            log_file.write(separator)
    except Exception as e:
        raise FileOperationError(
            f"Failed to add separator to log file: {e}",
            str(self._log_file),
            "write",
        ) from e