Explorer des logs ELK avec JsonPath

Bonjour !

Aujourd'hui, je veux vous parler d'une bibliothèque Python3 : jsonpath2.

Un peu de contexte

Il s'agit d'une petite bibliothèque de code qui permet de filtrer des données au format JSON. Vous me direz, on peut déjà faire ça avec des petites fonctions utilitaires, quelques coups de liste en compréhension. Il y a bien sûr un grand nombre de choses que la bibliothèque ne permet pas de faire, on y reviendra, mais concentrons-nous d'abord sur ce qu'elle permet, et les avantages qu'elle procure.

Pour cela, je vous propose tout simplement de vous présenter l'exemple que j'ai eu à traiter.

Un exemple

Supposez que vous ayez comme moi une application qui écrive un journal d'exécution au format JSON, dont les entrées sont relevées périodiquement par un ELK, et que vous ayez à analyser une journée complète, ce qui vous donne environ 50 Mo de logs compressés en gzip, et 700 Mo une fois décompressés. Vous rentrez ça dans un interpréteur ipython et bam, 3 Go de RAM supplémentaires utilisés. (Dans les cas où vous utilisez beaucoup de mémoire dans ipython, rappelez-vous que celui-ci stocke tout ce que vous taper dans des variables nommées _i1, _i2... et les résultats de ces opérations dans les variables correspondantes _1, _2, ce qui peut faire que votre interpréteur consomme une très grande quantité de mémoire, pensez à la gérer en créant vous-même des variables et en les supprimant avec del si nécessaire. Mais je m'égare.)

Il peut y avoir plusieurs raisons qui font que ces logs ne seront pas complètement homogènes :

  • vous avez plusieurs applications qui fontionnent en microservices ;
  • les messages comportant des exceptions ont des champs que les autres messages plus informatifs n'ont pas ;
  • etc.

Toujours est-il que pour analyser ce JSON, vous pouvez être dans un beau pétrin au moment où vous vous rendez compte que chacune des petites fonctions utilitaires que vous écrivez doit :

  • gérer un grand nombre de cas ;
  • gérer des cas d'erreur ;
  • être facilement composable, même pour les cas d'erreur.

Je ne dis pas que ce soit infaisable, et il m'est arriver de le faire ainsi pour certaines actions plutôt qu'en utilisant JsonPath.

Pour l'installation, c'est comme d'habitude dans le nouveau monde Python :

# dans un virtualenv Python3
pip install jsonpath2

Cas pratiques

Exemple de code n°1

Par exemple, si je souhaite obtenir toutes les valeurs du champ message où qu'il se trouve dans mon JSON, je peux le faire ainsi :

import json
from jsonpath2.path import Path as JsonPath
with open("/path/to/json_file/file.json", mode='r') as fr:
    json_data = json.loads(fr.read())
pattern = "$..message"
ls_msg = [
    match.curent_value
    for match in JsonPath.parse_str(pattern).match(json_data)
]

La variable qui nous intéresse ici, c'est pattern. Elle se lit ainsi :

  1. $ : racine de l'arbre analysé
  2. .. : récursion sur tous les niveaux
  3. message : la chaîne de caractères recherchés

Avantage

Le premier avantage que l'on voit ici, c'est la possibilité de rechercher la valeur d'un champ quelle que soit la profondeur de ce champ dans des logs.

Exemple de code n°2

On peut aussi raffiner la recherche. Dans mon cas, j'avais une quantité de champs "message", mais tous ne m'intéressaient pas. J'ai donc précisé que je souhaitais obtenir les champs "message" seulement si le champ parent est, dans mon cas, "_source" de la manière suivante :

pattern = "$.._source.message"

Par rapport au motif précédent, le seul nouveau caractère spécial est :

  1. . : permet d'accéder au descendant direct d'un champ.

Avantage

L'autre avantage qu'on vient de voir, c'est la possibilité de facilement rajouter des contraintes sur la structure de l'arbre, afin de mieux choisir les champs que l'on souhaite filtrer.

Exemple de code n°3

Dans mon cas, j'avais besoin de ne récupérer le contenu des champ "message" que si le log sélectionné était celui associé à une exception, ce qui correspondait à environ 1% des cas sur à peu près 600 000 entrées.

Le code suivant me permet de sélectionner les "message" des entrées pour lesquelles il y a un champ "exception" présent :

pattern = "$..[?(@._source.exception)]._source.message"

Il y a pas mal de nouveautés par rapport aux exemples précédents :

  1. @ : il s'agit de l'élément couramment sélectionné
  2. [] : permet de définir un prédicat ou d'itérer sur une collection
  3. ?() : permet d'appliquer un filtre

Avantage

On peut facilement créer un prédicat simple pour le filtrage d'éléments, même lorsque l'élément sur lequel on effectue le prédicat n'est pas le champ recherché in fine.

Au sujet de jsonpath2

Si vous êtes intéressé par le projet, je vous mets à disposition les liens suivants (ils sont faciles à trouver en cherchant un peu sur le sujet) :

jsonpath2 utilise le générateur de parseur ANTLR, qui est un projet réputé du Pr. Terence Parr.

Inconvénients

Parmi les prédicats qu'on peut faire, on peut tester si une chaîne de caractères est égale à une chaîne recherchée, mais les caractères qu'on peut mettre dans la chaîne recherchée sont assez limités : je n'ai pas essayé de faire compliqué, seulement de rechercher des stacktraces Python ou Java, qui ont peu de caractères spéciaux.

Il paraît qu'on peut effectuer des filtrages plus puissants avec une fonctionnalité supplémentaire que je n'ai pas présentés parce que je n'ai pas pris le temps de l'utiliser :

  1. () : s'utilise afin d'exécuter des expressions personnalisées.

J'espère que tout ceci pourra vous être utile. Je vous recommande notamment de tester vos motifs sur un petit jeu de données, on peut facilement faire des bêtises et consommer beaucoup de mémoire et pas mal de temps sans cela.

Joyeux code !

Motius