כיצד לכתוב Trace אפקטיבי?

כיצד לכתוב Trace אפקטיבי?

Print Friendly, PDF & Email

במאמרים קודמים שלי בנושא ה-Tracing כבר התייחסתי לאופן בו ניתן להגדיר Tracing באפליקציות שונות (WinForms ו-WebForms) הפעם ברצוני להתייחס לאופן בו כדאי לעשות זאת על מנת לייצר Trace אפקטיבי מספיק אשר יהיה ניתן להשתמש בו הן כמנגנון לאיתור ופתרון תקלות (Troubleshooting) והן כמנגנון אשר יכול לסייע בניתוח הפעילויות המבוצעות במערכת.

השלב הראשון  – באפליקציות WebForms מקובל בדרך כלל להשתמש ב-Tracing ממרחב השמות System.Web. ל-Tracing ממרחב שמות זה יש חיסרון משמעותי כאשר האפליקציה מתרסקת (כלומר נזרקת Exception) כל ה-Trace שנרשם עד לאותו רגע נעלם כלא היה שכן הוא היה שמור בזיכרון של האפליקציה. חיסרון נוסף הוא שדף ה-axd מוגבל בכמות הרשומות שאותן הוא מחזיק על ידי ה-Attribute ששמו requestLimit, לאחר מספר הפניות שהוגדר ב-Attribute זה לא יירשמו עוד הערות ל-Trace עד לביצוע Clear trace messages. חיסרון נוסף הוא העובדה שהדף הוירטואלי המשמש להצגתו בנוי באופן היררכי, כלומר רשימה של פניות לשרת כאשר כל רשומה ברשימה זו מהווה קישור לדף וירטואלי המציג את פרטי הפניה, כאשר ברצונך לאתר פנייה מסוימת על סמך הנתונים שהועברו בה ולחקור את הודעות ה-Trace שנרשמו במסגרתה תצטרך לעבור ידנית פניה אחר פניה בכדי לאתר את זו שאתה מחפש. הפתרון האלגנטי לבעיה זו הוא היכולת באופן קונפיגורטיבי בלבד להגדיר כי כל הודעות ה-Trace הנשלחות למרחב השמות System.Web יישלחו באופן אוטומטי גם ל-Tracing ממרחב השמות System.Diagnostics והרי כבר הסברתי במאמרים הקודמים כי הודעות Trace שנשלחו אל מרחב שמות זה ניתן לנתב למעשה לכל מקום באמצעות הגדרת Listener מתאים. ובכן, על מנת להגדיר כי כל הודעות ה-Trace הנשלחות (ע"י קוד התכנית) אל Tracing ממרחב השמות System.Web יישלחו אוטומטית גם ל-Tracing ממרחב השמות System.Diagnostics יש לעדכן את השורה הבאה ב-web.config של המערכת הרלוונטית כך שתיראה באופן כזה:

<system.web>
    <trace localonly="false" requestlimit="40" enabled="true" writeToDiagnosticTrace="true"/>
</system.web>
גם במקרה בו האפליקציה היא אפליקציית Web אשר משתמשת ב-Tracing של System.Diagnostics לצורך כתיבת הודעות Trace עדיין ניתן לנתב אותן גם אל ה-Tracing של ה-System.Web (כלומר, כך שיוצגו ב-trace.axd המתאים), לצורך כך יש לעדכן את השורה הבאה כפי שמופיע להלן:

<system.diagnostics>
<trace>
<listeners>
<add name="WebPageTraceListener" type="System.Web.WebPageTraceListener, System.Web, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/> </listeners> </trace> </system.diagnostics>

השלב הבא הוא כמובן בחירה של סוגי הנתונים אשר ייכתבו כחלק מה-Trace ומתי למעשה לבצע כתיבה אל ה-Trace. הנתונים שחשוב מאד לכלול אותם כחלק מכל הודעת Trace שונים מטבע הדברים בין אפליקציה לאפליקציה. למרות זאת בשורות הבאות אני אנסה לספק מספר כללי אצבע שיסייעו בהחלטה האם נתון מסוים נדרש להופיע כחלק מה-Trace:

1. תאריך ושעה: לדעתי זהו הנתון החשוב ביותר ב-Trace במיוחד אם נתון זה נרשם בדיוק גבוה (מילישניות לפחות). בעזרת נתון זה ניתן בקלות להצליב הודעות שגיאה שנרשמו מול רישום פעילויות במערכות אחרות המקושרות אל המערכת הבעייתית. בנוסף, במקרה של בעיות איטיות יהיה ניתן בקלות לזהות איזו פעולה הייתה הכי "כבדה".

2. מספר גרסת הרכיב הרלוונטי: אם כל רכיב אשר רושם ל-Trace יציין את גרסתו המלאה יהיה ניתן בקלות לזהות כי מקור התקלה הוא בגרסת הרכיב הרלוונטי במידה ואכן קיימת תקלה ידועה באותו רכיב, בנוסף, יהיה ניתן גם לוודא בקלות לאחר עדכון כי אכן העדכון בוצע בהצלחה ובמידה והבעיה נותרה על כנה יהיה ניתן בקלות לראות שהבעיה עדיין בעינה עומדת למרות שדרוג הגרסה. עם זאת, מילה אחת של אזהרה: אני ממליץ בחום להשתמש בפקודת "Assembly.GetExecutingAssembly().GetName().Version" על מנת לאחזר את גרסת הרכיב ולא להשתמש ב-Constants כתחליף, זאת מכיוון שביצוע קומפילציה לא ישנה את ערכו של ה-Constant הרלוונטי ולכן ייתכן בהחלט שלמרות שבמקרה כזה יבוצע רישום ל-Trace ע"י רכיב מסוים בגרסאות שונות ולמרות זאת הוא יוצג ב-Trace כגרסה זהה. (כמובן שיש לעדכן את קובץ ה-AssemblyInfo.cs כך שישנה את מספר הגרסה בכל קומפילציה ולא להסתמך רק על תוכניתן שישנה String במיקום מסויים בכל העלאת גרסה)

3. שם המחשב (ואולי גם כתובתו ברשת) והמסלול ממנו מורץ הרכיב: נתון זה, כמו התאריך והשעה, מאפשר להצליב את הנתונים שנרשמו ב-Trace עם נתונים ממערכות אחרות (למשל Windows Event Logs). אם ניקח, למשל, מצב שבו אחד השרתים המשתמש בתוכנית שלנו חדל מלתפקד באופן תקין בעת השימוש בתוכנית שלנו, נוכל להצליב את האירועים הרשומים ב-Trace שלו (אשר אותו נזהה בעזרת שם המחשב וכתובתו הרשומים ב-Trace) עם אירועים אשר נרשמו באותו תאריך ושעה באותו שרת ב-System Event Log למשל וכך נוכל לראות בקלות שבאותן שעות שהשרת חדל מלהגיב אירע (למשל) עדכון של רכיבי מערכת ההפעלה המהווים רכיבים קריטיים למערכת שבנינו. כמו שכתבתי בסעיף הקודם גם כאן ישימה אותה מילת אזהרה: אני ממליץ בחום להשתמש בפקודת "Assembly.GetExecutingAssembly().Location" על מנת לאחזר את שם הקובץ המהווה את הרכיב אשר רושם כעת ל-Trace ואת מיקומו הפיזי בשרת/תחנה.

4. שם המתודה אשר מבצעת את הפעולה אשר בגינה נרשמת רשומת ה-Trace הנוכחית: לצורכי Troubleshooting מתקדם יהיה מאד חשוב לתכניתן לדעת איזו מתודה היא זו שמבצעת את הפעולה השגויה. כאן אני בטוח שיהיו כאלו שיגידו שאפשר להשתמש ב-Stack Trace, ובכן אפשרות זו, על אף שהיא קיימת, איננה מספקת משתי סיבות: ראשית, משום שהמטרה העיקרית והחשובה של כתיבת ה-Trace היא לדעת בקלות ובבירור באיזו מתודה אירעה השגיאה, והכוונה כאן היא גם ואולי בעיקר לשגיאות לוגיות (Intent errors) אשר בעקבותיהן לא בהכרח תוצג הודעת שגיאה ומכאן ששליפת ה-Stack Trace מורכבת יותר ויעילה פחות. שנית, משום שבהחלט ייתכן שכתכניתן תעדיף לרכז את הפניות ל-Trace במתודה מסוימת (ואולי אף ב-Class נפרד) על מנת לדאוג למבנה אחיד של הודעת ה-Trace (יוסבר בהמשך), במקרה כזה פירוט מלא של ה-Stack Trace יגרום ל"ניפוח" מיותר של ה-Trace ויסרבל את השימוש בו ובכך למעשה גורם לאובדן התועלות המצופות ממנו. בנוסף, במקרה של שימוש ברקורסיה ה-Stack Trace עשוי להיות ממש בלתי קריא.

5. ערכי הכניסה והיציאה של כל מתודה בעלת משמעות עסקית: רישום מדוקדק ב-Trace של ערכי הכניסה (Input Values) וערכי היציאה (Output Values) אל/מאת מתודות המהוות חלק מהתהליך העסקי אותו מיישמת המערכת עשוי להוות כלי חשוב ביותר הן עבור מנתח המערכות ואיש ה-QA והן עבור ה-Troubleshooting של המערכת בשוטף, זאת מכיוון שבאופן כזה במקרה של שגיאות (ובמיוחד שגיאות לוגיות) יכול התומך הרלוונטי (או מנתח מערכות או אפילו תוכניתן) לזהות במהירות ובקלות איזו מתודה מבצעת את העיבוד השגוי על פי הקלטים והפלטים אליה וממנה. חשוב להבהיר שאין כאן כוונה לרישום ערכי קלט/פלט של כל המתודות ובוודאי לא נחוץ (במרבית המקרים) רישום של הערכים עצמם במקרה של ערכים שצפויים להיות גדולים במיוחד. במקרים כאלו מספיק הרבה פעמים לרשום מידע לגבי הנתון שנקלט/שהוחזר (למשל במקרה של DataSet אפשר לרשום את שמות הטורים, שמות הטבלאות, שם ה-DataSet, כמות השורות וכדו').

אם כך, ברור כעת מה כדאי לרשום ב-Trace אבל מתי כדאי לרשום אליו? התשובה לשאלה זו כמו רוב התשובות לשאלות החשובות בחיינו, אינה חד משמעית, זאת משום שלא ניתן לתת כללים שיהיו באמת אפקטיביים ל-כ-ל האפליקציות שנכתבו או יכתבו אי פעם. עם זאת, ניתן להגדיר מספר Ground rules שיהיו רלוונטיים במרבית האפליקציות העסקיות:

1. רישום ל-Trace בכל התפצלות לוגית המבוססת על ביטוי שלא ניתן להסיק את ערכו באופן וודאי לאחר ביצוע התוכנית – המשמעות היא שאם קיים לדוגמה משפט if-then-else הבודק ערך מסוים שניתן בקלות להסיק את ערכו בעזרת רישום אחר ואשר אופן הפעולה של התוכנית בכל אחד מהערכים האפשריים של אותו ביטוי ברור וידוע ממסמכי האפיון/תיעוד.

2. רישום ל-Trace בכל פנייה לשירות או משאב חיצוני תוך ציון פרטיו המלאים – למשל, בעת רישום/קריאה מקובץ נתונים רצוי מאד לציין ברישום ל-Trace את שמו ואת מסלולו המלא כמו גם את סוג הפניה המבוקשת ואת מטרתה. בנוסף, אם מדובר בקובץ שהמסלול אליו או שמו נקבע בהסתמך על מרכיבים אחרים, מומלץ מאד לפרטם ואת דרכי הגישה לעדכנם במדה והדבר נחוץ.

3. רישום ל-Trace בתחילת וסיום כל "בלוק" בעל משמעות עסקית נפרדת את שמו ותיאורו – הכוונה כאן היא לאו דוקא לפונקציות או מתודות אלא לבלוקים שיש להם משמעות עסקית, למשל: תחילת חישוב פרמיה לפוליסת ביטוח בריאות מסוג XYZ למבוטח, חישוב לוח סילוקין למשכנתא או הלוואה וכו'.

4. רישום ל-Trace באופן מבני (Structured) על מנת לאפשר גמישות רבה בניתוח הנתונים שנרשמו. הכוונה היא לרשום את הנתונים במבנה מוגדר כלשהו, למשל  CSV או מבנה דומה יאפשר לנתח בקלות רבה מאד את הנתונים שנרשמו. בהקשר הזה חשוב להדגיש שיש להימנע ככל הניתן מרישום של

5. רישום מתוזמן ל-Trace – במקרים מסויימים, במיוחד באפליקציות המהוות שירות (כגון Web Service או Windows Service), מומלץ להפעיל Thread נפרד בלולאה אינסופית (המסתיימת עם סגירת היישום) אשר רושם באופן מתוזמן ל-Trace טקסט כלשהו ואת חתימת הזמן. תוכנו של טקסט זה אינו מהותי אם כי המלצתי היא שטקסט זה יכלול נתונים סטטיסטיים לגבי העומס על השירות במונחים עסקיים (למשל פניות משתמשים, כמות משימות בתור העבודה וסוגן, כמות תחנות שכרגע ממתינות, כמות פניות ליחידת זמן וכדו') לצד נתוני עומס במונחים טכנולוגיים  (למשל אחוז העומס על ה-CPU והזיכרון המצוי בשימוש, קצב הפניות לדיסקים הקשיחים וכו') בנוסף, מומלץ לרשום נתוני מינימום, מקסימום וממוצע של הנתונים הללו. באופן זה ניתן בנקל לדעת שהשירות עדיין "חי" ומה העומס עליו ברגע זה מבחינה עסקית וכיצד הוא מתמודד עם העומס הזה מבחינות טכנולוגיות. במידה וקיים ארכיב של נתוני Trace כאלו לאורך זמן מספיק, ניתן אפילו להשוות נתוני עומס ונתוני "התמודדות" כאלו בין שרת שמתפקד באופן בעייתי לאותם נתונים באותו שרת בתקופה בה תפקד באופן משביע רצון וכך לקבל כיוון לגבי הסיבה לבעיה.

בהזדמנות זו חשוב להדגיש נושא שאי אפשר להפריז בחשיבותו בכל הקשור לרישום Log ו-Trace, נושא זה הינו קריטי לכל אפליקציה המבצעת רישום ל-Log ול-Trace. העניין הוא שהקוד המבצע את הרישום ל-Trace צריך להיות כזה שלא יכול לגרום בשום מצב לשום שגיאה שהיא, זאת משום שאחרת, עלולה להיווצר שגיאה בקוד הרושם את ה-Log ויהיה כמעט בלתי אפשרי לזהות את מקור השגיאה. ניקח למשל, את הקוד הבא:

Trace.Write("Server name is: " + serverName);
בהנחה ש-serverName הוא משתנה או מאפיין (Property) הרי שקוד זה אינו בטוח כלל, משום שערכו של משתנה זה בהחלט עשוי להיות null ובמקרה כזה תיווצר שגיאת Null Reference, מצב דומה עשוי לקרות גם כאן:
Trace.Write("Server name is: " + serversDataSet.Tables[1].Rows[1]["ServerName"].ToString());
פה המצב מורכב עוד יותר, ראשית, לא בטוח שערכו של ה-serversDataSet אינו null ושנית לא בטוח שאכן ישנה לפחות טבלה אחת וכו'.
נושא נוסף שיש להדגיש כשבאים לדבר על Tracing הוא נושא אבטחת המידע. נתוני ה-Trace עשויים לכלול נתונים רגישים ביותר הן בפן הטכנולוגי (כמו למשל כתובות IP של שרתים, חשיפת אופן פעולת המערכת וכדו') והן בפן העסקי (סיסמאות, תעודות זהות, שמות משתמשים, נתוני פוליסת ביטוח וכו'). לפיכך כאשר מגדירים כתיבת נתוני Trace חשוב לוודא שהם נכתבים למקום מוגן ושמור היטב אשר לא יאפשר לגורם בלתי מורשה לקרוא את תוכנם. בנוסף, במידה והדבר ניתן (לא תמיד אפשרי, הדבר תלוי בעיקר במקור הנתונים הנכתבים ל-Trace) יש להימנע מרישום סיסמאות לקובץ ה-Trace .
במאמר הבא אני אנסה להסביר כיצד ניתן להגדיר ב-Dot Net רכיב Trace Listener משלנו אשר יוכל להיות שימושי מאד עבור כתיבת Trace של אפליקציה ספציפית שפיתחנו.

השאר תגובה