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 :
$
: racine de l'arbre analysé..
: récursion sur tous les niveauxmessage
: 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 :
.
: 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 :
@
: il s'agit de l'élément couramment sélectionné[]
: permet de définir un prédicat ou d'itérer sur une collection?()
: 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) :
- un article présentant le filtrage JSON d'après l'équivalent XPath pour XML ;
- le lien vers PyPI de la bibliothèque
- le lien GitHub de la bibliothèque
- le lien vers une implémentation JavaScript populaire de JsonPath.
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 :
()
: 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