"""General-purpose logging utilities with task context support.""" import logging import os import sys from contextvars import ContextVar LOGGER_LEVEL = getattr(logging, os.environ.get('LOGGER_LEVEL', 'INFO')) # This variable is unique to each thread/task execution task_id_var: ContextVar[str] = ContextVar('task_id', default='') class TaskContextFilter(logging.Filter): """Filter that injects the current task_id into log records.""" def filter(self, record: logging.LogRecord) -> bool: """Attach the current task ID to the record. Args: record: The log record. Returns: bool: Always True, indicating the record should be processed. """ record.task_id = task_id_var.get() return True class TaskContextFormatter(logging.Formatter): """Formatter that conditionally includes task_id if present.""" def format(self, record: logging.LogRecord) -> str: """Format the log record, including task_id only if present. Args: record: The log record. Returns: str: Formatted log message. """ # Build format string based on whether task_id is present (non-empty) task_id_fmt = '[%(task_id)s]' if record.task_id else '' # type: ignore[attr-defined] fmt = f'[%(name)s][%(asctime)s][%(levelname)s]{task_id_fmt} %(message)s' # Update both _fmt and _style for compatibility self._style._fmt = fmt self._fmt = fmt return super().format(record) def create_handler(level: int | None = None) -> logging.Handler: """Create a configured handler with task context support. Args: level: Optional logging level. If not provided, uses LOGGER_LEVEL. Returns: Configured StreamHandler that can be added to a logger. """ handler = logging.StreamHandler(sys.stdout) handler.setLevel(level or LOGGER_LEVEL) handler.setFormatter(TaskContextFormatter()) handler.addFilter(TaskContextFilter()) return handler