Source code for radical.orbit.logging_config
"""
Logging configuration for radical.orbit
This module sets up standard Python logging with:
- Uvicorn-style colored output
- Support for correlation IDs in request context
- Structured log format
Import this module early in your application to configure logging.
"""
import logging
import os
import sys
import copy
import contextvars
from typing import Optional
# Context variable for request correlation ID
correlation_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
'correlation_id', default=None
)
[docs]
def set_correlation_id(req_id: str) -> None:
"""Set the correlation ID for the current async context."""
correlation_id.set(req_id)
[docs]
def get_correlation_id() -> Optional[str]:
"""Get the correlation ID for the current async context."""
return correlation_id.get()
[docs]
def clear_correlation_id() -> None:
"""Clear the correlation ID for the current async context."""
correlation_id.set(None)
[docs]
class ColoredFormatter(logging.Formatter):
"""
Log formatter with Uvicorn-style coloring and correlation ID support.
"""
def __init__(self, fmt: Optional[str] = None, datefmt: Optional[str] = None,
style: str = '%', use_colors: Optional[bool] = None):
super().__init__(fmt, datefmt, style)
if use_colors is None:
use_colors = sys.stdout.isatty()
self.use_colors = use_colors
self.COLORS = {
logging.DEBUG: "\033[36m", # Cyan
logging.INFO: "\033[32m", # Green
logging.WARNING: "\033[33m", # Yellow
logging.ERROR: "\033[31m", # Red
logging.CRITICAL: "\033[31;1m", # Bold Red
}
self.RESET = "\033[0m"
self.DIM = "\033[2m"
[docs]
def format(self, record: logging.LogRecord) -> str:
record = copy.copy(record)
# Add correlation ID if available
req_id = correlation_id.get()
if req_id:
# Truncate for readability
short_id = req_id[:8] if len(req_id) > 8 else req_id
if self.use_colors:
record.msg = f"{self.DIM}[{short_id}]{self.RESET} {record.msg}"
else:
record.msg = f"[{short_id}] {record.msg}"
if not self.use_colors:
return super().format(record)
levelname = record.levelname
if record.levelno in self.COLORS:
# Match Uvicorn: "INFO: " (Colored, with colon, padded to 9)
levelname_with_sep = f"{levelname}:"
padded_levelname = f"{levelname_with_sep:<9}"
record.levelname = (f"{self.COLORS[record.levelno]}"
f"{padded_levelname}{self.RESET}")
return super().format(record)
[docs]
def configure_logging(level: int = logging.INFO,
format_string: Optional[str] = None,
log_file: Optional[str] = None) -> None:
"""
Configure logging for radical.orbit.
Args:
level: Logging level (default: logging.INFO).
format_string: Custom format string for the stdout handler
(optional; ignored by the file handler which
always uses a plain timestamped format).
log_file: If given, also write logs to this file
(appended on open). Parent directory is
created if missing. Stdout output stays
colored; the file is plain text with
timestamps so it survives ``less`` / ``grep``
and Dragon's stdio capture.
"""
if format_string is None:
format_string = '%(levelname)s %(message)s'
handlers: list = []
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(ColoredFormatter(fmt=format_string))
handlers.append(stdout_handler)
if log_file:
log_dir = os.path.dirname(log_file)
if log_dir:
os.makedirs(log_dir, exist_ok=True)
file_handler = logging.FileHandler(log_file, mode='a')
file_handler.setFormatter(logging.Formatter(
fmt='%(asctime)s %(levelname)-8s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'))
handlers.append(file_handler)
# Attach handlers to the ``radical.orbit`` and ``rhapsody`` loggers
# directly with propagate=False so external libraries that call
# ``logging.basicConfig(force=True, ...)`` during their own init
# — Dragon's launcher and rhapsody V3 backend bringup are the
# observed offenders — cannot wipe our file handler. Without
# this, log output past V3 init silently vanishes from the file,
# which is exactly what we hit at 16-node scale.
#
# Idempotent across re-calls: drop any handlers we previously
# attached before re-installing.
for name in ('radical.orbit', 'rhapsody'):
protected = logging.getLogger(name)
for h in list(protected.handlers):
protected.removeHandler(h)
try:
h.close()
except Exception:
pass
for h in handlers:
protected.addHandler(h)
protected.setLevel(level)
protected.propagate = False
# Root is still configured so third-party libraries (psij, dragon,
# websockets, uvicorn) keep showing up in stdout / file. This
# channel can be wiped by a foreign basicConfig(force=True), but
# the radical.orbit channel above is now immune.
logging.basicConfig(force=True, level=level, handlers=list(handlers))
# Auto-configure on import. Honor ``RADICAL_ORBIT_LOG_LVL`` (falling
# back to the generic ``RADICAL_LOG_LVL``) so that client scripts
# (amsc.py, etc.) inherit the level via env without needing a code
# edit; entry-point scripts call ``configure_logging`` again after
# argparse, so this has no effect on them.
_env_level = (os.environ.get('RADICAL_ORBIT_LOG_LVL')
or os.environ.get('RADICAL_LOG_LVL') or 'INFO').upper()
configure_logging(getattr(logging, _env_level, logging.INFO))