Le blog tech de Nicolas Steinmetz (Time Series, IoT, Web, Ops, Data)
Suite de notre épopée :
Dans ce sixième et dernier billet pour cette série, nous continuons avec les Fichier d’Ecritures Comptables (FEC) pour produire le compte de résultat et déterminer ainsi le bénéfice de l’exercice en cours. Il faut donc prendre toutes les opérations en classe 6 (charges) et 7 (produits). Pour chaque classe de compte, il peut y avoir des crédits ou des débits (ex pour un compte de classe 7 : un avoir sur une facture émise). C’est donc un chouilla plus compliqué que le compte de trésorerie.
Depuis le dernier billet, j’ai légèrement fait évoluer le modèle de données :
<société>.<bilan ou resultat>.<classe de compte>.<type d'opération: credit ou debit>
<société>.<bilan ou resultat>.<classe de compte>
; le type d’opération est maintenant un labelPour un crédit de 100€ avec une référence de pièce à 1234 pour le compte 706, on passe donc de :
<Timestamp de l'écriture comptable>// cerenit.resultat.706.credit{PieceRef=1234} 100
à :
<Timestamp de l'écriture comptable>// cerenit.resultat.706{PieceRef=1234, operation=credit} 100
"<readToken>" "readToken" STORE
// Récupération de toutes les opérations de crédit pour les comptes charges (classe 6xx)
// Le SORT permet d'être sur d'avoir toutes les opérations triées par date
// Stockage du résultat dans une variable
[ $readToken '~comptabilite.resultat.6.*' { "operation" "credit" } '2020-01-01T00:00:00Z' '2020-12-31T23:59:59Z' ] FETCH
MERGE
SORT
'charges_credit' RENAME
'charges_credit' STORE
// Récupération de toutes les opérations de débit pour les comptes charges (classe 6xx)
// Le SORT permet d'être sur d'avoir toutes les opérations triées par date
// Stockage du résultat dans une variable
[ $readToken '~comptabilite.resultat.6.*' { "operation" "debit" } '2020-01-01T00:00:00Z' '2020-12-31T23:59:59Z' ] FETCH
MERGE
SORT
'charges_debit' RENAME
'charges_debit' STORE
// Fusion des deux listes de séries en une liste qui va avoir l'ensemble des opérations
// Les opérations de débit sont mis en valeur négative du calcul du solde
// Le SORT permet d'être sur d'avoir toutes les opérations triées par date
// Stockage du résultat dans une variable qui contient l'ensemble des opérations
[
$charges_debit -1 *
$charges_credit
] MERGE
SORT
'charges_flux' RENAME
'charges_flux' STORE
// Même opération pour les comptes de produit (7xx)
[ $readToken '~comptabilite.resultat.7.*' { "operation" "credit" } '2020-01-01T00:00:00Z' '2020-12-31T23:59:59Z' ] FETCH
MERGE
SORT
'produits_credit' RENAME
'produits_credit' STORE
[ $readToken '~comptabilite.resultat.7.*' { "operation" "debit" } '2020-01-01T00:00:00Z' '2020-12-31T23:59:59Z' ] FETCH
MERGE
SORT
'produits_debit' RENAME
'produits_debit' STORE
[
$produits_debit -1 *
$produits_credit
] MERGE
SORT
'produits_flux' RENAME
'produits_flux' STORE
// Fusion des 2 flux d'opérations (charges et produits) pour avoir une vision temporelle de ces opérations
// Le SORT permet d'être sur d'avoir toutes les opérations triées par date
// Renommage de la série en "compte_resultat" qu'elle va permettre de batir
// Somme cumulée de l'ensemble des opérations pour avoir un solde à date
// Stockage sous la forme d'une variable
// Affichage de la variable
[
$produits_flux
$charges_flux
] MERGE
SORT
'compte_resultat' RENAME
[ SWAP mapper.sum MAXLONG 0 0 ] MAP
'compte_resultat' STORE
$compte_resultat
Ce qui nous donne dans le Studio :
Du précédent billet et ce celui-ci, nous avons donc :
Tout ce qu’il faut donc pour faire un dashboard avec Discovery. Il faut dire que le billet Covid Tracker built with Warp 10 and Discovery et dans une moindre mesure Server monitoring with Warp 10 and Telegraf donnent accès à plein d’options pour réaliser ses dashboards.
Je pourrais mettre le code de mes requêtes directement dans les dashboards mais j’aime pas trop quand des tokens se balladent dans les pages web. Du coup, je vais déporter le code dans des macros. J’ai églément rendu les macro dynamiques dans le sens où elles prennent une année en paramètre pour afficher les données de l’année en question.
On a déjà vu le fonctionnement des macros précédemment, je ne reviendrais donc pas dessus.
La macro du compte de résultat à titre d’exemple :
<%
{
'name' 'cerenit/accountancy/compte-resultat'
'desc' 'Function to calculate the cumulative benefit (or loss) of the company'
'sig' [ [ [ [ 'year:LONG' ] ] [ 'result:GTS' ] ] ]
'params' {
'year' 'Year, YYYY'
'result' 'GTS'
}
'examples' [
<'
2020 @cerenit/accountancy/compte-resultat
'>
]
} INFO
// Actual code
SAVE 'context' STORE
TOLONG // When called from dashboard, it's a string - so convert paramter to LONG first
'year' STORE // Save parameter as year
// Compute 1st Jan of given year
[ $year 01 01 ] TSELEMENTS-> ISO8601
'start' STORE
// Compute 31 Dec of given year
[ $year 12 31 23 59 59 ] TSELEMENTS-> ISO8601
'end' STORE
"<readToken>" "readToken" STORE
[ $readToken '~comptabilite.resultat.6.*' { "operation" "credit" } $start $end ] FETCH
MERGE
SORT
'charges_credit' RENAME
'charges_credit' STORE
[ $readToken '~comptabilite.resultat.6.*' { "operation" "debit" } $start $end ] FETCH
MERGE
SORT
'charges_debit' RENAME
'charges_debit' STORE
[
$charges_debit -1 *
$charges_credit
] MERGE
SORT
{ NULL NULL } RELABEL
'charges_flux' RENAME
'charges_flux' STORE
[ $readToken '~comptabilite.resultat.7.*' { "operation" "credit" } $start $end ] FETCH
MERGE
SORT
'produits_credit' RENAME
'produits_credit' STORE
[ $readToken '~comptabilite.resultat.7.*' { "operation" "debit" } $start $end ] FETCH
MERGE
SORT
'produits_debit' RENAME
'produits_debit' STORE
[
$produits_debit -1 *
$produits_credit
] MERGE
SORT
{ NULL NULL } RELABEL
'produits_flux' RENAME
'produits_flux' STORE
[
$produits_flux
$charges_flux
] MERGE
SORT
'compte_resultat' RENAME
[ SWAP mapper.sum MAXLONG 0 0 ] MAP
'compte_resultat' STORE
$compte_resultat
$context RESTORE
%>
'macro' STORE
$macro
Comme le décrit l’exemple, si on veut le compte de résultat de l’année 2020, on utilisera le code suivant :
2020 @cerenit/accountancy/compte-resultat
J’ai profité de ce billet pour utiliser Warpfleet Synchronizer & Warpfleet Resolver pour simplifier le déploiement des macros ; cela explique que les signatures pour appeler les macros changent par la suite dans le dashboard.
Ci-après le code du dashboard :
<%
{
'title' 'Comptabilité CérénIT'
'description' 'Trésorerie et compte de résultat'
'vars' {
'myYear' 2020
}
'tiles' [
{
'title' 'Informations'
'type' 'display'
'w' 11 'h' 1 'x' 0 'y' 0
'data' {
'data' 'Résultat de la série <a href="https://www.cerenit.fr/blog/premiers-pas-avec-warp10-comptabilite-et-previsions/">Ma comptabilité, une série temporelle comme les autres</a> et de l'ingestion des Fichiers d'écritures comptables.'
'globalParams' { 'timeMode' 'custom' }
}
}
{
'title' 'Année'
'type' 'input:list'
'w' 1 'h' 1 'x' 11 'y' 0
'data' {
'data' [ '2017' '2018' '2019' '2020' ]
'events' [ { 'type' 'variable' 'tags' 'year' 'selector' 'myYear' } ]
'globalParams' { 'input' { 'value' '2020' } }
}
}
{
'title' 'Trésorerie (annuel)'
'type' 'line'
'w' 6 'h' 2 'x' 0 'y' 1
'macro' <% $myYear @cerenit/macros/treso %>
'options' { 'eventHandler' 'type=(variable),tag=year' }
}
{
'title' 'Compte de résultat (annuel)'
'type' 'line'
'w' 6 'h' 2 'x' 6 'y' 1
'macro' <% $myYear @cerenit/macros/compteresultat %>
'options' { 'eventHandler' 'type=(variable),tag=year' }
}
{
'title' 'Trésorerie (pluri-annuelle)'
'type' 'line'
'w' 12 'h' 2 'x' 0 'y' 3
'macro' <% [ 2017 $myYear ] @cerenit/macros/treso_multi %>
'options' { 'eventHandler' 'type=(variable),tag=year' }
}
]
}
{ 'url' 'https://w.ts.cerenit.fr/api/v0/exec' }
@senx/discovery2/render
%>
et son rendu :
Dans le bloc global du dashboard, on définir une variable myYear
, initialisée à 2020. Cette variable est mise à jour dynamiquement lorsque l’on choisit une valeur dans la liste déroulante du bloc “Année”.
<%
{
'title' 'Comptabilité CérénIT'
'description' 'Trésorerie et compte de résultat'
'vars' {
'myYear' 2020
}
...
Le bloc Année justement :
{
'title' 'Année'
'type' 'input:list'
'w' 1 'h' 1 'x' 11 'y' 0
'data' {
'data' [ '2017' '2018' '2019' '2020' ]
'events' [ { 'type' 'variable' 'tags' 'year' 'selector' 'myYear' } ]
'globalParams' { 'input' { 'value' '2020' } }
}
}
C’est une liste déroulante (type: input:list
) avec pour valeurs les années 2017 à 2020. Par défaut, elle est initialisée à 2020. Via le mécanisme des “events”, lorsqu’une valeur est choisie, celle-ci est émise sous la forme d’une variable, nommée myYear
et ayant pour tag
la valeur year
.
Ainsi, si je sélectionne 2017 dans la liste, la variable myYear prendra cette valeur. Maintenant que la valeur est définie suite à mon choix et émise vers le reste du dashboard, il faut que les autres tiles récupèrent l’information.
Regardons le tile Trésorerie :
{
'title' 'Trésorerie (annuel)'
'type' 'line'
'w' 6 'h' 2 'x' 0 'y' 1
'macro' <% $myYear @cerenit/macros/treso %>
'options' { 'eventHandler' 'type=(variable),tag=year' }
}
La récupération de la variable se fait via la proriété options
et la récupération de l’eventHandler associé et défini précédemment.
Une fois récupérée, la variable myYear
peut être utilisée dans le bloc macro
et le tile est mis à jour dynamiquement.
En conséquence :
Ainsi s’achève cette série sur les données comptable et les séries temporelles. Des analyses complémentaires pourraient être menées (analyse de stocks, réparition d’activité, etc) mais mes données comptables sont insuffisantes pour en valoir l’intérêt. J’espère néanmoins que cela aura sucité votre intérêt et ouvert des horizons.
Cette série fut aussi l’occasion de faire un tour de la solution Warp 10 et de voir :
Si vous souhaitez poursuivre l’aventure et le sujet, n’hésitez pas à me contacter.
package.json
: "resolutions": { "ua-parser-js": "^0.7.30" }
via Security issue: compromised npm packages of ua-parser-js (0.7.29, 0.8.0, 1.0.0) - Questions about deprecated npm package ua-parser-jsAnnonces & Produits :
Articles & Vidéos :
yield()
peut être très pratique pour débugguer son code flux mais permet aussi de récupérer le résultat de plusieurs requêtes pour faire des aggrégationspivot()
pour revenir à des manipulations en ligne.Pour le retour sur les InfluxDays North America qui ont lieu cette semaine, ce sera pour un prochain billet ou édition du Time Series France Meetup
Il y a quelques temps et sachant que j’utilisais n8n pour automatiser la génération des brèves du BigData Hebdo, Mathias m’a demandé s’il était possible de faire la même chose entre n8n et Warp 10 qu’avec node-red et Warp 10.
La réponse est oui mais voyons comment faire cela.
Pour ceux qui ne connaissent pas n8n, c’est un clone open source (sous licence fair-code) à des services comme Zapier ou IFTTT. Il permet d’automatiser des processus via la création de workflows. Ces workflows sont composés d’étapes et d’actions. n8n dispose d’un grand nombre de connecteurs vers les différents services existants, des opérateurs génériques (faire un appel http, appliquer une fonction), des opérateurs logiques (si, etc), des opérateurs de transformation de données, etc. Chacun de ces éléments est implémenté via une node. A chaque étape du workflow, une node est instanciée puis paramétrée. Les nodes peuvent être reliées entre-elles et la sortie d’une node peut alimenter la suivante.
Le workflow se veut basique et va être le suivant :
Ce n’est pas le workflow le plus passionnant du monde, mais cela permet de faire deux appels à l’API HTTP de Warp 10 :
/api/v0/exec
; vu le code, j’aurais pu passer par /api/V0/fetch
mais cela me permet de tester l’exécution de code WarpScript./api/v0/update
pour insérer une donnée dans une série. Cela permet de tester le passage du token d’authentification via un header.Pour commencer le workflow, la donnée de départ est la valeur en pourcentage du métrique “CPU Idle” d’un de mes serveurs.
En WarpScript, cela donne:
'<readToken>' 'readToken' STORE
[ $readToken 'crnt-ovh.cpu.usage_idle' { "host" "crnt-d10-gitlab" "cpu" "cpu-total" } NOW -1 ] FETCH
Et la réponse :
[
[{
"c": "crnt-ovh.cpu.usage_idle",
"l": {
"host": "crnt-d10-gitlab",
"cpu": "cpu-total",
"source": "telegraf",
".app": "io.warp10.bootstrap"
},
"a": {},
"la": 0,
"v": [
[1634505650000000, 91.675025]
]
}]
]
n8n dispose d’une node HTTP Request, qui comme son nom l’indique permet de faire des requêtes HTTP vers un serveur distant. Toutefois, il n’est pas possible de passer notre code WarpScript directement dans l’appel HTTP. Il faut créer un objet avec le code WarpScript et passer ensuite l’objet créé et le nom de la propriété contenant le code WarpScript à la node HTTP Request.
Pour stocker le code WarpScript dans un objet, il faut utiliser la node Set. Une fois la node Set ajoutée dans le workflow, aller dans Parameters > Add Value > Type: String
Saisir:
En cliquant sur “Execute Node”, on peut valider la variable (la partie grisée étant mon token) :
On peut maintenant ajouter une node HTTP Request dans le workflow et la relier à la node Set nouvellement créée. Ainsi, la node HTTP Request aura directement accès au résultat de la node Set.
Pour les ajustements à faire :
http://url.de.votre.instance.warp.10/api/v0/exec
En cliquant sur “Execute Node”, le résultat de la requête est visible (la partie grisée étant un bout de mon token) :
On retrouve notre objet JSON mais il est imbriqué dans des Array Javascript, on va applanir tout ça et extraire le timestamp et la valeur du cpu via l’ajout de deux nodes Function que l’on relie à la node HTTP Request. La node Function permet d’exécuter du code javascript sur les données et de réaliser des transformations que l’on ne peut pas forcément faire avec les autres nodes. Cela n’étant pas le coeur du sujet, cela ne sera pas détaillé.
A l’issue des deux exécutions, les données sont réduites à ce qui suit :
[{
"ts": 1634503660000000,
"cpu": 93.219488
}]
La node IF ne sera pas détaillée non plus ; elle sert juste à introduire un semblant de logique dans le workflow. En l’occurence, si la valeur de “cpu” >= 90, alors le test est considéré comme vrai et faux sinon. Dans le cas où c’est faux, une node noOp a été ajoutée pour matérialiser la fin du workflow.
Dans le cas où le test est vrai (valeur de “cpu” >= 90), on veut alors insérer le timestamp et la valeur dans une autre série sur une instance Warp 10. Comme précédemment, cela va se faire en deux fois:
On ajoute une node Set, ensuite dans Parameters > Add Value > Type: String
Saisir:
{{$json["ts"]}}// n8n{} {{$json["cpu"]}}
Ce qui nous donne l’écran suivant :
On revient à l’écran précédent en cliquant sur la croix à droite et en exécutant la node, on obtient :
Ensuite, il faut ajouter une nouvelle node HTTP Request avec le paramétrage suivant :
http://url.de.votre.instance.warp.10/api/v0/update
En haut du menu de gauche, une section “Credentials” est apparue ; dans la liste déroulante, cliquer sur “Create new” et remplissez le formulaire de la façon suivante:
Revener ensuite dans votre node HTTP Request dont on peut lancer l’exécution et on obtient :
Si je vais ensuite voir le contenu de ma série n8n :
'<readToken>' 'readToken' STORE
[ $readToken 'n8n' {} NOW -100 ] FETCH
J’obtiens comme réponse :
[
[{
"c": "n8n",
"l": {
".app": "io.warp10.bootstrap"
},
"a": {},
"la": 0,
"v": [
[1634503660000000, 93.219488],
[1634502790000000, 94.808468],
[1634501690000000, 93.7751],
[1634501550000000, 91.741742],
[1634478300000000, 92.774711]
]
}]
]
Avec une entrée pour chaque exécution du workflow sous réserve d’avoir un “CPU idle” >= 90%.
En conclusion, nous pouvons retenir que :
Le workflow était très basique pour permettre de montrer rapidement cette intégration. Des workflows plus complexes et riches sont laissés à votre imagination :
podman machine
est supporté nativement sur Linux et MacOS/Intel et en remote client sur Windows/Intel.docker compose xxx
). Pour Windows & OSX, il est fourni avec Docker Desktop.tar
de NodeJS directement (ou indirectement), il est judicieux de mettre à jour votre version de npm
et node
et de vérifier vos dépendances.first()
et last()
ainsi que les nouvelles fonctions timestamp_floor()
et timestamp_ceil()
pour gérer les arrondis inférieurs/supérieurs. Enfin, l’API HTTP accepte des paramètres liés au “Out Of Order”.exec
.