ذخیرهکردن گزارش انواع درخواستها برای یه برنامهیتحتوب تقریبا ضروریه، از طریق همین گزارشها میتونیم به مشکلات و باگ های احتمالی برناممون پی ببریم و اون هارو حلشون کنیم.
تو این پست قصد دارم نحوه ذخیرهکردن گزارش درخواستهای یه Web Application که با فریمورک Flask نوشته شده رو توضیح بدم.
کسایی که حوصله ندارن توضیحات رو بخونن، میتونن به آخر پست مراجعه کنن، یه Snippet از تمام کد هایی که مینویسیم قرار میدم.
نحوه پیادهسازیش بستگی داره به ساختار و سازماندهی کدهایبرنامتون، من سادهترین حالت رو توضیح میدم که توش یه فایل `app.py` داره پروژمون و تمام کدهامون داخل همون قرار میگیره.
# app.py from flask import Flask app = Flask(__name__) @app.route('/') def index(): return 'Hello world!' if __name__ == '__main__': app.run(debug=True)
خوشبختانه Flask خودش از کتابخونهی `Logging` برای گزارش دادن استفاده میکنه و ما هم میتونیم از همون برای ذخیرهسازی گزارشهامون استفاده کنیم.
یه نکتهی جدا از بحث: وقتی که Django یاد میگرفتم، دیدم تو فایل `settings.py` یه متغیر داره به نام `BASE_DIR` که آدرس مربوط به دایرکتوری پروژه رو نگه میداره، این متغیر خیلی کار هارو میتونه آسونتر کنه و بهتون پیشنهاد میکنم شما هم ازش استفاده کنید! من این متغیر رو `app.config` اضافه میکنم، شما هم دوست داشتید این کار رو بکنید، جلوتر ازش استفاده میکنیم
import os # ... app.config['BASE_DIR'] = os.path.abspath(os.path.dirname(__file__))
برای این که کدمون یکم تمیزتر باشه، به `app.config` چندتا کلید دیگه اضافه میکنیم
app.config['ENABLE_LOGGING'] = True app.config['LOGS_DIR'] = os.path.join(app.config['BASE_DIR'], 'logs/') app.config['REQUEST_LOG_FILE'] = os.path.join(app.config['LOGS_DIR'], 'requests.log')
از `ENABLE_LOGGING` برای فعالسازی و غیرفعالسازی گزارشات استفاده میکنیم، به این شکل دیگه نیاز نیست هربار که میخوایم گزارشات ذخیره نشن، قسمتی از کد رو Comment کنیم، با تغییر مقدار `ENABLE_LOGGING` به `False` این اتفاق میوفته
کلید `LOGS_DIR` به پوشهای که گزارشات داخلش ذخیره میشن اشاره میکنه و `REQUEST_LOG_FILE` هم نام فایل مربوط به گزارشهاست
from logging import Formatter, INFO from logging.handlers import RotatingFileHandler # ... if app.config['ENABLE_LOGGING']: if not os.path.exists(app.config['LOGS_DIR']): os.mkdir(app.config['LOGS_DIR']) file_handler = RotatingFileHandler(app.config['REQUEST_LOG_FILE'], maxBytes=10240, backupCount=10) file_handler.setFormatter(Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')) file_handler.setLevel(INFO)
تو قطعه کد بالا اول بررسی میکنیم در صورتی که `ENABLE_LOGGING` مقدارش True هست کدهای مربوط به ذخیرهسازی گزارشها اجرا بشن؛ در مرحله بعد بررسی میکنیم که پوشه مربوط به گزارشها وجود داره یا نه!؟ اگر وجود نداشت، ساخته میشه
بعد از ساختن پوشه، متغیر `file_handler` رو ایجاد کردیم، یه Object از کلاس RotatingFileHanlder، این کلاس، یه فایل رو به عنوان ورودی میگیره، ۲ پارامتر دیگه هم براش تعریف کردیم، `maxBytes` به حداکثر حجم هر Log File اشاره داره و `backupCount` به تعداد حداکثر فایل های گزارش، یه این معنی که برای مثال یه فایل با نام `requests.log` که بالاتر تعریفش کردیم ایجاد میشه، بعد از رسیدن به حجم ۱۰۲۴۰ بایت که تعریف کردیم، به `requests.log.1` تغییر نام داده میشه، فایل جدیدی مجددا به اسم `requests.log` ایجاد میشه، این عملیات تا موقعی که به فایل `requests.log.10` برسیم ادامه داره، این عدد ۱۰ رو با `backupCount` تعریف کردیم، بعد از رسیدن به این عدد مجددا این کار رو از شماره ۱ انجام میده و فایل های قبلی پاک میشن..
تو خط بعدی فرمت مربوط به ذخیرهسازی هر گزارش تو فایلمون رو توسط متود `setFormatter` مشخص کردیم، در مورد کلاس Formatter، تو مستندات کتابخونهی Logging میتونید اطلاعات بیشتری کسب کنید؛ اینجا ما بهش گفتیم که تاریخ، سطح گزارش ( Error, Debugging, Warning, Info و ... )، پیام گزارش و خطی که تو برناممون گزارش رو ایجاد کرده برامون تو فایل گزارشها بنویسه، قسمت آخر که مربوط به خطی میشه که برامون گزارش رو ایجاد کرده تو این آموزش خیلی بهدردمون نمیخوره، اما خوب میزاریم باشه، شاید بعدا استفاده کردید ازش..
با استفاده از متور `setLevel`، سطح گزارش رو تعریف میکنیم، سطوح مختلف رو میتونید اینجا ببینید، سطحی که ما تعریف کردیم Info هست و تمام اتفاقات رو گزارش میده، در صورتی که این سطح رو برای مثال Error تعریف کنیم، تنها زمان هایی که برنامه با خطا مواجه میشه برامون ذخیرش میکنه، تو لینکی که بالاتر دادم میتونید بیشتر در موردش مطالعه کنید.
if app.config['ENABLE_LOGGING']: # ... app.logger.addHandler(file_handler) app.logger.setLevel(INFO) app.logger.info('LOG START UP')
در ادامه کدهایی که زدیم از `app.logger` استفاده میکنیم، این متود مربوط میشه به قسمتی که گزارشات Flask رو نشون میده، برای مثال وقتی FLASK_ENVIRONMENT رو روی Development تنظیم میکنیم و یا Debug مربوط به برناممون رو فعال میکنیم، تمام درخواستها و خطاهایی که به وجود میان رو داخل کنسول برامون مینویسه، Flask هم از همین کتابخونه Logging استفاده میکنه
با استفاده از `app.logger.addHandler` اومدیم گفتیم از `file_handler` که بالاتر ایجاد کردیم به عنوان Handler برای برناممون استفاده کنه و گزارشها رو براش ارسال کنه، متود `setLevel` چیز جدیدی نداره، همونطور که قبلا استفاده کردیم ازش، اینجا هم اومدیم سطح گزارشهای برناممون رو به Info تغییر دادیم
تو قسمت آخر اومدیم به عنوان گزارش با سطح Info، پیامی با محتوای `LOG START UP` نوشتیم، الآن اگر برنامه Flaskتون رو اجرا کنید و `ENABLE_LOGGING` هم True باشه، باید تو محیط کنسول این پیام رو ببینید، یه نکته هست که اگر Environment روی Development باشه، این پیام رو دوبار میبینید، این باگ یا مشکل نیست، Flask وقتی برنامه رو با این Environment اجرا میکنید، یکبار اجرا میکندش و مجددا برای فعالسازی Debugger برنامه رو Restart میکنه
خوب، تا اینجا مقدمات کاری که میخوایم انجام بدیم، فراهم کردیم، در حال حاضر گزارش خاصی ذخیره نمیشه، بهتره بگم در حال حاضر، هیچ چیز به درد بخوری ذخیره نمیشه، وقتشه یه مقدار پیشرفته ترش کنیم گزارشگرمون رو
من خودم برای پروژم از یهسری کد استفاده کردم که گزارشهارو رنگی رنگی نشونم میده، خیلیا ممکنه از ویندوز استفاده کنن، به احتمال زیاد CMDشون از اون رنگها پشتیبانی نمیکنه، برای همین ادامه آموزش رو تو حالتی میگم که همهچیز خیلی سادست، اما آخر آموزش یه لینک قرار میدم که راجع به استفاده از رنگها توی گزارشها توضیح بده...
خوشبختانه تو Flask دو تا Decorator داریم به اسم `before_request` و `after_request`، با استفاده از این Decoratorها میتونیم به برناممون بگیم وقتی که یه درخواست رو دریافت کرد و زمانی که قصد داره به یه درخواست پاسخ بده چه کار هایی انجام بده
from flask import g from time import time as time_now # ... @app.before_request def before_reqeusts(): if app.config['ENABLE_LOGGING']: g.start = time_now()
تو کد بالا اومدیم تعریف کردیم، زمانی که یه درخواست ارسال شد، بررسی کنه اگر `ENABLE_LOGGING` مقدارش True بود، داخل global های برنامه، که برای هر کاربر جدا هستن، بیاد یه متغیر تعریف کنه به اسم `start` و زمان دریافت درخواست به Unix رو داخلش نگه داره، از این متغیر بعدا برای محاسبه زمان ارسال پاسخ استفاده میکنیم
دقت کنید که تابع before_requests رو خارج از شرطی که قبلا نوشته بودیم نوشتم، دلیلش هم اینه که داخل این تابع میتونید هرکار دیگهای به جزء ذخیره کردن گزارشها انجام بدید، اگر داخل اون شرط تعریفش میکردیم، اگر `ENABLE_LOGGING` رو میخواستیم غیرفعال کنیم، دیگه کار های دیگهای که بهش اضافه کردیم انجام نمیشد.
from ask import request from rfc3339 import rfc3339 from json import loads as json_loads import datetime # ... @app.after_request def after_requests(response): if app.config['ENABLE_LOGGING']: if request.path == '/favicon.ico' or request.path.startswith('/static'): return response now = time_now() duration = round(now - g.start, 2) dt = datetime.datetime.fromtimestamp(now) timestamp = rfc3339(dt, utc=True) ip = request.headers.get('X-Forwarded-For', request.remote_addr) host = request.host.split(':', 1)[0] args = dict(request.args) log_params = [ ('method', request.method), ('path', request.path), ('status', response.status_code), ('duration', duration), ('time', timestamp), ('ip', ip), ('host', host), ('params', args), ] parts = ["{}={}".format(name, value) for name, value in log_params] line = " | ".join(parts) app.logger.info(line) return response
کد بالا مربوط میشه به عملیاتی که بعد از دریافت درخواست، موقع ارسال پاسخ انجام میدیم، به نظر زیاد میاد، اما خیلی کد سادهایه، قدم به قدم جلو میریم
مرحله اول طبق معمول بررسی کردیم که ذخیرهسازی گذارشات فعال هست یا نه، اگر فعال نباشه پاسخ ارسال میشه و هیچکدوم از خطهای بعدی که برای ذخیرهسازی نوشته شدن اجرا نمیشن
یه سری متغیر تعریف کردیم:
متغیر now: زمان ارسال پاسخ به Unix
متغیر duration: زمانی که برنامهمون به پردازش درخواست و ارسال پاسخ اختصاص داد
متغیر dt: یه Object از نوع datetime که استفاده از متغیر now ساخته شده
متغیر timestamp: از متغیر dt زمان رو در فرمت RFC3339 یا ISO-8601 با نوع String میسازه
متغیر ip: آدرس IP رو از Header درخواست دریافتشده با عنوان `X-Forwarded-For` میگیره، اکثر اوقات همون آیپی که برناممون روش اجرا شده رو برمیگردونه
متغیر host: نام hostی که درخواست بهش ارسال شده رو نگه میداره، معمولا اگر از طریق IP، به برنامه دسترسی داشته باشن، همون مقدار متغیر ip رو نشون میده
متغیر args: پارامترهایی که توسط متود GET یا با URL ارسال میشن رو به عنوان Dictionary نگه میداره، دقت کنید که این متغیر، محتوایی که به عنوان Request Body با متود POST یا متودهای مشابه ارسال میشن رو نشون نمیده، این مورد رد میتونید خودتون با توجه به برنامتون و مستندات Flask اضافه کنید.
متغیر log_params: تمام متغیرهایی که تعریف کردیم، هرچیزی که میخوایم تو گزارشات ذخیره بشه، توی این متغیر که یه لیست هست قرار میگیره، هر عضوش یه Tupple هست که ۲ عضو دارن، عضو اول نامی که باهاش چیزی رو تو گزارشات نشون میده، عضو دوم مقدارش، برای مثال `('params', args)` برامون محتوای args رو با عنوان params ذخیره میکنه
متغیر parts: این متغیر هم یه لیست هست، تو این لیست تمام محتوای مربوط به log_params به این شکل در میان => `name=value`، تو log_params مثال params رو زدیم، تو این لیست تبدیل میشه به چیزی شبیه به این: `params={args in dict}`
متغیر line: این متغیر لیست parts رو تبدیل به یه رشته میکنه و بین هر عضو " | " رو قرار میده، نتیجه نهایی گزارش هر درخواست همین متغیره که تو خط بعدیش یعنی `app.logger.info(line)` همین رشته رو به عنوان گزارش ذخیره میکنیم
ودر آخر هم response رو بر میگردونیم
بهتون تبریک میگم، تقریبا کارمون تمومه
حالا اگر برنامه رو اجرا کنید و یه درخواست به صفحه اصلی یعنی http://localhost:5000 بدید، توی کنسولتون یه خط گزارش مینویسه، از همین چیزایی که ما نوشتیم به وجود اومده، برای امتحان کردن args هم میتونید یه آدرس http://localhost:5000/?hello=world&name=DarkSun بفرستید تا گزارش رو همراه با پارامتر های ارسالی روی URL بهتون نشون بده، گزارشات توی پوشه logs، که تو پوشه پروژتون هست ذخیره میشن
کل کدی که نوشتیم رو میتونید تو این مخزن Github مشاهده کنید
منابعی که ازشون کمک گرفتم:
این آموزش، اولین پستم توی ویرگول بود، خوشحال میشم دیدگاهتون رو راجع بهش بدونم
موفق باشید.