import logging
from collections.abc import MutableMapping
from typing import TYPE_CHECKING, Any, Literal, Optional
if TYPE_CHECKING:
from logging import LogRecord
from pynenc.app import Pynenc
# Define ANSI color codes
[docs]
class Colors:
"""ANSI color codes for terminal coloring."""
RESET = "\033[0m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
CYAN = "\033[36m"
RED_BG = "\033[41m"
WHITE = "\033[37m"
[docs]
def create_logger(app: "Pynenc", use_colors: bool = True) -> logging.Logger:
"""
Creates a logger for the specified app with timestamps and optional colored output.
:param Pynenc app: The app instance for which the logger is created.
:param bool use_colors: Whether to use colored output (defaults to True).
:return: The created logger.
:raises ValueError: If the logging level is invalid.
"""
logger = logging.getLogger(f"pynenc.{app.app_id}")
# Create handler
handler = logging.StreamHandler()
# Create formatter with timestamp
if use_colors:
formatter: logging.Formatter = ColoredFormatter(
fmt="%(asctime)s.%(msecs)03d %(levelname)s %(name)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
else:
formatter = logging.Formatter(
fmt="%(asctime)s.%(msecs)03d %(levelname)-8s %(name)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Set formatter to handler
handler.setFormatter(formatter)
# Remove any existing handlers and add our new one
logger.handlers = [handler]
# Set level
if level_name := app.conf.logging_level:
numeric_level = getattr(logging, level_name.upper(), None)
if not isinstance(numeric_level, int):
raise ValueError(f"Invalid log level: {level_name}")
logger.setLevel(numeric_level)
# Prevent propagation to root logger to avoid duplicate logs
logger.propagate = False
return logger
[docs]
class TaskLoggerAdapter(logging.LoggerAdapter):
"""
Logger adapter for tasks.
This adapter adds task and invocation context to log messages.
"""
def __init__(
self, logger: logging.Logger, task_id: str, invocation_id: Optional[str] = None
):
super().__init__(logger, {})
self.set_context(task_id, invocation_id)
[docs]
def set_context(self, task_id: str, invocation_id: Optional[str]) -> None:
"""
Sets the context for logging.
:param str task_id: The ID of the task.
:param Optional[str] invocation_id: The ID of the invocation.
"""
self.task_id = task_id
self.invocation_id = invocation_id
[docs]
def process(self, msg: Any, kwargs: MutableMapping[str, Any]) -> tuple:
"""
Processes a log message, adding task and invocation context.
:param Any msg: The log message.
:param MutableMapping[str, Any] kwargs: Additional keyword arguments.
:return: The processed message and kwargs.
"""
if self.invocation_id:
prefix = f"[{self.task_id}: {self.invocation_id}]"
else:
prefix = f"[{self.task_id}]"
return f"{prefix} {msg}", kwargs
[docs]
class RunnerLogAdapter(logging.LoggerAdapter):
"""
Logger adapter for runners.
This adapter adds runner context to log messages.
:param logging.Logger logger: The logger instance.
:param str runner_id: The ID of the runner.
"""
def __init__(self, logger: logging.Logger, runner_id: str):
super().__init__(logger, {})
self.runner_id = runner_id
[docs]
def process(self, msg: Any, kwargs: MutableMapping[str, Any]) -> tuple:
"""
Processes a log message, adding runner context.
:param Any msg: The log message.
:param MutableMapping[str, Any] kwargs: Additional keyword arguments.
:return: The processed message and kwargs.
"""
prefix = f"[runner: {self.runner_id}]"
return f"{prefix} {msg}", kwargs