11 Commits

Author SHA1 Message Date
5c82486422 [article] utilisation de JsonPath en Python 2020-07-05 00:33:08 +02:00
f13c69075f [artice] add TL;DR: 2020-06-09 22:16:44 +02:00
7060732b5c [article] On insert-ordered by default dict comparative advantages 2020-06-09 21:50:12 +02:00
cc664b5918 [article] add examples to dynamic enum 2020-06-05 19:57:33 +02:00
feee74665a add stuff to readme 2020-06-04 11:21:31 +02:00
656d093c87 rename 2 posts 2020-06-04 11:07:29 +02:00
57026c7fa5 [article] python dynamic enum 2020-06-02 21:00:13 +02:00
001420ea8d [article] fusermount umount sshfs 2020-05-30 16:50:47 +02:00
354abbdee8 fix svg test2 2020-05-18 16:50:29 +02:00
09e4032a62 fix svg test 2020-05-18 16:48:12 +02:00
a0d7eb3fbf fix images 2020-05-15 17:03:30 +02:00
7 changed files with 590 additions and 6 deletions

View File

@@ -9,7 +9,7 @@
### En-tête
L'en-tête contient les métodonnées de l'article et se présente sous la forme suivante :
L'en-tête contient les métadonnées de l'article et se présente sous la forme suivante :
```
---
@@ -52,9 +52,15 @@ Pour vérifier la cohérence des articles, des scripts sont disponibles dans le
Utilisés sans arguments, ces scripts vérifient l'ensemble des articles. Avec un argument, les scripts ne vérifient que le post dont l'argument est le nom de fichier.
__Il est très fortement recommandé d'exécuter ces scripts de vérification avant chaque commit ou push.__ Pour cela, les [hooks mis à disposision par git](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) peuvent vous être utiles.
## Publication
Pour publier, il suffit de pousser.
Pour publier, il suffit de pousser sur la branche `master`.
Comme seule la branche `master` est affichée sur le site, il est tout à fait possible d'utiliser des branches distinctes à la guise des auteurs (par exemple, pour enregistrer et versionner ses brouillons).
__Note :__ Dans un futur proche, une instance de test du blog sera déployée, et le contenu affiché correspondra à la branche `test`.
# Pour l'administrateur système

View File

@@ -3,6 +3,7 @@
<svg width="14.250cm" height="12.000cm" viewBox="31.811 27.910 46.061 39.910"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<g transform="scale(2)">
<rect x="31.861" y="27.960" width="14.150" height="11.900" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="31.861" y="27.960" width="14.150" height="11.900" fill="none" stroke="#FFFFFF" stroke-width="0.100" />
<rect x="33.530" y="28.893" width="2.034" height="1.525" fill="#B3B3B3" stroke="none" stroke-width="0"/>
@@ -59,4 +60,5 @@ Carol</text>
<polygon fill="none" stroke="#000000" stroke-width="0.100" points="39.623,35.920 39.672,35.363 39.820,35.600 40.098,35.625 "/>
<polygon fill="#000000" stroke="none" stroke-width="0.100" points="42.782,30.775 42.734,31.332 42.586,31.095 42.308,31.070 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.100" points="42.782,30.775 42.734,31.332 42.586,31.095 42.308,31.070 "/>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1,82 @@
---
title: Avantages et inconvénients des dictionnaires ordonnés
date: 2020-06-09
author: motius
template: post
tags: python,dict,hashmap,développement
---
Bonjour !
Aujourd'hui, je veux vous parler des dictionnaires en Python, et notamment de leur — relativement — nouvelle propriété d'ordre.
TL;DR: les considérations concernant les dictionnaires ordonnés tourne autour des performances.
On oublie de mentionner que cela facilite le débogage, mais que cela peut aussi cacher un bogue dans l'implémentation d'un algorithme, raison pour laquelle j'ai écrit le fragment de code ci-dessous.
# Un peu de contexte
Si l'on en croit [ce thread StackOverFlow](https://stackoverflow.com/questions/39980323/are-dictionaries-ordered-in-python-3-6), les clefs d'un dictionnaire dans Python3 depuis sa version 3.6 sont _de facto_ ordonnées **dans l'implémentation CPython** (dans l'ordre d'insertion), et Python3 dans sa version 3.7 standardise cet état de fait, ce qui veut dire que les autres implémentations (PyPy, Jython...) devront s'aligner afin de correctement implémenter le nouveau standard, pour assurer la compatibilité du code entre "interpréteurs".
(Je mets le lien StackOverFlow parce qu'il en contient d'autres vers la liste de diffusion de courriel, _etc._)
Si vous vous demandez ce qui a amené à cet état de fait, je vous recommande [la vidéo suivante](https://www.youtube.com/watch?v=p33CVV29OG8), par le curieux Raymond Hettinger.
Avec ça, vous aurez des éléments pour évaluer la pertinence des dictionnaires ordonnées en Python.
Notez, pour ceux qui n'ont pas regardé la documentation, que [Python3 met à disposition un OrderedDict](https://docs.python.org/3/library/collections.html#collections.OrderedDict) déjà disponible dans les versions antiques, antédiluviennes, je veux dire celles pré-3.4 (oui, je trolle, mais à peine).
# Le problème
Voyons un peu le contexte des deux problèmes qui m'ont amené à écrire cet article.
## Scénario 1
J'étais en train de déboguer un logiciel, appelons-le Verifikator, qui faisait des appels API afin de vérifier des données en bases.
Il se trouve que les résultats de Verifikator étaient aléatoires. De temps en temps, il retournait les bons résultats, _i.e._ il indiquait que certaines données en base étaient invalides, et de temps en temps, Verifikator n'indiquait pas d'erreur, alors qu'on pouvait vérifier _a la mano_ qu'il y avait effectivement une erreur en base.
Pour faire simple, la raison pour laquelle Verifikator n'était pas déterministe, c'est qu'il dépendait de l'ordre d'un dictionnaire dont la donnée provenait de l'API de la base de données.
Vous comprenez que j'exagère quand je dit que Verifikator n'était pas déterministe, il l'est heureusement, puisqu'il s'agit d'un système informatique, et qu'on néglige les rayons cosmiques et les bogues système.
Je me permet de rappeler à ceux d'entre vous qui s'étonnent de ce comportement erratique de Verifikator que j'étais en train de le déboguer (qui plus est, j'avais seulement participé à sa conception, pas à son implémentation).
Verifikator tourne en Python3.5, si l'on avait utilisé une version supérieure, on n'aurait pas eu ce problème, notamment, Verifikator aurait soit systématiquement planté, soit systématiquement fonctionné. Ç'eût été mieux afin de pouvoir localiser le bug par dichotomie, je vous avoue que mon état de santé mentale se dégradait à vu d'œil lorsque j'ai commencé à observer ce comportement aléatoire, en cherchant à localiser les lignes fautives par dichotomie.
Je reviens un instant sur cette histoire de dichotomie pour bien faire sentir à quel point c'est fatiguant.
Imaginez un peu, vous essayez de déterminer un point A où le programme est dans un état valide et un point B dans lequel l'état est invalide, puis vous regardez un nouveau point C "au milieu", afin de savoir si ce point C est le nouveau point valide A' ou bien le nouveau point B' pour l'itération suivante. Le problème, c'est que parfois vous pensez que l'état du programme est valide à ce point C, mais que ce n'est pas vrai en général. Vous continuez donc à itérer entre C et B, alors que le problème se trouve entre A et C.
Vous comprenez pourquoi j'apprécie fortement le fait que les dictionnaires soient ordonnés pour le débogage.
Mais. Comme vous imaginez, il y a un mais. Parce qu'il n'y a pas que des avantages aux dictionnaires ordonnés. C'est le sujet du second scénario.
# Scénario 2
Dans d'autres circonstances, j'ai déjà écrit — j'étais l'auteur du code cette fois-là, contrairement à Verifikator — un algorithme qui ne fonctionnait que si le dictionnaire sur lequel il tournait était ordonné. J'utilisais Python3.6 à l'époque, et par conscience professionnelle, j'ai compilé les versions 3.4 à 3.7 de Python afin de vérifier que les tests étaient corrects avec ces interpréteurs. Quelle ne fut pas ma surprise quand je m'aperçus que ce n'était point le cas. Ce n'était même pas le nouvel Python3.7 encore en bêta qui posait problème, mais les version 3.4 et 3.5. J'ai donc réécrit cet algorithme afin qu'il ne dépende plus de l'ordre du dictionnaire d'entrée (très simplement en construisant un OrderedDict à partir du dictionnaire et d'une liste correctement triée).
# Conséquences
À l'époque, je n'avais pas rajouté de code pour tester les fonctions de service du second programme, je m'assurai qu'il fonctionnait avec les 4 versions mineures de Python3 pour lesquelles je développais le programme.
J'ai fait les choses différemment cette fois, puisque j'ai codé cette fonction, qui randomise les clefs d'un dictionnaire plat (fonction non récursive sur d'éventuels dictionnaires en valeurs du dictionnaire passé en argument).
```python
import random as rnd
def shuffle_dict_keys(d: dict) -> dict:
"""shuffle the keys of a dictionary for testing purposes now that
Python dictionaries are insert-ordered. Does not compute inplace.
Does not work recursively.
"""
res = {}
l = list(d)
rnd.shuffle(l)
for k in l:
res[k] = d[k]
return res
```
J'avais en tête [cette planche XKCD](https://xkcd.com/1172/) en écrivant ce code. Non pas que je préfère l'ancien comportement, mais que pouvoir y souscrire de manière optionnelle me permet d'avoir des tests de meilleure qualité, et qu'il a donc fallu queje trouve un contournement afin de retrouver l'ancien comportement dans les cas de tests.
# Conclusion
De manière générale, je suis assez content que les dictionnaires soient ordonnés, mais je ne m'attendais pas à rencontrer de tels écueils, notamment puisque la majorité des conversations que j'avais lues sur le sujet s'attardaient sur les performances de cette nouvelle implémentation, et non sur ce genre de considérations.
Joyeux code !
Motius

View File

@@ -0,0 +1,258 @@
---
title: Créer un enum Python dynamiquement
date: 2020-06-02
author: motius
template: post
tags: python,enum,type,développement
---
Bonjour !
Aujourd'hui, je vous propose un micro tutoriel pour créer un `enum` dynamiquement en Python.
Je vous accorde qu'on ne fait pas ça tous les jours, c'est d'ailleurs la première fois que je suis tombé sur ce cas.
# Un peu de contexte
En général, un `enum`, ça ressemble à ça en Python :
```python
class MonEnumCustom(enum.Enum):
"""docstring
"""
VALIDATION = "VALIDATION"
SEND = "SEND"
```
Si l'`enum` contient un grand nombre de valeurs, chacune correspondant à un état, on peut aisément les formater avec un programme, afin d'écrire un `enum` comme ci-dessus.
Ça fait simplement un `enum` comme ci-dessus, mais en plus gros :
```python
class MonEnumCustom(enum.Enum):
"""docstring
"""
VALIDATION = "VALIDATION"
SEND = "SEND"
COMMIT = "COMMIT"
REVIEW = "REVIEW"
... # une centaine d'états supplémentaires disponibles
```
Mais que faire quand on récupère les valeurs dynamiquement depuis un appel à une API, qui retourne l'ensemble des états possibles sous forme d'une collection, comme ceci :
```python
>>> ls_remote_states = fetch_all_states(...)
>>> print(ls_remote_states)
[
"VALIDATION",
"SEND",
"COMMIT",
"REVIEW",
...
]
```
et que l'on souhaite créer un `enum` à partir de ladite collection afin d'écrire du code qui dépendra de valeurs d'états prise dans l'`enum` comme ci-dessous ?
```python
# pseudo-code
ls_remote_states = fetch_all_states(...)
MonEnumCustom = create_enum_type_from_states(ls_remote_states)
# dans le code
if state is MonEnumCustom.VALIDATION:
print("En cours de validation")
elif state is MonEnumCustom.COMMIT:
...
else:
raise
```
C'est le sujet de la discussion qui suit, qui va présenter les options disponibles, et quelques considérations quant à leurs usages.
# Le scénario
Je souhaitais pouvoir m'interfacer avec un service qui définit un grand nombre (un peu plus d'une centaine) de constantes qu'il me renvoie sous forme d'une collection de chaînes de caractères.
Trois choix s'offraient ainsi à moi :
* utiliser les constantes telles quelles dans une collection (`list`, `dict`...)
**Avantages** :
* rapide & facile
**Inconvénients** :
* sémantiquement pauvre
* la liste des états valides n'est jamais écrite dans le code
* faire un copié collé et du formatage (avec vim, c'est facile et rapide)
**Avantages** :
* rapide & facile
* l'intégralité des valeurs connues de l'`enum` peuvent être lues dans le programme à la seule lecture du code
**Inconvénients** :
* duplique un `enum` dont je ne suis pas responsable
* générer dynamiquement l'`enum`
**Avantages** :
* résout les inconvénients des solutions précédentes
* évite la duplication de code inutile.
Il arrive assez souvent que l'on doive définir un même `enum` à plusieurs endroits dans un système informatique, par exemple en base de donnée dans Postgre, puis en Java / Python, et enfin en Typescript si vous faites du développement sur toute la stack.
Mais dans ce cas-ci, j'aurais redéfini un enum sans que mon code ne soit la source d'autorité sur celui-ci, ce qui contrevient au principe du Single Source of Truth, c'est donc plus gênant que le simple problème de duplication de code.
* Plus facile à adapter lors de l'évolution de l'`enum`.
Ce dernier point est vrai dans mon cas où j'ai l'assurance que l'API ne fera qu'augmenter les états et n'invalidera jamais un état existant.
Dans le cas contraire, il faudra changer les fonctions qui utilisent les états définis dans l'`enum`, avec ou sans la génération d'`enum` automatique.
**Inconvénients** :
* à nouveau, la liste des états valides n'est jamais écrite dans le code
* Un tantinet plus long, surtout si tout ne va pas sur des roulettes du premier coup.
Et si je mentionne les roulettes, c'est que vous imaginez bien que ça n'a pas marché du premier coup (sinon je n'écrirais pas cet article).
# La théorie
Selon la [documentation officielle](https://docs.python.org/3/library/functions.html#type "Documentation Python3 sur type"), il suffit de passer 3 paramètres à la fonction `type` afin de créer une classe dynamiquement.
Si vous voulez mon avis, on est là dans la catégorie des fonctionnalités de Python vraiment puissantes pour le prototypage (à côté de `eval` et `exec`, même si ces derniers devraient être quasiment _interdits_ en production).
Chouette, me dis-je en mon fort intérieur, ceci devrait marcher :
```python
# récupération du dictionnaire des valeurs
d_enum_values: dict = fetch_all_states(...)
# création dynamique de l'enum
MonEnumCustom = type("MonEnumCustom", (enum.Enum,), d_enum_values)
```
# La pratique
Ne vous fatiguez pas, ça ne marche pas. La solution que j'ai trouvée est un contournement dégoûtant (dû justement au module `enum` de Python).
Je contourne les problèmes de création d'`enum` à l'aide d'un mapper qui ressemble à ça :
```python
class EnumMappingHelper(dict): # héritage pour contourner type
def __init__(self, mapping: dict = None):
self._mapping = mapping
def __getitem__(self, item):
if item == "_ignore_": # contournement d'enum
return []
return self._mapping[item]
def __delitem__(self, item):
return item # contournement d'enum
def __getattr__(self, key): # Accès au clefs de l'enum par attribut
return self._mapping[key]
@property
def _member_names(self): # contournement d'enum
return dict(self._mapping)
enum_mapping = EnumMappingHelper(d_enum_values)
MonEnumCustom = type("MonEnumCustom", (enum.Enum,), enum_mapping)
```
Je m'en sors donc en torturant un peu un classe personnalisée que j'appelle `EnumMappingHelper` afin que celle-ci se comporte d'une façon qui soit acceptable à la fois pour la fonction `type` et pour le module `enum`.
Je peux utiliser les variantes de mon `enum` ainsi :
```python
if state is MonEnumCustom.VALIDATION:
print("En cours de validation")
elif state is MonEnumCustom.COMMIT:
print("Validé")
elif state is MonEnumCustom.SEND:
print("Aucune modification possible")
elif ...:
...
else:
raise UnknownEnumStateValue("Code is not in sync with API")
```
# Un petit plus... avec un mixin
Je dispose bien entendu d'une fonction qui permet de mapper un état sous forme de chaîne de caractères à la valeur représentée dans l'`enum` de référence.
Moralement et en très très gros, elle ressemble à ça :
```python
@functools.lru_cache
def map_str_to_custom_enum(s: str) -> MonEnumCustom:
"""docstring
"""
return dict((v.value, v) for v in MonEnumCustom)[s]
```
L'idée étant donc de faire le mapping inverse de celui fourni par '`enum`, _i.e._ de générer automatiquement et efficacement une variante d'`enum` à partir de sa représentation sous forme de chaîne de caractères.
Sauf qu'en réalité, la fonction n'est pas du tout implémentée ainsi.
À la place, j'utilise un mixin, ce qui me permet d'avoir la même fonctionnalité sur tous les `enum`.
La fonction ci-dessus est remplacée par une méthode de classe.
Le cache `lru_cache` est remplacé par un attribut de classe de type dictionnaire, ce qui évite toutes sortes d'inconvénients.
```python
class EnumMixin:
_enum_values = {}
def __init__(self, *args, **kwargs):
self.__class__._enum_values[args[0]] = self
@classmethod
def convert_str_to_enum_variant(cls, value: str):
cls._enum_values
return cls._enum_values(value, object())
```
Avec le mixin de conversion, le code de génération d'`enum` devient :
```python
# inchangé
enum_mapping = EnumMappingHelper(d_enum_values)
# MonEnumCustom hérite en premier du mixin EnumMixin
MonEnumCustom = type("MonEnumCustom", (EnumMixin, enum.Enum), enum_mapping)
```
# Gestion de version de l'API pour les variantes de l'enum
Enfin en ce qui concerne la gestion de version de l'API, il est possible de rajouter un `assert` dans le code qui vérifie toutes les variantes de l'`enum`, afin de lever une exception le plus tôt possible et de ne pas faire tourner du code qui ne serait pas en phase avec la version de l'API utilisée.
```python
# je définis la variable suivante en copiant directement les valeurs
# récupérées par un appel à l'API au moment du développement :
ls_enum_variantes_copie_statiquement_dans_mon_code = [
"VALIDATION",
"SEND",
"COMMIT",
"REVIEW",
... # et une centaine d'états supplémentaires
]
# si la ligne suivante lance une AssertionError, l'API a mis à jour
# les variantes de l'enum
assert fetch_all_states(...) == ls_enum_variantes_copie_statiquement_dans_mon_code
```
Dans mon cas, je suis plus souple, car ayant la garantie que les variantes publiées
de mon `enum` ne seront pas dépréciées par de nouvelles versions de l'API, je vérifie
simplement cette assertion :
```python
# idem
ls_enum_variantes_copie = [
"VALIDATION",
"SEND",
"COMMIT",
"REVIEW",
... # et une centaine d'états supplémentaires
]
# si la ligne suivante lance une AssertionError, l'API n'a pas honoré son
# contrat de ne pas déprécier les variantes de l'enum.
assert set(fetch_all_states(...)).issuperset(set(ls_enum_variantes_copie))
```
Notez que si je souhaite être strict et n'autoriser aucun changement des
variantes de l'API, alors il est préférable de copier coller les variantes
directement dans le code et de comparer ces valeurs à l'exécution.
Le code en est rendu plus lisible. Mais ce n'est pas le cas de figure dans
lequel je me trouve.
# Conclusion
Seule la solution utilisant un copier coller permettait de voir la liste des valeurs de l'`enum` dans le code.
Je combine les avantages de cette solution et de la troisième que j'ai implémentée en copiant les valeurs prises par l'`enum` dans la docstring de sa classe et en mentionnant la version du service distant associé.
Cela me permet de combiner les avantages de toutes les solutions à l'exception, bien sûr, d'un peu de temps passé.
L'inconvénient inattendu de ce code, c'est celui de devoir créer une classe d'aide dont le seul but soit de forcer un comportement afin d'obtenir le résultat escompté.
Si vous avez des suggestions pour améliorer ce bazar, je prends.
Joyeux code !
Motius

View File

@@ -0,0 +1,60 @@
---
title: Démonter un disque distant après une erreur réseau
date: 2020-05-30
author: motius
template: post
tags: sshfs,réseau,umount,fusermount
---
Bonjour !
Aujourd'hui, un micro tutoriel d'administration système pour gérer des problèmes de déconnexion du réseau.
J'utilise `sshfs` afin de monter un disque distant. Ça me permet d'avoir de la synchronisation de données sur un seul disque dur de référence, qui reste solidement attaché à un serveur. Il m'est donc inutile de brancher et débrancher le disque, et de le déplacer.
Ce programme s'utilise ainsi :
```bash
sshfs user@remote:/chemin/du/répertoire/distant /mnt/chemin/du/répertoire/local
```
Note : il vous faudra avoir les droits d'écriture sur le répertoire local sur lequel vous montez le disque distant.
Et pour le démonter, j'utilise :
```bash
fusermount -u /mnt/chemin/du/répertoire/local
```
Cette commande peut échouer pour plusieurs raisons, notamment s'il y a toujours un programme qui utilise le disque distant. Ça peut être aussi bête que d'essayer de démonter le répertoire depuis un shell (`bash`, `zsh`) dont le répertoire courant est dans l'arborescence sous le disque distant.
Le message d'erreur ressemble à ceci :
```
fusermount: failed to unmount /mnt/chemin/du/répertoire/local: Device or resource busy
```
Il suffit d'utiliser la commande `lsof` (list open files) pour savoir quels programmes utilisent encore le disque distant de cette manière :
```bash
lsof | grep /mnt/chemin/du/répertoire/local
```
Mais ! mais mais, il peut arriver que la connexion réseau se coupe (ou que votre serviteur déplace son ordinateur hors de portée du Wi-Fi, il lui arrive d'être distrait...)
Dans ce cas, on peut essayer de recourir à `umount` en tant qu'utilisateur root, _si on lui passe les bonnes options_, que voici :
```bash
# Lazy unmount. Detach the filesystem from the file hierarchy now, and clean up all
# references to this filesystem as soon as it is not busy anymore.
umount -l /mnt/chemin/du/répertoire/local
# StackOverflow suggère aussi :
# https://stackoverflow.com/questions/7878707/how-to-unmount-a-busy-device
umount -f /mnt/chemin/du/répertoire/local
# mais la page de manuel indique que cette option est utile pour les disques NFS.
```
Joyeux code !
Motius

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

View File

@@ -47,7 +47,7 @@ Contrairement à OpenVPN qui par défaut a une vision client/serveur, c'est à d
- Alice
- Carol
![](%assets_dir%/wireguard-le-vpn-sauce-kiss/wg-p2p.svg)
![](%assets_url%/wireguard-le-vpn-sauce-kiss/wg-p2p.svg)
L'ennui, c'est que si on veut ajouter une nouvelle machine Dave en respectant cette topologie, il faut récupérer les infos de toutes les autres machines (Alice, Bob et Carol) pour dresser la liste de pairs de Dave, et ensuite on doit ajouter Dave à la liste de pairs d'Alice, Bob et Carol.
@@ -61,7 +61,7 @@ De plus, pour établir une liaison entre deux pairs, il faut un point d'accès c
- Alice
- Bob
![](%assets_dir%/wireguard-le-vpn-sauce-kiss/wg-p2p.svg)
![](%assets_url%/wireguard-le-vpn-sauce-kiss/wg-p2p.svg)
Ainsi, quand on voudra ajouter Dave dans la topologie, il suffira que Dave ait connaissance de Carol dans sa liste de pairs, et il faudra dire à Carol d'ajouter Dave à ses pairs.
@@ -158,7 +158,7 @@ On est pas obligé de ne mettre que l'adresse VPN du pair. D'ailleurs, notament
Reprenons notre scénario *roadwarrior* avec Alice et Bob en pair mobile et Carol en passerelle d'accès. On définit le réseau VPN 10.0.0.0/16. D'autre part, disons que le réseau interne auquel Carol doit servir de passerelle est en 192.168.0.0/16 et contient une machine Dave.
![](%assets_dir%/wireguard-le-vpn-sauce-kiss/wg-rw.svg)
![](%assets_url%/wireguard-le-vpn-sauce-kiss/wg-rw.svg)
Carol a donc une paire de clef :