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 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