در دنیای برنامههای پایتون با تراکم بالا، تفاوت بین یک سرویس پایدار و یک قطعی کامل، اغلب نه در خود منطق کسبوکار، بلکه در نحوه مشاهده و واکنش سیستم به خطاها نهفته است. دستورهای print سنتی و فراخوانیهای ساده logging.basicConfig زمانی که برنامه شما با هزاران درخواست همزمان سروکار دارد، ناکافی هستند. وقتی تاخیر افزایش مییابد یا خطایی رخ میدهد، توسعهدهندگان به دادههای دقیق و قابل خواندن توسط ماشین نیاز دارند تا مسائل را در زمان واقعی تشخیص دهند. این پست بررسی میکند که چگونه میتوان الگوهای لاگنویسی ساختاریافته و مدیریت دقیق خطاها را که برای محیطهای ناهمگام (Asynchronous) پایتون طراحی شدهاند، پیادهسازی کرد.
ضرورت لاگنویسی ساختاریافته
لاگنویسی ساختاریافته به معنای فرمتبندی پیامهای لاگ به صورت دادههای ساختاریافته، معمولاً JSON، به جای متن ساده است. این رویکرد در سیستمهای توزیعشده حیاتی است زیرا به ابزارهای جمعآوری لاگ (مانند ELK Stack، Datadog یا Splunk) اجازه میدهد تا ورودیهای لاگ را بهطور کارآمد تجزیه، فیلتر و ایندکس کنند. در یک محیط همزمان، یک خط لاگ که شامل شناسه درخواست، شناسه کاربر، تاخیر و کد وضعیت است، به شما امکان میدهد یک تراکنش خاص را بهطور فوری در سراسر میکروسرویسها ردیابی کنید. برای دستیابی به این مورد در پایتون، باید از کتابخانههایی مانند«بدون دادههای ساختاریافته، لاگها فقط نویز هستند. بدون زمینه، خطاها فقط حدسها هستند.»
python-json-logger یا مشابه آن استفاده کنید که با ماژول استاندارد logging یکپارچه میشوند. هدف این است که اطمینان حاصل شود هر رکورد لاگ یک دیکشنری خودکفا از حقایق است.
مثال پیادهسازی
در اینجا نحوه پیکربندی یک لاگر آماده برای تولید که بهطور خودکار زمینه را به JSON تبدیل میکند، آورده شده است:import logging
import json
from pythonjsonlogger import jsonlogger
import sys
def setup_structured_logger(level=logging.INFO):
logger = logging.getLogger("app")
logger.setLevel(level)
# ایجاد یک هندلر که به stderr (یا یک فایل در محیط تولید) مینویسد
handler = logging.StreamHandler(sys.stdout)
# تنظیم یک فرمتکننده JSON سفارشی
formatter = jsonlogger.JsonFormatter(
fmt='%(asctime)s %(name)s %(levelname)s %(message)s %(process)d',
datefmt='%Y-%m-%d %H:%M:%S',
)
handler.setFormatter(formatter)
logger.addHandler(handler)
return logger
logger = setup_structured_logger()
def process_request(request_id, user_id, data):
logger.info(
"Processing request",
extra={
"request_id": request_id,
"user_id": user_id,
"action": "validate_data",
"payload_size": len(data)
}
)
# منطق کسبوکار در اینجا
if not data:
raise ValueError("Data payload is empty")
مدیریت استراتژیک خطاها در زمینههای ناهمگام
کد پایتون ناهمگام چالشهای منحصربهفردی را برای مدیریت خطاها ایجاد میکند. یک بلوکtry-except استاندارد برای اجرای تکرشتهای کار میکند، اما در asyncio یا فریمورکهایی مانند FastAPI، استثناها میتوانند بهصورت بیصدا منتقل شوند اگر توسط حلقه رویداد یا میانیگیر (Middleware) گرفته نشوند. علاوه بر این، در سیستمهای با تراکم بالا، مسدود کردن حلقه رویداد در هنگام لاگنویسی خطاها میتواند باعث ایجاد طوفانی از تاخیر شود.
مدیریت خطا در سطح تولید به دو استراتژی متمایز نیاز دارد: **دستهبندی** و **انتقال زمینه**.
۱. دستهبندی خطاها
مگر در موارد ضروری،Exception عمومی را نگیرید. بهجای آن، یک سلسلهمراتب از استثناهای سفارشی ایجاد کنید که به کدهای وضعیت HTTP خاص یا استراتژیهای تلاش مجدد (Retry) mapping شوند. این کار به میانیگیر شما اجازه میدهد خطاهای منطق کسبوکار را بهطور متفاوت از شکستهای زیرساختی مدیریت کند.
class AppException(Exception):
def __init__(self, message, status_code, error_code=None):
super().__init__(message)
self.status_code = status_code
self.error_code = error_code
self.details = {}
class ValidationError(AppException):
def __init__(self, message):
super().__init__(message, status_code=400, error_code="VALIDATION_ERROR")
class RateLimitExceeded(AppException):
def __init__(self, limit):
super().__init__(f"Rate limit of {limit} exceeded.", status_code=429, error_code="RATE_LIMIT")
۲. انتقال زمینهای خطا
وقتی خطایی رخ میدهد، ورودی لاگ باید شامل تمام زمینه درخواست، از جمله ردپای استثنا (Stack Trace) و دادههای ساختاریافته جمعآوری شده در طول چرخه حیات درخواست باشد. در یک فریمورک ناهمگام، باید از هندلرهای استثنا سراسری استفاده کنید که این زمینه را به لاگر تزریق میکنند.import traceback
async def global_exception_handler(request, exc):
# تعیین اینکه چه کاربر یا سرویسی این را ایجاد کرده است
user_id = getattr(request.state, 'user_id', 'unknown')
request_id = request.headers.get('x-request-id', 'no-id')
logger.error(
"Unhandled exception in request",
extra={
"request_id": request_id,
"user_id": user_id,
"exception_type": type(exc).__name__,
"exception_message": str(exc),
"traceback": traceback.format_exc(),
"stack_level": "error"
}
)
# بازگرداندن یک پاسخ خطای استاندارد
return {"error": "Internal Server Error", "trace_id": request_id}
بهینهسازی برای عملکرد
لاگنویسی در برنامههای با تراکم بالا میتواند خود به یک گلوگاه تبدیل شود اگر بهدرستی بهینهسازی نشود. از ایجاد شیءهای رشته بزرگ یا انجام عملیات پرهزینه در داخل فراخوانیهای لاگ خودداری کنید. همیشه از ارزیابی تنبل (Lazy Evaluation) با استفاده از گذراندن آرگومانها به روش لاگ استفاده کنید که تنها در صورت فعال بودن سطح لاگ، پیام را محاسبه میکند. علاوه بر این، ترجیحاً از کتابخانههای لاگنویسی ساختاریافته که ازloguru یا فرمتکنندههای JSON بهینه استفاده میکنند، به جای پارسرهای سنگین XML استفاده کنید.
اطمینان حاصل کنید که خروجی لاگ شما ناهمگام است. نوشتن همگامانه به دیسک در حین اوج ترافیک میتواند کارگران (Workers) شما را مسدود کند. از هندلرهای لاگ استفاده کنید که نوشتن را بافر میکنند یا لاگها را به یک رشته پسزمینه ارسال میکنند (مانند QueueHandler) تا تولید لاگ را از ذخیرهسازی لاگ جدا کنند.