برنامه‌نویسی

هنر Log گرفتن

دراین مقاله سعی داریم، دستورالعمل‌های مفیدی برای Log گرفتن در برنامه‌های وب یا دسکتاپ به شما ارئه دهیم، همچنین به سوالاتی مانند چه موقع و چه‌چیزی را چگونه ثبت کنیم، پاسخ می‌دهیم.

بررسی اجمالی

در واقع تمامی این‌ها:

  • Log گرفتن (logging)
  • نوشتن مستندات (design documentation)
  • آزمایش‌واحد (unit testing)

فعالیت‌های ثانویه‌ای هستند که در هرنوع از توسعه، می‌بایست آن‌هارا انجام دهیم. برای اکثر این فعالیت‌ها مانند نوشتن مستندات یا آزمایش‌واحد، اطلاعات بسیار زیادی در سطح اینترنت و کتاب‌های الگوی طراحی موجود‌ است و در آن‌ها بهترین شیوه‌ها و تکنیک‌ها را فرامی‌گیریم.

با تمام این اوصاف به نظر می‌رسد، Log گرفتن سطح برابری از توجه را به خود جلب نکرده‌است، درنتیجه توسعه‌دهندگان به سختی از چالش‌های موجود در این مبحث آگاهی دارند. به نظر شما ثبت‌رخداد‌های موجود اهمیت دارد؟ اگر این سوال را از کسی که در عیب‌یابی برنامه، از راه‌حل Log گرفتن استفاده کرده‌باشد بپرسید، مطمئنا جواب او یک بله قاطعانه است.

اما اگر بخواهیم کمی از واقعیت‌ها بگوییم، هیچ چیز ناامید کننده تر از پیدا کردن یک فایل Log پر از جزئیات اضافی و فاقد اطلاعات حیاتی لازم، برای تشخیص عیب نیست.

متاسفانه، علی‌رغم اهمیت بالای این موضوع، وقتی که یک برنامه، وارد محیط پروداکشن می‌شود، توسعه‌دهندگان دوست دارند، آزمایش‌واحد یا Log گرفتن‌ها را چندین روز عقب‌بیندازند. اما توسعه‌دهندگان هنگام انجام آزمایش‌های‌واحد یک پاسخ سریع دریافت می‌کنند و این پاسخ مشوق آن‌هاست، اما در Log گرفتن اینچنین نیست و حتی سیستم Log را خیلی از افراد به برنامه خود اضاف نمی‌کنند.

سیتم Log را مانند unit test باید از روز اول اجرای پروژه در نظرگرفت. این سیستم یک ابزار ارزشمند، برای هر پروژه‌ای است، این مقاله تلاش‌می‌کند که اهمیت Log گرفتن را برای تمام خوانندگان روشن سازد.

Log گرفتن چیست؟

احتمالا شماهم مثل من فکر می‎‌کنید: “Log گرفتن شامل نوشتن یک‌سری چیزها در فایل است”. شاید به صورت کلی این تعریف درست باشد، اما این یک دیدگاه سطحی است.

اگر سوال را با جزئیات بیشتری تجزیه و تحلیل کنیم، به ما کمک می‌کند تا به سوالات مهم‌تر از جمله “چه چیزی را باید ثبت کنیم؟” پاسخ دهیم.

تمام برنامه‌های رایانه‌ای برای برقراری ارتباط با کاربر به نوعی رابط‌کاربری دارند. برای برنامه‌های دسکتاپ، این رابط توسط سیستم عامل و همچنین برای برنامه‌های وب، این رابط از راه دور به کمک مرورگر ارائه می‌شود.

این رابط‌کاربری، مکانیزم اصلی است که این نرم‌افزار برای برقراری و ارتباط با دنیای خارج از خود استفاده می‌کند. این امر، برای حفظ روند پیشرفت یک برنامه میتواند مفید باشد تا سابقه‌ای از عملکرد‌هایی که انجام می‌دهد و تغییراتی که بوجود می‌آید را، جمع آوری کند.

این اطلاعات برای یک کاربراصلی هیچ نگرانی ایجاد نمی‌کند. به هر‌صورت برنامه برای برقراری ارتباط، به یک رابط‌ثانویه احتیاج دارد، با داشتن این دیدگاه در ذهنمان، متوجه می‎‌شویم:

Log گرفتن، یک فرایند از ثبت عملکرد‌های برنامه و وضعیت آن در یک رابط ثانویه است.

این عبارات ما را ترغیب می‌کند، تا کمی بیشتر درمورد چگونگی، استفاده از اطلاعات در برنامه خود فکر کنیم. ما عملکرد‌های برنامه و وضعیت آن را ثبت می‌کنیم.

سوالاتی که با آن‌ها روبرو هستیم: کدام اقدامات و چه وضعیتی را باید ثبت کنیم؟ این اطلاعات باید در کدام رابط ثانویه ذخیره شوند؟ ما این رخدادها را می‌تونیم در فایل، یک وب‌سرویس یا حتی ایمیل ذخیره کنیم و ماهیت رابط‌ثانویه ما اهمیتی ندارد.

با این‌کار کسانی که وقایع را می‌بینند، به رابط اصلی برنامه دسترسی ندارد. این گزارشات ذخیره شده، از اهمیت بالایی برخوردارد هستند، اغلب مواقع منبع اصلی تشخیص مشکلات، این گزارشات هستند.

خوانندگان گزارشات ثبت‌شده می‌توانند دلایل مختلفی برای نگاه کردن به اقدامات برنامه یا وضعیت آن داشته‌باشند. مثال های ذکر شده در این مقاله، برای نگهداری نرم‌افزار و رفع اشکالات آن است. چنانکه ممکن است از این گزارشات در راستای ایجاد آمار مربوط به استفاده کابران از برنامه استفاده شود یا می‌تواند منجر به کشف الگوهایی برای استفاده تحقیقاتی در بازار شوند.

تابه‌حال برای شما اهمیت Log گرفتن واضح شده‌است، فقط نیاز به تفکری عمیق‌تر، در این زمینه است.

یک رویکرد ساختارمند برای Log گرفتن

برخی از تغییرات وضعیت یا اقدامات انجام شده توسط یک برنامه، مهم‌تر از سایرین است. برای انجام این کار، شما نیاز به طبقه‌بندی گزارشات خواهید‌ داشت.

سیستم‌های طبقه‌بندی معمول سطح گزارشات را به متن گزارش الحاق می‌کنند. سطوح گزارشات معمولا از سه نوع تشکیل شده‌اند، که توصیف کننده وقایع است.

اگر آن‌هارا از شدت پایین به بالا درنظربگیریم:

  • WARN
  • ERROR
  • FATAL

و یکی دیگر از آن‌ها برای اهداف کلی تر داده‌ها مورد استفاده قرار می‌گیرد:

  • INFO

و در آخر برای ثبت گزارشات مربوط به عیب‌یابی از برچسب زیر استفاده می‌کنیم:

  • DEBUG

فریم‌ورک های Log گرفتن در یک ساختار خاص، گزارشات را می‌نویسند که باعث آسانی و سهولت استفاده کارآمد از گزارشات می‌شود، بعضی از آن‌ها امکانات بیشتری را در اختیار شما قرارمی‌دهند.

برای مثال: پیام‌هایی با سطح FATAL را سریعا از طریق ایمیل به پشتیبان سرویس ارسال می‌کند.

با توجه به اهمیت گزارشات Log گرفتن، در هنگام شروع یک پروژه، تیم‌توسعه باید دقیقاً در‌ مورد نحوه استفاده از این سطوح، به منظور اطمینان از سازگاری بین‌توسعه‌دهندگان توافق کنند.

در زیر یک دستورالعمل پیشنهادی برای استفاده از هر سطح وجود دارد:

داده‌ها (INFO)

همانطور که از نام آن پیداست، این گزارشات دربرگیرنده اطلاعات است و نباید از آنها برای نشان دادن خطا در برنامه استفاده شود. برای استفاده موثر از این گزارشات، سعی کنید به اطلاعات عمومی که به تشخیص خطاهای موجود در برنامه کمک می‌کند، فکر کنید. برخی از اطلاعاتی که ممکن است مورد استفاده قرار گیرد:

  • اطلاعات مربوط به نسخه نرم‌افزار – غالباً ایده خوبی است که این مورد را در هنگام شروع برنامه در گزارشات قراردهید. وقتی حتی مطمئن نیستید از چه نرم افزاری استفاده می شود این امر به شما در تشخیص مشکلات کمک می‌کند.
  • اطلاعات کاربردی – چه کسی از نرم‌افزار استفاده می‌کند؟ آنها در حال حاضر چه کاری انجام می‌دهند؟
  • در برنامه فعلی از چه سرویس‌های خارجی (پایگاه‌داده – وب‌سرویس و …) استفاده می‌شود؟

هشدار (WARN)

این مورد برخی از اشکالات، عدم موفقیت برنامه در اجرای عملکرد‌هایش را نشان می‌دهد. گزارشات این سطح، سعی در نشان دادن روبرویی برنامه با یک مشکل بالقوه دارند.

به‌هر‌حال، تجربه کاربر به هیچ وجه تحت تأثیر قرار نمیگیرد. به عنوان مثال، در صورت عدم استفاده از سرویس خارجی هنگامی که، یک سرویس‌ثانویه که عملکردهای مشابه را انجام می‌دهد در دسترس بود، ممکن است یک پیام هشدار مناسب باشد.

به همین دلیل، در صورت نیاز به تلاش‌های مکرر برای دسترسی به یک منبع داده، یک پیام هشدار مناسب است.

خطا (ERROR)

در دومین سطح از اهمیت گزارشات به خطاها می‌پردازیم، این کلمه نشان‌دهنده این است اتفاق مهم‌تری رخ داده. از پیام‌های خطا در زمان مواجه‌شدن با مشکلات قابل توجه استفاده می‌شود یا به عبارت دیگر کاربر به نوعی تحت‌تاثیر قرار گرفته. به عنوان مثال، اتصال به بانک اطلاعاتی ناکام مانده است و در نتیجه قسمت‌هایی از برنامه غیرقابل استفاده می‌شوند.

مهلک (FATAL)

در این مرحله نه‌تنها تجربه‌کاربری تحت تاثیر قرار گرفته، بلکه تمام برنامه متوقف شده‌است. مثلا، کامپوننت‌ی که عملکرد اصلی برنامه را به دوش داشته است، و در وضعیت ناپایدار قرار گرفته و تنها مسیر عملی ممکن برای برنامه، خاتمه آن به‌طور کلی است.

اشکال‌زدایی کردن (DEBUG)

در این سطح، پیام‌ها با اهداف اشکال‌زدایی برنامه مورد استفاده قرارمی‌گیرند. به عبارت دیگر، این پیام‌ها بطور مستقیم به توسعه دهنده هدایت می‌شوند. آنچه که شما از آن استفاده می‌کنید، به برنامه کاربردی که در حال تهیه آن هستید، بستگی دارد.

بسیاری از مشکلات را می‌توان، از طریق اشکال زدایی برطرف کرد اما استفاده از پیام‌های DEBUG منسوخ شده است. با‌این‌حال، چند موقعیت وجود دارد که پیام‌های DEBUG کاملاً مفید هستند، به عنوان مثال:

  • در اشکال‌زدایی گرافیکی که برنامه های دسکتاپ را ارائه می‌دهند، فرآیند fire up دیباگر ممکن است، در فرآیند ارائه دهنده اختلال ایجاد کند، پیام‌های DEBUG می‌تواند، روشی مفید برای تماشای روند ارائه با روشی کم‌تحریک باشد.
  • برخی از انواع برنامه‌ها یا مراحل فرآیند توسعه برای اجرای برنامه به محیط‌های خاصی احتیاج دارند. در این موارد، استفاده از ابزار اشکال زدایی، ممکن است گزینه قابل قبولی نباشد. در این موارد، پیام‌های DEBUG می تواند ابزاری مفیدی برای یافتن راه حل برای اشکالات فریبنده باشند.

موارد فوق فقط دو نمونه از فواید DEBUG هستند. همانطور که قبلاً گفته شد، سودمندی به ماهیت برنامه در حال توسعه بستگی دارد.

چه چیزی باید ثبت شود؟

اکنون تعریفی از چگونگی برخی دستورالعمل‌ها برای استفاده از هر سطح گزارش داریم. اکنون زمان آن فرا رسیده است که این سؤال بزرگ مطرح شود: “چه چیزی را ثبت کنم؟”. این یک سؤال آسان برای پاسخ نیست، و وابسته به ماهیت برنامه در حال تهیه است.

اگر گزارش‌های برنامه، تنها منبع مفصلی از اطلاعات هستند که در هنگام تشخیص یک نقص در اختیار تیم پشتیبانی می‌گیرد، پس باید کاملاً دقیق باشد. اما اگر ابزار دیگری برای نظارت بر فعالیت‌های کاربر موجود باشد، اطلاعات واردشده می‌توانند پراکنده‌تر باشند.

در هر صورت، صرف‌نظر از سطح جزئیات لازم، کیفیت هر پیام شخصی که وارد شده مهم است، و این چیزی است که این بخش در درجه اول به آن توجه می‌کند.

اهمیت متن پیام گزارشات

اینکه یک برنامه با موفقیت یک کار را انجام‌دهد یا خیر، غالباً وابسته به ورودی کاربر است. در نتیجه، این اطلاعات متنی، ممکن است هنگام تلاش برای تشخیص یک عیب بسیار مهم مورد استفاده قرار بگیرند. متأسفانه، آخرین اطلاعات حیاتی، اغلب وجود ندارد.

به عنوان مثال، اگر گزارشات یک دستگاه اتوماتیک (ATM – یا وجه نقد) را بگیرید. گزارش آن برنامه ممکن است به صورت زیر باشد:

2009-06-05 08:15:23 [INFO] User credentials entered
2009-06-05 08:15:23 [WARN] Incorrect PIN
2009-06-05 08:15:37 [INFO] User credentials entered
2009-06-05 08:15:37 [INFO] User credentials validated
2009-06-05 08:15:46 [INFO] Cash withdrawal requested
2009-06-05 08:15:47 [ERROR] Communication failure
Acme.ATM.Integration.ServerException: Unable to connect to bank server
  at Acme.ATM.Integration.WithdrawalService.Connect(): line 23
  ...

اگر ما در حال شناسایی مشکلات حسابرسی روزانه از دستگاههای خودپرداز باشیم، گزارشات بالا به ما چه می‌گوید؟ کاربری پس از دو تلاش با موفقیت وارد سیستم شده و درخواست برداشت وجه داشته، اما به دلیل خطای ارتباطی، این درخواست سرویس‌دهی نشده است.

در اصل، موارد فوق اطلاعات بسیار کمی در اختیار ما قرار می‌گیرد و فقط رخداد یک خطا را اعلام می‌کند، به این صورت امیدواری ما برای رفع این خطا، بسیار کم است. این مشکل می تواند، مربوط به کاربر خاص یا یک سرور بانکی یا چیز دیگر باشد.

در گزارش بالا بخش اصلی اطلاعات وجود ندارد، اگر اطلاعات کاربر ثبت می‌شد، ما به داده‌های بیشتری دست پیدا می‌کردیم. مانند اطلاعات حساب آن‌ها یا بانکی که در آن حساب کاربری دارند (آیا مشکل از سرور این بانک است؟). چند جزئیات ساده در متن گزارش، دنیایی از تفاوت را ایجاد می‌کند:

2009-06-05 08:15:23 [INFO] User credentials entered, Acc=123765987
2009-06-05 08:15:23 [WARN] Incorrect PIN
2009-06-05 08:15:37 [INFO] User credentials entered, Acc=123765987
2009-06-05 08:15:37 [INFO] User credentials validated
2009-06-05 08:15:46 [INFO] Cash withdrawal requested, Amount=450.00
2009-06-05 08:15:47 [ERROR] Communication failure, server=acmebank.com:345
Acme.ATM.Integration.ServerException: Unable to connect to bank server
  at Acme.ATM.Integration.WithdrawalService.Connect(): line 23
  ...

این بار می‌دانیم کدام کاربر، چقدر درخواست داشته و درخواست به کدام سرور فرستاده شده. با این اطلاعات، ما فرصت بیشتری، برای رفع اشکال در دست داریم. حتی اگر با این اطلاعات نتوانستیم مشکل را رفع کنیم، یک الگو مفید برای رویارویی با خطاها پیش‌روی ما است.

به طور خلاصه اگر پیام گزارش، بدون اطلاعات مفید باشد، کاربردی ندارد و مانند این است که اصلا پیامی وجود ندارد.

هنگام قالب‌بندی پیام‌های گزارش، استانداردی را درنظر بگیرید. اگر شما فایل گزارشات را به کمک ابزار هایی مانند (grep – indispensible) آنالیز، این امر بسیار ارزشمندتر است. به عنوان مثال، ممکن است یک پیام log که نام کاربری را در بر دارد، به شرح زیر باشد:

2009-06-05 08:15:37 [INFO] User credentials entered, User=Bob Smith, Acc=123765987

با این وجود، محصور کردن مقادیر متغیرهای کاربر و حساب و در نهایت ایجاد یک Regular Expression یا شکل‌های دیگر از الگوی مطابقت، ایجاد مقادیر خاص در یک فایل بزرگ را آسان تر می‌کند:

2009-06-05 08:15:37 [INFO] User credentials entered, User={Bob Smith}, Acc={123765987}

Log گرفتن به صورت همزمان

ما با مثال قبلی خود یعنی ATM ادامه می‌دهیم، مثال‌های قبل، تعامل یک کاربر با یک سیستم را نشان می‌داد. در تجارت ما با ATM های متعدد و تعامل آن‌ها با نرم‌افزار (سرویس‌هایی مانند: وب‌سرور، مشتریان از راه دور) روبرو هستیم. گزارشات ممکن است به شرح زیر باشد:

2009-06-05 08:15:23 [INFO] User credentials entered, Acc={123765987}
2009-06-05 08:15:23 [WARN] Incorrect PIN
2009-06-05 08:15:37 [INFO] User credentials entered, Acc={567993322}
2009-06-05 08:15:23 [INFO] User credentials entered, Acc={123765987}
2009-06-05 08:15:23 [WARN] Incorrect PIN
2009-06-05 08:15:37 [INFO] User credentials validated
2009-06-05 08:15:23 [WARN] Account locked - reset required
2009-06-05 08:15:46 [INFO] Cash withdrawal requested, Amount={450.00}

در مثال بالا، بسیار آسان می توانید پیام گزارشات مربوط به هر حساب را به عنوان یک یادداشت جانبی ببینید. برخی framework های Log گیری می‌توانند برای اضاف کردن متن‌ ها به شکل یک موضوع یا هویت کاربر، پیکربندی شوند (توکن‌های %thread یا %identity را بررسی کنید).

اما در محیط سرور، درخواست کاربر ممکن است توسط یک نخ (thread) دیگر سرویس‌دهی شود، بنابراین برخی دیگر از شکل‌های نشست (session) مورد نیاز است.

چرا نباید همه‌چیز را ثبت کرد؟

هرچه اطلاعات بیشتری در اختیار ما باشد، تشخیص خطای برنامه آسان تر است، بنابراین این سؤال پیش می‌آید “چرا همه چیز را ثبت نمی‌کنیم؟”

پیام‌های زیاد گزارشات باعث دردسر هستند، به چند مورد از آن‌ها می‌پردازیم:

  • نوشتن کدها برای لاگ گرفتن بسیار شبیه هم هستند همچنین زمان زیادی را شما می‌گیرد و بسیار پرهزینه است.
  • با Log گرفتن‌های زیاد، عملکرد سیستم از حالت بهینه خارج می‌شود.
  • تجزبه و تحلیل لاگ‌های طولانی وقت زیادی می‌گیرد و به این صورت اطلاعات بسیار مهم در بین لاگ‌ها مخفی می‌شوند.
  • اگر به دلایلی واقعاً به این سطح جزئیات نیاز دارید، روش‌های بهتری برای افزودن آن وجود دارد تا اینکه به صورت دستی همه این‌ها را خودتان تایپ کنید. فریم‌ورک‌های AOP مانند PostSharp می‌توانند این نیاز را رفع کنند.

همه استثنائات خطا نیستند!

یک الگوی معمولی که مشاهده می شود، موارد زیر است:

try
{
  ComponentX.DoStuff();
}
catch (Exception e)
{
  log.Error("ComponentX threw an exception", e);
}

با رخ دادن هر استثنا، یک پیام با سطح خطا به لاگ ارسال می‌شود. مشکل این است که، اغلب استثنائات پیش‌بینی یا حتی انتظارت هستند.

با تعریفی که در بالا گفته شده بود، پیام‌های سطح ERROR برای چیزهایی در نظر گرفته شده است که تجربه کاربر به نوعی تحت تأثیر قرار گرفته یا تخریب می‌شود. قبل از ثبت یک استثنا، به عنوان خطا، دوباره به آن فکر کنید …

با لاگر‌های نام‌گذار آشنا شوید

برنامه های بزرگ معمولاً از اجزای پیچیده‌ای تشکیل شده‌اند. همانطور که در بخش قبل گفته‌شد، پیام‌های خوب، کوتاه هستند و مهم‌ترین اطلاعات را ارائه می‌دهند.

بعضی اوقات با کامپوننت‌هایی که غالباً وظایف مشابه مانند تأیید هویت یا اتصال به پایگاه داده‌ها را انجام می‌دهند، می‌توان تعیین کرد که دقیقاً کدام کامپوننت، پیام خاصی را لاگ بگیرد.

در اینجاست که مفهوم لاگر‌های نام‌گذار مفید واقع می‌شود. برنامه فعلی ما دارای یک سیستم نام‌گذاری برای سازمان‌دهی نام class و namespace ها دارد. Log گرفتن با روش‌های معمول، مارا به استفاده از نام‌ هر class در کنار اطلاعات موجود وادار می‌کند.

namespace Acme.Components.Things
{
  class WidgetClass 
  { 
    static Logger logger = LoggerService.GetLogger("Acme.Components.Things.WidgetClass"); 

    // other class members go here  
  }
}

یا می‌توانید به شکل زیر عمل کنید:

namespace Acme.Components.Things
{  
  class WidgetClass 
  { 
    static Logger logger = LoggerService.GetLogger(typeof(WidgetClass).FullName); 

    // other class members go here  
  }
}

با این تکه کد، هر لاگ همراه با نام class خود ثبت‌ می‌شود.

منبع: https://www.codeproject.com/Articles/42354/The-Art-of-Logging