diff --git a/content/posts/explorer-des-logs-elk-avec-jsonpath.md b/content/posts/explorer-des-logs-elk-avec-jsonpath.md new file mode 100644 index 0000000..3410453 --- /dev/null +++ b/content/posts/explorer-des-logs-elk-avec-jsonpath.md @@ -0,0 +1,176 @@ +--- +title: Explorer des logs ELK avec JsonPath +date: 2020-07-04 +author: motius +template: post +tags: python,JSON,log,ELK,développement +--- + +Bonjour ! + +Aujourd'hui, je veux vous parler d'une bibliothèque Python3 : +[`jsonpath2`](https://pypi.org/project/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](https://fr.wikipedia.org/wiki/Elk#Sigle), 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 : +```bash +# 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 : + +```python +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 : +```python +pattern = "$.._source.message" +``` + +Par rapport au motif précédent, le seul nouveau caractère spécial est : + +4. `.` : 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 : + +```python +pattern = "$..[?(@._source.exception)]._source.message" +``` + +Il y a pas mal de nouveautés par rapport aux exemples précédents : + +5. `@` : il s'agit de l'élément couramment sélectionné +6. `[]` : permet de définir un prédicat ou d'itérer sur une collection +7. `?()` : 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](https://goessner.net/articles/JsonPath/) présentant le filtrage + JSON d'après l'équivalent XPath pour XML ; +- [le lien vers PyPI de la bibliothèque](https://pypi.org/project/jsonpath2/) +- [le lien GitHub de la bibliothèque](https://github.com/pacifica/python-jsonpath2/) +- [le lien vers une implémentation JavaScript populaire](https://www.npmjs.com/package/jsonpath) + de JsonPath. + +`jsonpath2` utilise le générateur de parseur [ANTLR](https://www.antlr.org/), +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 : + +8. `()` : 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