Source code for kelvin.sdk.logging

"""Logging configuration using structlog + stdlib logging.

This module provides structured logging that:
- Uses structlog for nice API (key-value logging)
- Routes through stdlib logging for library compatibility
- Outputs human-readable logs in interactive mode
- Outputs JSON logs when --json flag is used
- Always logs to stderr (never pollutes stdout)

Usage in commands/:
    import structlog

    # Use __name__ to get logger under kelvin.* namespace
    logger = structlog.get_logger(__name__)

    def my_function():
        logger.info("doing something", workload_id="w1", action="delete")
"""

import logging
import sys

import structlog
from structlog.types import Processor

# Shared processors for both structlog and stdlib.
# Defined at module level so configure_structlog() and setup_logging()
# use the exact same chain.
SHARED_PROCESSORS: list[Processor] = [
    structlog.contextvars.merge_contextvars,
    structlog.stdlib.add_log_level,
    structlog.stdlib.PositionalArgumentsFormatter(),
    structlog.processors.TimeStamper(fmt="iso"),
    structlog.processors.StackInfoRenderer(),
    structlog.processors.UnicodeDecoder(),
]


[docs] def configure_structlog() -> None: """Configure structlog to route through stdlib logging. MUST be called before any kelvin modules are imported, so that module-level ``structlog.get_logger()`` calls create lazy proxies that carry the correct processors and logger factory. This is format-agnostic — the JSON vs console choice is made later in ``setup_logging()`` on the stdlib handler's ProcessorFormatter. """ structlog.configure( processors=[ *SHARED_PROCESSORS, # Prepare event dict for stdlib ProcessorFormatter structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, )
[docs] def setup_logging( verbose: bool = False, json_mode: bool = False, ) -> None: """Configure stdlib logging handler and format. Log level from ``-v`` only affects ``kelvin.*`` loggers. The root logger stays at WARNING so third-party libs stay quiet. Args: verbose: Enable debug logging for kelvin.* loggers. json_mode: If True, output logs as JSON. Otherwise human-readable. """ kelvin_level = logging.DEBUG if verbose else logging.WARNING if json_mode: renderer: Processor = structlog.processors.JSONRenderer() else: renderer = structlog.dev.ConsoleRenderer( colors=sys.stderr.isatty(), exception_formatter=structlog.dev.plain_traceback, ) formatter = structlog.stdlib.ProcessorFormatter( foreign_pre_chain=list(SHARED_PROCESSORS), processors=[ structlog.stdlib.ProcessorFormatter.remove_processors_meta, renderer, ], ) # Single handler shared by root and kelvin loggers handler = logging.StreamHandler(sys.stderr) handler.setFormatter(formatter) handler.setLevel(logging.DEBUG) # Wide open — filtering is on the loggers # Root logger: always WARNING — keeps third-party libs quiet. # Propagated records bypass this level, so kelvin.* is unaffected. root_logger = logging.getLogger() root_logger.handlers.clear() root_logger.addHandler(handler) root_logger.setLevel(logging.WARNING) # kelvin.* logger: level controlled by -v flag only kelvin_logger = logging.getLogger("kelvin") kelvin_logger.handlers.clear() kelvin_logger.addHandler(handler) kelvin_logger.setLevel(kelvin_level) kelvin_logger.propagate = False # Suppress noisy third-party loggers noisy_loggers = [ "mlflow", "mlflow.tracking", "mlflow.store", "mlflow.models", "urllib3", "httpx", "httpcore", "hpack", "docker", "botocore", "boto3", "azure", "filelock", "keyring", ] for logger_name in noisy_loggers: lib_logger = logging.getLogger(logger_name) lib_logger.setLevel(logging.ERROR) lib_logger.propagate = False