CérénIT

Le blog tech de Nicolas Steinmetz (Time Series, IoT, Web, Ops, Data)

IoT - Qualité de l'air avec un esp32 (TTGo T-Display), le service ThingSpeak, du MQTT, Warp 10 et Discovery

iotmqttarduinowarp10thingspeakco2esp32discoverydashboard

Le projet Nous Aérons propose de réaliser ses propres détecteurs de CO2 avec un ESP32 avec un écran comme le Lilygo TTGo T-Display et un capteur Senseair S8-LP.

L'idée est donc de déployer plusieurs capteurs, faire remonter les valeurs via ThingSpeak et ensuite les ingérer puis analyser avec Warp 10 et faire un dashboard avec Discovery.

schema du projet

Montage

Pour le montage, je vous invite à consuler principalement :

ThingSpeak

L'exemple de code fourni utilise le service ThingSpeak pour la remontée des valeurs. Comme il s'agit de mon premier projet Arduino et que cela fonctionne, j'ai cherché à rester dans les clous du code proposé et tester par la même occasion ce service. J'aurais pu directement poster les valeurs sur mon instance Warp 10 mais c'est aussi l'occasion de tester la récupération d'informations via le client MQTT de Warp 10.

Il vous faut :

  • Créer un compte
  • Créer un channel avec le champ 1 qui accueillera vos données
  • Récupérer la clé d'API en écriture
  • Noter votre Channel ID

Code Arduino

Disclaimer : c'est mon premier projet Arduino.

En repartant du code fourni sur le site Capteur de CO2, j'ai fait quelques ajustements :

  • Remplacer l'appel HTTP par la librairie ThingSpeak
  • La librairie retourne des status code : 200 si c'est OK, 40x si incorrect et -XXX si erreur ; j'ai un amélioré le message de debug pour savoir si l'insertion était OK ou KO.
  • Ajustement de positionnement du "CO2" et du "ppm" sur l'écran pour une meilleure lisibilité.

Il vous faut modifier :

  • la configuration wifi : ssid1, password1 et éventuellement ssid2, password2
  • la configuration ThingSpeak : channelID, writeAPIKey

Compiler le tout et uploader le code sur votre ESP32.

/************************************************
 * 
 *   Capteur de CO2 par Grégoire Rinolfi
 *   https://co2.rinolfi.ch
 * 
 ***********************************************/
#include <TFT_eSPI.h>
#include <SPI.h>
#include <Wire.h>
#include <WiFiMulti.h>
#include <ThingSpeak.h>

WiFiMulti wifiMulti;
TFT_eSPI tft = TFT_eSPI(135, 240);

/************************************************
 * 
 *   Paramètres utilisateur
 * 
 ***********************************************/
 
#define TXD2 21         // série capteur TX
#define RXD2 22         // série capteur RX
#define BOUTON_CAL 35
#define DEBOUNCE_TIME 1000

const char* ssid1     = "wifi1";
const char* password1 = "XXXXXXXXXXXXXXXX";
const char* ssid2     = "wifi2";
const char* password2 = "XXXXXXXXXXXXXXXX";

unsigned long channelID = XXXXXXXXXXXXXXXX;
char* readAPIKey = "XXXXXXXXXXXXXXXX";
char* writeAPIKey = "XXXXXXXXXXXXXXXX";
unsigned int dataFieldOne = 1;                       // Field to write temperature data
const unsigned long postingInterval = 12L * 1000L;   // 12s
unsigned long lastTime = 0;

// gestion de l'horloge pour la validation des certificats HTTPS
void setClock() {
  configTime(0, 0, "pool.ntp.org", "time.nist.gov");

  Serial.print(F("Waiting for NTP time sync: "));
  time_t nowSecs = time(nullptr);
  while (nowSecs < 8 * 3600 * 2) {
    delay(500);
    Serial.print(F("."));
    yield();
    nowSecs = time(nullptr);
  }

  Serial.println();
  struct tm timeinfo;
  gmtime_r(&nowSecs, &timeinfo);
  Serial.print(F("Current time: "));
  Serial.print(asctime(&timeinfo));
}

/************************************************
 * 
 * Thinkgspeak functions
 * https://fr.mathworks.com/help/thingspeak/read-and-post-temperature-data.html
 * 
 ***********************************************/

float readTSData( long TSChannel,unsigned int TSField ){
    
  float data =  ThingSpeak.readFloatField( TSChannel, TSField, readAPIKey );
  Serial.println( " Data read from ThingSpeak: " + String( data, 9 ) );
  return data;

}

// Use this function if you want to write a single field.
int writeTSData( long TSChannel, unsigned int TSField, float data ){
  int  writeSuccess = ThingSpeak.writeField( TSChannel, TSField, data, writeAPIKey ); // Write the data to the channel
  if(writeSuccess == 200){
    Serial.println("Channel updated successfully!");
  }
  else{
    Serial.println("Problem updating channel. HTTP error code " + String(writeSuccess));
  }
  return writeSuccess;
}

// Use this function if you want to write multiple fields simultaneously.
int write2TSData( long TSChannel, unsigned int TSField1, long field1Data, unsigned int TSField2, long field2Data ){

  ThingSpeak.setField( TSField1, field1Data );
  ThingSpeak.setField( TSField2, field2Data );
    
  int writeSuccess = ThingSpeak.writeFields( TSChannel, writeAPIKey );
  if(writeSuccess == 200){
    Serial.println("Channel updated successfully!");
  }
  else{
    Serial.println("Problem updating channel. HTTP error code " + String(writeSuccess));
  }
  return writeSuccess;
}

/************************************************
 * 
 *   Code de gestion du capteur CO2 via ModBus
 *   inspiré de : https://github.com/SFeli/ESP32_S8
 * 
 ***********************************************/
volatile uint32_t DebounceTimer = 0;

byte CO2req[] = {0xFE, 0X04, 0X00, 0X03, 0X00, 0X01, 0XD5, 0XC5};
byte ABCreq[] = {0xFE, 0X03, 0X00, 0X1F, 0X00, 0X01, 0XA1, 0XC3}; 
byte disableABC[] = {0xFE, 0X06, 0X00, 0X1F, 0X00, 0X00, 0XAC, 0X03};  // écrit la période 0 dans le registre HR32 à adresse 0x001f
byte enableABC[] = {0xFE, 0X06, 0X00, 0X1F, 0X00, 0XB4, 0XAC, 0X74}; // écrit la période 180
byte clearHR1[] = {0xFE, 0X06, 0X00, 0X00, 0X00, 0X00, 0X9D, 0XC5}; // ecrit 0 dans le registe HR1 adresse 0x00
byte HR1req[] = {0xFE, 0X03, 0X00, 0X00, 0X00, 0X01, 0X90, 0X05}; // lit le registre HR1 (vérifier bit 5 = 1 )
byte calReq[] = {0xFE, 0X06, 0X00, 0X01, 0X7C, 0X06, 0X6C, 0XC7}; // commence la calibration background
byte Response[20];
uint16_t crc_02;
int ASCII_WERT;
int int01, int02, int03;
unsigned long ReadCRC;      // CRC Control Return Code 

void send_Request (byte * Request, int Re_len)
{
  while (!Serial1.available())
  {
    Serial1.write(Request, Re_len);   // Send request to S8-Sensor
    delay(50);
  }

  Serial.print("Requete : ");
  for (int02 = 0; int02 < Re_len; int02++)    // Empfangsbytes
  {
    Serial.print(Request[int02],HEX);
    Serial.print(" ");
  }
  Serial.println();
  
}

void read_Response (int RS_len)
{
  int01 = 0;
  while (Serial1.available() < 7 ) 
  {
    int01++;
    if (int01 > 10)
    {
      while (Serial1.available())
        Serial1.read();
      break;
    }
    delay(50);
  }

  Serial.print("Reponse : ");
  for (int02 = 0; int02 < RS_len; int02++)    // Empfangsbytes
  {
    Response[int02] = Serial1.read();
    
    Serial.print(Response[int02],HEX);
    Serial.print(" ");
  }
  Serial.println();
}

unsigned short int ModBus_CRC(unsigned char * buf, int len)
{
  unsigned short int crc = 0xFFFF;
  for (int pos = 0; pos < len; pos++) {
    crc ^= (unsigned short int)buf[pos];   // XOR byte into least sig. byte of crc
    for (int i = 8; i != 0; i--) {         // Loop over each bit
      if ((crc & 0x0001) != 0) {           // If the LSB is set
        crc >>= 1;                         // Shift right and XOR 0xA001
        crc ^= 0xA001;
      }
      else                            // else LSB is not set
        crc >>= 1;                    // Just shift right
    }
  }  // Note, this number has low and high bytes swapped, so use it accordingly (or swap bytes)
  return crc;  
}

unsigned long get_Value(int RS_len)
{

// Check the CRC //
  ReadCRC = (uint16_t)Response[RS_len-1] * 256 + (uint16_t)Response[RS_len-2];
  if (ModBus_CRC(Response, RS_len-2) == ReadCRC) {
    // Read the Value //
    unsigned long val = (uint16_t)Response[3] * 256 + (uint16_t)Response[4];
    return val * 1;       // S8 = 1. K-30 3% = 3, K-33 ICB = 10
  }
  else {
    Serial.print("CRC Error");
    return 99;
  }
}

// interruption pour lire le bouton d'étalonnage
bool demandeEtalonnage = false;
void IRAM_ATTR etalonnage() {
  if ( millis() - DEBOUNCE_TIME  >= DebounceTimer ) {
    DebounceTimer = millis();
    
    Serial.println("Etalonnage manuel !!");

    tft.fillScreen(TFT_BLACK);
    tft.setTextSize(3);
    tft.setTextColor(TFT_WHITE);
    tft.drawString("Etalonnage", tft.width() / 2, tft.height()/2);

    demandeEtalonnage = true;
  } 
}

// nettoie l'écran et affiche les infos utiles
void prepareEcran() {
  tft.fillScreen(TFT_BLACK);
  // texte co2 à gauche
  tft.setTextSize(4);
  tft.setTextColor(TFT_WHITE);
  tft.drawString("CO",25, 120);
  tft.setTextSize(3);
  tft.drawString("2",60, 125);

  // texte PPM à droite ppm
  tft.drawString("ppm",215, 120);

  // écriture du chiffre
  tft.setTextColor(TFT_GREEN,TFT_BLACK);
  tft.setTextSize(8);
}

void setup() {
  // bouton de calibration
  pinMode(BOUTON_CAL, INPUT);

  // ports série de debug et de communication capteur
  Serial.begin(115200);
  Serial1.begin(9600, SERIAL_8N1, RXD2, TXD2);

  // initialise l'écran
  tft.init();
  delay(20);
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  tft.setTextDatum(MC_DATUM); // imprime la string middle centre
  
  // vérifie l'état de l'ABC
  send_Request(ABCreq, 8);
  read_Response(7);
  Serial.print("Période ABC : ");
  Serial.printf("%02ld", get_Value(7));
  Serial.println();
  int abc = get_Value(7);

  // active ou désactive l'ABC au démarrage
  if(digitalRead(BOUTON_CAL) == LOW){
    if(abc == 0){
      send_Request(enableABC, 8);
    }else{
      send_Request(disableABC, 8);
    }
    read_Response(7);
    get_Value(7);
  }
  
  tft.setTextSize(2);
  tft.setTextColor(TFT_BLUE,TFT_BLACK);
  tft.drawString("Autocalibration", tft.width() / 2, 10);
  if( abc != 0 ){
    tft.drawString(String(abc)+"h", tft.width() / 2, 40);
  }else{
    tft.drawString("OFF", tft.width() / 2, 40);
  }

  // gestion du wifi
  wifiMulti.addAP(ssid1, password1);
  wifiMulti.addAP(ssid2, password2);

  Serial.print("Connexion au wifi");
  tft.setTextSize(2);
  tft.setTextColor(TFT_WHITE,TFT_BLACK);
  tft.drawString("Recherche wifi", tft.width() / 2, tft.height() / 2);
  
  int i = 0;
  while(wifiMulti.run() != WL_CONNECTED && i < 3){
    Serial.print(".");
    delay(500);
    i++;
  }
  if(wifiMulti.run() == WL_CONNECTED){
    tft.setTextColor(TFT_GREEN,TFT_BLACK);
    Serial.println("Connecté au wifi");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
    tft.drawString("Wifi OK", tft.width() / 2, 100);
    setClock();
  }else{
    tft.setTextColor(TFT_RED,TFT_BLACK);
    Serial.println("Echec de la connexion wifi");
    tft.drawString("Pas de wifi", tft.width() / 2, 100);
  }
  delay(3000); // laisse un temps pour lire les infos

  // préparation de l'écran
  prepareEcran();

  //interruption de lecture du bouton
  attachInterrupt(BOUTON_CAL, etalonnage, FALLING);
}  

unsigned long ancienCO2 = 0;
int seuil = 0;

void loop() {

  // effectue l'étalonnage si on a appuyé sur le bouton
  if( demandeEtalonnage ){
    demandeEtalonnage = false;
    // nettoye le registre de verification
    send_Request(clearHR1, 8); 
    read_Response(8); 
    delay(100);
    // demande la calibration
    send_Request(calReq, 8); 
    read_Response(8); 
    delay(4500); // attend selon le cycle de la lampe

    // lit le registre de verification
    send_Request(HR1req, 8); 
    read_Response(7); 
    int verif = get_Value(7);
    Serial.println("resultat calibration "+String(verif));
    if(verif == 32){
      tft.setTextColor(TFT_GREEN);
      tft.drawString("OK", tft.width() / 2, tft.height()/2+30);
    }else{
      tft.setTextColor(TFT_RED);
      tft.drawString("Erreur", tft.width() / 2, tft.height()/2+20);
    }
    delay(3000);
    prepareEcran();
    seuil = 0;
  }

  // lecture du capteur
  send_Request(CO2req, 8);
  read_Response(7);
  unsigned long CO2 = get_Value(7);
  
  String CO2s = "CO2: " + String(CO2);
  Serial.println(CO2s);

  // efface le chiffre du texte
  if(CO2 != ancienCO2){
    tft.fillRect(0,0, tft.width(), 60, TFT_BLACK);
  }

  if( CO2 < 800 ){
    tft.setTextColor(TFT_GREEN,TFT_BLACK);
    if( seuil != 1 ){
      tft.setTextSize(2);
      tft.fillRect(0,61, tft.width(), 25, TFT_BLACK);
      tft.drawString("Air Excellent", tft.width() / 2, tft.height() / 2 + 10);
    }
    seuil = 1;
  }else if( CO2 >= 800 && CO2 < 1000){
    tft.setTextColor(TFT_ORANGE,TFT_BLACK);
    if( seuil != 2 ){
      tft.setTextSize(2);
      tft.fillRect(0,61, tft.width(), 25, TFT_BLACK);
      tft.drawString("Air Moyen", tft.width() / 2, tft.height() / 2 + 10);
    }
    seuil = 2;
  }else if (CO2 >= 1000 && CO2 < 1500){
    tft.setTextColor(TFT_RED,TFT_BLACK);
    if( seuil != 3 ){
      tft.setTextSize(2);
      tft.fillRect(0,61, tft.width(), 25, TFT_BLACK);
      tft.drawString("Air Mediocre", tft.width() / 2, tft.height() / 2 + 10);
    }
    seuil = 3;
  }else{
    tft.setTextColor(TFT_RED,TFT_BLACK);
    if( seuil != 4 ){
      tft.setTextSize(2);
      tft.fillRect(0,61, tft.width(), 25, TFT_BLACK);
      tft.drawString("Air Vicie", tft.width() / 2, tft.height() / 2 + 10);
    }
    seuil = 4;
  }

  tft.setTextSize(8);
  tft.drawString(String(CO2), tft.width() / 2, tft.height() / 2 - 30);


  // envoi de la valeur sur le cloud
  if((millis() - lastTime) >= postingInterval) {
    if((wifiMulti.run() == WL_CONNECTED)) {
      WiFiClient client;
      ThingSpeak.begin( client );
      
      writeTSData( channelID , dataFieldOne , CO2 );  
      lastTime = millis();
    }
  }

  ancienCO2 = CO2;
  delay(10000); // attend 10 secondes avant la prochaine mesure
}

MQTT

Sur ThingSpeak, aller dans Devices > MQTT et compléter si besoin avec la lecture de la documentation MQTT Basics:

  • Créer un device,
  • Ajouter la/les channel(s) voulu(s),
  • Limiter les droits à la partie "subscribe" ; notre client en effet n'est pas prévu pour publier des données sur ThingSpeak,
  • Conserver précautionneusement vos identifiants MQTT.

Sur l'instance Warp 10, déployer le plugin MQTT :

Avec le script /path/to/warp10/mqtt/test.mc2 :

// subscribe to the topics, attach a WarpScript™ macro callback to each message
// the macro reads ThingSpeak message to extract the first byte of payload,
// the server timestamp, the channel id and the value

'Loading MQTT ThingSpeak Air Quality Warpscript™' STDOUT
{
  'host' 'mqtt3.thingspeak.com'
  'port' 1883
  'user' 'XXXXXXXXXX'
  'password' 'XXXXXXXXXX'
  'clientid' 'XXXXXXXXXX'
  'topics' [
    'channels/channelID 1/subscribe'
    'channels/channelID 2/subscribe'
    'channels/channelID 3/subscribe'
  ]
  'timeout' 20000
  'parallelism' 1
  'autoack' true

  'macro'
  <%
    //in case of timeout, the macro is called to flush buffers, if any, with NULL on the stack.
    'message' STORE
    <% $message ISNULL ! %>
    <%
      // message structure :
      // {elevation=null, latitude=null, created_at=2022-01-11T10:02:27Z, field1=412.00000, field7=null, field6=null, field8=null, field3=null, channel_id=1630275, entry_id=417, field2=null, field5=null, field4=null, longitude=null, status=null}
      $message MQTTPAYLOAD 'ascii' BYTES-> JSON-> 'TSmessage' STORE
      $TSmessage 'created_at' GET TOTIMESTAMP 'ts' STORE
      $TSmessage 'channel_id' GET 'channelId' STORE
      $TSmessage 'field1' GET 'sensorValue' STORE

      $message MQTTTOPIC ' ' +
      $ts ISO8601 + ' ' +
      $channelId TOSTRING + ' ' +
      $sensorValue +
      STDOUT // print to warp10.log
    %> IFT
  %>
}

Vous devriez avoir dans /path/to/warp10/log/warp10.log :

Loading MQTT ThingSpeak Air Quality Warpscript™
channels/<channelID 1>/subscribe 2022-01-11T10:30:51.000000Z <channelID 1> 820.00000
channels/<channelID 2>/subscribe 2022-01-11T10:30:53.000000Z <channelID 2> 715.00000
channels/<channelID 3>/subscribe 2022-01-11T10:30:54.000000Z <channelID 3> 410.00000

Maintenant que l'intégration MQTT est validée, supprimez ce fichier et passons à la gestion de la persistence des données dans Warp 10.

Avec le script suivant :

// subscribe to the topics, attach a WarpScript™ macro callback to each message
// the macro reads ThingSpeak message to extract the first byte of payload,
// the server timestamp, the channel id and the value.

{
  'host' 'mqtt3.thingspeak.com'
  'port' 1883
  'user' 'XXXXXXXXXX'
  'password' 'XXXXXXXXXX'
  'clientid' 'XXXXXXXXXX'
  'topics' [
    'channels/channelID 1/subscribe'
    'channels/channelID 2/subscribe'
    'channels/channelID 3/subscribe'
  ]
  'timeout' 20000
  'parallelism' 1
  'autoack' true

  'macro'
  <%
    //in case of timeout, the macro is called to flush buffers, if any, with NULL on the stack.
    'message' STORE
    <% $message ISNULL ! %>
    <%
      // message structure :
      // {elevation=null, latitude=null, created_at=2022-01-11T10:02:27Z, field1=412.00000, field7=null, field6=null, field8=null, field3=null, channel_id=1630275, entry_id=417, field2=null, field5=null, field4=null, longitude=null, status=null}
      $message MQTTPAYLOAD 'ascii' BYTES-> JSON-> 'TSmessage' STORE
      $TSmessage 'created_at' GET TOTIMESTAMP 'ts' STORE
      $TSmessage 'channel_id' GET 'channelId' STORE
      $TSmessage 'field1' GET 'sensorValue' STORE

      // Tableau de correspondance entre mes channel IDs et mes devices en vue de définir des labels pour les GTS
      {
        <channelID 1> 'air1'
        <channelID 2> 'air2'
        <channelID 3> 'air3'
      } 'deviceMap' STORE
      // Récupération du nom du device dans la variable senssorId
      $deviceMap $channelId GET 'sensorId' STORE

      // Création d'une GTS air.quality.home
      // Le label "device" aura pour valeur le nom du device, via la variable sensorId
      // On crée une entrée qui correspond à la valeur que nous venons de récupérer
      // sensorValue est une string, il faut la repasser sur un format numérique
      // Une fois la GTS reconstituée avec son entrée, on la periste en base via UPDATE
      '<writeToken>' 'writeToken' STORE
      NEWGTS 'air.quality.home' RENAME
      { 'device' $sensorId } RELABEL
      $ts NaN NaN NaN $sensorValue TODOUBLE TOLONG ADDVALUE
      $writeToken UPDATE
    %> IFT
  %>
}

Depuis le WarpStudio, vérifiez la disposnibilité de vos données :

'<readToken>' 'readToken'  STORE
[ $readToken 'air.quality.home' {} NOW -1000 ] FETCH

warp10 - validation de l'ingestion des données IoT

Ensuite, il nous reste plus qu'à faire une petite macro et un dashboard pour présenter les données.

Pour la macro :

  • On lui passe un nom de device en paramètre qui servira à filtrer sur le label
  • Elle retourne une GTS avec l'ensemble des valeurs disponibles
<%
  {
    'name' 'cerenit/iot/co2'
    'desc' 'Provide CO2 levels per device'
    'sig' [ [ [ [  'device:STRING' ] ]  [ 'result:GTS' ] ] ]
    'params' {
        'device' 'String'
        'result' 'GTS'
    }
    'examples' [
    <'
air1 @cerenit/iot/co2
    '>
    ]
  } INFO

  // Actual code
  SAVE 'context' STORE

  'device' STORE // Save parameter as year

  '<readToken>' 'readToken' STORE
  [ $readToken 'air.quality.home' { 'device' $device } MAXLONG MINLONG ] FETCH
  0 GET

  $context RESTORE
%>
'macro' STORE
$macro

Et pour le dashboard Discovery :

  • Chaque tile utilise la macro que l'on vient de réaliser en lui passant le device en paramètre,
  • Chaque tile affiche un système de seuils avec des couleurs associées.
<%
{
    'title' 'Home CO2 Analysis'
    'description' 'esp32 + Senseair S8 sensors at home'
    'options' {
      'scheme' 'CHARTANA'
    }
    'tiles' [
        {
            'title' 'Informations'
            'type' 'display'
            'w' 6 'h' 1 'x' 0 'y' 0
            'data' {
                'data' 'D&eacute;tails et informations compl&eacute;mentaires :  <a href="https://www.cerenit.fr/blog/air-quality-iot-esp32-senseair-thingspeak-mqtt-warp10-discovery/">IoT - Qualit&eacute; de l air avec un esp32 (TTGo T-Display), le service ThingSpeak, du MQTT, Warp 10 et Discovery</a>'
            }
        }
        {
            'title' 'Device AIR1'
            'type' 'line'
            'w' 6 'h' 2 'x' 0 'y' 1
            'macro' <% 'air1' @cerenit/macros/co2 %>
            'options' {
                'thresholds' [
                    { 'value' 400 'color' '#008000' }
                    { 'value' 600 'color' '#329932' }
                    { 'value' 800 'color' '#66b266' }
                    { 'value' 960 'color' '#ffdb99' }
                    { 'value' 1210 'color' '#ffa500' }
                    { 'value' 1760 'color' '#ff0000' }
	        ]
	    }
        }
        {
            'title' 'Device AIR2'
            'type' 'line'
            'w' 6 'h' 2 'x' 6 'y' 1
            'macro' <% 'air2' @cerenit/macros/co2 %>
            'options' {
                'thresholds' [
                    { 'value' 400 'color' '#008000' }
                    { 'value' 600 'color' '#329932' }
                    { 'value' 800 'color' '#66b266' }
                    { 'value' 960 'color' '#ffdb99' }
                    { 'value' 1210 'color' '#ffa500' }
                    { 'value' 1760 'color' '#ff0000' }
	        ]
	    }
        }
        {
            'title' 'Device AIR3'
            'type' 'line'
            'w' 6 'h' 2 'x' 0 'y' 3
            'macro' <% 'air3' @cerenit/macros/co2 %>
            'options' {
                'thresholds' [
                    { 'value' 400 'color' '#008000' }
                    { 'value' 600 'color' '#329932' }
                    { 'value' 800 'color' '#66b266' }
                    { 'value' 960 'color' '#ffdb99' }
                    { 'value' 1210 'color' '#ffa500' }
                    { 'value' 1760 'color' '#ff0000' }
                ]
           }
        }
    ]
}
{ 'url' 'https://w.ts.cerenit.fr/api/v0/exec'  }
@senx/discovery2/render
%>

Le résultat est alors :

warp10 - dashboard IoT CO2

Bilan de ce que nous avons vu :

  • Comment monter son capteur de CO2 en profitant des ressources mises à disposition par le projet "Nous Aérons",
  • Comment envoyer les données du capteur vers le service ThingSpeak,
  • Comment récupérer les données du service ThingSpeak via le protoole MQTT et les stocker dans Warp 10,
  • Comment créer un dashboard Discovery avec une macro permettant de récupérer les données et mettre en place un système de seuils.

L'ensemble des fichiers peuvent être récupérés depuis cerenit/iot-air-quality.

Ma comptabilité, une série temporelle comme les autres - partie 6 - Les FEC et le compte de résultat

warp10timeseriescomptabilitérésultatfecdashboarddiscovery

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 :

  • Initialement, j'avais : <société>.<bilan ou resultat>.<classe de compte>.<type d'opération: credit ou debit>
  • Cela a évolué vers : <société>.<bilan ou resultat>.<classe de compte> ; le type d'opération est maintenant un label

Pour 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

Compte de résultat

"<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 :

warp10 - compte de résultat

Du précédent billet et ce celui-ci, nous avons donc :

  • Un compte de résultat annuel
  • Un compte de trésorerie annuel

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.

Macros

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.

Dashboards

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&eacute;sultat de la s&eacute;rie <a href="https://www.cerenit.fr/blog/premiers-pas-avec-warp10-comptabilite-et-previsions/">Ma comptabilit&eacute;, une s&eacute;rie temporelle comme les autres</a> et de l&apos;ingestion des Fichiers d&apos;&eacute;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 :

warp10 - dashboard

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 :

  • Les deux premiers tiles afficheront le solde de trésorerie et le compte de résultat de l'année sélectionnée
  • Le dernier tile affichera la trésorerie depuis début 2017 jusqu'à la fin d'année sélectionnée. Donc au minimum 2017 et au maximum 2017 > 2020.

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 :

  • l'ingestion de données,
  • la manipulation et l'analyse des données,
  • la mise en place de dashboards,
  • la projection de données avec les algorythmes de machine learning.

Si vous souhaitez poursuivre l'aventure et le sujet, n'hésitez pas à me contacter.

Web, Ops, Data et Time Series - Avril 2021

falcosysdigsécuritégrafanadashboardraspberrypipicodockerdocker-composegrafanahashicorpvaultvectorcontainerdgitgit-filter-repokubernetespspgitlab-cipodmanwarp10sqliteterraformtimescalevelerodockerdocker-composegrafanalokitempokubernetesminioinfluxdatanotebookgeospatialagplbme680co2

Code

Conteneur et orchestration

  • Electro Monkeys - Docker Compose avec Nicolas de Loof : Retour sur la Developper Experience autour de Docker, l'historique et le futur de docker-compose, la création de la spécification Compose, les intégrations AWS/ECS et Azure/ACI, l'intégration Kubernetes, etc.
  • nerdctl: Docker-compatible CLI for contaiNERD : une CLI qui imite la CLI Docker mais en interagissant directement avec containerd. Elle permet aussi de bénéficier de certaines fonctionnalités de containerd qui ne sont pas prévues pour tout de suite dans Docker apparemment.
  • Blog: Kubernetes 1.21: Power to the Community : au programme de cette nouvelle version : Cronjobs GA, Immutable Secrets and ConfigMaps GA, IPv4/IPv6 dual-stack support, Graceful Node Shutdown, PersistentVolume Health Monitor mais aussi PodSecurityPolicy Deprecation et TopologyKeys Deprecation
  • PodSecurityPolicy Deprecation: Past, Present, and Future: article plus détaillé sur la dépréciation des PSP.
  • Podman v3.1.0 Released : ajout de la gestion des secrets, améliorations des commandes kube avec notamment la génération des PersistentVolumeClaim ou encore la gestion des propriétaires des volumes.
  • Velero 1.6.0 : améliorations diverses comme le support des identifiants par buckets (et non globaux uniquement), mise à jour de restic vers 0.12.0, etc.
  • Compose CLI Tech Preview : compose devrait devenir une sous-commande officiel de la CLI Docker ; on pourra alors faire docker compose up -d
  • Docker 20.10.6 : version de maintenance avec le support des puces Apple Silicon M1.
  • Kubernetes : vers 3 releases par an au lieu de 4 : de quoi courrir un peu moins derrière les versions et à relier avec le support de chaque version étendue à 1 an depuis la 1.19.

Data

  • sq: swiss-army knife for data : le jq pour les données relationelles. Du SQL ou des fichiers Excel/CSV/JOSN/XML en entrée et les mêmes formats en sortie (et un peu plus).
  • SQLite is not a toy database : On a souvent une fausse image de sqlite - l’article permet de se mettre à jour...

IaC

IoT

  • Pico 2 Pi Adapter Board : un petit adapteur sympathique pour Raspeberry Pi Pico et vous permettre de brancher facilement vos composants sans soudure et mener ainsi vos expériences.
  • Piper Make : Pour programmer facilement votre Raspberry Pi Pico en MicroPython mais avec une logique de blocs à la Scratch.
  • Utilisation des BME680 et RV3028 avec Raspberry Pi Pico : le composant BME680 permet d'évaluer la qualité de l'air - le projet permet donc de capturer et d'afficher cette information avec un Raspberry Pi. Son successeur, le BME688 dispose d'une pincée d'IA.
  • Projet CO2 et Makers CO2 : pour mieux comprendre les enjeux autour de l'aération des pièces et comment faire vos capteurs.

Observabilité & Monitoring

Réseau

  • The Mystery of AS8003 : Une entité inconnue jusque là mais liée à l'administration américaine a annoncé la gestion d'une très grande plage réseau. Les implications et les motivations sont encore à éclaircir. Le billet émet différents hypothèses. Le thread twitter associé est intéressant aussi.

Sécurité

Time Series

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

warp10timeseriescomptabilitédiscoverydashboard

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

Web, Ops & Data - Décembre 2020

podmandockerruncrootlessswarmcgroupskubernetescridockershimvectorawstimestreamwarp10dashboardptsmtimescalecentosrhelpodmaninfluxdbtimeseries

Containers et orchestration

  • Electro Monkeys #37 – Podman, l’alternative de Redhat à Docker avec Benjamin Vouillaume : je me demandais si podman permettait un management de containers à distance et l'étendue de son écosystème. Le podcast permet de compléter le tour du propriétaire et de se faire une bonne idée de son positionnement.
  • How to deploy on remote Docker hosts with docker-compose : dans la même quête, je me demandais s'il était possible de piloter un noeud docker à distance quand je me suis rappelé qu'il était possible de le faire au travers d'une connexion ssh. En creusant un peu plus, j'ai découvert la notion de contexte qui permet ainsi de cibler un noeud docker, docker swarm ou kubernetes.
  • Don't Panic: Kubernetes and Docker et Dockershim Deprecation FAQ : A partir de kubernetes 1.20 (fin 2020) et définitivement à partir de la version 1.23 (fin 2021), le retrait du binaire docker comme CRI de Kubernetes est annoncé. Cela ne devrait pas changer grand chose et c'est essentiellement de la plomberie interne. Plutôt que de passer par Dockershim qui implémentait l'interface CRI et qui discutait ensuite avec Docker pour lancer les conteneurs via containerd, l'appel sera directement fait à containerd. Il n'y a que ceux qui montent la socket docker dans les pods qui vont avoir un souci. Si c'est pour builder des images, il y a des alternatives comme img, kaniko, etc. Pour les autres cas, il faudra peut être passer par l'API kubernetes ou trouver les alternatives qui vont bien.
  • What developers need to know about Docker, Docker Engine, and Kubernetes v1.20 et Mirantis to take over support of Kubernetes dockershim : Mirantis et Docker Inc vont assurer le support de cette interface dockershim pour permettre à ceux qui ont en besoin de pouvoir continuer à l'utliiser. La limite étant que si vous êtes sur du service managé et que votre provider ne le fournit pas, vous ne pourrez pas l'utiliser...
  • Kubernetes 1.20: The Raddest Release : voilà, la version 1.20 est sortie et apporte son lot de nouveautés et de stabilisation.
  • Announcing General Availability of HashiCorp Nomad 1.0 : Nomad 1.0 est également disponible.
  • Introducing Docker Engine 20.10 - Docker Blog : Docker 20.10 arrive avec des profondes nouveautés comme le support des cgroupsv2 et un mode rootless, docker logs fonctionne avec tous les drivers de log et non unqiement json & journald et plein d'autres améliorations/harmonisations au niveau de la CLI. Pour ceux sous Fedora qui avaient bidouillé avec firewalld précédemment pour faire fonctionner docker et qui ont un problème lié à l'interface docker0 au démarrage du service docker, allez voir par ici.
  • Docker Engine Release Notes - Version 20.10 : En plus des points précédents, il y a l'arrivée des jobs dans swarm - depuis le temps que je l'attendais 🤩 (même si on peut se toujours se poser la question de la pérénnité de swarm depuis qu'il a été racheté par Mirantis)
  • New features in Docker 20.10 (Yes, it’s alive) : un billet décrivant plus en détail certaines feautres de docker 20.10 comme support Fedora/CentOS, le rootless mode, l'option -mount, les jpbs swarm et une synthèse de l'actualité de l'écosystème docker.
  • Podman Release 2.2.0 : ajout des commandes network (dis)connect, support des alias avec des noms courts, amélioration des commandes play|generate kube et capacité de monter une image OCI dans un container.

Observabilité

  • Vector - Collect, transform, & route all observability data with one simple tool. (via) : vector est un outil en rust qui permet de collecter et manipuler des métriques/logs/événements et de les envoyer vers différentes destinations. De quoi remplacer filebeat/journalbeat voire même telegraf ? ;-)
  • Our new partnership with AWS gives Grafana users more options : AWS vient d'annoncer un service managé pour Prometheus basé sur Cortex et pour Grafana (version Entrepsise). Grafana et Cortex étant des projets édités par Grafana Labs sous licence OSS (et des déclinaisons Cloud et Entreprise). AWS changerait-il sa façon de travailler avec les projets OSS lorsqu'il souhaite en faire des services managés et prendre ainsi une attitude plus positive vis à vis de la communauté OSS ?

OSS

  • Death of an Open Source Business Model : Analyse du passage de Mapbox GL JS v2 sous licence propriétaire, le modèle de l'open core et les menaces des top cloud providers sur le reste de l'économie du logiciel. On peut étendre cela aussi "VC funded OSS company".

Système

  • CentOS Project shifts focus to CentOS Stream, CentOS Stream: Building an innovative future for enterprise Linux et la FAQ associée : Historiquement CentOS Linux était batie sur les sources de Red Hat Entreprise Linux un fois celle-ci disponible. CentOS Stream renverse un peu la tendance avec un cycle d'intégration plutôt Fedora -> CentOS Stream -> RHEL. L'initiative CentOS Stream avait été annoncée en septembre 2019 et ce changement permet donc aux équipes CentOS de se focoaliser sur une seule version (CentOS Stream) et non plus deux versions (CentOS Stream et CentOS Linux). CentOS Linux 7 sera maintenue jusqu'à la fin du support de RHEL 7 et et CentOS 8 jusqu'à fin 2021 (et non pas 2029 comme prévu). Il n'y aura pas de version 9 de CentOS Linux. A tester pour voir si CentOS Stream est plus stable que Fedora mais moins conservateur que RHEL et pourrait alors s'avérer être un bon compromis.
  • Before You Get Mad About The CentOS Stream Change, Think About… : un billet assez long d'un employé de Red Hat qui exprime son opinion et cherche à remettre les choses en perspective en dépassionnant le débat.

Time Series

  • PTSM Edition #8 - Amazon TimeStream 101 : Edition du Paris time Series Meetup sur AWS Timestream
  • TimescaleDB vs. Amazon Timestream: 6000x higher inserts, 5-175x faster queries, 150x-220x cheaper : avec toutes les réserves habitudelles sur les benchmarks, Timescale a comparé son produit avec AWS Timestream et la conclusion semble clairement en faveur de Timescaledb. A noter que l'update des données est arrivé dans AWS Timestream entre temps.
  • Truly Dynamic Dashboards as Code : les équipes de SenX ont implémenté leur vision du "Dashboards as Code" sous le nom Discovery. Discovery veut aller plus loin que la simple description d'un dashboard comme on peut le voir dans Grafana ou InfluxDB 2.0 en apportant une touche de dynamisme et de génération dynamique des dashboards en fonction des élements obtenus (ex si valeur X > Y alors afficher la procédure AAA de résolution d'incident). J'ai commencé à jouer avec, un billet de blog sur ce sujet devrait bientôt arriver.
  • NeuralProphet : un modèle neuronal orienté série temporelles, inspiré de Facebook Prophet et développé avce PyTorch.
  • Release Notes InfluxDB 2.0.3 et Release Announcement: InfluxDB OSS 2.0.3 : build arm64 en preview, un petit conflit de packaging entre influxdb et influxdb2 à passer pour ceux qui étaient déjà en 2.0 et ceci afin d'éviter que des gens en 1.x passent involontairement en 2.x, le "delete with predicate" a été réactivé, améliorations sur le process d'upgrade, des commandes autour des actions en mode V1, mise à jour de flux, et plein d'autres corrections/améliorations.

Web

  • Web Almanac 2020 - Rapport annuel de HTTP Archive sur l’état du Web : une synthèse de l'état du web d'après HTTP Archive sur une base de 7.5 millions de sites testés, soit 31.3 To de données traitées. De quoi relativisez un peu les biais de notre bulle technologique : non tout le monde ne fait pas du React/Angular/Vue.js par ex mais plutôt du JQuery ! Suivant vos usages, plein d'enseignements et de choses intéressantes à tirer de cette synthèse.

Il ne me reste plus qu'à vous souhaiter de bonnes fêtes de fin d'année et on se retrouve l'année prochaine !

Web, Ops & Data - Septembre 2020

podmantimezonegrafanadashboardterraformsécuritéterrascanterracostnvidiaarmcnicsinetworkstorageciliumcalicolonghornportworxopenebsrancherpythongkewarp10influxdbdata-engineerdate-scientistsql

Cloud

  • terrascan : terrascan va scanner vos fichiers terraform et les valider contre 500+ règles de sécurité (au format Open Policy Agent) afin d'identifier les éventuels problèmes de sécurité. L'outil supporte AWS, GCP et Azure.
  • infracost : estimez le coût de vos projets terraform à l'heure ou au mois. Il est même possible de faire apparaitre les évolutions de vos coûts d'infra lors d'une MR/PR. A défaut d'être forcément précis, cela pourra au moins donner une idée et permettra peut être de sensibiliser les développeurs et/ou les clients aux évolutions de couts de leurs projets.

Code

  • All Python versions before 3.6 are now totally unsupported : Python 2 n'est plus supporté depuis le début de l'année - c'est au tour de Python 3.5 de ne plus l'être depuis le 13 sept. Pour Python 3.6, ce sera décembre 2021.
  • nackjicholson/aiosql : juste milieu (?) entre du SQL brut et un ORM, aiosql semble permettre d'associer une requête SQL à une fonction pour une manipulation assez simple ensuite dans le code par la suite.

Container et orchestration

(Big) data

  • #19. Lucien Fregosi - Hugo Larcher - Erika Gelinard - Dessine moi un data engineer : Pour cette saison 2 de DataBuzzWord, des réflexions intéressantes autour du Data Engineer / Data Scientists, le Data Engineer qui fait du Build/Run, les pipelines & job as a service et de l'importance de simplifier / déporter le run pour que le Data Engineer et a fortiori le Data Scientist se concentrent sur leurs pipelines ou leur exploitation et gérer moins d'infrastructure.

Hardware

Time Series

  • InfluxDB OSS 2.0 General Availability Roadmap : un bon résumé sur les avancées d'Influx 2.0 OSS et la transition 1.x vers 2.x ; Début septembre, j'étais sceptique quand même avec le retour du stockage et du requêtage da la V1 dans la branche v2 (cf la PR "Port TSM1 storage engine") et ce à un mois de la date de release prévue annoncés aux Influxdays de Londres (ie fin septembre). Au final, la version 2.0 OSS et Entreprise auront les feautres "frontend" de la V2 (Tasks, Dashobards, etc) mais uniquement le moteur de stockage de la V1. Si je comprends le besoin pour ne pas perdre leurs clients dans la migration, c'est un écart de plus entre les version OSS/Entreprise et la version Cloud. Les couches hautes (API, UI, fonctionnalités type Task/Dashboards/...) seront commmunes mais sous le capot (stockage, ingestion), cela diffère. On peut raisonnablement se demander si c'est une phase intermédiaire avant une migration ultérieure sur le moteur de stockage de la 2.0 quand InfluxData aura plus de recul sur le sujet ou bien si les projets Cloud et OSS/Entreprise ne vont pas diverger significativement à moyen terme. Ceux qui ont commencé à alimenter leur base InfluxDB 2.0 sur la base des versions beta devront repartir de zéro du fait de cette incompatibilité de version de moteur de stockage.
  • Popular community plugins that can improve your Grafana dashboards : une collection de plugins Grafana pour améliorer vos dashboards.
  • September 2020: Warp 10 release 2.7.0, ready for FLoWS : la version 2.7 de Warp 10 est disponible et est la première version qui va supporter FLoWS, la syntaxe fonctionnelle alternative à WarpScript. Pour en savoir plus sur FLoWS, je vous renvoie à l'édition 5 du Paris Time Series Meetup avec la présentation de FLoWS. D'autres améliorations font partie de cette release, tant d'un point de vue fonctionnalités que performances.

Premiers pas avec Warp 10 : comptabilité et prévisions de fin d'année

warp10timeseriesforecastdashboardwarpstudioarima

Suite de notre épopée :

Cherchant à me familiariser avec la base de données orientée série temporelles Warp 10 d'une part et à améliorer mes tableaux de bord comptables pour me faire des projections à fin d'année (parce que bon, faire juste la moyenne des mois précédents comme valeur pour les mois à venir, c'est un peu trop facile), je me suis dit que c'était un exercice qui pouvait répondre aux deux besoins après avoir lu Time series forecasts in WarpScript.

Pour ceux qui ne connaissent pas encore Warp 10 , c'est une solution de geo-timeseries (séries spatio temporelles) open source, éditée par SenX, société française basée à Brest. Pour en savoir plus sur Warp 10 , vous pouvez regarder l'éditions 1 et l'édition 5 du Paris Time Series Meetup.

Pour prendre en main Warp 10 et appréhender le langage de programmation Warpscript, je vous invite à suivre le tutoriel sur les cyclones en utilisant la Sandbox Warp10 mise à disposition par Senx.

Le jeu de données

Pour le jeu de données, j'ai donc récupéré de mes tableaux de bords mon chiffre d'affaires et mes dépenses mensuels sur la période Janvier 2017 à Mai 2020.

Nous allons donc créer 2 séries (appelées aussi GTS)

Soit crnt-revenue.gts:

# 2017
1483225200000000// revenue{company=cerenit} 0
1485903600000000// revenue{company=cerenit} 13800
1488322800000000// revenue{company=cerenit} 11325
1490997600000000// revenue{company=cerenit} 300
1493589600000000// revenue{company=cerenit} 6825
1496268000000000// revenue{company=cerenit} 8450
1498860000000000// revenue{company=cerenit} 5425
1501538400000000// revenue{company=cerenit} 10650
1504216800000000// revenue{company=cerenit} 13650
1506808800000000// revenue{company=cerenit} 0
1509490800000000// revenue{company=cerenit} 11200
1512082800000000// revenue{company=cerenit} 19225
# 2018
1514761200000000// revenue{company=cerenit} 8300
1517439600000000// revenue{company=cerenit} 8850
1519858800000000// revenue{company=cerenit} 10285
1522533600000000// revenue{company=cerenit} 8850
1525125600000000// revenue{company=cerenit} 8850
1527804000000000// revenue{company=cerenit} 9450
1530396000000000// revenue{company=cerenit} 12000
1533074400000000// revenue{company=cerenit} 11250
1535752800000000// revenue{company=cerenit} 15013
1538344800000000// revenue{company=cerenit} 15750
1541026800000000// revenue{company=cerenit} 13750
1543618800000000// revenue{company=cerenit} 10125
# 2019
1546297200000000// revenue{company=cerenit} 15375
1548975600000000// revenue{company=cerenit} 14750
1551394800000000// revenue{company=cerenit} 11600
1554069600000000// revenue{company=cerenit} 20622
1556661600000000// revenue{company=cerenit} 6376
1559340000000000// revenue{company=cerenit} 13350
1561932000000000// revenue{company=cerenit} 11250
1564610400000000// revenue{company=cerenit} 7050
1567288800000000// revenue{company=cerenit} 14750
1569880800000000// revenue{company=cerenit} 12326
1572562800000000// revenue{company=cerenit} 12513
1575154800000000// revenue{company=cerenit} 9082
# 2020
1577833200000000// revenue{company=cerenit} 13000
1580511600000000// revenue{company=cerenit} 12375
1583017200000000// revenue{company=cerenit} 15500
1585692000000000// revenue{company=cerenit} 5525
1588284000000000// revenue{company=cerenit} 15750

et crnt-expenses.gts:

# 2017
1483225200000000// expense{company=cerenit} 219
1485903600000000// expense{company=cerenit} 5471
1488322800000000// expense{company=cerenit} 7441
1490997600000000// expense{company=cerenit} 6217
1493589600000000// expense{company=cerenit} 5676
1496268000000000// expense{company=cerenit} 5719
1498860000000000// expense{company=cerenit} 5617
1501538400000000// expense{company=cerenit} 5690
1504216800000000// expense{company=cerenit} 5831
1506808800000000// expense{company=cerenit} 9015
1509490800000000// expense{company=cerenit} 8903
1512082800000000// expense{company=cerenit} 11181
# 2018
1514761200000000// expense{company=cerenit} 9352
1517439600000000// expense{company=cerenit} 9297
1519858800000000// expense{company=cerenit} 8506
1522533600000000// expense{company=cerenit} 8677
1525125600000000// expense{company=cerenit} 10136
1527804000000000// expense{company=cerenit} 10949
1530396000000000// expense{company=cerenit} 8971
1533074400000000// expense{company=cerenit} 9062
1535752800000000// expense{company=cerenit} 9910
1538344800000000// expense{company=cerenit} 10190
1541026800000000// expense{company=cerenit} 10913
1543618800000000// expense{company=cerenit} 13569
# 2019
1546297200000000// expense{company=cerenit} 11553
1548975600000000// expense{company=cerenit} 11401
1551394800000000// expense{company=cerenit} 10072
1554069600000000// expense{company=cerenit} 10904
1556661600000000// expense{company=cerenit} 9983
1559340000000000// expense{company=cerenit} 11541
1561932000000000// expense{company=cerenit} 11065
1564610400000000// expense{company=cerenit} 10359
1567288800000000// expense{company=cerenit} 10450
1569880800000000// expense{company=cerenit} 9893
1572562800000000// expense{company=cerenit} 10014
1575154800000000// expense{company=cerenit} 15354
# 2020
1577833200000000// expense{company=cerenit} 9673
1580511600000000// expense{company=cerenit} 9933
1583017200000000// expense{company=cerenit} 9815
1585692000000000// expense{company=cerenit} 9400
1588284000000000// expense{company=cerenit} 9381

Pour chaque fichier:

  • le premier champ est un timestamp correspondant au 1er jour de chaque mois à 00h00.
  • la partie // indique qu'il n'y a pas de position spatiale (longitude, lattitude, élévation)
  • expense et revenue sont les noms des classes qui vont stocker mes informations
  • company est un label que je positionne sur mes données avec le nom de mon entreprise
  • le dernier champ est la valeur de mon chiffre d'affaires ou de mes dépenses mensuels.

Pour plus d'information sur la modélisation, cf GTS Input Format.

Insertion des données

Lorsque vous utilisez la Sandbox, 3 tokens vous sont donnés :

  • un token pour lire les données ; j'y ferai référence via <readToken> par la suite
  • un token pour écrire les données ; j'y ferai référence via <writeToken> par la suite
  • un token pour supprimer les données ; j'y ferai référence via <deleteToken> par la suite
#!/usr/bin/env bash

for file in crnt-expenses crnt-revenue ; do
	curl -v -H 'Transfer-Encoding: chunked' -H 'X-Warp10-Token: <writeToken>' -T ${file}.gts 'https://sandbox.senx.io/api/v0/update'
done

Premières requêtes

Pour ce faire, nous allons utiliser le Warp Studio ; pour la datasource, il conviendra de veiller à ce que la SenX Sandbox soit bien sélectionnée.

L'équivalent de "SELECT * FROM *" peut se faire de la façon suivante :

# Authentification auprès de l'instance en lecture
'<readToken> 'readToken' STORE
# FETCH permet de récupérer une liste de GTS, ici on demande toutes les classes via ~.* et tous les labels en prenanr les 1000 dernières valeurs ; on récupère donc toutes les séries.
[ $readToken '~.*' {} NOW -1000 ] FETCH

Si vous cliquez sur l'onglet "Dataviz", vous avez alors immédiatement une représentation graphique de vos points.

warp10 - fetch

Maintenant que nos données sont bien présentes, on va vouloir aller un peu plus loin dans nos manipulations.

Premières manipulations

Ce que nous voulons faire :

  • Sélectionner chaque série et la stocker dans une variable,
  • Calculer le résultat mensuel et le persister dans une troisième série temporelle
  • Afficher les trois séries.

Pour sélectionner chaque série et la stocker dans une variable:

# Authentification auprès de l'instance en lecture
'<readToken>' 'readToken' STORE

# FETCH : permet de récupérer une liste de série, ici on filtre sur la classe expense, sur le label company = cerenit et sur les dates du 01/12/2016 au 01/06/2020.
# 0 GET : on sait que l'on a qu'une seule série qui correspond à la requête. Donc on ne retient que le 1er élément pour passer d'une liste de GTS à une seule et unique GTS.
# STORE : stocke le résultat dans une variable exp.
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE

# Idem pour la classe revenue, stockée dans une variable revenue.
[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE

# Affiche les 2 séries
$exp
$revenue

A ce stade, vous avez la même représentation graphique que précédemment si vous cliquez sur Dataviz.

warp10 - fetch

Calculons maintenant le résultat mensuel (chiffre d'affaires - dépenses) :

# Authentification auprès de l'instance en lecture
'<readToken>' 'readToken' STORE

# Le même bloc que précédemment
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE

[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE

# Calcul: il suffit se soustraire les deux éléments pour avoir le résultat
$revenue $exp -
# on affiche également les deux autres variables pour la dataviz
$exp
$revenue

warp10 - result

A ce stade :

  • Au niveau de la dataviz, la légende ne fournit aucune information sur la nature de la série
  • Cette donnée n'est pas encore persistée

Jusqu'à présent, nous avons utilisé que le <readToken> pour lire les données. Pour la persistence, nous allons utilier le <writeToken>.

# Authentification auprès de l'instance en lecture
'<readToken>' 'readToken' STORE
# Authentification auprès de l'instance en écriture
'<writeToken>' 'writeToken' STORE

[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE

[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE

# La première ligne est inchangée, elle calcule le résultat mensuel et la donnée est de type GTS
# Du coup, comme nous sommes dans une pile et que l'on hérite de ce qu'il s'est passé avant, on peut lui assigner un nom via RENAME
# Puis lui ajouter le label company avec pour valeur cerenit
# Et utiliser la fonction UPDATE pour stocker en base la GTS ainsi obtenue.
$revenue $exp -
"result" RENAME
{ "company" "cerenit" } RELABEL
$writeToken UPDATE

# Comme pour revenue et expense, on récupère les données sous la forme d'une GTS que l'on stocke dans une variable
[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'result' STORE

# On vide la pile
CLEAR
# On affiche les variables créées
$revenue
$exp
$result

warp10 - result store

Et voilà !

Prévoyons le futur

Warp10 dispose d'une extension propriétaire et payante permettant d'appliquer des algorithmes de prévisions sur des séries temporelles : warp10-ext-forecasting. Il est possible d'utiliser cette extension sur la Sandbox Warp10 mise à disposition par SenX.

Il existe une fonction AUTO et SAUTO (version saisonnière) qui applique automatiquement des algorythmes d'AutoML sur vos données.

# Authentification auprès de l'instance en lecture
'<readToken>' 'readToken' STORE

# Récupération des trois séries
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE

[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE

[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'result' STORE

CLEAR
$revenue
$exp
$result
# MAP: la fonction `AUTO` s'attend à manipuler des nombres au format `DOUBLE` et non des entiers. Il faut donc faire la conversion.
# FORECAST: sur les données obtenues du MAP, on applique la fonction AUTO et on demande les 8 prochaines occurents (pour aller jusqu'à la fin d'année)
# Le .ADDVALUES permet de "fusionner" les prévisions avec la série parente (sans les persister en base à ce stade)
# Commes les 3 projections sont disponibles dans la pile, elles sont également affichées
[ $result mapper.todouble 0 0 0 ] MAP
AUTO 8 FORECAST.ADDVALUES
[ $revenue mapper.todouble 0 0 0 ] MAP
AUTO 8 FORECAST.ADDVALUES
[ $exp mapper.todouble 0 0 0 ] MAP
AUTO 8 FORECAST.ADDVALUES

warp10 - forecast auto

Du fait du FORECAST.ADDVALUES, on pourrait se passer d'afficher les trois premières séries. Mais vistuellement, cela permet de voir la différence entre la série originale et la projection.

Une fois l'effet Whaou passé, on peut se demander quel modèle a été appliqué. Pour cela il y a la fonction MODELINFO

# Authentification auprès de l'instance en lecture
'<readToken>' 'readToken' STORE

# Récupération des trois séries
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE

[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE

[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'result' STORE

CLEAR
[ $result mapper.todouble 0 0 0 ] MAP
AUTO MODELINFO
[ $revenue mapper.todouble 0 0 0 ] MAP
AUTO MODELINFO
[ $exp mapper.todouble 0 0 0 ] MAP
AUTO MODELINFO

Dans l'onglet des résultats, on voit l'information: "model": "ARIMA".

warp10 - forecast modelinfo

Si on veut alors faire la même chose en utilisant le modèle ARIMA :

# Authentification auprès de l'instance en lecture
'<readToken>' 'readToken' STORE

# Récupération des trois séries
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE

[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE

[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'result' STORE

CLEAR

# SEARCH.ARIMA: applique un modèle ARIMA (ARMA ou ARIMA) sur la GTS passée en paramètre
# FORECAST.ADDVALUES: fait une précision sur les 7 prochaines occurences et les fusionne avec la série sur laquelle la projection est faite.
# Les 3 projections restant dans la pile, elles sont affichées
[ $revenue mapper.todouble 0 0 0 ] MAP
SEARCH.ARIMA
7 FORECAST.ADDVALUES

[ $exp mapper.todouble 0 0 0 ] MAP
SEARCH.ARIMA
7 FORECAST.ADDVALUES

[ $result mapper.todouble 0 0 0 ] MAP
SEARCH.ARIMA
7 FORECAST.ADDVALUES

warp10 - forecast arima

Et nous obtenons bien le même résultat.

On peut se poser alors la question de voir si la projection sur le résultat est la même que la soustraction entre la projection de chiffres d'affaires et de dépenses et mesurer l'éventuel écart.

# Authentification auprès de l'instance en lecture
'<readToken>' 'readToken' STORE

[[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE

[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE

[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'result' STORE

CLEAR
# La différence sur les 3 projections est que l'on stocke chaque résultat dans une variable
[ $result mapper.todouble 0 0 0 ] MAP
AUTO 8 FORECAST.ADDVALUES
'fresult' STORE

[ $revenue mapper.todouble 0 0 0 ] MAP
AUTO 8 FORECAST.ADDVALUES
'frevenue' STORE

[ $exp mapper.todouble 0 0 0 ] MAP
AUTO 8 FORECAST.ADDVALUES
'fexp' STORE

$frevenue
$fexp
$fresult
# ici on calcule la projection du résultat sur la base des projections de chiffres d'affaires et de dépenses
$frevenue $fexp -

On constate bien un écart entre la courbe orange (la soustraction des projections) et la courbe bleu (la projection du résultat).

warp10 - forecast arima

Nous voilà à la fin de ce billet, j'espère que ce tour du propriétaire vous aura permis d'apprécier Warp10 et ses capacités.

Il ne reste plus qu'à voir en fin d'année dans quelles mesures ces projections seront valides ou pas !

Mon bilan sur Warp10 à ce stade :

  • Il faut absolument suivre le tutoriel sur les cyclones, cela permet de se mettre progressivement à Warpscript et de comprendre les mécanismes de fonctionnement du langage et de la pile (stack).
  • la notation de Warpscript est particulière au début mais on s'y fait petit à petit et sinon FLoWS devrait lever les dernières réticences.
  • Comme on a une pile, il ne faut pas hésiter à utiliser STOP et TYPOEOF notamment pour savoir ce que l'on manipule comme donnée à un instant T ; cf Debugging WarpScript
  • Le WarpStudio ou l'extension VSCode permettent d'avoir un contexte de développement agréable avec l'autocomplétion, la documentation des fonctions, etc.
  • Contrairement à InfluxDB où tout s'articule autour du measurement (équivalent de la classe ici), le fait d'avoir un langage de manipulation (et pas principalement de requêtage avec quelques transformations) de données permet d'avoir une modélisation plus souple et de réconcilier les données ensuite. Le même exercice dans InfluxDB supposerait d'avoir un seul measurement et d'avoir les différentes valeurs en son sein. Du coup, cela empêcherait le calcul et le stockage du résultat par ex. Il aurait fallu le calculer au préalable et l'insérer dans la base en même temps que les données de chiffre d'affaires et de dépenses pour que les données soient ensemble. Certes Flux et InfluxDB 2.0 vont lever quelques contraintes de modélisation et de manipulation de données, mais le prisme de la modélisation autour du measurement reste primordial dans le produit.
  • Avoir un langage de manipulation de données et non uniquement de requêtage et quelques transformation évite aussi de devoir sortir les données pour les analyser puis les renvoyer vers la base de données le cas échéant. Non seulement on reste au plus près de la donnée (éxécution coté serveur) mais on évite aussi les problématiques de drivers, conversion dans les structures de données du langage cible, etc.
  • Les classes de warpscript sont certes mono valeurs (même si le multivalue existe) mais je présume que cela sert surtout pour des données de même nature plutôt que pour des données hétérogènes. En effet, il s'agit d'une liste et non d'un tableau de données. (NDLR: Mathias me précise qu'une multivalue est une GTS et non une simple liste - cela est précisé si on lit bien la totalité de la doc de multivalue)

Une expérience au final positive qui pousse à aller creuser plus loin les fonctionnalités de cette plateforme. Ce sera l'opportunité de rédiger d'autres billets à l'avenir.

Web, Ops & Data - Décembre 2019

influxdbdockerkubernetestraefikgrafanadashboardcassandrareaperwarp10timeseriestimescaledbhelmmachine-learning

Rendez-vous le 21 janvier prochain à la troisième édition du Paris Time Series Meetup consacré à TSL (billet introductif à TSL : TSL: a developer-friendly Time Series query language for all our metrics) et le module RedisTimeSeries qui apporte des fonctionnalités et des structures Time Seriies à Redis. Le meetup était prévu initialement le mardi 17 décembre mais a été reporté du fait des grèves.

Container et orchestration

  • DockerSlim : le projet vise à réduire la taille de vos images et à améliorer leur sécurité en procédant à différentes optimisations. Cela peut être intéressant dans une stratégie d'améliorations de vos images docker mais à tester néanmoins. Les exemples données partent d'Ubuntu 14.04 dont l'image fait 60 / 65 Mo alors que l'image Ubuntu 16.04 fait moitié moins et Alpine fait 30 fois moins. Donc certains gains semblent faciles à obtenir, à creuser plus en détail.
  • Kubernetes 1.17: Stability : après une version 1.16 marquée notamment par la dépréciation de certaines APIs, cette version se veut plus une consolidation autour des "Cloud Provider Labels" qui passent en GA, le snapshot de volumes qui passe en beta, ainsi que la couche de stockage CSI avec la poursuite de la migration des plugins "in-tree" vs "out-of-tree". La fin de cette migration est prévue pour les versions 1.19 / 1.20 et le retrait complet des plugins "in-tree" pour les versions 1.21 / 1.22.
  • A visual guide on troubleshooting Kubernetes deployments : un guide du troublehooting des déploiements sous kubernetes avec un joli diagramme des cas possibles et les explications associées en repartant d'un exemple simple.
  • How to migrate from Helm v2 to Helm v3 : les opérations à mener pour migrer de Helm V2 à Helm V3.
  • Traefik 2.1 : le provider Consul Catalog fait son retour (il était absent en 2.0.x) et diverses améliorations sur la CRD Kubernetes ont été apportées pour mieux gérer le mirroring du traffic, les déploiements canary et la gestion des sessions. La migration ne consistant pas seulement à changer le numéro de version et suite à une remarque de ma part, une note a été ajoutée pour la migration 2.0.x vers 2.1.x

Dataviz

NoSQL

  • Cassandra Reaper 2.0 was released : la solution de réparation de vos clusters Cassandra passe en 2.0 ; elle apporte un déploiement en mode sidecar (reaper est lancé dans la même jvm que Cassandra), le support d'Apache Cassandra 4.0 (pas encore officiellement disponible), de nouveaux thèmes, une amélioration du support de Postgresql comme backend de déploiement et pleins d'autres choses.

Time Series

Je n'ai plus qu'à vous souhaiter des bonnes fêtes de fin d'année ; nous nous retrouvons l'année prochaine !

Web, Ops & Data - Semaine 48

traefikdockerkubernetesgrafananullawsalertemonitoringdashboardreverse-proxy

Container & orchrestration

  • Kompose: a tool to go from Docker-compose to Kubernetes : en cours d'incubation chez Kubernetes, ce projet permet de faciliter la transition de Docker à Kubernetes en transformant les fichier docker-compose.yml en fichiers Manifests Kubernetes. Un pivot intéressant si on considère que les développeurs vont utiliser Docker (et au mieux Swarm) pour leurs environnements de développement et en production éventuellement Swarm et ensuite vouloir migrer vers Kubernetes pour ses fonctionnalités plus évoluées.
  • Introducing Distributed Cheese: Traefik 1.1 Camembert Is Out! : Traefik, le reverse proxy moderne qui sait s'interfacer notamment avec Docker, Consul, Kubernetes et plein d'autres est sorti en version 1.1.x. Cette version apporte notamment le support de l'API Docker Swarm (Docker 1.12+), une meilleure gestion des contraintes, le support de Mesos, une meilleure gestion des affinités de sessions, un mode cluster pour Traefik (expérimental) et une image officielle docker basée sur alpine. J'ai profité de l'installation d'un nouveau serveur pour déployer Traefik et c'est vraiment agréable de pouvoir déclarer dynamiquement ses containers docker. Par ailleurs, je rappelle que je maintiens des images docker de Traefik pour sa version ARM
  • Docker for AWS Public Beta : l'offre Docker pour AWS passe d'une version alpha à une verion beta. On notera essentiellement qu'il s'agit d'une version plus intégrée avec l'offre AWS que si l'on déployait un docker soi-même. A suivre mais attention à vérifier qu'il n'y a pas un lock-in qui se crée à la fin, que l'intégration apporte un plus et ne nuit pas à la portabilité des containers.

Dashboard, Monitoring

  • What's new in Grafana v4.0 : la fonctionnalité phare de cette version est la capacité de définir des alertes au niveau de chaque élément d'un dashboard. Pour se faire il faut définir les règles à appliquer et ensuite s'appuyer sur la partie notification. Pour ceux qui étaient tentés d'installer Chronograf 1.1 (béta) pour avoir cet alerting en plus des dashboards adhoc de Grafana, ils pourraient bien finalement rester dans Grafana (dont la maturité n'est plus à prouver) plutôt que d'attendre que Chronograf se stabilise... A moins que Chonograf n'apporte une valeur ajoutée de part son intégration native avec Telegraf.

Pépites

  • Si vous pensiez que toutes les villes commencent par une lettre, maintenant vous savez que c'est faux avec la ville de 's_Herenelderen. Vous pouvez mettre vos regexp à jour !
  • These unlucky poeple have names that break computers : même si l'article concède que ce problème est de moins en moins vrai au fur et à mesure des progrès réalisés et de la prise de conscience par les développeurs, mais s'appeller "Null" ou avoir un nom de famille très long (36 caractères dans l'exemple donné ou même 8 au Japon quand l'habitude est de 4...), cela pose des tas de problèmes dans la vie du quotidien.
1 / 1