تسجيل بيثون: برنامج تعليمي متعمق
نشرت: 2022-03-11نظرًا لأن التطبيقات تصبح أكثر تعقيدًا ، فإن الحصول على سجلات جيدة يمكن أن يكون مفيدًا للغاية ، ليس فقط عند تصحيح الأخطاء ولكن أيضًا لتوفير نظرة ثاقبة لمشاكل التطبيق / الأداء.
تأتي مكتبة Python القياسية مع وحدة تسجيل توفر معظم ميزات التسجيل الأساسية. من خلال إعداده بشكل صحيح ، يمكن أن تجلب رسالة السجل الكثير من المعلومات المفيدة حول متى وأين يتم تشغيل السجل بالإضافة إلى سياق السجل ، مثل العملية / سلسلة الرسائل قيد التشغيل.
على الرغم من المزايا ، غالبًا ما يتم التغاضي عن وحدة التسجيل لأنها تستغرق بعض الوقت لإعدادها بشكل صحيح ، وعلى الرغم من اكتمالها ، في رأيي ، مستند التسجيل الرسمي على https://docs.python.org/3/library/logging.html لا يعطي حقًا أفضل ممارسات التسجيل أو يسلط الضوء على بعض مفاجآت التسجيل.
لا يُقصد من هذا البرنامج التعليمي الخاص بالتسجيل في Python أن يكون مستندًا كاملاً في وحدة التسجيل ، بل دليل "البدء" الذي يقدم بعض مفاهيم التسجيل بالإضافة إلى بعض "مشكلات التسجيل" التي يجب الانتباه إليها. سينتهي المنشور بأفضل الممارسات وسيحتوي على بعض المؤشرات لموضوعات التسجيل الأكثر تقدمًا.
يرجى ملاحظة أن جميع مقتطفات الشفرة في المنشور تفترض أنك قمت بالفعل باستيراد وحدة التسجيل:
import logging
مفاهيم لبايثون التسجيل
يقدم هذا القسم نظرة عامة على بعض المفاهيم التي غالبًا ما يتم مواجهتها في وحدة التسجيل.
مستويات تسجيل بايثون
يتوافق مستوى السجل مع "الأهمية" المعطاة للسجل: يجب أن يكون سجل "الخطأ" أكثر إلحاحًا من سجل "warn" ، بينما يجب أن يكون سجل "التصحيح" مفيدًا فقط عند تصحيح أخطاء التطبيق.
هناك ستة مستويات لوغاريتمية في بايثون ؛ يرتبط كل مستوى بعدد صحيح يشير إلى خطورة السجل: NOTSET = 0 و DEBUG = 10 و INFO = 20 و WARN = 30 و ERROR = 40 و CRITICAL = 50.
جميع المستويات مباشرة إلى حد ما (DEBUG <INFO <WARN) باستثناء NOTSET ، والتي سيتم تناول خصوصيتها بعد ذلك.
بيثون تنسيق التسجيل
يعمل منسق السجل بشكل أساسي على إثراء رسالة السجل عن طريق إضافة معلومات السياق إليها. قد يكون من المفيد معرفة متى يتم إرسال السجل ، ومكان (ملف Python ، ورقم السطر ، والطريقة ، وما إلى ذلك) ، والسياق الإضافي مثل مؤشر الترابط والعملية (يمكن أن يكون مفيدًا للغاية عند تصحيح أخطاء تطبيق متعدد مؤشرات الترابط).
على سبيل المثال ، عند إرسال سجل "hello world" من خلال منسق السجل:
"%(asctime)s — %(name)s — %(levelname)s — %(funcName)s:%(lineno)d — %(message)s"
سوف تأتي
2018-02-07 19:47:41,864 - abc - WARNING - <module>:1 - hello world
معالج تسجيل بيثون
معالج السجل هو المكون الذي يكتب / يعرض السجل بشكل فعال: قم بعرضه في وحدة التحكم (عبر StreamHandler) ، أو في ملف (عبر FileHandler) ، أو حتى عن طريق إرسال بريد إلكتروني إليك عبر SMTPHandler ، إلخ.
يحتوي كل معالج سجل على حقلين مهمين:
- منسق يضيف معلومات السياق إلى السجل.
- مستوى السجل الذي يقوم بتصفية السجلات التي تكون مستوياتها أدنى. لذلك لن يتعامل معالج السجل مع مستوى المعلومات مع سجلات DEBUG.
توفر المكتبة القياسية مجموعة من المعالجات التي يجب أن تكون كافية لحالات الاستخدام الشائعة: https://docs.python.org/3/library/logging.handlers.html#module-logging.handlers. أكثرها شيوعًا هي StreamHandler و FileHandler:
console_handler = logging.StreamHandler() file_handler = logging.FileHandler("filename")
بيثون المسجل
ربما يكون المسجل هو الأكثر استخدامًا بشكل مباشر في الكود وهو أيضًا الأكثر تعقيدًا. يمكن الحصول على مسجل جديد عن طريق:
toto_logger = logging.getLogger("toto")
يحتوي المسجل على ثلاثة مجالات رئيسية:
- نشر: يقرر ما إذا كان ينبغي نشر السجل إلى أصل المسجل. بشكل افتراضي ، قيمته هي True.
- المستوى A: مثل مستوى معالج السجل ، يتم استخدام مستوى المسجل لتصفية السجلات "الأقل أهمية". باستثناء ، على عكس معالج السجل ، يتم فحص المستوى فقط عند المسجل "التابع" ؛ بمجرد نشر السجل لوالديه ، لن يتم فحص المستوى. هذا هو بالأحرى سلوك غير بديهي.
- المعالجات: قائمة المعالجات التي سيتم إرسال السجل إليها عندما يصل إلى المسجل. يتيح ذلك معالجة مرنة للسجلات — على سبيل المثال ، يمكن أن يكون لديك معالج سجل ملف يسجل جميع سجلات DEBUG ومعالج سجل البريد الإلكتروني الذي سيتم استخدامه فقط للسجلات الهامة. في هذا الصدد ، تكون علاقة المُسجل والمعالج مشابهة لعلاقة الناشر والمستهلك: سيتم بث السجل إلى جميع المعالجات بمجرد اجتيازه للتحقق من مستوى المسجل.
يعتبر المسجل فريدًا بالاسم ، مما يعني أنه إذا تم إنشاء مسجل يحمل الاسم "toto" ، فإن الاستدعاءات اللاحقة لـ logging.getLogger("toto")
ستعيد نفس الكائن:

assert id(logging.getLogger("toto")) == id(logging.getLogger("toto"))
كما قد تكون خمنت ، فإن الحطابين لديهم تسلسل هرمي. على رأس التسلسل الهرمي يوجد مسجل الجذر ، والذي يمكن الوصول إليه عبر logging.root. يتم استدعاء هذا المسجل عند استخدام أساليب مثل logging.debug()
. بشكل افتراضي ، يكون مستوى سجل الجذر هو WARN ، لذلك سيتم تجاهل كل سجل بمستوى أقل (على سبيل المثال عبر logging.info("info")
). خصوصية أخرى لمسجل الجذر هي أنه سيتم إنشاء المعالج الافتراضي الخاص به في المرة الأولى التي يتم فيها تسجيل سجل بمستوى أكبر من WARN. لا يُنصح عمومًا باستخدام مسجل الجذر بشكل مباشر أو غير مباشر عبر طرق مثل logging.debug()
.
بشكل افتراضي ، عندما يتم إنشاء مسجل جديد ، سيتم تعيين الأصل الخاص به على مسجل الجذر:
lab = logging.getLogger("ab") assert lab.parent == logging.root # lab's parent is indeed the root logger
ومع ذلك ، فإن المسجل يستخدم "تدوين النقطة" ، مما يعني أن المسجل الذي يحمل الاسم "ab" سيكون تابعًا للمسجل "a". ومع ذلك ، هذا صحيح فقط إذا تم إنشاء المسجل "a" ، وإلا فإن الأصل "ab" يظل هو الجذر.
la = logging.getLogger("a") assert lab.parent == la # lab's parent is now la instead of root
عندما يقرر المسجل ما إذا كان يجب أن يمر السجل وفقًا لفحص المستوى (على سبيل المثال ، إذا كان مستوى السجل أقل من مستوى المسجل ، فسيتم تجاهل السجل) ، فإنه يستخدم "المستوى الفعال" الخاص به بدلاً من المستوى الفعلي. المستوى الفعال هو نفسه مستوى المسجل إذا لم يكن المستوى NOTSET ، أي جميع القيم من DEBUG حتى CRITICAL ؛ ومع ذلك ، إذا كان مستوى المسجل هو NOTSET ، فسيكون المستوى الفعال هو المستوى الأول الذي يحتوي على مستوى غير NOTSET.
بشكل افتراضي ، يكون للمسجل الجديد مستوى NOTSET ، وبما أن مسجل الجذر له مستوى WARN ، فسيكون المستوى الفعال للمسجل هو WARN. لذلك ، حتى إذا كان لدى المسجل الجديد بعض المعالجات المرفقة ، فلن يتم استدعاء هذه المعالجات ما لم يتجاوز مستوى السجل WARN:
toto_logger = logging.getLogger("toto") assert toto_logger.level == logging.NOTSET # new logger has NOTSET level assert toto_logger.getEffectiveLevel() == logging.WARN # and its effective level is the root logger level, ie WARN # attach a console handler to toto_logger console_handler = logging.StreamHandler() toto_logger.addHandler(console_handler) toto_logger.debug("debug") # nothing is displayed as the log level DEBUG is smaller than toto effective level toto_logger.setLevel(logging.DEBUG) toto_logger.debug("debug message") # now you should see "debug message" on screen
بشكل افتراضي ، سيتم استخدام مستوى المسجل لتقرير مرور السجل: إذا كان مستوى السجل أقل من مستوى المسجل ، فسيتم تجاهل السجل.
أفضل ممارسات تسجيل بيثون
تعد وحدة التسجيل مفيدة للغاية بالفعل ، ولكنها تحتوي على بعض المراوغات التي يمكن أن تسبب ساعات طويلة من الصداع حتى لأفضل مطوري Python. فيما يلي أفضل الممارسات لاستخدام هذه الوحدة في رأيي:
- قم بتكوين مسجل الجذر ولكن لا تستخدمه مطلقًا في التعليمات البرمجية الخاصة بك - على سبيل المثال ، لا تستدعي أبدًا وظيفة مثل
logging.info()
، والتي تستدعي بالفعل مسجل الجذر خلف الكواليس. إذا كنت تريد التقاط رسائل الخطأ من المكتبات التي تستخدمها ، فتأكد من تكوين مسجل الجذر للكتابة إلى ملف ، على سبيل المثال ، لتسهيل تصحيح الأخطاء. بشكل افتراضي ، يخرج مسجل الجذر فقط إلىstderr
، لذلك يمكن أن يضيع السجل بسهولة. - لاستخدام التسجيل ، تأكد من إنشاء مسجل جديد باستخدام
logging.getLogger(logger name)
. عادةً ما أستخدم__name__
كاسم المسجل ، ولكن يمكن استخدام أي شيء طالما كان ثابتًا. لإضافة المزيد من المعالجات ، عادةً ما يكون لدي طريقة تقوم بإرجاع المسجل (يمكنك العثور على الجوهر على https://gist.github.com/nguyenkims/e92df0f8bd49973f0c94bddf36ed7fd0).
import logging import sys from logging.handlers import TimedRotatingFileHandler FORMATTER = logging.Formatter("%(asctime)s — %(name)s — %(levelname)s — %(message)s") LOG_FILE = "my_app.log" def get_console_handler(): console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(FORMATTER) return console_handler def get_file_handler(): file_handler = TimedRotatingFileHandler(LOG_FILE, when='midnight') file_handler.setFormatter(FORMATTER) return file_handler def get_logger(logger_name): logger = logging.getLogger(logger_name) logger.setLevel(logging.DEBUG) # better to have too much log than not enough logger.addHandler(get_console_handler()) logger.addHandler(get_file_handler()) # with this pattern, it's rarely necessary to propagate the error up to parent logger.propagate = False return logger
بعد أن تتمكن من إنشاء أداة تسجيل جديدة واستخدامها:
my_logger = get_logger("my module name") my_logger.debug("a debug message")
- استخدم فئات RotatingFileHandler ، مثل TimedRotatingFileHandler المستخدمة في المثال بدلاً من FileHandler ، حيث ستقوم بتدوير الملف تلقائيًا عندما يصل الملف إلى حد الحجم أو يفعل ذلك كل يوم.
- استخدم أدوات مثل Sentry و Airbrake و Raygun وما إلى ذلك ، لالتقاط سجلات الأخطاء تلقائيًا نيابة عنك. هذا مفيد بشكل خاص في سياق تطبيق الويب ، حيث يمكن أن يكون السجل مطولًا جدًا ويمكن أن تضيع سجلات الأخطاء بسهولة. ميزة أخرى لاستخدام هذه الأدوات هي أنه يمكنك الحصول على تفاصيل حول القيم المتغيرة في الخطأ حتى تتمكن من معرفة عنوان URL الذي يؤدي إلى الخطأ ، والمستخدم المعني ، وما إلى ذلك.
إذا كنت مهتمًا بمزيد من أفضل الممارسات ، فاقرأ الأخطاء العشرة الأكثر شيوعًا التي ارتكبها مطورو Python بواسطة زميلك Toptaler Martin Chikilian.