txs:infra:monitoring_p17:elk:services_logstash_technique

Afin de pouvoir manipuler les messages de log de la bonne manière, Logstash est configurable à l’aide d’un fichier de configuration (/DATA/monitoring/logstash/logstash.conf). Ce fichier s’articule en 3 parties:

  • Les inputs
  • Les filtres
    • Les output

    Représentent les sources desquelles proviennent les messages de log. Dans notre cas, les messages de log sont émis par des conteneurs Docker et sont acheminées via le protocole TCP sur les bases de syslog.

Permettent de segmenter les messages de log afin d’obtenir un objet JSON bien formé pouvant être stocké dans la base de données Elasticsearch. Cette étape est la plus importante car elle représente les différentes manipulations à faire sur les messages de log. Afin de construire l’objet JSON que l’on souhaite stocker en base de donnée, il est nécessaire d’utiliser des expressions régulières qui vont “matcher” les valeurs de nos futurs champs JSON. Afin d’effectuer ce matching, il faut utiliser le filtre “Grok”. Grok est aujourd’hui le meilleur moyen de transformer des messages de log mal formatés, en des objets bien formés, et pouvant être filtrés par des requêtes.

Un fichier de configuration très simple de Logstash serait :

input {
  file {
    path => "/var/log/http.log"
  }
}
filter {
  grok {
    match => { "message" => "%{IP:client} %{WORD:method} %{GREEDYDATA:syslog_message" }
  }
}

À l’aide du fichier de configuration ci-dessus, un message du type: 92.243.158.67 function rest of the message provenant du fichier /var/log/http.log serait passé en quelque chose de la forme:

logstash_1       |{
logstash_1       |           "client" => "92.243.158.67",
logstash_1       |                 "method" => "function",
logstash_1       |         "syslog_message" => " rest of the message",
logstash_1       | }

Et serait donc stocké en base de donnée (Elasticsearch) sous la forme: {“client”: “92.243.158.67”, “method”: “function”, “syslog_message”: “ rest of the message”}

De cette manière on s’aperçoit vite de l’utilité des filtres Logstash. Notre message de base, qui était un message sous forme de texte “brut”, peut désormais être stocké sous la forme d’un objet structuré. De cette manière, il sera possible d’effectuer des requêtes filtrées sur la base, pour ne récupérer des messages de log qui ne satisferont que certaines conditions. Par exemple, ceux dont le champ “client” vaut “92.243.158.67”.

Afin de pouvoir construire des messages de log bien structurés, la configuration de Logstash se base sur des regex, et, plus particulièrement, sur des patterns de Grok. Une liste de patterns de base peut être consultées ici.

Dans le but de simplifier la configuration de Logstash qui peut s’avérer rebutante au premier abord, nous avons décidé, dans le cadre de la TX, de définir notre propre bibliothèque de patterns Grok, correspondants aux patterns à matcher dans les messages de log de nos services. Le but ici, est d’avoir une configuration Logstash la plus simple possible. De cette manière, le fichier logstash.conf ne contient que l’instruction suivante pour construire des logs structurés:

     grok {
       patterns_dir => ["/etc/logstash/conf.d/patterns"]
       match => { "message" => "%{PICA_LOG_MESSAGE}" }
     }

Autrement dit, notre Grok “matche” des messages de log de Picasoft. Cette configuration minimaliste et très simple requiert d’être très organisée du point de vue de la conception des regex. Voici un extrait de la bibliothèque.

 # Pattern qui matche la première partie des messages de log
 # Cette partie est toujours la même et respecte le format imposé par syslog. Cf:
 # https://tools.ietf.org/html/rfc5424
 PICA_LOG_MESSAGE_PREFIX %{SYSLOGTIMESTAMP:syslog_timestamp} %{PICA_LOG_HOSTNAME:pica_machine}?%{PROG:service}(?:\[%{POSINT:pid}\])?:

Ce pattern vise à définir le préfixe des messages de log de nos services. Ce préfixe est invariant dans le sens où les messages de log respectent la norme RFC5424 de syslog. Cette “conversion” du format texte vers le format syslog est prise en charge par Docker, lorsque l’on précise le système de logging.

De cette manière, chaque message de log sera divisé en un champ:

  • syslogtimestamp * picamachine (optionnel) (si on utilise un cluster Swarm)
  • service: le nom du service qui produit les messages de log
  • pid

Toutefois, bien que l’en-tête des messages reste toujours la même peu importe les services, le “corps”/“contenu” des messages de log peut varier puisque ce dernier est défini par le développeur de l’application qui tourne dans le conteneur Docker.

Bien que certaines conventions existent, elles ne sont pas appliquées de la même manière selon les applications, ce qui nous force à définir des expressions régulières différentes pour chaque service. De ce fait, le but de la “librairie de patterns Grok” et de palier ces disparités, et de définir autant de regex que nécessaire dans le but de construire une regex finale: PICALOGMESSAGE à partir de laquelle on va matcher tous nos messages de log. Ainsi, comme c’est le contenu des messages de log qui diffère d’un service à un autre, on défini une regex est de la forme: PICALOGMESSAGEBODY %{LOGMESSAGE1}|%{LOGMESSAGE2}|… qui teste les différentes regex définis par nos soins et permet de matcher différents types de contenus de message de log. La regex finale PICALOGMESSAGE sera simplement de la forme: PICALOGMESSAGE %{PICALOGMESSAGEPREFIX} %{PICALOGMESSAGE_BODY}, la première étant invariante et le suffixe représentant toutes les variétés de contenus de messages générés par les services de l’infrastructure.

 # Pattern qui matche l'ensemble des messages de log des services de Picasoft
 PICA_LOG_MESSAGE %{PICA_LOG_MESSAGE_PREFIX} %{PICA_LOG_MESSAGE_BODY}

En géneral, les messages de log sont de la forme: LOGLEVEL message avec LOGLEVEL un mot indiquant la “gravité” du message de log. Ce LOGLEVEL peut être: INFO, DEBUG, WARN, FATAL et bien d’autre. L’ensemble des LOGLEVEL est très grand. En plus de cela, ces derniers peuvent être écrit en majuscules, minuscules, avec la première lettre majuscule et le reste minuscule, ou encore entre crochets, etc. De ce fait, pour pouvoir marcher le plus de LOGLEVEL possible, nous avons décidé de redéfinir le grok pattern LOGLEVEL, défini dans la configuration de base de Logstash:

PICA_LOGLEVEL_PRIMITIVES (?i)(alert|trace|debug|notice|info|fatal|severe|log|note|warn?(?:ing)?|err?(?:or)?|crit?(?:ical)?|emerg(?:ency)?)
PICA_LOGLEVEL \[%{PICA_LOGLEVEL_PRIMITIVES}\]|%{PICA_LOGLEVEL_PRIMITIVES}

Explications:

  • Le (?i) est un “flag” indiquant que l’on ne prête pas attention à la casse en faisant le matching. De ce fait, (?i)alert matchera les mots tels que “Alert”, “ALERT”, “AlErT”, “alert” et ainsi de suite.
  • Nous avons également défini le pattern PICA_LOGLEVEL qui permet de matcher les logs levels étant dans des crochets ou non.

À l’aide de ces deux patterns nous pouvons matcher la totalité des logs levels de nos services.

Dans le fichier de configuration de Logstash, si jamais le message de log entrant ne peut pas être “matché”/“reconnu” par la regex (aka le pattern grok), alors une erreur de type grokparsefailure est levée, et les champs de notre objet JSON ne peuvent pas être construit à partir du message de log. Si cela arrive, un objet JSON sera tout de même formé avec des champs par defaut de Logstash, et un tag de type grokparsefailure sera ajouté au message. Le log pourra tout de même être stocké en base de donnée, MAIS, comme il sera mal formé, nous ne pourrons pas appliquer de filtres dessus.

Exemple:

Un message tel que: <27>serviceX[1687]: FATAL message, ne respecte pas la norme syslog RFC5424, et, de ce fait, ne sera pas reconnu par notre grok pattern. Ceci engendrera un grokparsefailure, comme suit:

logstash_1       |{
logstash_1       |     "@timestamp" => 2017-05-11T22:33:07.325Z,
logstash_1       |           "port" => 53363,
logstash_1       |       "@version" => "1",
logstash_1       |           "host" => "172.20.0.1",
logstash_1       |        "message" => "<27>serviceX[1687]: FATAL message",
logstash_1       |           "type" => "syslog",
logstash_1       |           "tags" => [
logstash_1       |         [0] "_grokparsefailure"
logstash_1       |     ]
logstash_1       | }

L’objet JSON est quand même formé, mais si l’on souhaite, appliquer un filtre portant sur les noms de services de l’infrastructure, ce message figurera pas dans la liste des entrées car il ne contient pas de champ “service”.

C’est la raison pour laquelle, afin d’assurer un monitoring optimal de l’infrastructure, nous avons ajouté une condition supplémentaire au fichier de configuration, qui est:

  if ("_grokparsefailure" in [tags]) {
    grok {
      patterns_dir => ["/etc/logstash/conf.d/patterns"]
      match => { "message" => "%{PICA_LOG_MESSAGE_PREFIX} %{GREEDYDATA:message_body}" }
    }
  }

Explication: Si une erreur de matching a été soulevée, alors, cela signifie que le contenu du message a une forme que nous n’avons pas prise en compte. En effet, étant donné que les messages de log sont formatés selon RFC5424, tous nos préfixes de log seront les même. Si une erreur de parsing apparaît ce sera exclusivement dû au contenu du message en lui-même. De ce fait, afin de ne pas perdre de message lors de requête filtrées sur le nom des services, nous avons décidé que si une erreur grokparsefailure arrivait, alors, on referait un matching qui ne porterait QUE sur le préfixe du message de log, pour en extraire le nom du service. Le reste serait matché par %{GREEDYDATA} et stocké en tant que valeur du champ message_body de notre log formaté. En effet, GREEDYDATA, correspond à une regex du type .* et qui marche n’importe quelle séquence de caractères.

De cette manière, on pourra toujours faire des requêtes sur les noms des services pour en avoir la liste des messages de log.

  • txs/infra/monitoring_p17/elk/services_logstash_technique.txt
  • de qduchemi