reranker / src /common /logging.py
Hemprasad Badgujar
Add JD to image; logging banners; safe explain
d40bd07
Raw History Blame Contribute Delete
6.77 kB
"""Structured logging: colored console renderer + the `step()` stage timer.
Logging standard for this project: every pipeline stage runs inside ``with step("<concern>.<action>") as
metrics:`` — it logs ``step.start`` then ``step.end`` with the elapsed seconds and whatever you attach via
``metrics["key"] = value``. No ``print()`` in the engine; no engine/library names in user-facing strings.
Configuration is read from the environment (so this module has no dependency on the settings layer yet):
``RERANKER_LOG_LEVEL`` (default ``INFO``), ``RERANKER_LOG`` (set ``0`` to silence), ``NO_COLOR`` to drop
ANSI. The stdlib ``import logging`` below is an ABSOLUTE import — Python resolves top-level imports against
``sys.path``, never this same-named sibling — so ``logging.INFO`` references the standard library correctly.
"""
from __future__ import annotations
import contextlib
import logging
import os
import sys
import time
from contextlib import contextmanager
from typing import Any
import structlog
from structlog.typing import EventDict
_log_configured = False
# this module's own functions to skip when locating the real caller (logging plumbing, not user code)
_CALLSITE_SKIP = {"step", "get_logger", "_callsite", "_configure_logging"}
_ANSI = {
"reset": "\033[0m", "bold": "\033[1m", "gray": "\033[90m", "red": "\033[31m",
"green": "\033[32m", "yellow": "\033[33m", "blue": "\033[94m", "magenta": "\033[35m", "cyan": "\033[36m",
}
_LEVEL_COLOR = {"debug": "blue", "info": "green", "warning": "yellow", "error": "red", "critical": "red"}
def _callsite(_logger: Any, _method_name: str, event_dict: EventDict) -> EventDict:
"""Add ``at`` (file:line) + ``func`` to each line, pointing at the code that opened the step."""
frame: Any = sys._getframe()
while frame is not None:
filename = frame.f_code.co_filename
func = frame.f_code.co_name
skip = (
"structlog" in filename
or "contextlib" in filename
or (os.path.basename(filename) == "logging.py" and func in _CALLSITE_SKIP)
)
if not skip:
event_dict["at"] = f"{os.path.basename(filename)}:{frame.f_lineno}"
event_dict["func"] = func
break
frame = frame.f_back
return event_dict
def _make_console_renderer(colors: bool):
"""Build a renderer: ``time [level] [step] [file:line func] event key=value …``."""
def paint(text: str, *styles: str) -> str:
if not colors:
return text
prefix = "".join(_ANSI[style] for style in styles)
return f"{prefix}{text}{_ANSI['reset']}"
def render(_logger: Any, _method: str, event_dict: EventDict) -> str:
extra = dict(event_dict)
timestamp = str(extra.pop("timestamp", ""))
level = str(extra.pop("level", "info"))
event = str(extra.pop("event", ""))
name = extra.pop("step", None) or extra.pop("logger", None)
extra.pop("logger", None)
location = f"{extra.pop('at', '')} {extra.pop('func', '')}".strip()
blocks = [paint(timestamp, "gray"), paint(f"[{level:<7}]", "bold", _LEVEL_COLOR.get(level, "green"))]
if name:
blocks.append(paint(f"[{name}]", "cyan"))
if location:
blocks.append(paint(f"[{location}]", "yellow"))
blocks.append(paint(event, "bold", "blue"))
header_line = " ".join(block for block in blocks if block)
pairs = " ".join(
f"{paint(key, 'magenta')}={paint(str(value), 'green')}" for key, value in extra.items()
)
return f"{header_line} {pairs}".rstrip()
return render
def _configure_logging(force: bool = False) -> None:
"""Configure structlog once from the environment (idempotent)."""
global _log_configured
if _log_configured and not force:
return
if os.environ.get("RERANKER_LOG", "1") == "0":
# Silence: filter at CRITICAL so info/warning/error are dropped (the engine never logs CRITICAL).
# NOTE: structlog only accepts standard levels here — CRITICAL+1 raises KeyError.
structlog.configure(
wrapper_class=structlog.make_filtering_bound_logger(logging.CRITICAL),
cache_logger_on_first_use=True,
)
_log_configured = True
return
with contextlib.suppress(Exception):
sys.stdout.reconfigure(encoding="utf-8", line_buffering=True) # type: ignore[union-attr]
colors = "NO_COLOR" not in os.environ
if colors:
with contextlib.suppress(Exception): # wrap stdout so ANSI works on legacy Windows consoles
import colorama
colorama.init()
level = getattr(logging, os.environ.get("RERANKER_LOG_LEVEL", "INFO").upper(), logging.INFO)
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="%H:%M:%S", utc=False),
_callsite,
_make_console_renderer(colors),
],
wrapper_class=structlog.make_filtering_bound_logger(level),
cache_logger_on_first_use=True,
)
_log_configured = True
def get_logger(name: str | None = None):
"""Return a configured structlog logger, with the concern ``name`` bound as ``logger=``."""
_configure_logging()
log = structlog.get_logger(name)
return log.bind(logger=name) if name else log
def _phase_banner(text: str) -> None:
"""A plain full-width divider line bracketing every phase, for grep/scroll tracing in a long log stream
(``grep -A20 "PHASE START: rank.explain"``). Bypasses the structured renderer on purpose — this is a visual
marker, not a parseable event — but still respects ``RERANKER_LOG=0`` silencing."""
if os.environ.get("RERANKER_LOG", "1") == "0":
return
print(f"--------------------------- {text} ---------------------------")
@contextmanager
def step(label: str, **context: Any):
"""Log ``step.start`` / ``step.end`` (+seconds, +metrics), bracketed by PHASE banners. Yields a mutable
metrics dict."""
_phase_banner(f"PHASE START: {label}")
log = get_logger("step").bind(step=label, **context)
log.info("step.start")
metrics: dict[str, Any] = {}
start = time.perf_counter()
try:
yield metrics
except Exception as exc:
log.error("step.fail", seconds=round(time.perf_counter() - start, 3), error=str(exc))
_phase_banner(f"PHASE FAILED: {label} ({round(time.perf_counter() - start, 3)}s)")
raise
else:
log.info("step.end", seconds=round(time.perf_counter() - start, 3), **metrics)
_phase_banner(f"PHASE END: {label} ({round(time.perf_counter() - start, 3)}s)")