أستمع الى المقال

عند حدوث استثناءات Exceptions أثناء عمل برنامجنا في فترة الـ Runtime، يمكننا معالجة هذه الاستثناءات بالعديد من الطرق، لتفادي تحطمه. منها استخدام كتل try-catch-finally، والتي يمكننا عبرها ترك رسالة مفهومة للمستخدم، وإغلاق الموارد المفتوحة، وإعطاء المجال لباقي أجزاء البرنامج أن تواصل عملها. ولكن إذا لم نكن نعرف من الأساس ما هي الاستثناءات التي يمكن أن تحدث، فلا يمكننا معالجتها بالطريقة المناسبة.

إذًا، لتفادي حدوث الأخطاء والاستثناءات الغير متوقعة مستقبلًا، نحتاج كمبرمجين أن نعرف ما هو الخطأ الذي حدث بالضبط، وفي أي جزء من البرنامج، وما الذي تسبب في حدوثه. أي نحتاج لتسجيل بعض خطوات عمل البرنامج، منذ بداية تنصيبه في جهاز المستخدم، وبداية عمله، وحتى إغلاقه من قبل المستخدم، لنتمكن لاحقًا من تحليلها ومن ثم معالجتها. كل هذه المعلومات المسجلة، يتم الاحتفاظ بها في ما يعرف بالسجل Log.

تتبع الأحداث:

لنتخيل أنه حدثت عملية سطو وسرقة لبنكٍ ما. وكان مطلوبًا منا أن نحقق فيها، ومعرفة ما حدث، ومن هو الجاني. لدينا شهود رأوا الحدث، ولكن شهاداتهم تبدو غامضة ولا تؤدي إلى أي دليل. سيكون من الصعب معرفة الجناة والأحداث في ظل ما هو متاح من معلومات. ولكن، ماذا لو كان هناك كاميرات مراقبة تسجل ما حدث وتحتفظ به في سجل معين؟ حينها يمكننا أن نراجع سجلات الكاميرات، ونرى ما حدث في وقت السرقة، ونراجع التسلسل الزمني للأحداث.

فلنفترض الآن أن عملية السرقة، هي خطأ برمجي حدث في برنامجنا. وأن الشهود – المستخدمين – قد يرسل بعضهم رسائل تنبهنا إلى الخطأ، ولكنها بالطبع ليست واضحة كفاية لنفهم ما حدث. في هذه الحالة، ما هي “كاميرات المراقبة” التي لدينا لنسترجع بها الأحداث أثناء عمل البرنامج؟ وبما أنه لن تتوفر لنا هذه الكاميرات، فنحن بحاجة إلى سجل يسجل الأحداث أثناء عمل البرنامج. إذًا، ما هو هذا السجل؟ وكيف يعمل؟ هذا ما سنعرفه تفصيليًا في الفقرات أدناه.

ما هو السجل Log:

يشير مصطلح الـ Logging، إلى عملية التسجيل أثناء تنفيذ أحد التطبيقات. والـ Log، هو سجل يحتفظ بمعلومات حول بعض الأحداث في تطبيق برمجي. قد يحتوي هذا السجل على رسائل كافية لفهم الحدث الذي حدث، عندما يتضمن زمن حدوثه timestamp ووصف الحدث description ومستوى level خطورته. يمكن أن تكون هذه الأحداث قد تسبب فيها المستخدم أو نظام التشغيل. ولحفظ المعلومات عن هذه الأحداث، يمكننا أن نستخدم إما ملف File، أو أحيانًا نستخدم الإخراج القياسي standard output. (الإخراج القياسي هو أن يتم طباعة المعلومات في الـ console أو سطر الأوامر وإذا كنا نستخدم برنامج IntelliJ، تبويب Run أو Debug).

الحاجة إلى استخدام السجلات Logs:

نحن بحاجة لاستخدام الـ Logs لعدة أسباب:

  • توفير الكثير من الوقت عندما نقوم باستكشاف أخطاء تطبيقنا في مرحلة ما مستقبلًا. إذا حدث استثناء أو حدث خطأ في البرنامج أثناء عمله، سنتمكن من العثور على اللحظة التي حدث فيها الخطأ بالضبط في السجل. هذا يجعل عملية تصحيح الخطأ Debugging أسهل بكثير.
  • من الممكن أيضًا تتبع من استخدم البرنامج أثناء حدوث الخطأ. إذا كان برنامجنا، كمثال، عبارة عن تطبيق ويب، سنتمكن حينها من معرفة من الذي أرسل طلب فتح الموقع. 
  • تساعد السجلات أيضًا في مراقبة تشغيل نظام معين، مما يسهل عملية التحقق لاحقًا. بهذه الطريقة، نحن دائمًا على دراية بكيفية عمل برامجنا ومدى أدائه إن كان جيدًا أم لا.

هكذا نكون قد تعرّفنا على ما هو التسجيل Logging والسجل Log نظريًا. ويتبقى أن نعرف عمليًا: كيف يمكننا استخدام التسجيل في برنامجنا؟.

تسجيل الأحداث عبر دالتي ()println و ()print:

قد تكون أسهل وأقرب الطرق لفعل ذلك، هي تسجيل الأحداث عبر استخدام دوال الطباعة، في الأماكن التي نريد أن نرى إذا كان بها أخطاء أم لا، أثناء فترة تطوير البرنامج. ولكن، هذه ليست طريقة عملية في تتبع أحداث التطبيق. لأنه لو استخدمنا الكثير من دوال الطباعة، سنحتاج أن نزيلها بعد أن يصبح البرنامج جاهزًا للتصدير ليعمل عند المستخدم. وإذا عدنا لتعديل البرنامج في يومٍ ما، سيتوجب علينا إعادة دوال الطباعة مرة أخرى.

تسجيل الأحداث في ملف خارجي:

ويمكننا أيضًا، إلتقاط الأحداث وحفظها في ملف خارجي، بنفس الطريقة التي كتبنا فيها على الملفات في الدرس السابق، أثناء فترة عمل البرنامج، كالتالي:

import java.io.File
import java.io.FileWriter

fun main() {

    val targetedDirectory = File("logs")

    if (!targetedDirectory.exists()) targetedDirectory.mkdir()

    FileWriter("logs/main-log.log").use {
        it.write("Hello, Logging")
    }
}

في الشفرة نُنشيء مجلد جديد إذا لم يكن موجودًا باسم logs، باستخدام دالة ()mkdir. (يتم إنشاء المجلدات عبر الصنف File أيضًا). داخل هذا المجلد، نقوم بإنشاء ملف باسم main-log أو أي اسم آخر والامتداد log، حتى يفهم نظام التشغيل أن هذا ملف خاص بالـ Log. (الصنف FileWriter سيقوم بإنشاء الملف إذا لم يكن موجودًا مسبقًا في المجلد logs ومن ثم سيتم الكتابة عليه عبر دالة ()write). أي نص سنضعه بين قوسي دالة ()write، سيتم حفظه في هذا الملف. وبعد تنفيذ الشفرة، ستكون النتيجة هي:

ولكن، الحفظ في ملف خارجي بهذه الطريقة، سيكون مكلفًا فيما يتعلق بالذاكرة. هل سيكون الملف مفتوحًا طوال فترة عمل البرنامج أم سيتم فتحه وغلقه مئات المرات لحفظ مئات الأسطر من الأحداث والرسائل؟ حسنًا، إذا فعلنا ذلك، فسنتعرض للكثير من مشكلات التعامل مع الموارد، والتي استعرضنا بعضها في درس تنظيف الموارد

إذًا، ما الحل؟ الحل هو استخدام المكتبات الجاهزة والتي بنيت أساسًا لغرض التسجيل Logging والتعامل مع الأحداث بأكثر الطرق الآمنة. بالتالي، تحتوي هذه المكتبات على الكثير من الميزات المُختبرة من قبل المبرمجين على مر السنين. هذا عدا أن هذه المكتبات، ستوفر لنا رسائل الـ Log، بها تاريخ ووقت حدوث الحدث ومصنفة ومرتبة حسب أهميتها، فيما يعرف بالـ Log Levels.

مستوى السجل Log Level:

قلنا سابقًا أنه يمكن إضافة الكثير من المعلومات المهمة إلى ملف السجل. لكن أي نوع من المعلومات هذه؟ وما هي أهميتها؟ حسنًا، تقسم رسائل الـ Log، إلى عدة مستويات حسب أهميتها، وتعرف بمستوى السجل log level أو خطورة السجل log severity. وهو جزء من المعلومات التي توضح مدى أهمية رسالة سجل معينة. وهذه الطريقة رغم بساطتها الظاهرة، لكنها قوية جدًا لتمييز أحداث السجل عن بعضها البعض. إذا تم استخدام مستويات السجل بالشكل الصحيح في التطبيق، فكل ما نحتاجه هو النظر إلى درجة خطورة الرسالة أولاً:

  • TRACE: يلتقط هذا المستوى جميع التفاصيل حول سلوك التطبيق. غالبًا ما يكون تشخيصيًا ولكن أكثر دقة وتفصيلًا من المستوى DEBUG. يتم استخدام مستوى السجل هذا في المواقف التي نحتاج فيها إلى معرفة ما حدث في تطبيقنا أو ما حدث في مكتبات الجهات الخارجية المستخدمة فيه بطريقة عامة.
  • DEBUG: يتم استخدامه لتسجيل المعلومات اللازمة لتشخيص التطبيق أو استكشاف الأخطاء وإصلاحها أو اختبارها بطريقة مفصلة. عادة ما تكون المعلومات في هذا المستوى، مطوّلة.
  • INFO: يستخدم لتسجيل الرسائل العامة التي لا تؤثر على سير عمل التطبيق (مثل، بدء / إيقاف عمل التطبيق).
  • WARN: يتم استخدامه لتسجيل الأحداث التي قد تؤثر على عمل التطبيق.
  • ERROR: يستخدم لتنبيهنا إلى أن هناك عملية فشلت. تعد السجلات في هذا المستوى مهمة دائمًا لأن الأخطاء التي يتم التبليغ عنها عبره، يمكن أن تؤثر على المستخدمين (كمثال: لا يمكن الاتصال بقاعدة البيانات أو أن الملف المطلوب مفقود …الخ).

 بعد النظر إلى مستوى الخطورة الذي يظهر مع السجل، يمكننا تحديد ما إذا كنا سنقوم بإصلاح الخطأ البرمجي فورًا، أم أنه ليس بهذه الخطورة ويمكننا إصلاحه لاحقًا. أم أنه مجرد تتبع والتطبيق يعمل بكل كفاءة ولا يوجد شيء لإصلاحه، وفي هذه الحالة يمكننا أن نراقبه لمعرفة ما إذا كان هناك فرصة لتحسين عمله.

يمكننا إنشاء مكتبة تقوم بإظهار مستويات الخطورة ووقت وتاريخ حدوث الحدث في رسالة منسقة، أو استخدام مكتبات الـ Logging الكثيرة، والتي ستقوم بكل ذلك ويقتصر دورنا على إعدادها داخل مشروع تطبيقنا.

مكتبات الـ logging في لغة جافا:

إذا كان برنامجنا هو تطبيق كوتلن موجه لنظام الـ JVM، يمكننا بكل بساطة استخدام أصناف الـ Logging الخاصة بلغة جافا المتواجدة في حزمة logging والتي يمكن جلب كل أصنافها لمشروعنا عبر الرابط:

import java.util.logging.*

وعلى الرغم من الفائدة التي تقدمها هذه الحزمة التي تأتي مضمنة مع مجموعة حزم تطوير برامج جافا JDK، لكن لطالما عانى المبرمجين من مشاكل في الأداء، أو عدم المرونة، أو نقص في الميزات …الخ. لذا قام البعض بإنشاء العديد من المكتبات تقدم أداءً وميزات أفضل. من ضمنها مكتبات مثل: Log4J2 و Logback وغيرهما. ثم أتت:

Simple logging facade for Java (SLF4J)

التي عملت على تبسيط عمليات الـ Logging، بإضافة طبقة (واجهة Facade) على العديد من مكتبات الـ Logging. بالإضافة لكل السابق، يمكننا أيضًا استخدام مكتبة خاصة بلغة كوتلن مثل: Kotlin-logging.

مكتبة Kotlin-logging:

هي مكتبة Logging مفتوحة المصدر تم تصميمها لتعمل على تطبيقات كوتلن. وهي عبارة عن طبقة (واجهة Facade) لمكتبة SLF4J، والتي بدورها كما قلنا في الفقرة السابقة، عبارة عن طبقة صممت لتسهيل التعامل مع العديد من مكتبات الـ Logging. وتتميز هذه المكتبة بطابع البساطة الذي يميز كوتلن عمومًا.

إضافة المكتبة للمشروع:

يمكننا قراءة تفاصيل إضافة والتعامل مع هذه المكتبة، بالذهاب إلى رابط المكتبة الرسمي في GitHub. ولتحميل المكتبة في مشروعنا، نحتاج لإضافة السطر التالي في ملف build.gradle (تم شرح كيفية إضافة المكتبات في درس أداة البناء Gradle):

implementation 'io.github.microutils:kotlin-logging:2.1.23'

ولأن المكتبة هي طبقة على مكتبة SLF4J، يجب أن نحملها هي الأخرى داخل مشروعنا:

implementation 'org.slf4j:slf4j-simple:2.0.0'

ولفهم طريقة عمل ذلك، يمكننا النظر إلى الصورة التالية من برنامج IntelliJ:

  1. الضغط مرتين بزر الفأرة الأيسر على ملف build.gradle لفتحه.
  2. إضافة رابطي المكتبتين عبر استخدام التكوين الافتراضي implementation.
  3. نضغط على زر المزامنة هذا لتقوم أداة الـ Gradle بتحميل المكتبتين في مشروعنا.

استخدام المكتبة:

الآن أصبحت المكتبة جاهزة لاستخدامها في مشروعنا. ولتجربة ذلك، يمكننا أن نُنشيء كائن logger، ونسنده لمتغير في أعلى الملف، لنستطيع الوصول إليه في أي مكان داخل الملف:

package preventingFailure.logging
import mu.KLogging

private val log = KLogging().logger

fun main() {

    log.trace("this a trace level from Kotlin-Logging library!")
    log.debug("this a debug level from Kotlin-Logging library!")
    log.info("this a info level from Kotlin-Logging library!")
    log.warn("this a warn level from Kotlin-Logging library!")
    log.error("this a error level from Kotlin-Logging library!")
}

في أعلى الملف، نُضمِّن المكتبة عبر الرابط: 

import mu.KLogging

ثم بعد ذلك، نُنشيء متغير على مستوى الملف (أي خارج أي مكوِّن آخر مثل الأصناف والدوال …الخ)، لنتمكن من استخدامه في أي مكان داخل هذا الملف. في دالة ()main، نستخدم المتغير log والذي يُمثل كائن الـ logger، في استدعاء دوال تُمثل مستويات الـ logging. وعند تنفيذ الشفرة، سيتم طباعة الأسطر التالية في تبويب Run في برنامج IntelliJ:

[main] INFO mu.KLogging - this is INFO level from Kotlin-Logging library!
[main] WARN mu.KLogging - this is WARN level from Kotlin-Logging library!
[main] ERROR mu.KLogging - this is ERROR level from Kotlin-Logging library!

تبدأ كل رسالة في النتيجة أعلاه، بالاسم main. وهذا يعني أن الرسالة تم إلتقاطها، في الخيط الحاسوبي الرئيسي main. (الخيوط الحاسوبية Threads، هي موضوع متقدم وسنخصص لها درس منفصل لاحقًا). ثم مستوى الرسالة INFO أو WARN أو ERROR. ثم اسم كائن الـ logger، وهو حسب مكتبة Kotlin-logging، اسمه mu.KLogging. وأخيرًا، الرسالة المُخصصة التي أرسلناها لكل دالة.

ونلاحظ في النتيجة أيضًا، أنه لم يتم طباعة المستوى TRACE والمستوى DEBUG. ولا وجود لوقت وتاريخ الرسالة. هذا لأن هذه هي الاعدادات Configurations الافتراضية لمكتبة SLF4J والتي تستخدمها مكتبة Kotlin-logging للقيام بعمليات الـ Logging. إذًا، أين توجد هذه الإعدادات؟ وكيف يمكننا تغييرها أو تعديلها؟ 

إعدادات مكتبة الـ Logging:

لتغيير الإعدادات الافتراضية، يجب أن نضيف ملف آخر يحتوي على هذه الإعدادات. وحسب هذه الصفحة من موقع مكتبة SLF4J الرسمي في Github، يجب أن يكون اسمه simplelogger وينتهي بالامتداد أو الصيغة properties:

simplelogger.properties

ويجب أن نضع هذا الملف، في المجلد resources والذي يمكن إيجاده في المسار: src/main/ داخل مشروع برنامجنا. (إذا كان برنامجنا يستخدم أداة البناء Gradle):

وفي نفس الصفحة من موقع المكتبة، نجد بعض الإعدادات المُضافة لهذا الملف. يمكننا نسخها وإضافتها له:

نلاحظ في النص الذي ألصقناه في الملف، أن هناك العلامة # يسار كل سطر. هذا حتى يتم اعتباره مجرد تعليق ولا يتم تنفيذه. تمامًا كما نفعل عندما لانريد تنفيذ سطر برمجي في لغة كوتلن، نضيف يساره //. ونجد أن كل الإعدادات التي يمكننا أن نقوم بتعديلها، تبدأ بالكلمة org. إذًا، سنزيل العلامة # من يسار الإعداد الذي نريده، ثم نعدِّل عليه حسب الحاجة.

كمثال، نجد أول إعداد هو:

org.slf4j.simpleLogger.defaultLogLevel=info

هذا الإعداد الافتراضي لطباعة رسائل الـ Logging عبر هذه المكتبة. يمكننا تغييره إلى trace:

org.slf4j.simpleLogger.defaultLogLevel=trace

وعند تننفيذ الشفرة في دالة ()main مرة أخرى ستكون النتيجة:

[main] TRACE mu.KLogging - this is TRACE level from Kotlin-Logging library!
[main] DEBUG mu.KLogging - this is DEBUG level from Kotlin-Logging library!
[main] INFO mu.KLogging - this is INFO level from Kotlin-Logging library!
[main] WARN mu.KLogging - this is WARN level from Kotlin-Logging library!
[main] ERROR mu.KLogging - this is ERROR level from Kotlin-Logging library!

بعد تغيير الإعداد إلى trace، تم إظهار رسائل الـ TRACE و DEBUG أيضًا. وهذا لأن مستوى الـ TRACE يعد أشمل وسيتم إظهار الرسائل من كل المستويات الأخرى، إذا تم اختياره في الإعدادات.

أمّا إذا غيرنا الإعداد إلى error:

org.slf4j.simpleLogger.defaultLogLevel=error

فستكون النتيجة هي رسالة سجل من مستوى الـ ERROR فقط:

[main] ERROR mu.KLogging - this is ERROR level from Kotlin-Logging library!

فقط رسائل مستوى الـ ERROR هي ما ستظهر في سجل الـ Log. ويمكننا فهم الترتيب من المستوى الأشمل إلى المستوى الأكثر تخصصًا، كالتالي:

TRACE -> DEBUG -> INFO -> WARN -> ERROR

إعدادات أخرى:

بنفس الطريقة في الفقرة السابقة، يمكننا تخصيص إعدادات مكتبة الـ Logging حسب حاجتنا. كمثال، يمكننا إضافة الوقت والتاريخ، بإزالة العلامة # من الإعداد التالي وتغيير القيمة false الافتراضية إلى القيمة true:

org.slf4j.simpleLogger.showDateTime=true

وستكون نتيجة التنفيذ هي:

2022-09-03 12:27:06:043 +0200 [main] TRACE mu.KLogging - 
this is TRACE level from Kotlin-Logging library!
2022-09-03 12:27:06:044 +0200 [main] DEBUG mu.KLogging - 
this is DEBUG level from Kotlin-Logging library!
2022-09-03 12:27:06:044 +0200 [main] INFO mu.KLogging - 
this is INFO level from Kotlin-Logging library!
2022-09-03 12:27:06:044 +0200 [main] WARN mu.KLogging - 
this is WARN level from Kotlin-Logging library!
2022-09-03 12:27:06:045 +0200 [main] ERROR mu.KLogging - 
this is ERROR level from Kotlin-Logging library!

تم إضافة التاريخ والوقت في يسار سطر الرسالة. ولتغيير طريقة ظهور التاريخ والوقت في الرسالة، يمكننا الذهاب إلى إعداد آخر:

org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z

هذا هو الإعداد الافتراضي للتاريخ والوقت في المكتبة. يمكننا تغييره حسب الأنماط الخاصة بهذا الصنف SimpleDateFormat. وهو صنف خاص بلغة جافا يمكننا الإطلاع على الأنماط التي يمكننا استخدامها معه في هذا الموقع. أنا عدلت على الإعداد كالتالي:

org.slf4j.simpleLogger.dateTimeFormat=d.MM.yyyy - HH:MM zzzz

وكانت النتيجة:

3.09.2022 - 12:09 Central European Summer Time [main] TRACE mu.KLogging - 
this is TRACE level from Kotlin-Logging library!
3.09.2022 - 12:09 Central European Summer Time [main] DEBUG mu.KLogging - 
this is DEBUG level from Kotlin-Logging library!
3.09.2022 - 12:09 Central European Summer Time [main] INFO mu.KLogging - 
this is INFO level from Kotlin-Logging library!
3.09.2022 - 12:09 Central European Summer Time [main] WARN mu.KLogging - 
this is WARN level from Kotlin-Logging library!
3.09.2022 - 12:09 Central European Summer Time [main] ERROR mu.KLogging - 
this is ERROR level from Kotlin-Logging library!

إعداد آخر يمكننا تغييره، هو إزالة اسم كائن الـ logger المستخدم، من رسائل السجل. وهو الذي يظهر باسم mu.KLogging. لفعل ذلك، يمكننا الذهاب إلى الإعداد التالي وتغيير القيمة true الافتراضية إلى القيمة false:

org.slf4j.simpleLogger.showLogName=false

وستكون النتيجة:

3.09.2022 - 12:09 Central European Summer Time [main] TRACE 
this is TRACE level from Kotlin-Logging library!
3.09.2022 - 12:09 Central European Summer Time [main] DEBUG 
this is DEBUG level from Kotlin-Logging library!
3.09.2022 - 12:09 Central European Summer Time [main] INFO 
this is INFO level from Kotlin-Logging library!
3.09.2022 - 12:09 Central European Summer Time [main] WARN 
this is WARN level from Kotlin-Logging library!
3.09.2022 - 12:09 Central European Summer Time [main] ERROR 
this is ERROR level from Kotlin-Logging library!

وهكذا نمضي في تخصيص إعدادات مكتبة الـ Logging حسب حاجتنا. المزيد من الإعدادات وشرحها، نجدها في الموقع الرسمي للمكتبة.

الخلاصة:

كل مطوري البرامج يرتكبون الأخطاء، وجميع البرامج هي عرضة للأعطال أو فشل أجزاء منها، في أي لحظة. لذلك، من المهم تحديد الأخطاء والعمل على حلها بسرعة. ويعد الـ Logging واحد من ضمن أهم الأدوات المتاحة لنا، لتتبع خطوات عمل تطبيقنا ومن ثم معرفة إذا كانت هناك أخطاء أو حتى إن كانت هناك إمكانية لتحسين عمل التطبيق أكثر. ولكن ليس من السهل تعلم واستخدام الـ Logging وإعداده بالطريقة الصحيحة حيث يجب أن نمارسه كثيرًا قبل أن يكون واضحًا لنا. وأسهل طريقة اتبعناها في هذا الدرس، هي استخدام مكتبة كوتلن سهلة الاستخدام. ولكن بالطبع يمكننا استخدام مكتبات أخرى سواء تلك التي تأتي في حزمة جافا، أو حتى المكتبات التي تصميمها بناءً عليها مع ميزات إضافية.

هذا الدرس هو جزء من سلسلة تعليم مبادئ البرمجة بلغة كوتلن. لمُتابعة الدروس منذ البداية ومُشاهدة فهرس المحتويات يمكنك الانتقال إلى الدرس الأول من هنا

هل أعجبك المحتوى وتريد المزيد منه يصل إلى صندوق بريدك الإلكتروني بشكلٍ دوري؟
انضم إلى قائمة من يقدّرون محتوى إكسڤار واشترك بنشرتنا البريدية.