[article] utilisation de JsonPath en Python
This commit is contained in:
parent
f13c69075f
commit
5c82486422
|
@ -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
|
Loading…
Reference in New Issue