Architecte de vos plateformes/produits et agitateur de séries temporelles

Conception, développement, déploiement et exploitation de vos plateformes, applications et données.

Contactez-nous !

SAFT

timeseries influxdb flux alerting dashboard

Contexte

Dans le cadre du maintien en condition opérartionnelle de sa plateforme et pour faire des analyses plus poussées de ses données, la SAFT souhaite mettre à jour sa stack InfluxDB 1.x OSS, Chronograf et Kapacitor vers InfluxDB 2.0 OSS. Fort des précédentes missions, la SAFT nous a confié cette mission, à laquelle s’ajoute une période de support post déploiement en production de la nouvelle plateforme.

Notre réponse

  • Montée de version InfluxDB OSS 1.x vers 2.x
  • Migration des dashboards Chronograf vers InfluxDB2
  • Migration des alertes en TickScript vers des “custom checks” (tasks flux)
  • Restructuration des données avec une shard duration conforme aux recommandations InfluxData
  • Mise à jour de Telegraf et de sa configuration
  • Mise à jour de la partie exploitation (backups, etc)
  • Documentation
  • Support post déploiement en production

Bénéfices pour le client

  • Expertise sur InfluxDB
  • Connaissance prélable du contexte suite aux précécentes missions

Ma comptabilité, une série temporelle comme les autres - partie 5 - Les FEC et le compte 512

warp10 timeseries comptabilité trésorerie banque fec

Suite de notre épopée :

Dans ce cinquième billet, nous allons parler de Fichier d’Ecritures Comptables (FEC) et d’un compte simple à analyser : le compte 512 qui correspond à votre compte en banque.

Le Fichier des Ecritures Comptables (FEC)

Le Fichier des Ecritures Comptables (FEC) est un format de fichier normalisé. Sa spécification est disponible et grosso modo, ce qu’il faut en savoir à ce stade :

  • C’est un fichier TSV (un CSV avec les champs séparés par des tabulations)
  • Il contient 18 champs permettant de décrire les différentes écritures comptables :
    • JournalCode
    • JournalLib
    • EcritureNum
    • EcritureDate
    • CompteNum
    • CompteLib
    • CompAuxNum
    • CompAuxLib
    • PieceRef
    • PieceDate
    • EcritureLib
    • Debit
    • Credit
    • EcritureLet
    • DateLet
    • ValidDate
    • Montantdevise
    • Idevise

En partant de ces informations et après quelques précisions fournies par mon expert-comptable Fabrice Heuvrard sur le fichier, nous avons convenu de commencer par l’analyse du compte 512 correspondant aux opérations bancaires. Facile à calculer (somme des crédits - somme des débits) et facile à vérifier, il me suffit de regarder mon compte en banque et/ou mon bilan en fin d’année.

Continuant à utiliser Warp 10 pour y stocker mes séries temporelles, j’ai réalisé un script en Go qui prend le fichier FEC en entrée et envoie les données dans Warp 10 avec le formalisme suivant : <société>.<bilan ou resultat>.<classe de compte>.<type d'opération: credit ou debit> :

  • <société> est juste le début de l’arborescence
  • <bilan ou résultat> : le Plan Comptable Général Francais défini que si les comptes de classe 1 à 5 sont des classes de bilan et les classes 6 et 7 sont des classes de compte de résultat. Je suis donc le même principe de séparation des comptes et défiinr la valeur bilan et resultat. Le compte 512 que nous allons étudier commençant par 5, c’est un compte de bilan. Il sera donc dans la série cerenit.bilan.*
  • <classe de compte> : le plan comptable général est normalisé sur ces trois premiers chiffres. Les trois suivants sont à la discrétion du comptable. Du coup, pour ne pas avoir une série par code comptable, je retrouve par classe du plan de compte. Ainsi, toutes les opérations ayant le code 512xxx se retrouvera dans la série cerenit.bilan.512.*
  • <type d'opération: crédit ou débit> : suivant si l’opération est un débit ou crédit, cela prend la valeur adéquat. Ainsi, toutes les opérations ayant le code 512xxx se retrouvera dans la série cerenit.bilan.512.credit ou ``cerenit.bilan.512.debit`
  • Le montant de l’écriture comptable sera la valeur associée à mon point dans la série.
  • Les autres informations seront mises sous la forme de labels (ie meta données de mon point) pour d’éventuelles analyses ultérieures. Il s’agit de couple clé/valeurs.

Ainsi, un crédit de 100€ avec une référence de pièce à 1234 sera représenté sous la forme :

<Timestamp de l'écriture comptable>// cerenit.bilan.512.credit{PieceRef=1234} 100

La modélisation est peut être un peu naive à ce stade, il sera toujours temps de la faire évoluer dans un second temps mais a priori :

  • J’ai ma séparation bilan / compte de résultat
  • J’ai ma séparation par classe de compte
  • J"ai ma séparation débit / crédit
  • au pire via les labels, j’ai des axes complémentaires de recherche / sélection.

Contrôle des données

Avant de commencer la moindre analyse, j’ai voulu vérifier l’intégrité de mes données.

"<readToken>" "readToken"  STORE

// Récupération des données de 2020 pour le compte 512
[ $readToken 'cerenit.bilan.512.credit' {} '2020-01-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
// Fusion de l'ensemble des séries temporelles en une seule série
MERGE
// Calcul de la somme de l'ensemle des valeurs de la séries -
// MAXLONG permet de tout récupérer sans calculer la taille exacte de la liste (pour peu que votre liste soit plus petite que la valeur de MAXLONG)
// 1 permet de ne sortir qu'une valeur en sortie
[ SWAP mapper.sum MAXLONG MAXLONG 1 ] MAP
// C'est une liste avec une liste à 1 élément, on "applatit" tout ça
MERGE
VALUES
0 GET
// On stocke la valeur finale dans totalCredit
'totalCredit' STORE

// Même opération sur les débits
[ $readToken 'cerenit.bilan.512.debit' {} '2020-01-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
MERGE
[ SWAP mapper.sum MAXLONG MAXLONG 1 ] MAP
MERGE
VALUES
0 GET
'totalDebit' STORE

// Calcul du solde
$totalCredit $totalDebit -

Cela me donne : 27746.830000000075

Exploration de la trésorerie

"<readToken>" "readToken"  STORE

// Récupération des données de 2020 pour le compte 512
[ $readToken 'cerenit.bilan.512.credit' {} '2020-01-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
// Fusion de l'ensemble des séries temporelles en une seule série
MERGE
// Tri des points par date
SORT
// Renommage de la série
'credit' RENAME
// Suppression des labels
{ NULL NULL } RELABEL
// Stockage dans une variable
'credit' STORE

// Même opération sur les débits
[ $readToken 'cerenit.bilan.512.debit' {} '2020-01-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
MERGE
SORT
'debit' RENAME
{ NULL NULL } RELABEL
'debit' STORE

// Affichage des deux séries
$credit
$debit

// Création de la série de mouvements
$credit $debit -
'mouvements' RENAME

Cela nous donne ces courbes:

warp10 - exploration treso

Mais on voit bien à fin décembre qu’il y a des opérations de débit qui ne sont pas prises en compte dans le solde (la ligne orange s’arrête avant la verte).

En cherchant un peu, je me dis qu’il faudrait que je calcule une nouvelle série avec tous les éléments de crédit et débit et faire l’addition de tout cela. Je vois également que FLATTEN (doc)permet de fusionner plusieurs listes en une seule. Mais finalement, seul MERGE sera nécessaire.

Cela me donne la piste suivante :

"<readToken>" "readToken"  STORE

// Récupération des données de 2020 pour le compte 512
[ $readToken 'cerenit.bilan.512.credit' {} '2020-01-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
MERGE
SORT
'credit' RENAME
{ NULL NULL } RELABEL
'credit' STORE

// Même opération sur les débits
[ $readToken 'cerenit.bilan.512.debit' {} '2020-01-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
MERGE
SORT
'debit' RENAME
{ NULL NULL } RELABEL
// Je multiplie les debits par -1 pour pouvoir faire l'opération de solde ensuite
[ SWAP -1 mapper.mul 0 0 0 ] MAP
'debit' STORE

// Je fusionne les deux séries avec MERGE
[
  $credit
  $debit
] MERGE
// Je trie les éléments par date
SORT
'mouvements' RENAME

Cette fois-ci, mon solde prend bien en compte toutes les opérations de l’année.

warp10 - mouvements treso

Pour la version consolidée avec le solde du compte :

// Récupération des données de 2020 pour le compte 512
[ $readToken 'cerenit.bilan.512.credit' {} '2020-01-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
MERGE
SORT
'credit' RENAME
{ NULL NULL } RELABEL
'credit' STORE

// Récupération des données de 2020 pour le compte 512
[ $readToken 'cerenit.bilan.512.debit' {} '2020-01-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
MERGE
SORT
'debit' RENAME
{ NULL NULL } RELABEL
-1 *
'debit' STORE

// Fusion des débits/crédits comme vu précédemment
[
  $credit
  $debit
] MERGE
SORT
'mouvements' RENAME

// On applique mapper.sum sur l'ensemble des points précédents le point qui est considéré
// Le premier point ne va donc prendre que lui même
// Le 2nd point va prendre sa valeur et ajouter celle du précédédent
// Le 3ème point va prendre sa valeur et la somme des points précédents
// Et ainsi de quiste
[ SWAP mapper.sum MAXLONG 0 0 ] MAP

Et le résultat en images :

warp10 - mouvements treso

Et voilà !

Il ne me reste plus qu’à :

  • ingérer les FEC des autres années pour envisager des comparaisons entre les différents exercices comptables, voire du prédictif pour l’exercice en cours et à venir,
  • étendre ces analyses à d’autres comptes maintenant,
  • et à créer les dashboards adéquats avec Discovery.

Ma solution pour le Warp 10 Code Contest - partie 2

timeseries warp10 geospatial challenge

Suite et fin de ma réponse au code contest après la première partie. Dans ce billet, nous allons voir comment calculer les émissions de CO2 pour la partie de trajet sur la route 66.

// Define points from the car journey on the US66 road
[
  // Here is the gts of the car datalogger
  @senx/dataset/route66_vehicle_gts

  // Here is the route 66 geoshape (+/- 20meters)
  @senx/dataset/route66_geoshape
  mapper.geo.within 0 0 0
] MAP
"onTheRoad" STORE

$onTheRoad
{
 'timesplit' 60 s
}
MOTIONSPLIT
0 GET
'sectionOnTheRoad' STORE

// Compute speed - result in m/s
[ $sectionOnTheRoad mapper.hspeed 1 0 0 ] MAP
// Convert in km/h so x3600 /1000 = 3.6 - mapper.mul expects a constant
[ SWAP 3.6 mapper.mul 0 0 0 ] MAP
'speedFrames' STORE

// Get distance between each points in km (first in meters, then in km)
[ $sectionOnTheRoad mapper.hdist 0 1 0 ] MAP
[ SWAP 0.001 mapper.mul 0 0 0 ] MAP
'distFrames' STORE

// fuel consumption approximation is  (8 liters/100km) × (speed (km/h) / 80) +1
// So it's Speed * 8 / 80 / 100 + 1 = V/10 + 1
// F = False => does not return the index
$speedFrames
<%
  0.1 *
  1.0 +
%> F LMAP
'hundredKmFuelConsumption' STORE

[ ] 'instantConsumption' STORE

<%
  'i' STORE // store index

  // Get each list and compute one by another
  // So we compute consumption for 100 km at given speed (computed previously)
  // with related distance
  // then we divide by 100 as first value is for 100 km
  $distFrames $i GET
  $hundredKmFuelConsumption $i GET
  *
  100 /
  'r' STORE
  $instantConsumption $r +!
%>
'C' STORE
0 7 $C FOR
CLEAR

// For each GTS, compute fuel consumption as 1 point
[
  $instantConsumption
  mapper.sum
  MAXLONG
  MAXLONG
  1
] MAP
// Sum all points to get total consumption
0 SWAP <% VALUES 0 GET + %> FOREACH
// 1L = 2392g CO2
2392 *
// Enjoy !

Le premier et le second bloc sont les mêmes que dans la premièr partie. Je vous y renvoie donc si besoin.

A ce stade, nous avons une liste de 8 séries correspondant à chaque section passée sur la route 66. Chaque série comporte un liste de timestamps et de points géospatiaux (lattitude, longitude, élévation).

Concernant le troisième bloc :

  • il s’agit de calculer la vitesse en m/s entre chaque point de la série (ce qui explique le 1 0 0 pour prendre le point précédent, aucun point suivant et appliquer cette opération sur l’ensemble de la liste - voir la tips 3 de 12 tips to apply sliding window algorithms like an expert). Pour cela, on utilise mapper.hspeed (doc) qui consomme une série et calcule la vitesse en m/s en tenant compte de la longitude/lattitude/élévation.
  • Ce résultat, on le convertit en km/h dans la foulée en utilisant mapper.mul (doc) en notant au passage qu’il lui faut une constante (on ne peut pas mettre 3600 * 1000 / mais 3.6)
  • On a donc une liste de 8 séries temporelles avec chacune un timestamp, les données géospatiales et une vitesse entre chaque point. C’est stocké dans la variable speedFrames.

Concernant le 4ème bloc :

  • Sur le même modèle que pour la vitesse, on calcul la distance entre chaque point des 8 séries via mapper.hdist que l’on a vu dans le premier billet. Cette fois-ci, plutôt que de calculer la distance totale, on la distance entre le point et le point suivant et on le fait pout tout les points de la liste, d’où le 0 1 0
  • La distance étant en mètres, on la divisie par 1000 pour avoir des kilomètres. Mais comme il n’y a pas de mapper de division, alors on utilise mapper.mulet la valeur 0.001
  • On a donc une liste de 8 séries temporelles avec chacune un timestamp, les données géospatiales et une distance entre chaque point. C’est stocké dans la variable distanceFrames.

Concernant le 5ème bloc :

  • j’ai voulu calculer la consommation d’essence sur la base de la formule:

(8 liters/100km) × (speed (km/h) / 80) +1

  • Cela se simplifie en Speed/10 + 1.
  • Si on multiplie ce coefficient par les vitesses entre deux points obtenues précédemment (dans speedFrames), on obtient une consommation pour 100km avec chaque vitesse. Il faudra dans un second temps le pondérer par la distance parcourue entre deux points (distanceFrames) pour avoir un instantané de consommation pour la vitesse et la distance parcourue.
  • Pour faire cette consommation au 100km non pondérée, on utilise LMAP (doc)pour appliquer une MACRO à chaque élément de la liste. Cette macro contient le coefficient de consommation d’essence. LMAP retourne normalement l’index et la valeur associée. Or l’index ne nous sert à rien, on met donc l’argument concernant l’index à False (abrégé F) pour qu’il ne soit pas retourné.
  • On stocke le résultat dans hundredKmFuelConsumption et on a donc une liste de 8 series avec la consommation pour 100km à la vitesse donnée. Il nous faut maintenant pondérée cette liste par la distance pour avoir un instantané de consommation.

Concernant le 6ème bloc :

  • On commence par créer une liste vide appelée instantConsumption.
  • On sait que l’on a une liste de 8 éléments, donc on peut faire un boucle FOR (doc) dessus avec un indice allant de 0 à 7. FOR prend comme dernier argument une MACRO que j’ai nommé C
  • Dans la MACRO définie au dessus, je commence par stocker l’index de la boucle. Mes deux listes de 8 séries temporelles sont identiques en terme de points, avec l’une contenant les consommations pour 100km hundredKmFuelConsumptionet la seconde les distances entre chaque point distFrames. L’idée est donc de multiplier chaque série de hundredKmFuelConsumption par la série équivalente dans distFrames et de diviser par 100 pour finir notre proportionnalité.
  • On stocke cet consommation instantanée dans la variable r.
  • On ajoute ce résultat r dans la liste instantConsumption, ce qui permet de reconstituer notre liste de 8 séries mais ayant pour valeur cette fois ci les instantanés de consommation entre chaque point de chaque série.

Un petit interlude visuel avant le dernier bloc :

warp10 - instantané de consommation

Concernant le 7ème bloc :

  • Le but est de faire la somme de chaque instantanné de consommation pour avoir la consommation totale.
  • Comme dans la première partie, on utilise cette fois-ci mapper.sum (doc) en prenant l’ensemble des données des listes capturées via MAXLONG et on récupère 1 seule valeur qui s’avère être le total. On a donc la consommation totale de chaque série
  • Comme vu aussi en fin de première partie, on fait alors la somme de chaque liste pour avoir la consommation totale (9.823366576601234)
  • On sait que 1L = 2392g CO2, il nous reste donc à faire cette multiplication.
  • On obtient alors : 23497.492851230152 ou 23,497 kg de CO2.

J’espère avoir été clair dans ces explications - si ce n’est pas le cas - dites le moi (via Twitter, Mail, LinkedIn, etc) et je préciserai les choses.

Bilan personnel de ce code contest :

  • Opportunité de découvrir une partie des fonctionnalités géospatiales de Warp 10 que je n’avais pas encore utilisé
  • Améliorer mon usage autour de MAP, les mapper, les MACRO et LMAP et plein de petites choses ici ou là.
  • MAP s’applique sur des GTS mais aussi des listes de GTS sans rien avoir à faire. Pas besoin de se rajouter des boucles supplémentaires !
  • MAXLONG utilisé dans les MAP permet de ne pas avoir à se soucier de la taille de l’élément sur laquelle on applique MAP. Cela ne fait pas non plus une erreur du style index out of range.
  • en bonus, obtenir quelques lots sympathiques 😎

warp10 - goodies recto

warp10 - goodies verso

J’espère néanmoins apprendre des choses du corrigé officiel : Working with GEOSHAPEs: code contest results.

Ma solution pour le Warp 10 Code Contest - partie 1

timeseries warp10 geospatial challenge

La société SenX a proposé un code contest suite à la publication de son article sur les formes géospatiales. L’objet du concours porte sur le trajet d’un véhicule aux USA et il consiste à déterminer :

  • la distance réalisée sur la fameuse route 66 durant ce trajet,
  • de déterminer les émissions de CO2 réalisées durant ce trajet sur la route 66.

Maintenant que le gagnant a été annoncé (TL;DR: moi 😎🎉) et en attendant le corrigé officiel, voici ma proposition de solution.

Distance parcourue sur la route 66

Les données de départ sont :

  • @senx/dataset/route66_vehicle_gts : le trajet réalisé par le véhicule
  • @senx/dataset/route66_geoshape : la route 66
// Define points from the car journey on the US66 road
[
  // Here is the gts of the car datalogger
  @senx/dataset/route66_vehicle_gts

  // Here is the route 66 geoshape (+/- 20meters)
  @senx/dataset/route66_geoshape
  mapper.geo.within 0 0 0
] MAP
"onTheRoad" STORE

$onTheRoad
{
 'timesplit' 60 s
}
MOTIONSPLIT
0 GET
'sectionOnTheRoad' STORE

// Compute distance for each GTS and output it as a single point
[ $sectionOnTheRoad mapper.hdist MAXLONG MAXLONG 1 ] MAP
// Sum all GTS
0 SWAP <% VALUES 0 GET + %> FOREACH
// Convert to km
1000 /
// Enjoy !

Explications :

  • Le premier bloc utilise le mapper mapper.geo.within (doc). Ce mapper compare deux zones géographiques et ne retient que les poits qui sont dans la zone voulue. Ici, je prends donc tous les points du trajet et les compare avec ceux de la route 66. Seuls les points sur la route 66 sont conservés. Le résultat est une aggrégation de points que l’on stocke dans la variable onTheRoad.
  • Pour le second bloc : dans le studio, lorsque l’on regarde la liste des points obtenus dans l’onglet “Tabular view”, on peut voir que les points sont espacés en général de minimum 10 secondes et jusqu’à une minute environ. Après avoir relu le billet “Use motion to automatically split GTS”, j’ai retenu ce seuil d’une minute et la fonction MOTIONSPLIT (doc) pour calculer la distance entre deux points. Obtenant une liste de 1 élément contenant une liste, j’ai rajouté le 0 GET pour supprimer la liste parente. On obtient alors une liste de 8 séries temporelles (GTS) correspondant à chaque tronçon sur la route. On stocke cela dans la variable sectionOnTheRoad.

warp10 - section on the road

  • Pour le dernier bloc - partie 1 : mapper.hdist (doc) permet de calculer la distance totale sur une fenêtre glissante de points. L’utilisation de MAXLONG permet d’avoir une valeur suffisamment grande pour notre cas d’espèce pour prendre l’ensemble des données de chacune des 8 listes - il n’est pas nécessaire de connaitre la taille exacte de la liste pour travailler dessus et cela ne crée pas d’erreur non plus ; ça peut déstabiliser !. Le 1 permet de n’avoir qu’une valeur en sortie. On a donc en sortie la distance de chacune des 8 sections.

warp10 - total distance of each section

  • Pour le dernier bloc - partie 2 : là, j’avoue la syntaxe est un peu cryptique 🤯. L’idée est donc de faire la somme de toutes les distances totales obtenues précédemment. Il faut donc faire 0 (pour initialiser l’opération d’addition) et ajouter la première valeur de la liste et ainsi de suite. Une fois qu’on a la somme, on divise par 1000 pour avoir des kilomètres
  • La réponse est alors: 79.82147744769853

Pour comprendre la partie 2, on peut réécrire la chose de la façon suivante :

[ $sectionOnTheRoad mapper.hdist MAXLONG MAXLONG 1 ] MAP
'totalDistancePerSection' STORE

0 $totalDistancePerSection <% VALUES 0 GET + %> FOREACH

Non, toujours pas ? Vous me rassurez, j’ai du creuser plus loin aussi.

Commençons par :

$totalDistancePerSection <% VALUES 0 GET %> FOREACH

VALUES (doc) consomme une série temporelle et en sort les valeurs sous la forme d’une liste. Nous avons une liste initiale de 8 séries que nous avons ramené à 8 points. Avec FOREACH (doc), on applique donc la fonction VALUES sur chaque série contenant un seul point. Plutôt que d’avoir en sortie des listes à un seul point, le 0 GET permet d’avoir directement la valeur.

warp10 - values et foreach

Pour faire une addition, en WarpScript, c’est :

1 1 +

ou :

1
1
+

Par celà, j’entends que pour appliquer +, il faut que les deux éléments soient définis dans la pile.

Notre boucle FOREACH emet dans la pile chaque valeur qu’il faut ajouter à la précédente. On peut donc rajouter le + dans la boucle FOREACH :

$totalDistancePerSection <% VALUES 0 GET + %> FOREACH

Mais si je cherche à exécuter cela, cela ne fonctionne pas - cela reviendrait à faire:

valeur1IssueDuForeach +
valeur2IssueDuForeach +
valeur3IssueDuForeach +
valeur4IssueDuForeach +
...

Si on part de la fin, la valeur 4 va pouvoir être additionnée à la valeur 3 car celle-ci existe dans la pile. MAIS la valeur 1 n’est additionnée à rien à ce stade et l’opération est invalide. D’où la nécessité de rajouter le 0 pour pouvoir avoir deux éléments pour notre première addition.

Ce qui nous donne bien :

0 $totalDistancePerSection <% VALUES 0 GET + %> FOREACH

Maintenant que la brume s’est éclaircie et que le 🤯 est passé à 😎 pour cette syntaxe de fin, je vous propose de nous retrouver dans un prochain billet pour la suite de ma solution au concours.

Ma comptabilité, une série temporelle comme les autres - partie 4 - dashboards

warp10 timeseries comptabilité discovery dashboard

Suite de notre épopée :

Nous allons voir aujourd’hui comment présenter ces données à l’aide de Discovery, la solution de Dashboard as Code pour Warp 10 fournie par SenX.

Installation de Discovery

Tout est décrit dans le billet Truly Dynamic Dashboards as Code

Dans mon cas, warp 10 est dans une partition dédiée /srv/warp10 - warp 10 est donc installé dans /srv/warp10/warp10. C’est la valeur de $WARP10_HOME.

Pour la configuration du plugin HTTP, j’ai un fichier $WARP10_HOME/etc/conf.d/80-discovery.conf contenant :

# Load the HTTP Plugin
warp10.plugin.http = io.warp10.plugins.http.HTTPWarp10Plugin
# Define the directory where endpoint spec files will reside
http.dir = /srv/warp10/discovery
# Define the host and port the plugin should bind to
http.host = 127.0.0.1
http.port = 8081
# Expose the Directory and Store so FETCH requests can be performed via the plugin
egress.clients.expose = true

Le plugin HTTP sera donc accessible via une url de base en http://127.0.0.1:8081/

J’ai ensuite créé le fichier /srv/warp10/discovery/discovery.mc2/srv/warp10/discovery est la valeur associée à http.dir dans le fichier précédent.

{
  'path' '/discovery/'
  'prefix' true
  'parsePayload' true
  'macro' <%
    'cerenit/dashboards/' @senx/discovery/dispatcher
  %>
}

Ce fichier indique que :

  • Les dashboards seront disponibles à partir de l’url /discovery/<nom_du_dashboard> ou /discovery/<dossier_ou_arborescence>/<nom_du_dashboard>
  • Le dossier où seront les dashboards seront stockés dans $WARP10_HOME/macros/cerenit/dashboards. Il s’agira de fichier WarpScript ou Flows avec l’extension en .mc2.

Avec ces deux fichiers, nous savons maintenant que :

  • nous accèderons aux dashboards via http://127.0.0.1:8081/discovery/<nom_du_dashboard>.
  • les dashboards seront des fichiers stockés dans ``$WARP10_HOME/macros/cerenit/dashboards`
  • donc le dashboard $WARP10_HOME/macros/cerenit/dashboards/mon_dashboard.mc2 sera accessible via http://127.0.0.1:8081/discovery/mon_dashboard.

Création du premier dashboard

Un dashboard se décompose en différentes parties. Celle contenant les données a le mot clé tiles et contient différente tile. Chaque tile affiche un graphique, un zone de texte, un titre ou tout composant warpView. Pour le reste, on s’appuiera sur le template par défaut.

Donc créeons un fichier $WARP10_HOME/macros/cerenit/dashboards/comptabilite/compta1.mc2 contenant :

<%
   {
     'tiles' [
       {
         'type' 'display'
         'w' 4 'h' 1 'x' 3 'y' 0
         'data' 'Compta - Exemple 1'
       }
       {
         'type' 'line'
         'w' 4 'h' 2 'x' 1 'y' 3
         'data' [
           @cerenit/accountancy/revenue
           'revenue' STORE
           $revenue
         ]
       }
       {
         'type' 'line'
         'w' 4 'h' 2 'x' 5 'y' 3
         'data' [
           @cerenit/accountancy/expense
           'expense' STORE
           $expense
         ]
       }
       {
         'type' 'line'
         'w' 4 'h' 2 'x' 3 'y' 5
         'data' [
           $revenue $expense -
         ]
       }
     ]
   }
   @senx/discovery/render
%>

Comme indiqué précédemment, je me focalise sur le contenu de tiles. La grille de présentation des dashboards est fixé à 12 colonnes par défaut.

Ici, je cherche donc à afficher 4 éléments :

  • Le premier élément est un bloc titre, contenant “Compta - Exemple 1”. Il est placé sur la 3ème item de la grille en parant de la gauche (valeur de x), il est en haut (valeur y de 0) et il occupe une largeur de 4 (valeur de w) et une hauteur de 1 (valeur de h),
  • les trois éléments suivants sont des graphiques de tyme “line” avec un positionnement pour deux les deux premiers soient sur une ligne et le dernier en dessous et plutôt centré par rapport aux deux premmiers.
  • le deuxième et troisième éléments font appels à des macros @cerenit/accountancy/xxx. Je pourrais mettre du code Warpscript directement dans le fichier comme dans l’exemple. Toutefois, le code exécuté dans le dashboard est visible dans le navigateur. Dans la mesure où mes requêtes pour récupérer les données demandent de l’authentification avec un passage de token, je déporte ce code dans une macro et je ne fais donc qu’appeler cette macro. Ainsi, le code sera généré coté serveur et seul le résultat sera retourné dans le navigateur.
  • le dernier élement illustre la capacité intéressante et différentiante de Discovery : on ne fait pas que décrire le dashboard avec du code, on peut aussi faire des opérations sur nos données. Ici je calcule dynamiquement le résultat à partir des données de chiffre d’affaires (revenue) et de dépenses (expense). J’aurais pu faire l’appel à une troisième macro comme les deux éléments précédents, mais vu que j’ai cette capacité de réaliser des opérations au sein d’un dashboard, pourquoi me priver ?
  • enfin, on appelle la fonction @senx/discovery/render pour générer le dashboard.

Revenons sur nos macros ; Warp 10 permet d’avoir des macros exécutées coté serveur. Ces macros peuvent être utiles pour créer/partager du code, elles peuvent prendre des paramètres en entrée si besoin et elles sont exécutées coté serveur. Dans notre cas, pour éviter que nos tokens se balladent dans le navigateur comme indiqué précédemment, c’est cette propriété qui va nous intéresser.

La macro @cerenit/accountancy/revenue se trouve donc dans le fichier $WARP10_HOME/macros/cerenit/accountancy/revenue.mc2 et contient :

<%
  {
    'name' 'cerenit/accountancy/revenue'
    'desc' 'Provide revenue'
  } INFO

  // Actual code
  SAVE 'context' STORE

  '<readToken>' 'readToken' STORE
  [ $readToken 'revenue' { 'company' '=cerenit' } NOW [ 2016 12 1 ] TSELEMENTS-> ] FETCH
  0 GET

  $context RESTORE
%>
'macro' STORE
$macro

Je ne vais pas m’étendre sur la rédaction des macros mais succintement :

  • Le permier bloc donne le nom de la macro et sa description
  • La suite indique le code qui va être réalisé - pour ma part, un FETCH sur la classe “revenue” pour la compagny “cerenit” du 01/12/2016 jusqu’à maintenant. Cette liste n’a qu’un élément que je récupère. C’est ce qui me sera retourné.

La macro @cerenit/accountancy/expense est sur le même modèle en remplaçant revenue par expense.

Ces deux macros nous retournent donc chacune une série temporelle sur la période 12/2016 jusqu’à ce jour : une pour le chiffre d’affaires, une pour les dépenses.

Si vous allez sur http://127.0.0.1:8081/discovery/comptabilite/compta1, vous verrez le dashboard suivant :

warp10

Le template par défaut est assez minimaliste et on note la présence d’un logo SenX. Je n’ai rien contre, mais comme c’est la compatabilité de mon entreprise que je présente, autant changer cet aspect des choses.

Ajustements graphiques

Pour continuer progressivement, nous allons :

  • Rajouter un titre et une description à notre dashboard en précisant les propriétés title et description en début de fichier
  • Rajouter un footer en bas de page en précisant la propriété footer
  • Supprimer le logo SenX en précisant la propriété template
  • Ajuster la position des blocs pour qu’ils soient centrés comme le reste des éléments

On met cela dans un nouveau fichier $WARP10_HOME/macros/cerenit/dashboards/comptabilite/compta2.mc2.

<%
   {
     'title' 'Comptabilité CerenIT'
     'description' 'Comptabilité CérénIT depuis 2016'
     'tiles' [
       {
         'type' 'display'
         'w' 4 'h' 1 'x' 4 'y' 0
         'data' 'Compta - Exemple 2'
       }
       {
         'title" "Chiffre d'affaires'
         'type' 'line'
         'w' 4 'h' 2 'x' 2 'y' 3
         'data' [
           @cerenit/accountancy/revenue
           'revenue' STORE
           $revenue
         ]
       }
       {
         'title' 'Dépenses'
         'type' 'line'
         'w' 4 'h' 2 'x' 6 'y' 3
         'data' [
           @cerenit/accountancy/expense
           'expense' STORE
           $expense
         ]
       }
       {
         'title' 'Résultat'
         'type' 'line'
         'w' 4 'h' 2 'x' 4 'y' 5
         'data' [
           $revenue $expense -
         ]
       }
     ]
     'footer' '<p style="text-align: center;">CérénIT &copy; 2021 - Réalisé avec Discovery et Warp 10 de SenX</p>'
     'template'
   <'
<!DOCTYPE html><html><head><title id="pageTitle"></title>
  {{{CSS}}}
  {{{HEAD}}}
</head>
<body>
<div class="heading">
  <div class="header"><h1 id="title" class="discovery-title"></h1><p id="desc" class="discovery-description"></p></div>
</div>
{{{HEADER}}}
{{{GRID}}}
{{{FOOTER}}}
{{{JS}}}
</body></html>
    '>
   }
   @senx/discovery/render
%>

Si les propriétés title, description et footer vont de soi, pour trouver comment supprimer le logo SenX, il m’a fallu lire le contenu de la macro @senx/discovery/html pour mieux comprendre les différents placehoders et leur fonctionnement.

Si vous allez sur http://127.0.0.1:8081/discovery/comptabilite/compta2, vous verrez le dashboard suivant :

warp10

A ce stade, on note que les propriétés title de chaque graphique n’est pas affiché. En dehors de ça, nous retrouvons bien tous nos éléments ajustés.

Néanmoins, cette lecture de @senx/discovery/html permet de voir que l’on a pas mal de points d’entrée pour rajouter des éléments spécifiques. Le tout sera de veiller à ne pas impacter les composants graphiques WarpView dans leur sémantique pour ne pas créer de dysfonctionnement.

Un petit coup de bar(re)

Pour finir ce tutoriel, nous allons :

  • lisser un peu ces lignes d’une part, en remplaçant le type de line à spline pour les trois graphiques déjà réalisés (pour les autres modes de réprésentation, voir les options de chart)
  • rajouter un histogramme avec le cumul annuel de nos données avec un chart de type bar.

On met cela dans un nouveau fichier $WARP10_HOME/macros/cerenit/dashboards/comptabilite/compta3.mc2.

<%
   {
     'title' 'Comptabilité CerenIT'
     'description' 'Comptabilité CérénIT depuis 2016'
     'tiles' [
       {
         'type' 'display'
         'w' 4 'h' 1 'x' 4 'y' 0
         'data' 'Compta - Exemple 3'
       }
       {
         'title' 'Chiffre d\'affaires'
         'type' 'spline'
         'w' 4 'h' 2 'x' 2 'y' 3
         'data' [
           @cerenit/accountancy/revenue
           'revenue' STORE
           $revenue
         ]
       }
       {
         'title' 'Dépenses'
         'type' 'spline'
         'w' 4 'h' 2 'x' 6 'y' 3
         'data' [
           @cerenit/accountancy/expense
           'expense' STORE
           $expense
         ]
       }
       {
         'title' 'Résultat'
         'type' 'spline'
         'w' 4 'h' 2 'x' 4 'y' 5
         'data' [
           $revenue $expense -
         ]
       }
       {
          'title' 'Consolidation annuelle'
          'type' 'bar'
          'w' 4 'h' 2 'x' 4 'y' 7
          'data' [
            [ $revenue bucketizer.sum ] @senx/cal/BUCKETIZE.byyear 1970 TIMESHIFT
            [ $expense bucketizer.sum ] @senx/cal/BUCKETIZE.byyear 1970 TIMESHIFT
            [ @cerenit/accountancy/result bucketizer.sum ] @senx/cal/BUCKETIZE.byyear 1970 TIMESHIFT
          ]
          'options' { 'timeMode' 'timestamp' }
       }
     ]
     'footer' '<p style="text-align: center;">CérénIT &copy; 2021 - Réalisé avec Discovery et Warp 10 de SenX</p>'
     'template'
   <'
<!DOCTYPE html><html><head><title id="pageTitle"></title>
  {{{CSS}}}
  {{{HEAD}}}
</head>
<body>
<div class="heading">
  <div class="header"><h1 id="title" class="discovery-title"></h1><p id="desc" class="discovery-description"></p></div>
</div>
{{{HEADER}}}
{{{GRID}}}
{{{FOOTER}}}
{{{JS}}}
</body></html>
    '>
   }
   @senx/discovery/render
%>

Pour ce dernier graphique, il est donc de type bar. Pour le détail des requêtes, je vous renvoie à la partie 2 qui explique cela. Dans notre cas, il faut juste veiller à passer une option supplémentaires pour que timeMode interprête la date issue de la requête comme un timestamp et non comme une date par défaut. D’autres options comme la gestion de la présentation en mode vertical/horizontal ou en mode “stacked” ou pas.

Si vous allez sur http://127.0.0.1:8081/discovery/comptabilite/compta3, vous verrez le dashboard suivant :

warp10

Pour résumer ce billet, nous aovns pu voir que :

  • Warp 10 dispose d’une solution de Dashboard avec Discovery
  • Comme on pouvait le voir dans le studio, à partir du moment où l’on a nos requêtes, il est aisé de les mettre dans des dashboards
  • Les macros permettent de ne pas exposer des tokens dans le navigateur et de faire des calculs cotés serveur
  • Il est possible de personnaliser l’apparence des dashboards - des points d’entrée et des mécanismes de surcharges sont à notre disposition pour en faire ce que l’on souhaite
  • Les composants WarpView sont assez riches et se configurent facilement