106 lines
3.3 KiB
Python
106 lines
3.3 KiB
Python
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 '<unknown>'
|
|
|
|
# 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 |