Source code for kstlib.logging

"""Logging module with Rich console output and async-friendly wrappers.

This module provides a LogManager class for structured logging with:

- Rich console output (colored, emoji icons, traceback with locals)
- File logging with rotation
- Preset configurations (dev, prod, debug)
- Async wrappers executed via thread pool
- Structured logging with context key=value pairs
- Opt-in lazy auto-init driven by the config file

Two ways to use LogManager:

- ``LogManager(preset="dev")`` returns an isolated instance. ``logging.getLogger("kstlib")``
  is untouched, so this form is safe for local use and tests.
- ``LogManager(preset="dev", register=True)`` (or the ``init_logging()`` alias)
  registers the instance as the global root of the ``"kstlib"`` logger hierarchy
  so that ``logging.getLogger("kstlib.foo")`` child loggers propagate records
  back to it.

Consumers who never call ``init_logging()`` can still activate internal
kstlib logs by flipping ``kstlib.logging.enabled`` in ``kstlib.conf.yml``.
The first call to ``get_logger()`` from any kstlib module then triggers
``LogManager(preset=..., register=True)`` transparently. Any error on that
cascade is swallowed silently so the library never breaks the host
application.

Example:
    >>> from kstlib.logging import get_logger
    >>> logger = get_logger(__name__)
    >>> logger.info("Server started")  # doctest: +SKIP

"""

from __future__ import annotations

import logging
import sys
from typing import Any

from kstlib.logging.manager import (
    HAS_ASYNC,
    LOGGING_LEVEL,
    SUCCESS_LEVEL,
    TRACE_LEVEL,
    LogManager,
    list_available_presets,
)

# Singleton root logger for kstlib
_root_logger: LogManager | None = None

# Default preset used when ``kstlib.logging.preset`` is missing or invalid
_DEFAULT_AUTO_INIT_PRESET = "prod"

# One-shot flag so the invalid-preset stderr notice is printed at most once
# per process even if get_logger() is called many times with the same broken
# config. Module-level so tests can reset it between runs.
_preset_warning_emitted: bool = False


[docs] def init_logging( *, preset: str | None = None, config: dict[str, Any] | None = None, ) -> LogManager: """Initialize the kstlib root logger (alias of ``LogManager(register=True)``). This is a thin backward-compatible wrapper around ``LogManager(name="kstlib", preset=..., config=..., register=True)``. The heavy lifting (logger-dict injection, ``.trace``/``.success`` monkey-patch, level propagation to pre-existing children) happens inside ``LogManager.__init__`` when ``register=True``. Call this function early in the application startup to configure logging. If it is never called explicitly, ``get_logger()`` may still trigger a silent auto-init based on ``kstlib.logging.enabled`` in ``kstlib.conf.yml``. Args: preset: Logging preset ("dev", "prod", "debug", or custom from config). config: Explicit configuration dict. Returns: The root LogManager instance (same as ``logging.getLogger("kstlib")``). Example: >>> from kstlib.logging import init_logging >>> logger = init_logging(preset="dev") # doctest: +SKIP """ global _root_logger _root_logger = LogManager(name="kstlib", preset=preset, config=config, register=True) return _root_logger
[docs] def get_logger(name: str | None = None) -> logging.Logger: """Get a logger for the given module name. Returns a child logger under the 'kstlib' namespace. When the root logger is initialized via ``init_logging()`` or CLI ``--log-level``, child loggers inherit its handlers and configuration. On first call, if no explicit ``init_logging()`` has been made, this function reads ``kstlib.logging.enabled`` from ``kstlib.conf.yml`` and triggers a silent auto-init when the flag is true. Config errors are swallowed: the cascade simply falls back to Python's default logging. Args: name: Module name (typically ``__name__``). If None, returns the root kstlib logger. Returns: A logger instance. Example: >>> from kstlib.logging import get_logger >>> logger = get_logger(__name__) >>> logger.debug("Processing item", extra={"item_id": 123}) # doctest: +SKIP """ global _root_logger, _preset_warning_emitted # Lazy opt-in auto-init: harmless no-op if already initialized or disabled. if _root_logger is None: try: from kstlib.config import get_config cfg = get_config() kstlib_section = cfg.get("kstlib") if hasattr(cfg, "get") else None # type: ignore[no-untyped-call] log_section = kstlib_section.get("logging") if kstlib_section and hasattr(kstlib_section, "get") else None if log_section and bool(log_section.get("enabled", False)): requested = log_section.get("preset") or _DEFAULT_AUTO_INIT_PRESET available = list_available_presets() if requested not in available: if not _preset_warning_emitted: sys.stderr.write( f"kstlib logging: preset '{requested}' not found, " f"available: {available}, " f"falling back to '{_DEFAULT_AUTO_INIT_PRESET}'.\n" ) _preset_warning_emitted = True requested = _DEFAULT_AUTO_INIT_PRESET _root_logger = LogManager(name="kstlib", preset=requested, register=True) except Exception as exc: # Silent by design: auto-init MUST NEVER raise. kstlib cannot # break the host application through the logging cascade. # The exception type is surfaced on the kstlib_logging_internal # logger (hors hierarchie kstlib, default WARNING and silent # unless explicitly activated) so an operator who turned that # logger on can see the lazy-init failure. logging.getLogger("kstlib_logging_internal").debug( "[INIT] Lazy auto-init failed silently: %s", type(exc).__name__, ) if name is None: # Return root logger (create if needed) if _root_logger is not None: return _root_logger return logging.getLogger("kstlib") # Ensure logger is under kstlib namespace for proper propagation logger_name = name if name.startswith("kstlib.") else f"kstlib.{name}" return logging.getLogger(logger_name)
__all__ = [ "HAS_ASYNC", "LOGGING_LEVEL", "SUCCESS_LEVEL", "TRACE_LEVEL", "LogManager", "get_logger", "init_logging", "list_available_presets", ]