צעד אחר צעד: הקמת תשתית לטיפול בלוגים מבוססת ELK (חלק שני)

בחלק הקודם של הפוסט הצגתי דיזיין אפשרי לתשתית לוגים– שכולל את RabbitMQ כמערכת ההודעות שלנו, logstash בתפקיד רכיב פירסור הלוגים, ElasticSearch בתפקיד מנוע האינדוקס (ולמעשה מסד הנתונים של כל התשתית) ו- Kibana בתור רכיב הויזואליזציה שיאפשר לנו לדעת מה קורה במערכת. להזכירכם – כך זה נראה:

בפוסט הקודם הראיתי איך מבצעים את שלבי הקמת התשתית:  התקנת ElasticSearch גם על windows וגם על linux וקינפוג של cluster, התקנת RabbitMQ והתקנת וקינפוג logstash. בפוסט הזה נמשיך עם התהליך ונשלים את שתי הקוביות החסרות: service שיאפשר כתיבה ל-log והתקנת וקינפוג Kibana, שיאפשר לנו לצפות בהודעות הלוג שלנו.

Service קליטת ההודעות

בתשתית שאנחנו מקימים – ברורה לנו מה המטרה הסופית. אנחנו רוצים יכולת לראות ולנהל את הלוגים שלנו בצורה נוחה. בפוסט הקודם גם הקמנו את התשתית הדרושה לנו לאכסון ולאחזור המידע, ובהמשך נדבר על הויזואליזציה של זה עבורנו – איך נתחקר את הלוגים.
אבל, שאלה בסיסית היא איך מתבצעת קליטת ההודעות מהאפליקציות השונות שכותבות ללוג – מדובר למעשה על החלק שאני סימנתי בתרשים למעלה בתור ה- “Logging Service”. מטרתו של ה- logging service פשוטה: להיות endpoint שמולו האפליקציות שכותבות ללוג מדברות, לקבל מהם את ההודעה ולכתוב ל- RabbitMQ בפורמט JSON מוסכם.

אפשר לשאול, ובצדק, מה התועלת בסרביס הזה אם דברים יכולים לכתוב את ההודעות ישירות ל- RabbitMQ? יותר מזה – אפשר לקנפג את logstash עצמו כדי שיקרא את הלוג הטקסטואלי של כל אחת מהאפליקציות שלנו ויכתוב אותו פשוט ישירות ל- ElasticSearch.

יש לכך כמה סיבות:

  • סכימה: כל אחת ואחת מהתחנות בדרך, היא schema-less. אף אחת מהן לא אוכפת עבורנו באף רמה את הסכימה של המידע שהיא מעבדת. זה יתרון מצד אחד, וחסרון מצד שני. החסרון בא לידי ביטוי כשנרצה לתשאל – אם יהיו לנו מיליון שדות שאומרים אותו דבר, ועבור כל אפליקציה חדשה שמגיעה לכתוב הודעות היא תכתוב אותם בפורמט קצת שונה, ככה שיתקבלו שדות קצת שונים – אם נרצה לשאול שאלות פשוטות כמו “איזה הודעות קריטיות היו” או “איזה exceptions חוזרים הכי הרבה פעמים” או כל שאלה הכי פשוטה שתהיה – זאת תהיה משימה מורכבת שתדרוש תחזוקה, כי דברים לא יהיו באותו מבנה.
    אם כולם ידברו מול service מרכזי, שיקבל את המידע וייצר JSON שאותו הוא יכתוב לראביט שהוא כבר בפורמט המוסכם – אזי נפתור את הבעייה הזאת.
  • להימנע מ- Polling: העבודה מול RabbitMQ כשה- logstash מקבל ממנו input טובה משמעותית מלקנפג את ה- logstash לעשות tail ללוגים של אפליקציות שונות מבחינת ביצועים – כי ל- polling יש עלות.
  • להקל את התחזוקה: אם יש service מרכזי שאוכף פורמט מוסכם, לא נידרש לערוך קונפיגורציות כשנרצה להכניס אפליקציות נוספות.

פורמט ההודעה

כאמור בפוסט הקודם, הפורמט שבחרנו להודעות הוא JSON, כאשר לטובת הנוחות הגדרתי את ה- JSON במבנה שטוח לחלוטין (לקנפג את logstash עם מבנים יותר מורכבים, שכוללים למשל מערכים, או אובייקטים nested זה כמובן אפשרי – אבל סתם מסבך דברים, ולא צריך את זה במקרה שלנו). למשל, כך ייראה JSON של הודעה:

{

  "EventDateTime": "2016-12-14-21:02:07.149",

  "SourceApplication": "MyApp",

  "SourceModule": "MyModule",

  "SourceFile": null,

  "TextMessage": "error occured while doing math operation",

  "Level": "Error",

  "MessageVersion": 1,

  "ExceptionMessage": "Attempted to divide by zero.",

  "ExceptionType": "DivideByZeroException",

  "ExceptionStackTrace": "   at LogGenerator.Program.Main(String[] args) in c:\\users\\shaha\\Google Drive\\Documents\\Visual Studio 2015\\Projects\\LogGenerator\\LogGenerator\\Program.cs:line 18",

  "ExceptionString": "System.DivideByZeroException: Attempted to divide by zero.\r\n   at LogGenerator.Program.Main(String[] args) in c:\\users\\shaha\\Google Drive\\Documents\\Visual Studio 2015\\Projects\\LogGenerator\\LogGenerator\\Program.cs:line 18",

  "AdditionalField_FirstNum": 5,

  "AdditionalField_SecondNum": 0

}

האובייקט שהגדרתי אצלי לטובת הבדיקות נראה כך:

public enum VerboseLevel

  {

      Unspecified = 0,

      Debug,

      Verbose,

      Error,

      Critical

  }

  public class LogMessage

  {

      private readonly Dictionary<string, object> _additionalFields = new Dictionary<string, object>();

      public LogMessage(string sourceApp, string sourceModule)

      {

          SourceApplication = sourceApp;

          SourceModule = sourceModule;

      }

 

      public LogMessage(string sourceApp, string sourceModule, string msg, Exception exception = null) : this(sourceApp, sourceModule)

      {

          TextMessage = msg;

          if (exception != null)

          {

              Exception = exception;

              VerboseLevel = VerboseLevel.Error;

          }

      }

 

      public readonly int MessageVersion = 1;

      public string SourceApplication { get; set; }

      public string SourceModule { get; set; }

      public string SourceFile { get; set; }

      public Exception Exception { get; set; }

      public string TextMessage { get; set; }

      public DateTime EventDateTime { get; set; } = DateTime.Now;

      public VerboseLevel VerboseLevel { get; set; }

 

      public object this[string fieldName]

      {

          get { return _additionalFields[fieldName]; }

          set { _additionalFields[fieldName] = value; }

      }

 

      public string ToJson()

      {

          JObject obj = new JObject

          {

              {"EventDateTime", EventDateTime.ToString("yyyy-MM-dd-HH:mm:ss.fff")},

              {"SourceApplication", SourceApplication },

              {"SourceModule", SourceModule },

              {"SourceFile", SourceFile },

              {"TextMessage",TextMessage },

              {"Level",VerboseLevel.ToString() },

              {"MessageVersion", MessageVersion },

          };

          if (Exception != null)

          {

              obj.Add("ExceptionMessage", Exception.Message);

              obj.Add("ExceptionType", Exception.GetType().Name);

              obj.Add("ExceptionStackTrace", Exception.StackTrace);

              obj.Add("ExceptionString", Exception.ToString());

          }

          foreach (var additionalField in _additionalFields)

          {

              obj.Add($"AdditionalField_{additionalField.Key}", JToken.FromObject(additionalField.Value));

          }

          return obj.ToString(Formatting.Indented);

      }

  }

זמנים

אחד הדברים החשובים במעקב אחרי הודעות הוא נושא הזמן. למעשה, הזמן הוא השדה היחיד ש-logstash בקונפיגורציה שעשינו בפוסט הקודם מתייחס אליו באופן מיוחד. הוא מזהה את שדה ה- EventDateTime לפי הפורמט שקבענו בקונפיגורציית logstash בפוסט הקודם (שהוא בכוונה שונה מהפורמט ה-JSON-י הרגיל כדי להראות איך מקנפגים את זה), ומתייחס אליו בתור שדה ה- timestamp שמולו נעשה את כל ה- time based filtering כשנגיע לתחקור המידע. ולכן, חשוב לוודא שתמיד קיים השדה הזה ושהוא מפורסר כהלכה.

נקודה נוספת שחשוב לשים לב אליה בהקשרי זמנים היא רמת הדיוק. שדה התאריך והשעה ב-ElasticSearch נשמר עד לרמת המילי-שניות. זה אולי נשמע מדוייק, אבל זה ממש לא מדוייק מספיק. למשל, באפליקציה שפועלת וכותבת לוגים בקצב גבוהה יחסית, אנחנו עלולים בהחלט להיות במצב שכמה רשומות לוג נכתבו כשהשוני בינהן הוא מעבר לרוזולציית המילי-שניות. במקרה כזה, כאשר ננסה לחקור איזושהי בעייה שדורשת מאיתנו להסתכל על הודעות הלוג האלה, ונרצה באופן טבעי למיין אותם לפי הזמן שבו הם נכתבו – כדי שנעקוב אחרי הדברים בסדר ההתרחשות שלהם, אנחנו עלולים לראות דברים בסדר לא נכון. אירועים שקרו קודם יופיעו כאילו הם קרו אח”כ ולהיפך. צריך להיות מודעים לכך, כי זה עלול לבלבל. ניתן גם לעשות workaround מסויים שיתאים בחלק מהמקרים (בעיקר אם יש לנו פיקים במהלך השנייה, אבל באופן כללי יש לנו פחות מ-1000 הודעות בשנייה) והוא ברמת ה- service לדרוס את שדה המילי-שניות בהודעה ככה שהוא יהיה עולה באופן מלאכותי לפי סדר קליטת ההודעה ב- service כך שההודעה הראשונה בשנייה מסויימת תקבל את הערך 1 ל-ms שלה, השנייה את הערך 2 וכו’. כמובן שזה לא מתאים בכל המצבים, ופוגע את היכולת לעשות התאמות-זמנים מול מקורות מידע אחרים (אם ישנם כאלה), אבל זה יכול להיות שימושי.

ה- service עצמו

כאמור, הפונקציונאליות שה- service הזה אמור לספק היא מאד פשוטה: לקבל את המידע הנדרש (בד”כ שדות מסגרת מסויימים, כפי שניתן לראות באובייקט שהגדרתי קודם ובנוסף תמיכה בשדות חופשיים שממומשים באובייקט ששמתי קודם באמצעות Dictionary שאנחנו מסרלזים להודעה את הערכים שלו) ולכתוב אותו בתור JSON כמו שהראיתי קודם ל-RabbitMQ.

מכיוון שהפוסט הזה והקודם לא מוגבלים לפלטפורמות מסויימות, או לשפות פיתוח מסויימות – אני לא רואה טעם להעמיק ולהראות איך לממש service פשוט כזה, וכל אחד יכול לממש אותו בהתאם לצרכים שלו ולהעדפות שלו (ASP.NET Core, WCF, Node.JS, Python, PHP…). עם זאת, אני כן אפנה לתיעוד המעולה של RabbitMQ שמסביר טוב מאד איך כותבים אליו הודעות מכל שפה שיש SDK רשמי של RabbitMQ עבורה. מרגע שקיבלתם את המידע, וכתבתם את ההודעה ל- RabbitMQ (הודעה בדומה לפורמט שהראיתי קודם) – logstash שקינפגנו קודם יעשה את השאר, יקבל את ההודעה, יעשה עליה פירסור מינימלי בשביל לחלץ את שדה הזמן, ויכניס אותה ל-ElasticSearch.

Kibana

הרכיב הבא שנתייחס אליו הוא Kibana. למעשה, Kibana זה מנוע שמאפשר לנו להציג מידע שמאוכסן ב- ElasticSearch ולייצר dashboard-ים שונים מעליו. מדובר בממשק WEB-י בעל לא מעט אפשרויות (שהרבה מהם לוקחות השראה מ- Splunk).  יחסית קל ללמוד אותו, כך שאני אתמקד בעיקר באיך מתקינים אותו ובמונחים הבסיסיים לעבודה מולו.

התקנה

תחילה יש להיכנס לעמוד ההורדה ולהוריד את הגרסא המתאימה. בהסבר פה אני מדבר על גרסא 5.1.1 ל- Windows. את ה- ZIP שהורדנו נפתח לתוך c:\Kibana. לאחר מכן ניכנס ל c:\Kibana\Config ונערוך את kibana.yml כדי להגדיר את הכתובת לשרת ה- ElasticSearch שלנו (השדה שנקרא elasticsearch.url).  בנוסף, נרצה לשנות את זה שנוכל לגשת ל- Kibana מרחוק (ולא רק מהשרת שעליו היא מותקנת). כך זה ייראה אחרי שערכנו את ההגדרות (שימו לב שפה ה-Kibana מותקנת על אותו node שעליו מותקן ElasticSearch):

image_thumb5

לאחר מכן, נריץ את ה- bat שנמצא ב c:\kibana\bin\kibana.bat. אחרי כמה שניות נראה את ההודעות הבאות:

image_thumb1

וזה אומר ש- Kibana מוכנה לשימוש – רק צריך לגלוש על המכונה שבה התקנו ל http://localhost:5601. כשנגלוש, יופיע לנו המסך הבא:

image_thumb3

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

[הערת אגב: כמובן שבסביבת production נרצה להתקין את Kibana בתור windows service, ואת זה אפשר לעשות עם nssm כמו שהראיתי בפוסט הקודם בסדרה עבור logstash, פשוט עם הכוונה ל- batch file של kibana.bat במקום לזה של logstash].

שימוש

image_thumb7

image_thumb12כאשר ניכנס ל- Kibana פעם ראשונה זה מה שנראה. בואו נבין רגע איזה חלקים יש לנו למסך. מימין למעלה, יש לנו את פילטר הזמנים. מדובר באחד האלמנטים הכי חשובים, וזה שמאפשר למעשה לעבד ולנהל כמות גדולה של לוגים בביצועים טובים – פשוט לא מסתכלים על כולם. בקונפיגורציה שבה עשינו, למעשה יש אינדקס לכל יום. גם בתוך אותו היום, יש לנו שדה @timestamp שמאפשר פילטור לפי הזמנים. לכל חיפוש שלא נעשה, כל תצוגה שלא נסתכל עליה – פילטר הזמנים מלווה אותנו. אנחנו יכולים לשנות ולהגדיל אותו מ-15 דק’ לזמנים ארוכים יותר, לזמנים שבין תאריכים מסויימים – ולפי זה ייקבע איזה נתונים אנחנו רואים.

 

מצד שמאל, יש לנו למעשה את הבחירה בין שלושת סוגי התצוגה העיקריים של Kibana: תצוגת ה- Discover (שעליה אנחנו נמצאים עכשיו), ה- Visualise (שמאפשרת לנו להכין ויזואליזציות של נתונים שונים) וה- Dashboard שמאפשר לנו לשלב את כל אותם הויזואליזציות ל- dashboards נוחים.

מלמעלה, אנחנו יכולים לראות את תיבת החיפוש. כזכור, הכל מתבסס פה על ElasticSearch שהוא מעל הכל שרת אינדוקס טקסטואלי. אנחנו יכולים לחפש מילה מסויימת, לחפש ביטויים לוגיים שונים (exception AND (sql OR hadoop)) וכו’.

אם נלחץ על החץ שליד אחד השורות שמופיעות, נוכל לראות בצורה טבלאית את השדות ששמורים עבור ה- document המסויים:

image_thumb14

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

image_thumb16

נוכל כמובן גם לשמור את החיפוש הזה (שזה אומר גם את החיפוש, הפילטורים שעשינו, והתצוגה שבחרנו) ע”י לחיצה על כפתור ה- save. למשל, אם נלחץ על סימן הכוכב שיש כשפותחים את אחת הרשומות ליד שדה ה- Exception Message, נוכל לייצר לנו תצוגה שכוללת רק את השגיאות שקרו – כלומר, תוכלו לראות שאין כבר שורות שאין להן Exception Message, ומתחת לשורת החיפוש מופיע ה-filter שלנו שמיישם את זה:

image_thumb18

יצירת Dashboard

אחרי שקיבלנו טעימה קטנה של איך מתחילים עם Kibana, המטרה שלנו תהיה לייצר dashboard-ים שימושיים עבורנו. למשל, dashboard שיציג לנו את השגיאות הקריטיות האחרונות מכל האפליקציות + פילטור של אפליקציות בעייתיות, או שגיאות שחוזרות על עצמן כדי שנדע לזהות מגמות. או dashboard שמציג לנו את ה- latency של משתמשים בביצוע מגוון פעולות – שנוכל לאתר גם פה בעיות שונות ומשונות.

מדובר בנושא שלא יעיל להסביר בפוסט טקסטואלי, ולכן אני מפנה אתכם לסרטון טוב שמראה איך עושים את כל הדברים הללו.

 

חלופות

ה- stack הטכנולוגי שראינו בפוסט הקודם ובפוסט הזה, מתבסס כולו על כלי open-source חינמיים, כאלה שאתם יכולים כבר היום להוריד ולהתקין. עם זאת, כמובן שלא מדובר בפיתרון היחיד בתחום ניהול הלוגים. אי אפשר לכתוב פוסט על לוגים בלי לכל הפחות להזכיר את Splunk,  המתחרה המסחרי החזק ביותר בתחום כנראה. בנוסף, יש לא מעט חברות שמציעות גם את פלטטפורמת ה- ELK בתור service ענני – במקום להקים את התשתיות אצלכם. ניתן למנות ברשימה את logit.io, או את הסטארטאפ הישראלי logz.io.

סיכום

בפוסט הקודם ובפוסט הזה סקרתי את כל מה שצריך כדי להתחיל ולהרים תשתית לוגים מבוססת על ELK stack: משתמשת ב- ElasticSearch כמנוע אינדוקס, logstash כמנוע קליטת הלוגים (למרות שהשימוש שעשינו בו היה מינימליסטי מאד בהשוואה ליכולות הרחבות שלו) ו- Kibana שנגענו בה קצת לטובת ויזואליזציה של הלוגים.

כאמור, מדובר רק בנגיעה התחלתית – יש נושאים רבים נוספים שאפשר להרחיב עליהם, כאשר הדבר המשמח הוא שכל הרכיבים שדיברנו עליהם בשני הפוסטים הללו מתועדים היטב, יש עליהם הרבה מאד מידע (גם רשמי וגם לא רשמי) – ואפשר לחפש ולהרחיב בקלות יחסית את הפיתרון.

בהצלחה.

צעד אחר צעד: הקמת תשתית לטיפול בלוגים מבוססת ELK (חלק ראשון)

כאשר מדברים על טיפול בכמויות גדולות של מידע (“ביג-דאטה”), אחת הדוגמאות הנפוצות היא טיפול בלוגים. הסיבה היא שטיפול בלוגים משלב ביחד כמה אתגרים: יש הרבה מאד “event-ים” (הודעות לוג במקרה הזה), קצב ההגעה שלהם גדול (אפליקציות כותבות הרבה הודעות לוג), אנחנו רוצים לנתח אותם בצורות שונות שכוללות בין היתר גם חיפוש full text search על מחרוזות שונות כדי למצוא הודעות שמעניינות אותנו, וגם ביצוע אגרגציות על שדות שונים שיש בהודעת הלוג. למעשה, אפשר להגיד שמדובר פה בשילוב של לא מעט תחומים, מה שהופך באמת את נושא הלוגים לנושא “חם” יחסית.
כאשר מסתכלים על עולם הפתרונות לבעייה הזאת, אחד המוצרים העיקריים המתחרים בקטגוריה הזאת הוא ה- “ELK stack”. כאשר, בפועל מדובר בשילוב של שלושה מוצרים: Elasticsearch, Logstash ו-Kibana.

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

דיזיין כללי

הפיתרון שלנו יורכב ממס’ חלקים, כפי שאפשר לראות בשרטוט הכללי הבא:

image_thumb1_thumb

תחילה, יש לנו את האפליקציות השונות שלנו שיכתבו לוגים לתוך תשתית ה- logging שנפרוס. אנחנו נרצה לעשות איזושהי סטנדרטיזציה של צורת השליחה, ולכן בדיזיין שלנו נעשה את זה באמצעות סרביס שאחראי  על כתיבת ההודעות בפורמט המתאים ל- RabbitMQ שנתקין. RabbitMQ הוא message broker, שישמש אותנו להעברת ההודעות בין ה- service שאנחנו מחצינים כלפי האפליקציות לבין הרכיב שבסוף אחראי לקרוא משם את ההודעות, לעשות עליהם עיבוד כלשהו (במקרה שלנו זה יהיה עיבוד די בסיסי) ולהכניס אותם למעשה ל-Elasticsearchשבמקרה הזה ישמש בתור בסיס הנתונים שלנו לאכסון ההודעות.
בסוף, יש לנו את משתמש הקצה, הבנאדם המסכן שצריך לדבג משהו תוך הסתמכות על הלוגים. הוא יעבוד מול ממשק גרפי, שנקרא Kibana, שמאפשר יצירת dashboards שמתשאלים מידע שנמצא ב- Elasticsearch ועבודה מולם.

ככה בגדול נראה הפיתרון. תשימו לב, שעל אף שהתרחיש שאני אדבר עליו בפוסט הזה הוא “לוגים”, הפיתרון הזה (הדיזיין והפלטפורמות שבהם אנחנו נשתמש) יכול להתאים לתרחישים מגוונים נוספים שדורשים אינדוקס מידע לטובת חיפוש שמשלב full text search ואגרגציות.

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

RabbitMQ

בשלב הראשון, נרצה להתקין  RabbitMQ. אבל לפני שנגיע לאיך מתקינים (שזה די פשוט), נסביר חלק מהמונחים שמשמשים אותנו בעבודה מול RabbitMQ. אני ממליץ בחום שלא להסתפק בהסבר שלי ולקרוא את ה- tutorial המוצלח שזמין באתר שלהם.

RabbitMQ הוא כאמור message broker שמאזן מאד טוב בין ביצועים גבוהים לבין נוחות.
Message broker הוא רכיב שהתפקיד שלו זה להעביר הודעות מהצד השולח (Producer) לצד המקבל (Consumer). מכאן, אפשר להבין שהישות הבסיסית שעליה אנחנו מדברים כשאנחנו עובדים עם RabbitMQ היא “הודעה”.
הודעה  יכולה להיות למעשה כל רצף בינארי שאנחנו רוצים. עם כי, בפועל די נפוץ להיצמד לפורמטים טקסטואליים, כמו JSON או XML.

כאשר אפליקציה רוצה לקבל הודעה, היא למעשה פותחת connection מול RabbitMQ, באמצעות ה- connection הזה היא פותחת channel (שעליו אפשר לחשוב כמו “ערוץ שיחה” לוגי) ומאזינה בפועל ל- Queue. זה יכול להיות תור שנוצר דינמית (עוד רגע נדבר מול מה הוא נוצר), או תור שהיה קיים כבר קודם והצטברו בו הודעות.
כאשר אפליקציה רוצה לשלוח הודעה, היא יכולה לשלוח אותו לתור ספיציפי (שעליו אפליקציה אחרת אמורה להאזין), אבל זה לא מקובל.  בד”כ, כאשר אפליקציה תשלח הודעה היא תשלח אותה ל- Exchange.

ה- Exchange הוא למעשה “גשר” בין  מי שרוצה לשלוח הודעה למי שרוצה לקבל הודעה והוא זה שמאפשר לנו למעשה לפצל הודעה שנשלחת פעם אחת כך שהיא תגיע למס’ אפליקציות, או לאפשר לאפליקציה להירשם רק להודעות מסוג מסויים שמעניין אותה וכו’.
למשל, בדוגמא שלנו אנחנו נשלח הודעות log. כאשר את ההודעות log לא נשלח ישירות ל- queue שעליו מאזין logstash, הרכיב שבפועל יטפל בהודעות שלנו. את ההודעות נשלח ל- Exchange שייקרא, למשל LogMessages. אנחנו יכולים להגדיר אפס או יותר תורים שהם binded ל-exchange הזה. כלומר, לשכפל את ההודעה לאפס או יותר תורים. במקרה שלנו, למשל, נגדיר תור שיהיה binded ל- exchange הזה שאליו יגיעו ההודעות ומשם יימשכו לעיבוד ע”י logstash. אם מחר נחליט שאנחנו רוצים שעוד אפליקציה תקבל את ההודעות הללו (למשל, שהם ייכנסו ל- Splunk) – לא נצטרך לכתוב את ההודעה פעמיים. רק נגדיר עוד queue שיהיה גם הוא binded ל- exchange הזה ויקבל למעשה שכפול של כל ההודעות, באופן בלתי תלוי בתור של logstash, ויאפשר גם לאפליקציה האחרת להאזין ולקבל הודעות.

המנגנון של ה- Exchnage אפילו יותר מתוחכם מזה. עד עכשיו, מה שהזכרנו זה רק היכולת “לשכפל” הודעות למס’ תורים, כלומר exchange מסוג Fanout. אנחנו יכולים גם להגדיר exchange שמפצל הודעות לתורים לפי topic, כלומר לפי routing key.
ה- routing key הוא string אופציונאלי שמוצמד להודעה, ומורכב למעשה מאוסף של פרמטרים מופרדים בנקודות (הפרמטרים הם מה שנגדיר שיהיו). למשל, routing key של אפליקציה יכול להיראות משהו בסגנון הזה: MyAppName.ModuleName.Critical או MyAppName.OtherModuleName.Verbose . כאשר, במקרה הזה כשתור הוא binded ל-exchange, הוא יכול לבחור להיות binded רק לסוג מסויים של הודעות. למשל, ל MyAppName.*.Verbose [כל הודעות ה- verbose של MyAppName, בלי תלות בשם הרכיב באפליקציה] או MyAppName.# [כל ההודעות מ- MyAppName] או *.*.Critical [כל ההודעות הקריטיות] וכו’.

אחרי שהבנו מה נותן לנו RabbitMQ, בואו נתקין אותו. כדי להתקין את RabbitMQ, צריך קודם כל להתקין את Erlang, השפה וה- framework שעל בסיסם מפותח RabbitMQ. ניתן להוריד אותם מהאתר הרשמי, כאשר יש installer-ים עבור windows. משתמשי לינוקס יכולים להתקין באמצעות ה- package manager. לאחר שמורידים ומתקינים את Erlang, יש להוריד ולהתקין גם את RabbitMQ. ההתקנה פשוטה והיא למעשה כמה לחיצות על Next.

אחת הסיבות שאני אוהב את RabbitMQ היא ממשק הניהול הנוח שלו. כדי להפעיל את ממשק הניהול הזה, צריך למעשה להפעיל את הפלאגין שכולל אותו. כדי לעשות זאת, נפתח את RabbitMQ Command Prompt (יופיע לנו ב- Start Menu לאחר ההתקנה) ונריץ את הפקודה:

rabbitmq-plugins enable rabbitmq_management

לאחר ההרצה, ממשק הניהול יהיה זמין בפורט 15672 (עד כדי לאפשר ב- firewall קודם לכן). כלומר, ניתן מהמכונה עצמה לגלוש ל http://localhost:15672 ולהיכנס לממשק ניהול עם היוזר והסיסמא guest / guest. עם זאת, לא ניתן יהיה להתחבר עם השם משתמש והסיסמא האלה מרחוק.
כדי לאפשר התחברות מרחוק, ניצור יוזר אדמיניסטרטיבי על גבי המכונה, דרך אותו ה- command prompt שפתחנו קודם, באמצעות הפקודות הבאות:

rabbitmqctl add_user test test

rabbitmqctl set_user_tags test administrator

rabbitmqctl set-permissions -p / test ".*" ".*" ".*"

לאחר הקשת הפקודות האלה, נוכל להתחבר מרחוק (למשל ע”י גלישה במקרה שלי ל http://192.168.1.104:15672/) עם היוזר test והסיסמא test. כמובן, שבסביבת production נרצה לדאוג שב- firewall לא נאםשר גישה בפורטים של RabbitMQ משרתים שאינם שרתי האפליקציה שלנו.

עכשיו, כל מה שנשאר לנו לעשות מבחינת קונפיגורציית RabbitMQ, זה להגדיר את ה- Exchange שלנו ואז להגדיר את התור. נתחבר לממשק הניהול, ונלך לטאב Exchanges:

image_thumb111

נמלא את הפרטים בחלק של ה- Add a new exchange בתחתית העמוד,  בהתאם לתמונה המצורפת:

image_thumb3

לאחר שנלחץ על Add exchange, נראה אותו ברשימה. לאחר מכן, נעבור לטאב Queues בראש העמוד ונייצר Queue חדש:

image8_thumb

ונלחץ על Add queue כדי להוסיף.  בשלב הזה, יש לנו ביד תור ו- Exchange – אבל אין קשר בינהם. נרצה להוסיף binding כזה שיעביר את כל ההודעות שמגיעות ל- exchange שנקרא LogExchange שיצרנו קודם לתור שיצרנו עכשיו.
מכיוון שהגדרנו את ה- Exchange כ- topic, אנחנו יכולים להעביר pattern מסויים שנקבל הודעות שה- routing key שלהם מתאים ל- pattern הזה. בתור התחלה לא נעשה את זה, ונרשם לקבל את כל ההודעות.

לצורך כך ניכנס לעמוד של התור, ונפתח את הטאב של Bindings ונמלא את הפרטים לפי הצילום:

image13_thumb

לאחר שמילאנו את הפרטים, נלחץ על Bind.ונראה שזה התווסף לרשימת ה- Bindings שאנחנו רואים (הרשימה ריקה בצילומסך שלמעלה, טרם הוספת ה- binding). למעשה, מה שהגדרנו פה זה שכל הודעה שמגיעה ל- Exchange שנקרא LogExchange תגיע לתור הזה ותמתין שמישהו ייקח אותה. מי זה המישהו הזה? במקרה שלנו, מדובר ברכיב בשם Logstash שבהמשך נגיע ללהתקין ולקנפג אותו.

Elasticsearch

בסוף התהליך, אנחנו רוצים לשמור את המידע של הלוגים ב-DB מסוג כלשהו. הדרישה העיקרית שלנו בעבודה עם לוגים היא יכולת חיפוש טקסטואלי, כלומר full text search. אנחנו רוצים לחפש על ההודעה, או על שדות ספיציפיים בצורה חופשית. למשל, לחפש את כל ההודעות שמופיעה בהם NullReferenceException או לחפש הודעות שמופיעה בהם שם משתמש, או שם של קובץ קוד שלנו – בקיצור, חיפוש טקסטואלי רחב. עם זאת, כמובן שחיפוש טקסטואלי לא מספיק לנו. אנחנו רוצים יכולת לעשות אגרגציות  מסוגים שונים על מידע (כדי לקבל למשל סטטיסטיקות על רכיבים בעייתיים). ושאילתות גם יותר כלליות. את כל אלה מספק לנו Elasticsearch.

אפשר לחשוב על Elasticsearchכעל Document DB (בדומה, למשל, למונגו) שההתמקדות שלו היא חיפוש טקסטואלי. הוא מבוסס על מנוע חיפוש טקסטואלי בשם Lucene (שעליו מבוסס גם מתחרה עיקרי שלו, גם הוא אופן סורסי, Solr) ומציע למעשה ממשק שמאפשר לשמור documents ואז לבצע עליו חיפושים מסוגים שונים. הוא מותאם ל- scale-out, ויש לו גם יכולת בדומה לרוב המנועים המבוזרים לרפליקציה פנימית שנותנת מענה במקרה של נפילת node אחד או יותר (בהתאם לקונפיגורציה).

מונחים בסיסיים ב- Elasticsearch

כאשר אנחנו מתקינים Elasticsearch, אנחנו למעשה נתקין node בודד. בסופו של דבר, node זה שרת שמריץ service של Elasticsearch שיודע לעשות עבודה, וגם לדבר עם nodes אחרים שפועלים במקביל אליו בשרתים אחרים. כל ה- nodes מאוגדים ביחד תחת ישות לוגית שהיא ה- cluster.
ברגע נתון, יש node שהוא ה- master – הוא זה שאחראי על הניהול של ה- cluster. אם ה- master נופל, אז נבחר עבורו מחליף ע”י ה- nodes האחרים (מבין שרתים שמוגדר להם שיכולים לשמש כ- master).

מרבית ה- nodes ב- cluster של Elasticsearch משמשים כ- data nodes. כלומר, הם מחזיקים אצלם חלק מהמידע שמאוכסן ב- cluster, יודעים לקבל מידע חדש, ולשרת שליפות שונות על מידע קיים.

מה זה המידע שהם מחזיקים? אז בסופו של דבר, היחידה הבסיסית ביותר היא document. מדובר למעשה באובייקט JSON, שמכיל שדות שונים וערכים שונים (בכל סכימה שהיא, לא נדרש שום סוג של הצהרה על הסכימה מראש). כברירת מחדל, כל השדות מתאנדקסים וניתן לחפש על כולם. הזיהוי של ה- data types מתבצע גם הוא לפי הערך הראשון שהוכנס (אבל יש API שלם שמאפשר שליטה על כל אחת ואחת מההגדרות האלה).

כל document נמצא למעשה כחלק מ- “index”. ב- cluster יכולים להיות אינדקסים רבים, והם נותנים חלוקה בין documents שהיא גם לוגית וגם פיזית. כלומר, אפשר להפריד אינדקסים למטרת הפרדה לוגית (אני מפריד את המערכת logging שלי מהמערכת של החיפוש על תוכן האתר) וגם למטרת הפרדה פיזית של מידע (יש לי לוגים של שנה אחורה, כאשר אני רוצה שהם יתאכסנו בקבצים שונים, אולי תהיה להם מדיניות שכפול שונה, אולי אני רוצה אפשרות למחוק את חלקם – ואז אני משיג באמצעות ההפרדה הפיזית יותר שליטה).

אינדקס יחיד יכול להכיל כמות נתונים גדולה, שפוטנציאלית יותר גדולה גם מכמה שנכנס ל- node בודד. בנוסף, Elasticsearch רוצה לתת מענה ל- scaling (רוצה לשפר ביצועים או להגדיל נפח? תדחוף עוד כמה שרתים ל- cluster) ולשרידות (מה קורה אם node נופל, או סתם יורד לשדרוג). את זה משיגים באמצעות שני מונחים חשובים: Shards ו- Replica. כל אינדקס מחולק פנימית למס’ shard-ים, שיכולים לשבת על nodes שונים. כלומר, שחלק מהמידע של האינדקס נמצא ב- node א’, חלק ב- node ב’ וכו’. ואז בשליפות העבודה מתחלקת על יותר שחקנים, מה שמאפשר ביצועי שליפות טובים יותר ביחס לאם זה היה נופל על כתפיו (ובעיקר, על ה- spindles) של שרת בודד.
כמובן, שאנחנו לא רוצים שיהיה העתק בודד לכל Shard – כי אז זה אומר שברגע שנפל node שמכיל את ה- shard הזה, השלמות של האינדקס כולו נפגעת. ולכן, יש לנו את מונח ה- Replica: כמה עותקים של אותו Shard נשמרים ב- cluster.

הן החיפוש, והן הניהול של cluster מבוסס Elasticsearch מתבצעים באמצעות REST API שנכיר אותו באופן בסיסי בהמשך. בכל אופן, בתהליך הזה אנחנו לא נידרש כמעט לנגיעה ב-API בהקשרי חיפוש, כי בהמשך “נלביש” רכיב (Kibana) שיספק לנו את ממשק המשתמש ואת כל יכולות החיפוש.

עכשיו, אחרי שאנחנו מבינים באופן בסיסי מה זה Elasticsearch, ניגש ללהתקין אותו. לא צריך להיות מומחה Elasticsearchכדי להתקין סביבה שכוללת מס’ nodes שונים. אני אראה כיצד עושים את זה, ונעשה את ההתקנה במקביל – גם על מכונת Windows Server 2016 וגם על מכונת Ubuntu 16.04.1.

התקנה על Windows

לפני שנוכל להתקין Elasticsearch, נצטרך להתקין JRE (סביבת הריצה של Java) מהלינק הזה (שימו לב להוריד את גרסת ה- 64 ביט). אחרי שהתקנו, נוריד את ה- ZIP של Elasticsearchמעמוד ההורדה (אני מתייחס לגרסא 5.0).  נחלץ מה- ZIP שהורדנו את התיקייה, ונשים אותה במקום כלשהו. בדוגמאות אני אניח שיש לנו עכשיו תיקייה C:\elasticsearch-5.0.0. דבר ראשון, נרצה להתקין את ה- service של Elasticsearchעל המכונה שלנו (כדי שההפעלה לא תהיה תלוייה בהרצת batch על ידנו…).

לטובת זאת, נצטרך לערוך תחילה את הקובץ C:\elasticsearch-5.0.0\config\jvm.options ולהוסיף אליו את השורה הבאה: –Xss1m, או שנקבל בעת ההתקנה את השגיאה “thread stack size not set”. הנה צילומסך של הקובץ לאחר הוספת השורה:

 

image_thumb10

לטובת התקנת הסרביס נריץ את הפקודות הבאות מ- cmd שמורץ כ- administrator:

cd c:\elasticsearch-5.0.0\bin

elasticsearch-service.bat install

כדי שנוכל להפעיל את ה- service, נצטרך גם להגדיר environment variable בשם JAVA_HOME. נעשה את זה ע”י הרצת הפקודה הבאה מ- PowerShell שרץ כ- administrator:

[Environment]::SetEnvironmentVariable("JAVA_HOME", "C:\Program Files\Java\jre1.8.0_111", "Machine")

לפני שנפעיל את ה- service שהותקן, נרצה לעשות עוד כמה שינויי קונפיגורציה קטנים בקובץ C:\elasticsearch-5.0.0\config\elasticsearch.yml. חלק מהשינויים האלה הם למעשה לשנות שורות שמסומנות בהערה, ולטובת הנוחות אני פשוט כותב את כל השינויים כבלוק אחד (ולא ב- section-ים שהם מופיעים בקובץ למטרת סדר):

cluster.name: logs-cluster

node.name: node-1

node.master: true

node.data: true

network.host: 0.0.0.0

http.cors.allow-origin: "*"

http.cors.enabled: true

נשמור את השינויים הללו בקובץ הקונפיגורציה, ולאחר מכן נעשה start ל- service שנקרא Elasticsearch5.0.0. כדי לראות שזה עובד, ננסה לגלוש למכונה שלנו בפורט 9300 (עדיף מבחוץ עם IP חיצוני של המכונה, ולא עם 127.0.0.1 כדי לאמת שאתם מצליחים לתקשר משרתים אחרים וה- firewall לא עומד באמצע – כי נזדקק לזה בהמשך). ככה למשל זה נראה אצלי:

image_thumb12

מזל טוב. יש לנו Elasticsearchמותקן.

ניהול Cluster של Elasticsearch

הממשק ש- Elasticsearchחושף כלפי חוץ הוא REST API. אנחנו יכולים לעבוד מולו, עם כלים כמו Postman ולתשאל את ה- REST API, אבל זה קצת פחות כיף.

גם אם עובדים עם cluster קטן של שני nodes בלבד, עדיין נעדיף ממשק נוח.  בד”כ, הממשק שבו אני משתמש הוא ElasticHQ.  ההורדה וההתקנה קלות מאד, והשימוש אינטואיטיבי ביותר. עם זאת, במדריך הזה הראיתי עד עכשיו איך מתקינים את Elasticsearchבגרסא 5.0, הגרסא העדכנית ביותר שיצאה. גרסא זו כללה כמה שינויי API, שגרמו לכך ש- ElasticHQ (וכלי ניהול אחרים) צריכים לעשות התאמות על מנת לעבוד. נכון לזמן שבו אני כותב את הפוסט הזה, ההתאמות הללו עוד לא התבצעו,  עם כי יש להניח (מניסיון העבר) שיתבצעו בקרוב.

התקנה על Linux

אחרי שהתקנו node אחד על Windows, נתקין את ה- node השני שלנו על לינוקס. בצורה הזאת נרוויח שני דברים: יהיה לנו node נוסף שיעזור לנו לשרידות של המידע שלנו ולביצועי החיפוש (כפי שהוסבר קודם), וגם אני אוכל להראות לכם את השלבים המקדימים להתקנה שעשינו קודם – על לינוקס.

אז דבר ראשון, צריך להתקין java באמצעות הרצת הפקודה הבאה:

sudo apt-get install default-jre

לאחר מכן צריך להתקין את ה-package העדכני שניתן להורדה מהעמוד הזה. אנחנו רוצים להוריד למעשה את ה- deb package. אחרי שהורדנו אותו, נריץ מה- shell את הפקודה הבאה (מהתיקייה המתאימה):

sudo dpkg -i elasticsearch-5.0.0.deb

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

sudo systemctl enable elasticsearch.service

הגדרה נוספת שאנחנו צריכים לעשות, היא להגדיל את כמות ה heap-ים הזמינים ב-JVM. נעשה זאת באמצעות:

sudo sysctl -w vm.max_map_count=262144

עכשיו, נצטרך ללכת ולערוך את קובץ הקונפיגורציה של elasticsearch. הקונפיגורציה שנגדיר תהיה למעשה די זהה לקונפיגורציה שהגדרנו קודם עבור ה- Windows.

כדי לערוך אותה, נכניס את הפקודה הבאה:

sudo nano /usr/share/elasticsearch/config/elasticsearch.yml

בואו נסתכל מה הערכים המעניינים (חלקם כבר קיימים, וצריך “למזג” אותם לקונפיגורציה הדיפולטית ע”י שינוי הערך):

cluster.name: logs-cluster

node.name: node-2

node.master: true

network.host: 0.0.0.0

http.port: 9200

discovery.zen.ping.unicast.hosts: ["192.168.1.105"]

 

בואו נעבור על הפרמטרים, שחלקם כבר ראינו בהתקנה שעשינו קודם על שרת ה- Windows ונראה מה מעניין פה:

  • cluster.name: אנחנו רוצים להגדיר את אותו שם cluster, כי אנחנו רוצים למעשה לצרף את ה- node החדש ל-cluster עם ה-node שהקמנו קודם על שרת windows
  • node.name: זה השם שניתן ל- node החדש, פשוט נמשיך עם מוסכמת השמות שהתחלנו קודם.
  • node.master: פה אנחנו מגדירים האם ה- node יכול לשמש בתור master. זה לא אומר שהוא יהיה ה- maser בפועל, אלא זה אומר שהוא משתתף בתהליך הבחירה (election) במקרה של כשל של ה- master הקיים, ויכול לקבל את התפקיד על עצמו
  • network.host: אנחנו אומרים להאזין במקרה הזה על כל האינטרפייסים בפורט שמופיע ב- http.port. כמובן שהיינו יכולים להגדיר IP ספיציפי
  • discovery.zen.ping.unicast.hosts: פה אנחנו יכולים להעביר לו שמות או IP-ים של node-ים אחרים, ופה שמתי לו את ה- IP של שרת ה- windows שהתקנו עליו Elasticsearchקודם לכן. ל- ElasticSearch יש מנגנון discovery מתוחכם. כדי לוודא שהקונפיגורציה במדריך הזה עובדת בדיוק כפי שהיא אנחנו מציינים פה ספיציפית את ה- IP של השרת השני, אבל כמובן שבסביבת production שבה יש הרבה שרתים, לא צריך לציין את כולם (ובכלל, יש מנגנון discovery מבוסס unicast-ים וכו’).

זהו, סיימנו את שלב הקונפיגורציה. עכשיו מה שנשאר לעשות זה רק להפעיל את ה- service:

sudo systemctl start elasticsearch

ונראה את הסטאטוס שלו באמצעות הפקודה:

sudo systemctl start elasticsearch

ואת הסטאטוס נוכל לראות באמצעות הפקודה הבאה:

image_thumb16

אם נגלוש עכשיו לכתובת של השרת Elasticsearchשהתקנו בפורט 9200 נצפה לקבל את הפלט הבא:

{

  "name" : "node-2",

  "cluster_name" : "logs-cluster",

  "cluster_uuid" : "g2In6mvAT3OXFmqTOemWPw",

  "version" : {

    "number" : "5.0.0",

    "build_hash" : "253032b",

    "build_date" : "2016-10-26T04:37:51.531Z",

    "build_snapshot" : false,

    "lucene_version" : "6.2.0"

  },

  "tagline" : "You Know, for Search"

}

בדיקת סטאטוס של cluster

יופי. התקנו שני node-ים של Elasticsearch. עכשיו אנחנו רוצים לראות שבאמת הכל עובד תקין, הם מדברים אחד עם השני, רואים שהם באותו ה- cluster וכו’, ועל הדרך נראה איך ניתן לעבוד עם ה- REST API של ElasticSearch.

כל מי שעובד או עבד בעבר עם REST API מכל סוג שהוא צריך להכיר את Postman.  הוא זמין גם בתור פלאגין לכרום, וגם (ולטעמי הרבה יותר נוח) בתור אפליקציה חלונאית. אחרי שנתקין ונריץ את Postman נרצה לפנות ל-API של ElasticSearch בבקשת GET אל /cluster/Health (המיקום של האנדרסקור קצת השתבש פה, תסתכלו בצילום המסך):

image_thumb17

סימנתי בצהוב את הכתובת שאליה צריך לפנות ואת שני השדות הכי מעניינים: ה- status שאמור להיות green (אחרי שהוספתם והרצתם שני nodes לפי הגדרות הברירת מחדל) והשדה של number_of_nodes שאמור להראות 2 (כי הוספנו node אחד על windows ואחד על לינוקס).

אם אנחנו רוצים לראות קצת יותר פירוט על ה- nodes השונים, אנחנו יכולים לשלוח בקשת GET ל- _nodes ולראות פירוט רב יותר:

image_thumb5

Elasticsearchas a Service

אם אתם לא רוצים להתקין בעצמכם Elasticsearchעל מכונות on-prem, אתם יכולים להשתמש באחד מבין השירותים השונים שמציעים Elasticsearchas a service ב- cloud. למעשה, החברה שמאחורי Elasticsearchמציעה בעצמה את שירות Elastic Cloud שמציע בדיוק את זה מעל התשתית של אמזון. יש גם עוד לא מעט מתחרים אחרים, חיפוש של “Hosted Elasticsearch” בגוגל מניב לא מעט תוצאות של שירותים שנותניםי בדיוק את זה.

Logstash

אז אמרנו כבר שאנחנו רוצים לאכסן מידע על לוגים, ושהשכבה שתשמש אותנו בתור database בסיפור הזה זה Elasticsearch– כדי שנוכל להנות מיתרונות החיפוש הטקסטואלי.
בתכלס, מה שנרצה לעשות זה לייצג כל הודעת לוג בתור document, בפורמט json, כאשר הפורמט של ה- json הזה יכלול מספר שדות metadata קבועים, ועוד מספר כלשהו של שדות שרלוונטיים לאפליקציה שכותבת את הודעות הלוג (למשל, אפליקציה שמעבדת קבצים תוכל לכתוב את נתיב הקובץ שההודעת לוג נוגעת אליו בתור שדה, אפליקציה שמשרתת משתמשים תוכל לשים את שם המשתמש בתור שדה וכו’).

בשרטוט שהוצג בתחילת הפוסט אחנו רואים שה-front מול האפליקציות הוא למעשה איזשהו web service כלשהו (שבתכלס, לא באמת משנה האם הוא WCF, ASP.NET WebAPI, NodeJS או כל דבר אחר) שמקבל את הודעות הלוג וכותב אותם ל- RabbitMQ.
שאלה לגיטימית וטובה היא למה לא לכתוב את ההודעות הללו ישירות ל- Elasticsearch? מסיבה פשוטה – אנחנו רוצים לאפשר רמה נוספת של גמישות.  למשל, מה נעשה אם מחר נרצה לשלב אפליקצייה שיודעת לכתוב לוגים רק ל- event log של windows? או אפליקציה ששולחת syslog-ים לכתובת מסויימת וזה כל הלוגים שהיא יודעת להוציא? או אולי יש לנו לוג טקסטואלי של שרת apache שנרצה להזרים גם לתשתית הלוגים שלנו?

המשותף לכל הדברים האלה, הוא שאנחנו נצטרך משהו שיידע לקחת אותם מאיזשהו input, לעשות עליהם איזשהו משחק של המרה כדי לנרמל אותם לפורמט שבסוף אנחנו רוצים לשמור ל- Elasticsearchואז יידע להכניס אותם ל- Elasticsearchעצמו.
על הדרך, אם הדבר הזה מטפל בלוג הלוגים שלנו, אז גם נרצה שהוא יידע אולי לא רק לשלוח ל- Elasticsearch. למשל, אולי נרצה לכתוב חלק מהלוגים האלה גם לאיזשהו קובץ טקסטואלי? לפלטר את הלוגים ועבור חלקם גם להריץ איזשהו סקריפט? אולי חלקם מייצגים באגים שאנחנו רוצים לפתוח ישר עבורם task ב- JIRA?

כלומר, אנחנו מבינים שה- front הזה של הסרביס שמאפשר לכתוב הודעות ב- push לפלטפורמת ה- logging שלנו הוא רק ממשק אחד להכנסת מידע, מתוך רבים פוטנציאליים אחרים. גם “לאכסן ב- Elasticsearch” היא פעולה אחת שאנחנו רוצים לעשות עם המידע – אבל לא בהכרח היחידה.

כדי לאפשר למעשה להתמודד עם כל התרחישים שמניתי קודם, נוכל לעשות שימוש בכלי בשם LogStash – וזאת הסיבה שכבר בשרטוט שציינתי קודם אנחנו מכניסים אותו כחלק אינטגרלי מה- pipeline שאנחנו בונים.

Logstash הוא כלי חינמי ואופן-סורסי שאפשר להריץ גם על windows וגם על linux. הוא מאפשר לנו לתת קונפיגורציה שמכילה למעשה את החלקים הבאים:

  • input: מאיפה אנחנו מקבלים מידע – זה יכול להיות קובץ, RabbitMQ, או הרבה דברים אחרים
  • output: לאן אנחנו כותבים את המידע – למשל, במקרה שלנו, ל- Elasticsearch
  • filter / data manipulation: אנחנו יכולים לקבוע חוקים שמפלטרים את המידע לפי פרמטרים שונים, ועושים איתו מניפולציות כאלה ואחרות (הוספת שדה, מחיקת שדה, שינויי פורמט) ולמעשה עושים איזשהו branching של הלוגיקה שאנחנו מפעילים עליו.

logstash תומך בפלאגינים, כך שבנוסף לפיצ’רים המובנים שהוא מגיע איתם יש לא מעט פלאגינים שמתוחזקים ע”י הקהילה שמאפשרים מקורות input ו- output נוספים – ודברים שונים שאפשר לעשות עם המידע.

חשוב לשים לב ש- Logstash הוא stateless. כלומר, אין מניעה להריץ אותו ביותר משרת אחד ולבזר את הפעילות שלו – גם בהקשרי ביצועים וגם בהקשרי שרידות.

התקנת וקינפוג Logstash

את ההתקנה הפעם נעשה על שרת ה- Windows שלנו. תחילה נוריד את ה- ZIP, ונפתח אותו לתוך c:\logstash-5.0.1.

נייצר קובץ c:\logstash-5.0.1\config\logs-pipeline.conf. נדביק בו את התוכן הבא:

input {

    rabbitmq {

        host => "127.0.0.1" 

        subscription_retry_interval_seconds => 90

        queue => "LogstashQueue"

        threads => 2

        passive => true

        codec => "json"

    }

}

filter {

    date {

        locale => "en"

        match => ["EventDateTime", "YYYY-MM-dd-HH:mm:ss.SSS"]

        target => "@timestamp"

        add_field => { "debug" => "timestampMatched"}

   }

}

output {

  elasticsearch{

        hosts => ["127.0.0.1"]

    }

}

נשים לב שיש לנו בקונפיגורציה 3 חלקים:

  • input: אנחנו מגדירים מקור מידע יחיד, שהוא ה- RabbitMQ. אנחנו מגדירים ל- Logstash להפעיל שני consumer threads על תור בשם LogstashQueue שהצהרנו עליו כבר (זאת המשמעות של passive, כלומר התור כבר קיים ואנחנו דאגנו לזה מראש).
  • filter: בהודעת ה- JSON שנוציא מהסרביס שלנו, נרצה שיהיה שדה שמתאר את הזמן שבו קרה האירוע שעליו היה הלוג. בדיפולט, Logstash מתייחס לשדה @timestamp בתור שדה הזמן, ויש פורמטים מסויימים של תאריך ושעה שהוא מצפה לקבל. נניח, לטובת ההדגמה, שאנחנו רוצים שהוא יוציא את התאריך והשעה שלפיהם הוא שומר את הלוגים בשדה אחר (הלוגים נשמרים כברירת מחדל באינדקסים מופרדים לפי ימים ב- Elasticsearch, וכשנסתכל בהמשך על איך מתבצעים החיפושים וכו’, אז הזמן הוא מרכיב די משמעותי בפילטור) . נניח ששדה הזמן מופיע בהודעה (שהיא ב-JSON) בשם EventDateTime. אנחנו רוצים לשנות לו את השם ל- @timestamp וכמובן להתאים אותו לפי הפורמט שבו אנחנו כותבים. כל זה אנחנו מבצעים פה בחלק של ה- filter.
  • output: אנחנו אומרים לו לכתוב ל – Elasticsearch שרץ על אותו השרת.

כדי להריץ את logstash ולראות שהקונפיגורציה שהכנסנו עובדת, נשתמש בפקודה הבאה שאותה נריץ ב- cmd:

c:\logstash-5.0.1\bin\logstash.bat -f c:\logstash-5.0.1\config\logs-pipeline.conf

הפלט שאנחנו אמורים לראות הוא משהו כזה:

image_thumb7

איך בודקים שזה עובד?

כדי לראות שהכל עובד, נשלח הודעת בדיקה ב- RabbitMQ, נראה שלא מופיעות שגיאות ב- logstash (אם יופיעו שגיאות נראה אותם ב- console שבו אנחנו מריצים את logstash) ואז נראה שבאמת נוצר לנו אינדקס ב- Elasticsearch ושיש בו document.

  1. נגלוש לממשק של ה- RabbitMQ .בהנחה שזה באותו השרת שהתקנו עליו קודם, הממשק יהיה זמין בכתובת http://127.0.0.1:15672
  2. נלך למעלה לטאב Queues
  3. ניכנס לתור LogstashQueue שיצרנו קודם
  4. אנחנו אמורים לראות את המסך הבא:

image_thumb18

כמו שאתם רואים, תחת ה- Consumers אנחנו רואים את שני ה- consumers שהעלה logstash.

קצת יותר למטה בעמוד, יש לנו חלק של Publish Message. נדביק בו את ההודעה הבאה:

{

    "msg": "Hello World!",

    "EventDateTime": "2016-01-01-13:23:00.999"

}

וכך זה אמור להראות:

image_thumb20

ולסיום – נלחץ על Publish message.

נסתכל שוב ב- console שבו הרצנו את Logstash ונראה שלא התווספו שורות שגיאה. אם אין שורות שגיאה – אנחנו בכיוון הנכון.

עכשיו נרצה לאמת שאכן נוצר לנו האינדקס. לטובת זה ניכנס שוב ל- Postman ונריץ שוב GET כמו שרואים בצילומסך:

image_thumb19

ניתן לראות שהאינדקס נוצר, והשם שלו הוא עם התאריך שאליו שייכת ההודעה (שפורסר מתוך השדה הרלוונטי בהודעה ע”י Logstash). כלומר, על אף ששלחתי את ההודעה בנובמבר 16, ההודעה נכנסה לינואר 16 כי היא מייצגת מידע ישן (על סמך התוכן שלה).

הגדרת Logstash בתור Windows Service

אחרי שעשינו את הבדיקה דרך command line (וכמובן כל שינוי קונפיגורציה שנעשה נוכל להרים instance של logstash שעובד מול הקונפיגורציה החדשה ב- cmd ולראות שהכל עובד טוב, ואיזה שגיאות יש) – אנחנו כמובן לא נרצה להשאיר את העבודה בתצורה של cmd שחייב לרוץ כל הזמן, אלא נרצה לתת ל- service manager לנהל את הסיפור הזה. לצערנו, Logstash לא כולל באופן מובנה התקנה כ- Windows Service כמו Elasticsearch. לשמחתנו, יש כלי בשם NSSM (Non-Sucking Service Manager) שמאפשר לנו בקלות להפוך את Logstash (ולמעשה הרבה דברים אחרים שכתובים ב- java) ל- WIndows Services.

תחילה, נוריד את הגרסא האחרונה של NSSM ונפתח אותה ל c:\nssm (שתחתיו יהיו התיקיות win32 ו- win64 שב-ZIP).  נריץ את הפקודה הבאה: c:\nssm\win64\nssm.exe install ונמלא את הפרטים בחלון שייפתח כך:

image_thumb15

נלחץ על Install service, ניכנס ל- services.msc ונעשה Start ל-service החדש שיצרנו (שייקרא logstash בדוגמה הזאת).  כמובן, כדאי לעשות שוב את בדיקת ה- sanity שתוארה קודם כדי לראות שגם הסרביס עובד כמו שצריך.

 

סיכום ביניים של מה שיש לנו עד עכשיו

עד עכשיו מימשנו למעשה את ה- core של המערכת שלנו:

  • יש לנו שני שרתי Elasticsearch ב- cluster מוכנים ומזומנים לקבל מידע ולאנדקס אותו
  • יש לנו תור (RabbitMQ) שישמש אותנו כדי לקבל את המידע
  • יש לנו רכיב שיודע לעבד מידע מהתור הזה (לא רק) ולהכניס אותו ל-Elasticsearch שהרמנו

מה שחסר לנו זה למעשה הממשקים. – הממשק האפליקטיבי, שיאפשר לאפליקציה לכתוב הודעות (למרות שכבר עכשיו היא יכולה לכתוב הודעות ל- RabbitMQ, אבל נעטוף את זה בממשק יפה יותר שידגים לנו גם התממשקות עם RabbitMQ) וממשק משתמש לטובת שליפות (Kibana). על אלה אכתוב בפוסט הבא בסדרה.