Skip to content

Logging

pyclif ships a Rich-enhanced logging system with a custom TRACE level (5), automatic secrets masking, and rotating file handler support.

get_logger

Main factory. Returns a logger pre-configured with Rich formatting.

pyclif.get_logger(name=None)

Factory function for creating loggers with Rich capabilities.

This function uses the global configuration system to provide loggers that automatically benefit from Rich enhancements.

Parameters:

Name Type Description Default
name str

Logger name (optional).

None

Returns:

Type Description
SupportsTraceLogger

Logger configured with Rich capabilities.

Source code in src/pyclif/core/log/__init__.py
def get_logger(name: str = None) -> SupportsTraceLogger:
    """Factory function for creating loggers with Rich capabilities.

    This function uses the global configuration system to provide
    loggers that automatically benefit from Rich enhancements.

    Args:
        name: Logger name (optional).

    Returns:
        Logger configured with Rich capabilities.
    """
    from .config import get_configured_logger

    return get_configured_logger(name)

get_configured_logger

Returns a logger with full configuration applied (handlers, level, masker).

pyclif.get_configured_logger(name=None)

Get a logger that automatically benefits from global Rich configuration.

This function simply retrieves a standard logger that automatically inherits the global Rich configuration.

Parameters:

Name Type Description Default
name str

Logger name (optional).

None

Returns:

Type Description
SupportsTraceLogger

Logger automatically configured with Rich capabilities.

Source code in src/pyclif/core/log/config.py
def get_configured_logger(name: str = None) -> SupportsTraceLogger:
    """Get a logger that automatically benefits from global Rich configuration.

    This function simply retrieves a standard logger that automatically
    inherits the global Rich configuration.

    Args:
        name: Logger name (optional).

    Returns:
        Logger automatically configured with Rich capabilities.
    """
    from pyclif import __app_name__

    return logging.getLogger(name or __app_name__)  # type: ignore[return-value]

configure_rich_logging

Low-level setup function. Called internally by get_configured_logger.

pyclif.configure_rich_logging(use_rich=True, rich_tracebacks=True, enable_secrets_filter=True, force_reconfigure=False)

Configure the Rich logging system once and centrally.

This function sets up the entire logging system in one place: - Global configuration via extraBasicConfig - Adds trace() method to all Python loggers - Prevents multiple configurations with built-in checks

Parameters:

Name Type Description Default
use_rich bool

Enable Rich logging capabilities.

True
rich_tracebacks bool

Enable Rich tracebacks for exceptions.

True
enable_secrets_filter bool

Enable automatic secrets filtering.

True
force_reconfigure bool

Force reconfiguration even if already configured.

False
Source code in src/pyclif/core/log/config.py
def configure_rich_logging(
    use_rich: bool = True,
    rich_tracebacks: bool = True,
    enable_secrets_filter: bool = True,
    force_reconfigure: bool = False,
) -> None:
    """Configure the Rich logging system once and centrally.

    This function sets up the entire logging system in one place:
    - Global configuration via extraBasicConfig
    - Adds trace() method to all Python loggers
    - Prevents multiple configurations with built-in checks

    Args:
        use_rich: Enable Rich logging capabilities.
        rich_tracebacks: Enable Rich tracebacks for exceptions.
        enable_secrets_filter: Enable automatic secrets filtering.
        force_reconfigure: Force reconfiguration even if already configured.
    """
    # Check if Rich configuration is already active
    if not force_reconfigure:
        root_logger = logging.getLogger()
        has_rich_handler = any(
            hasattr(handler, "_rich_handler") for handler in root_logger.handlers
        )
        if has_rich_handler:
            return  # Already configured, do nothing

    if use_rich and rich_tracebacks:
        from rich.traceback import install as install_rich_traceback

        # noinspection PyArgumentEqualDefault
        install_rich_traceback(show_locals=False)

    if use_rich:
        shared_handler = RichExtraStreamHandler(
            rich_tracebacks=rich_tracebacks,
            enable_secrets_filter=enable_secrets_filter,
        )
        shared_handler.setFormatter(RichExtraFormatter())

        # Save existing file handlers before force configuration wipes them
        root_logger = logging.getLogger()
        file_handlers = [h for h in root_logger.handlers if isinstance(h, TimedRotatingFileHandler)]

        _preconfigure_click_extra_logger(shared_handler)

        extraBasicConfig(
            stream_handler_class=RichExtraStreamHandler,
            formatter_class=RichExtraFormatter,
            force=True,
        )

        # Replace the generic handler added by extraBasicConfig with our configured shared instance
        root_logger = logging.getLogger()
        for h in list(root_logger.handlers):
            if type(h) is RichExtraStreamHandler and h is not shared_handler:
                root_logger.removeHandler(h)

        if shared_handler not in root_logger.handlers:  # pragma: no branch
            root_logger.addHandler(shared_handler)

        # Restore file handlers
        for h in file_handlers:
            if h not in root_logger.handlers:  # pragma: no branch
                root_logger.addHandler(h)

    add_trace_method(logging.Logger)

add_trace_method

Patches a logger instance with a .trace() method at level 5.

pyclif.add_trace_method(logger_class)

Add a trace method to a logger class.

Parameters:

Name Type Description Default
logger_class type

Logger class to extend.

required

Returns:

Type Description
type

The updated logger class.

Source code in src/pyclif/core/log/levels.py
def add_trace_method(logger_class: type) -> type:
    """Add a trace method to a logger class.

    Args:
        logger_class: Logger class to extend.

    Returns:
        The updated logger class.
    """

    # noinspection PyIncorrectDocstring
    def trace(self, msg, *args, **kwargs):
        """Log a message at TRACE level.

        Args:
            msg: Message to log.
            *args: Additional positional arguments for formatting.
            **kwargs: Additional keyword arguments for logging.
        """
        if self.isEnabledFor(TRACE):
            self._log(TRACE, msg, args, **kwargs)

    logger_class.trace = trace
    return logger_class

SecretsMasker

Log filter that redacts sensitive values from log records.

pyclif.SecretsMasker

Bases: Filter

Redact secrets from logs

Source code in src/pyclif/core/log/filters.py
class SecretsMasker(logging.Filter):
    """Redact secrets from logs"""

    ALREADY_FILTERED_FLAG = "__SecretsMasker_filtered"

    def __init__(self):
        """Initialize the filter with the default sensitive field regex."""
        super().__init__()
        self.replacer = REGEX_SENSITIVE_FIELDS

    def filter(self, record: logging.LogRecord) -> bool:
        """Filter a log record by redacting sensitive values.

        Checks whether the record was already processed to avoid duplicate
        redaction. If not processed, applies redaction to all record fields
        containing sensitive data.

        Args:
            record: The log record to filter.

        Returns:
            True if the record was successfully filtered or already processed,
            False if no replacer is configured.
        """
        record_dict = record.__dict__

        if self.ALREADY_FILTERED_FLAG in record_dict:
            return True

        if self.replacer:
            for k, v in record_dict.items():
                record_dict[k] = self.redact(v)
        else:
            return False

        record_dict[self.ALREADY_FILTERED_FLAG] = True
        return True

    def _redact_all(self, item, depth):
        """Recursively replace all string values with a censored placeholder."""
        if isinstance(item, str):
            return "*CENSORED*"
        if isinstance(item, dict):
            return {
                dict_key: self._redact_all(nested_item, depth + 1)
                for dict_key, nested_item in item.items()
            }
        elif isinstance(item, (tuple, set)):
            # Turn set in to tuple!
            return tuple(self._redact_all(nested_item, depth + 1) for nested_item in item)
        elif isinstance(item, list):
            return [self._redact_all(nested_item, depth + 1) for nested_item in item]
        else:
            return item

    def _redact(self, item, name, depth):
        """Recursively redact sensitive fields from a value based on the key name."""
        # Avoid spending too much effort on redacting on deeply nested
        # structures. This also avoids infinite recursion if a structure has
        # a reference to self.
        try:
            if name and should_hide_value_for_key(name):
                return self._redact_all(item, depth)
            if isinstance(item, dict):
                return {
                    dict_key: self._redact(nested_item, name=dict_key, depth=(depth + 1))
                    for dict_key, nested_item in item.items()
                }
            elif isinstance(item, tuple) and hasattr(item, "_asdict") and hasattr(item, "_fields"):
                named_tuple = item.__class__.__name__
                # noinspection PyProtectedMember
                item = item._asdict()
                masked_dict = {
                    dict_key: self._redact(nested_item, name=dict_key, depth=(depth + 1))
                    for dict_key, nested_item in item.items()
                }
                # noinspection PyArgumentList
                return namedtuple(named_tuple, masked_dict.keys())(**masked_dict)
            elif isinstance(item, str):
                return item
            elif isinstance(item, (tuple, set)):
                # Turn set in to tuple!
                return tuple(
                    self._redact(nested_item, name=None, depth=(depth + 1)) for nested_item in item
                )
            elif isinstance(item, list):
                return [
                    self._redact(nested_element, name=None, depth=(depth + 1))
                    for nested_element in item
                ]
            else:
                return item
        except Exception as e:
            logging.warning(
                "Unable to redact %s. Error was: %s: %s",
                repr(item),
                type(e).__name__,
                str(e),
            )

            return item

    def redact(self, item: Any, name: str | None = None) -> Any:
        """Redact sensitive information from the given input data.

        Processes the provided item and optionally uses the name for context
        during redaction to securely obfuscate sensitive fields.

        Args:
            item: The data object to be redacted.
            name: An optional name providing context for the redaction process.

        Returns:
            The redacted version of the input data.
        """
        return self._redact(item, name, depth=0)

__init__()

Initialize the filter with the default sensitive field regex.

Source code in src/pyclif/core/log/filters.py
def __init__(self):
    """Initialize the filter with the default sensitive field regex."""
    super().__init__()
    self.replacer = REGEX_SENSITIVE_FIELDS

filter(record)

Filter a log record by redacting sensitive values.

Checks whether the record was already processed to avoid duplicate redaction. If not processed, applies redaction to all record fields containing sensitive data.

Parameters:

Name Type Description Default
record LogRecord

The log record to filter.

required

Returns:

Type Description
bool

True if the record was successfully filtered or already processed,

bool

False if no replacer is configured.

Source code in src/pyclif/core/log/filters.py
def filter(self, record: logging.LogRecord) -> bool:
    """Filter a log record by redacting sensitive values.

    Checks whether the record was already processed to avoid duplicate
    redaction. If not processed, applies redaction to all record fields
    containing sensitive data.

    Args:
        record: The log record to filter.

    Returns:
        True if the record was successfully filtered or already processed,
        False if no replacer is configured.
    """
    record_dict = record.__dict__

    if self.ALREADY_FILTERED_FLAG in record_dict:
        return True

    if self.replacer:
        for k, v in record_dict.items():
            record_dict[k] = self.redact(v)
    else:
        return False

    record_dict[self.ALREADY_FILTERED_FLAG] = True
    return True

redact(item, name=None)

Redact sensitive information from the given input data.

Processes the provided item and optionally uses the name for context during redaction to securely obfuscate sensitive fields.

Parameters:

Name Type Description Default
item Any

The data object to be redacted.

required
name str | None

An optional name providing context for the redaction process.

None

Returns:

Type Description
Any

The redacted version of the input data.

Source code in src/pyclif/core/log/filters.py
def redact(self, item: Any, name: str | None = None) -> Any:
    """Redact sensitive information from the given input data.

    Processes the provided item and optionally uses the name for context
    during redaction to securely obfuscate sensitive fields.

    Args:
        item: The data object to be redacted.
        name: An optional name providing context for the redaction process.

    Returns:
        The redacted version of the input data.
    """
    return self._redact(item, name, depth=0)

RichExtraFormatter / RichExtraStreamHandler

Rich-aware formatter and stream handler. Wired up automatically by configure_rich_logging.

pyclif.RichExtraFormatter

Bases: ExtraFormatter

Enhanced ExtraFormatter with Rich text capabilities and TRACE level support.

Extends click-extra's ExtraFormatter to support Rich markup and custom TRACE level while preserving a click-extra's colorization system.

Source code in src/pyclif/core/log/formatters.py
class RichExtraFormatter(ExtraFormatter):
    """Enhanced ExtraFormatter with Rich text capabilities and TRACE level support.

    Extends click-extra's ExtraFormatter to support Rich markup and custom TRACE level
    while preserving a click-extra's colorization system.
    """

    def formatMessage(self, record: logging.LogRecord) -> str:
        """Enhanced formatting with Rich support and TRACE level.

        Args:
            record: LogRecord to format.

        Returns:
            Formatted message string.
        """
        # Handle TRACE level coloring
        if record.levelno == TRACE:
            # Style TRACE level with a custom color (dim blue, for example)
            record.levelname = click.style("TRACE", fg="blue", dim=True)
        else:
            # Let the parent handle click-extra's standard colorization
            level = record.levelname.lower()
            level_style = getattr(default_theme, level, None)
            if level_style:
                record.levelname = level_style(record.levelname.upper())

        # Let parent handle the standard formatting
        message = super().formatMessage(record)

        # Add Rich enhancements if needed
        if hasattr(record, "rich") and record.rich:
            # Allow records to specify Rich formatting
            rich_text = Text.from_markup(record.getMessage())
            record.msg = rich_text.markup

        return message

formatMessage(record)

Enhanced formatting with Rich support and TRACE level.

Parameters:

Name Type Description Default
record LogRecord

LogRecord to format.

required

Returns:

Type Description
str

Formatted message string.

Source code in src/pyclif/core/log/formatters.py
def formatMessage(self, record: logging.LogRecord) -> str:
    """Enhanced formatting with Rich support and TRACE level.

    Args:
        record: LogRecord to format.

    Returns:
        Formatted message string.
    """
    # Handle TRACE level coloring
    if record.levelno == TRACE:
        # Style TRACE level with a custom color (dim blue, for example)
        record.levelname = click.style("TRACE", fg="blue", dim=True)
    else:
        # Let the parent handle click-extra's standard colorization
        level = record.levelname.lower()
        level_style = getattr(default_theme, level, None)
        if level_style:
            record.levelname = level_style(record.levelname.upper())

    # Let parent handle the standard formatting
    message = super().formatMessage(record)

    # Add Rich enhancements if needed
    if hasattr(record, "rich") and record.rich:
        # Allow records to specify Rich formatting
        rich_text = Text.from_markup(record.getMessage())
        record.msg = rich_text.markup

    return message

pyclif.RichExtraStreamHandler

Bases: ExtraStreamHandler

Enhanced ExtraStreamHandler with Rich support and built-in security filtering.

Extends click-extra's ExtraStreamHandler to use Rich for beautiful logging while maintaining compatibility with click.echo() and color support. Automatically includes SecretsMasker for sensitive data protection.

Source code in src/pyclif/core/log/handlers.py
class RichExtraStreamHandler(ExtraStreamHandler):
    """Enhanced ExtraStreamHandler with Rich support and built-in security filtering.

    Extends click-extra's ExtraStreamHandler to use Rich for beautiful logging
    while maintaining compatibility with click.echo() and color support.
    Automatically includes SecretsMasker for sensitive data protection.
    """

    def __init__(
        self,
        stream: TextIOBase | None = None,
        rich_tracebacks: bool = True,
        enable_secrets_filter: bool = True,
        **kwargs: Any,
    ) -> None:
        """Initialize the Rich Extra Stream Handler.

        Args:
            stream: Output stream (defaults to sys.stderr).
            rich_tracebacks: Enable Rich tracebacks.
            enable_secrets_filter: Enable automatic secrets filtering.
            **kwargs: Additional keyword arguments passed to RichHandler.
        """
        # Initialize the parent ExtraStreamHandler
        super().__init__(stream or sys.stderr)

        # Create Rich console that uses the same stream
        self.rich_console = Console(
            file=self.stream,
            stderr=(self.stream == sys.stderr),
        )

        # Create Rich handler for advanced features
        self._rich_handler = RichHandler(
            console=self.rich_console,
            rich_tracebacks=rich_tracebacks,
            **kwargs,
        )

        # Add secret filter by default
        if enable_secrets_filter:
            self.addFilter(SecretsMasker())

    def emit(self, record: logging.LogRecord) -> None:
        """Use Rich handler for enhanced output while maintaining click-extra compatibility.

        Args:
            record: LogRecord to emit.
        """
        # noinspection PyBroadException
        try:
            # Use Rich handlers emit method for better formatting
            self._rich_handler.emit(record)
        except RecursionError:
            raise
        except Exception:
            # Fallback to parent's behavior
            super().emit(record)

__init__(stream=None, rich_tracebacks=True, enable_secrets_filter=True, **kwargs)

Initialize the Rich Extra Stream Handler.

Parameters:

Name Type Description Default
stream TextIOBase | None

Output stream (defaults to sys.stderr).

None
rich_tracebacks bool

Enable Rich tracebacks.

True
enable_secrets_filter bool

Enable automatic secrets filtering.

True
**kwargs Any

Additional keyword arguments passed to RichHandler.

{}
Source code in src/pyclif/core/log/handlers.py
def __init__(
    self,
    stream: TextIOBase | None = None,
    rich_tracebacks: bool = True,
    enable_secrets_filter: bool = True,
    **kwargs: Any,
) -> None:
    """Initialize the Rich Extra Stream Handler.

    Args:
        stream: Output stream (defaults to sys.stderr).
        rich_tracebacks: Enable Rich tracebacks.
        enable_secrets_filter: Enable automatic secrets filtering.
        **kwargs: Additional keyword arguments passed to RichHandler.
    """
    # Initialize the parent ExtraStreamHandler
    super().__init__(stream or sys.stderr)

    # Create Rich console that uses the same stream
    self.rich_console = Console(
        file=self.stream,
        stderr=(self.stream == sys.stderr),
    )

    # Create Rich handler for advanced features
    self._rich_handler = RichHandler(
        console=self.rich_console,
        rich_tracebacks=rich_tracebacks,
        **kwargs,
    )

    # Add secret filter by default
    if enable_secrets_filter:
        self.addFilter(SecretsMasker())

emit(record)

Use Rich handler for enhanced output while maintaining click-extra compatibility.

Parameters:

Name Type Description Default
record LogRecord

LogRecord to emit.

required
Source code in src/pyclif/core/log/handlers.py
def emit(self, record: logging.LogRecord) -> None:
    """Use Rich handler for enhanced output while maintaining click-extra compatibility.

    Args:
        record: LogRecord to emit.
    """
    # noinspection PyBroadException
    try:
        # Use Rich handlers emit method for better formatting
        self._rich_handler.emit(record)
    except RecursionError:
        raise
    except Exception:
        # Fallback to parent's behavior
        super().emit(record)

Constants

pyclif.TRACE = 5 module-attribute

pyclif.PYCLIF_LOG_LEVELS = {None: LOG_LEVELS, 'TRACE': TRACE} module-attribute