[article] utilisation de JsonPath en Python #6

Merged
motius merged 1 commits from motius into master 2020-07-05 00:34:46 +02:00
1 changed files with 176 additions and 0 deletions

View File

@ -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`
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 600000 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