import logging from colorlog import ColoredFormatter import os import threading from contextvars import ContextVar # Context for non-Flask usage (Celery tasks, scripts, background jobs, etc.) _request_id_ctx = ContextVar("request_id", default=None) def set_request_id(request_id: str) -> None: """Set a request/correlation id for the current execution context.""" _request_id_ctx.set(request_id) def clear_request_id() -> None: """Clear any request/correlation id set for the current execution context.""" _request_id_ctx.set(None) class RequestFilter(logging.Filter): """Populate record.request_id from ContextVar, Flask, Celery, or fallbacks.""" def filter(self, record): record.funcName = record.funcName if hasattr(record, 'funcName') else '' # Default when no explicit context is found record.request_id = 'no-context' # 1) Prefer an explicit ContextVar if the app set it try: ctx_id = _request_id_ctx.get() if ctx_id: record.request_id = ctx_id return True except Exception: pass # 2) Flask request context (if available) try: from flask import g, has_request_context if has_request_context() and hasattr(g, 'request_id'): record.request_id = g.request_id return True except Exception: pass # 3) Celery current task id (if running inside a Celery worker/task) try: from celery import current_task task = current_task if task and getattr(task, "request", None): cid = getattr(task.request, "id", None) or getattr(task.request, "correlation_id", None) if cid: record.request_id = cid return True except Exception: pass # 4) Fallback to process/thread identifiers to provide some context try: record.request_id = f"pid:{os.getpid()}|thr:{threading.current_thread().name}" except Exception: # Keep the default 'no-context' if something goes wrong pass return True # Define the log format with request_id log_format = ( "%(log_color)s%(asctime)s - %(levelname)s - RequestID:[%(request_id)s] - %(funcName)s - %(message)s" ) date_format = "%Y-%m-%d %H:%M:%S" # Configure the formatter with colors formatter = ColoredFormatter( log_format, datefmt=date_format, log_colors={ "DEBUG": "cyan", "INFO": "green", "WARNING": "yellow", "ERROR": "red", "CRITICAL": "bold_red", }, ) # Configure the handler handler = logging.StreamHandler() handler.setFormatter(formatter) # Configure the logger logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) logger.addFilter(RequestFilter()) logger.addHandler(handler) # Ensure the logs directory exists log_dir = "logs" if not os.path.exists(log_dir): os.makedirs(log_dir) # Configure the file handler file_handler = logging.FileHandler("logs/app.log") file_handler.setFormatter(formatter) file_handler.setLevel(logging.WARNING) logger.addHandler(file_handler) # Prevent logs from propagating to parent loggers (e.g., Celery's logger) logger.propagate = False