<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>GiwiSoft</title>
    <link>https://giwi.fr/</link>
    <description>Useless softwares</description>
    <language>fr</language>
    <copyright>All rights reserved 2026, Giwi</copyright>
    <lastBuildDate>Thu, 30 Apr 2026 20:23:30 GMT</lastBuildDate>
    <generator>Hexo</generator>
    <image>
      <url>https://giwi.fr/images/cropped-cropped-giwisoft-1.png</url>
      <title>GiwiSoft</title>
      <link>https://giwi.fr/</link>
    </image>
    <atom:link href="https://giwi.fr/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>DevOps pour Apache NiFi : Automatisez vos tests avec nifi-tester</title>
      <link>https://giwi.fr/2026-04-30-DevOps-pour-Apache-NiFi-Automatisez-vos-tests-avec-nifi-tester/</link>
      <description>
        <![CDATA[<p>Dans de nombreuses architectures data modernes, <a href="https://nifi.apache.org/">Apache NiFi</a> est utilisé pour orchestrer l’ingestio]]>
      </description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Java/">Java</category>
      <category domain="https://giwi.fr/categories/DevOps/">DevOps</category>
      <category domain="https://giwi.fr/tags/cicd/">cicd</category>
      <category domain="https://giwi.fr/tags/devops/">devops</category>
      <category domain="https://giwi.fr/tags/opensource/">opensource</category>
      <category domain="https://giwi.fr/tags/NiFi/">NiFi</category>
      <pubDate>Thu, 30 Apr 2026 19:51:37 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Dans de nombreuses architectures data modernes, <a href="https://nifi.apache.org/">Apache NiFi</a> est utilisé pour orchestrer l’ingestion, la transformation et la distribution de données entre systèmes hétérogènes. Sa capacité à modéliser des pipelines via un graphe de processors en fait une plateforme extrêmement puissante pour la construction de dataflows complexes.</p><p>Cependant, cette approche pose un problème majeur lorsqu’on veut appliquer des pratiques d’ingénierie modernes :</p><ul><li>tests automatisés</li><li>CI&#x2F;CD</li><li>reproductibilité</li><li>validation avant déploiement</li></ul><p>Contrairement aux microservices classiques, un flow NiFi est souvent défini visuellement, et non comme un artefact testable.</p><p>C’est précisément ce problème que j’ai voulu résoudre avec nifi-tester : un outil permettant de tester des flows NiFi de manière automatisée et reproductible.</p><p>Dans cet article, je détaille :</p><ul><li>le problème de test dans NiFi</li><li>l’architecture de l’outil</li><li>les choix techniques</li><li>comment intégrer ces tests dans une pipeline CI&#x2F;CD.</li></ul><hr><h2 id="Le-probleme-les-flows-NiFi-ne-sont-pas-naturellement-testables"><a href="#Le-probleme-les-flows-NiFi-ne-sont-pas-naturellement-testables" class="headerlink" title="Le problème : les flows NiFi ne sont pas naturellement testables"></a>Le problème : les flows NiFi ne sont pas naturellement testables</h2><p>Dans un projet classique, les tests sont une évidence :</p><ul><li>unit tests</li><li>integration tests</li><li>contract tests</li><li>end-to-end tests</li></ul><p>Mais avec NiFi, plusieurs difficultés apparaissent.</p><h3 id="1-Les-pipelines-sont-definis-dans-une-interface-graphique"><a href="#1-Les-pipelines-sont-definis-dans-une-interface-graphique" class="headerlink" title="1. Les pipelines sont définis dans une interface graphique"></a>1. Les pipelines sont définis dans une interface graphique</h3><p>Les flows NiFi sont généralement :</p><ul><li>créés dans le canvas web</li><li>versionnés via un registry</li><li>exportés sous forme de flow definitions</li></ul><p>Cela signifie que le pipeline est une configuration runtime, pas du code directement exécutable.</p><p>Conséquences :</p><ul><li>difficile de tester localement</li><li>difficile d’automatiser</li><li>difficile de reproduire un bug.</li></ul><h3 id="2-Les-pipelines-sont-fortement-integres"><a href="#2-Les-pipelines-sont-fortement-integres" class="headerlink" title="2. Les pipelines sont fortement intégrés"></a>2. Les pipelines sont fortement intégrés</h3><p>Un flow NiFi interagit souvent avec :</p><ul><li>Kafka</li><li>S3</li><li>bases de données</li><li>APIs REST</li><li>systèmes legacy.</li></ul><p>Le test devient alors un problème de simulation d’environnement.</p><p>Sans outil dédié, les équipes font souvent :</p><ul><li>des tests manuels</li><li>des validations dans un environnement de staging</li><li>des vérifications via la provenance NiFi.</li></ul><p>Cela ralentit énormément les cycles de développement.</p><h3 id="3-Les-pipelines-data-doivent-suivre-les-memes-standards-que-les-services"><a href="#3-Les-pipelines-data-doivent-suivre-les-memes-standards-que-les-services" class="headerlink" title="3. Les pipelines data doivent suivre les mêmes standards que les services"></a>3. Les pipelines data doivent suivre les mêmes standards que les services</h3><p>Aujourd’hui, dans une plateforme data mature, les pipelines doivent être :</p><ul><li>versionnés dans Git</li><li>validés automatiquement</li><li>déployés via CI&#x2F;CD.</li></ul><p>Autrement dit :</p><blockquote><p>les pipelines doivent être traités comme du code.</p></blockquote><p>C’est la philosophie derrière <a href="https://github.com/Giwi/nifi-tester">nifi-tester</a>.</p><h2 id="Objectif-rendre-les-flows-NiFi-testables"><a href="#Objectif-rendre-les-flows-NiFi-testables" class="headerlink" title="Objectif : rendre les flows NiFi testables"></a>Objectif : rendre les flows NiFi testables</h2><p>L’idée centrale du projet est simple :</p><blockquote><p>un flow NiFi doit pouvoir être testé comme une fonction.</p></blockquote><p>Conceptuellement :</p><figure class="highlight mathematica"><table><tr><td class="code"><pre><span class="line"><span class="built_in">Input</span> <span class="built_in">Dataset</span></span><br><span class="line">     │</span><br><span class="line">     ▼</span><br><span class="line"> <span class="variable">NiFi</span> <span class="variable">Flow</span></span><br><span class="line">     │</span><br><span class="line">     ▼</span><br><span class="line"><span class="variable">Output</span> <span class="built_in">Dataset</span></span><br></pre></td></tr></table></figure><p>Un test doit donc définir :</p><ul><li>une entrée</li><li>un flow à exécuter</li><li>un résultat attendu.</li></ul><p>Architecture de nifi-tester</p><p>L’outil repose sur trois composants principaux.</p><figure class="highlight asciidoc"><table><tr><td class="code"><pre><span class="line"><span class="code">+-------------------+</span></span><br><span class="line"><span class="section">| Test Definitions  |</span></span><br><span class="line"><span class="section">+---------+---------+</span></span><br><span class="line"><span class="code">          |</span></span><br><span class="line"><span class="section">          v</span></span><br><span class="line"><span class="section">+---------+---------+</span></span><br><span class="line"><span class="section">|   Test Runner     |</span></span><br><span class="line"><span class="section">+---------+---------+</span></span><br><span class="line"><span class="code">          |</span></span><br><span class="line"><span class="section">          v</span></span><br><span class="line"><span class="section">+---------+---------+</span></span><br><span class="line">| NiFi Execution    |</span><br><span class="line"><span class="section">| Environment       |</span></span><br><span class="line"><span class="section">+---------+---------+</span></span><br><span class="line"><span class="code">          |</span></span><br><span class="line"><span class="section">          v</span></span><br><span class="line"><span class="section">+-------------------+</span></span><br><span class="line"><span class="section">| Result Validator  |</span></span><br><span class="line"><span class="section">+-------------------+</span></span><br></pre></td></tr></table></figure><p>Chaque étape correspond à une responsabilité bien définie.</p><h2 id="Definition-des-tests"><a href="#Definition-des-tests" class="headerlink" title="Définition des tests"></a>Définition des tests</h2><p>Les tests sont définis avec Junit :</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="meta">@DisplayName(&quot;Deploy Groovy script pipeline&quot;)</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">testGroovyScriptPipeline</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    <span class="type">var</span> <span class="variable">result</span> <span class="operator">=</span> tester.deployPipeline(</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">java</span>.io.File(<span class="string">&quot;src/test/resources/pipelines/groovy-script-pipeline.yaml&quot;</span>),</span><br><span class="line">        <span class="string">&quot;root&quot;</span></span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    assertNotNull(result);</span><br><span class="line">    assertTrue(result.isSuccess(), <span class="string">&quot;Deployment failed: &quot;</span> + result.getMessage());</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (result.isSuccess()) &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">pgId</span> <span class="operator">=</span> result.getProcessGroupId();</span><br><span class="line">        <span class="type">var</span> <span class="variable">group</span> <span class="operator">=</span> tester.getProcessGroup(pgId);</span><br><span class="line">        assertNotNull(group);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Start the process group</span></span><br><span class="line">        tester.startProcessGroup(pgId);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Stop LogResult so flow file stays in the queue before it</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">logResultId</span> <span class="operator">=</span> tester.findProcessorIdByName(pgId, <span class="string">&quot;LogResult&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span> (logResultId != <span class="literal">null</span>) &#123;</span><br><span class="line">            tester.stopProcessor(logResultId);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Find the input port</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">inputPortId</span> <span class="operator">=</span> tester.findInputPortIdByName(pgId, <span class="string">&quot;Input&quot;</span>);</span><br><span class="line">        assertNotNull(inputPortId, <span class="string">&quot;Input port not found&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Wait for port to start up before pushing data via Site-to-Site</span></span><br><span class="line">        Thread.sleep(<span class="number">5000</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Inject &quot;hello&quot; into the input port</span></span><br><span class="line">        tester.injectFlowFile(inputPortId, <span class="string">&quot;hello&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Find the connection between AppendWorld and LogResult</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">connId</span> <span class="operator">=</span> tester.findConnectionId(pgId, <span class="string">&quot;AppendWorld&quot;</span>, <span class="string">&quot;LogResult&quot;</span>);</span><br><span class="line">        assertNotNull(connId, <span class="string">&quot;Connection not found&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Wait for processing</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">start</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">found</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line">        <span class="keyword">while</span> (System.currentTimeMillis() - start &lt; <span class="number">15000</span>) &#123;</span><br><span class="line">            <span class="type">var</span> <span class="variable">contents</span> <span class="operator">=</span> tester.getFlowFileContentsAsString(connId);</span><br><span class="line">            <span class="keyword">for</span> (String content : contents) &#123;</span><br><span class="line">                <span class="keyword">if</span> (content.contains(<span class="string">&quot;hello world&quot;</span>)) &#123;</span><br><span class="line">                    found = <span class="literal">true</span>;</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (found) <span class="keyword">break</span>;</span><br><span class="line">            Thread.sleep(<span class="number">500</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        assertTrue(found, <span class="string">&quot;Expected to find &#x27;hello world&#x27; in flow file content&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Stop the process group</span></span><br><span class="line">        tester.stopProcessGroup(pgId);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Cleanup</span></span><br><span class="line">        tester.deleteProcessGroup(pgId);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Et les flows, en Yaml</p><figure class="highlight yml"><table><tr><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Groovy</span> <span class="string">Script</span> <span class="string">Pipeline</span></span><br><span class="line"><span class="attr">description:</span> <span class="string">A</span> <span class="string">pipeline</span> <span class="string">that</span> <span class="string">uses</span> <span class="string">Groovy</span> <span class="string">script</span> <span class="string">to</span> <span class="string">append</span> <span class="string">&#x27;world&#x27;</span> <span class="string">to</span> <span class="string">&#x27;hello&#x27;</span></span><br><span class="line"><span class="attr">parentGroupId:</span> <span class="string">root</span></span><br><span class="line"></span><br><span class="line"><span class="attr">inputPorts:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Input</span></span><br><span class="line">    <span class="attr">x:</span> <span class="number">100</span></span><br><span class="line">    <span class="attr">y:</span> <span class="number">100</span></span><br><span class="line"></span><br><span class="line"><span class="attr">processors:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">AppendWorld</span></span><br><span class="line">    <span class="attr">type:</span> <span class="string">org.apache.nifi.processors.groovyx.ExecuteGroovyScript</span></span><br><span class="line">    <span class="attr">x:</span> <span class="number">400</span></span><br><span class="line">    <span class="attr">y:</span> <span class="number">100</span></span><br><span class="line">    <span class="attr">schedulingStrategy:</span> <span class="string">TIMER_DRIVEN</span></span><br><span class="line">    <span class="attr">schedulingPeriod:</span> <span class="string">&quot;1 sec&quot;</span></span><br><span class="line">    <span class="attr">properties:</span></span><br><span class="line">      <span class="attr">Script Body:</span> <span class="string">|</span></span><br><span class="line"><span class="string">        import org.apache.nifi.flowfile.FlowFile</span></span><br><span class="line"><span class="string">        import java.nio.charset.StandardCharsets</span></span><br><span class="line"><span class="string"></span>        </span><br><span class="line">        <span class="string">FlowFile</span> <span class="string">flowFile</span> <span class="string">=</span> <span class="string">session.get()</span></span><br><span class="line">        <span class="string">if</span> <span class="string">(flowFile</span> <span class="type">!=</span> <span class="literal">null</span><span class="string">)</span> &#123;</span><br><span class="line">            <span class="string">String</span> <span class="string">content</span> <span class="string">=</span> <span class="string">&quot;&quot;</span></span><br><span class="line">            <span class="string">session.read(flowFile</span>, &#123; <span class="string">inputStream</span> <span class="string">-&gt;</span></span><br><span class="line">                <span class="string">content</span> <span class="string">=</span> <span class="string">new</span> <span class="string">String(inputStream.readAllBytes()</span>, <span class="string">StandardCharsets.UTF_8)</span></span><br><span class="line">            &#125;<span class="string">)</span></span><br><span class="line">            <span class="string">content</span> <span class="string">=</span> <span class="string">content</span> <span class="string">+</span> <span class="string">&quot; world&quot;</span></span><br><span class="line">            <span class="string">flowFile</span> <span class="string">=</span> <span class="string">session.write(flowFile</span>, &#123; <span class="string">outputStream</span> <span class="string">-&gt;</span></span><br><span class="line">                <span class="string">outputStream.write(content.getBytes(StandardCharsets.UTF_8))</span></span><br><span class="line">            &#125;<span class="string">)</span></span><br><span class="line">            <span class="string">session.transfer(flowFile</span>, <span class="string">REL_SUCCESS)</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">LogResult</span></span><br><span class="line">    <span class="attr">type:</span> <span class="string">org.apache.nifi.processors.standard.LogAttribute</span></span><br><span class="line">    <span class="attr">x:</span> <span class="number">700</span></span><br><span class="line">    <span class="attr">y:</span> <span class="number">100</span></span><br><span class="line">    <span class="attr">schedulingStrategy:</span> <span class="string">TIMER_DRIVEN</span></span><br><span class="line">    <span class="attr">schedulingPeriod:</span> <span class="string">&quot;1 sec&quot;</span></span><br><span class="line">    <span class="attr">properties:</span></span><br><span class="line">      <span class="attr">Log Level:</span> <span class="string">info</span></span><br><span class="line">      <span class="attr">Log Prefix:</span> <span class="string">&quot;Result: &quot;</span></span><br><span class="line">    <span class="attr">autoTerminatedRelationships:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">success</span></span><br><span class="line"></span><br><span class="line"><span class="attr">connections:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Input</span> <span class="string">to</span> <span class="string">Groovy</span></span><br><span class="line">    <span class="attr">sourceId:</span> <span class="string">$&#123;Input&#125;</span></span><br><span class="line">    <span class="attr">destinationId:</span> <span class="string">$&#123;AppendWorld&#125;</span></span><br><span class="line">    <span class="attr">x:</span> <span class="number">250</span></span><br><span class="line">    <span class="attr">y:</span> <span class="number">100</span></span><br><span class="line"></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Groovy</span> <span class="string">to</span> <span class="string">Log</span></span><br><span class="line">    <span class="attr">sourceId:</span> <span class="string">$&#123;AppendWorld&#125;</span></span><br><span class="line">    <span class="attr">destinationId:</span> <span class="string">$&#123;LogResult&#125;</span></span><br><span class="line">    <span class="attr">relationships:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">success</span></span><br><span class="line">    <span class="attr">x:</span> <span class="number">550</span></span><br><span class="line">    <span class="attr">y:</span> <span class="number">100</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>Les pipelines de données sont aujourd’hui des composants critiques de l’infrastructure logicielle.</p><p>Pourtant, ils restent souvent moins testés que les applications classiques.</p><p>Avec <a href="https://github.com/Giwi/nifi-tester">nifi-tester</a>, l’objectif est simple :</p><p>apporter aux pipelines NiFi les mêmes pratiques d’ingénierie que pour le code applicatif.</p><p>En introduisant :</p><ul><li>des tests automatisés</li><li>des validations reproductibles</li><li>une intégration CI&#x2F;CD</li></ul><p>il devient possible de traiter les dataflows comme des artefacts logiciels à part entière.</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Présentation de GiwiCD : Une plateforme CI/CD moderne et auto-hébergée pour les développeurs</title>
      <link>https://giwi.fr/2026-04-05-GiwiCD/</link>
      <description>
        <![CDATA[<p>Si vous êtes un développeur lassé d’attendre des pipelines CI&#x2F;CD cloud trop lents ou frustré par des fichiers de configuration compl]]>
      </description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/DevOps/">DevOps</category>
      <category domain="https://giwi.fr/categories/IA/">IA</category>
      <category domain="https://giwi.fr/tags/cicd/">cicd</category>
      <category domain="https://giwi.fr/tags/devops/">devops</category>
      <category domain="https://giwi.fr/tags/opensource/">opensource</category>
      <category domain="https://giwi.fr/tags/nodejs/">nodejs</category>
      <category domain="https://giwi.fr/tags/angular/">angular</category>
      <pubDate>Sun, 05 Apr 2026 19:16:14 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Si vous êtes un développeur lassé d’attendre des pipelines CI&#x2F;CD cloud trop lents ou frustré par des fichiers de configuration complexes, un nouvel acteur entre en scène. Découvrez <strong>GiwiCD</strong> — un moteur CI&#x2F;CD léger et auto-hébergé, conçu avec Node.js, Express et Angular.</p><h2 id="Pourquoi-un-autre-outil-CI-CD"><a href="#Pourquoi-un-autre-outil-CI-CD" class="headerlink" title="Pourquoi un autre outil CI&#x2F;CD ?"></a>Pourquoi un autre outil CI&#x2F;CD ?</h2><p>Le secteur du CI&#x2F;CD est déjà bien rempli. Jenkins, GitHub Actions, GitLab CI, CircleCI — la liste est longue. Mais chacun impose des compromis :</p><ul><li><strong>Jenkins</strong> : Puissant mais lourd, avec une courbe d’apprentissage abrupte.</li><li><strong>GitHub Actions</strong> : Lié à GitHub, peut devenir coûteux pour les dépôts privés.</li><li><strong>GitLab CI</strong> : Excellent si vous utilisez GitLab exclusivement.</li><li><strong>CircleCI&#x2F;Travis</strong> : Cloud uniquement, la facturation à l’usage peut vite grimper.</li></ul><p>GiwiCD adopte une approche différente. Il est conçu pour les <strong>développeurs qui veulent une solution CI&#x2F;CD simple, rapide et auto-hébergée</strong> qui fonctionne, tout simplement.</p><h2 id="Qu’est-ce-que-GiwiCD"><a href="#Qu’est-ce-que-GiwiCD" class="headerlink" title="Qu’est-ce que GiwiCD ?"></a>Qu’est-ce que GiwiCD ?</h2><p>GiwiCD est une plateforme CI&#x2F;CD open-source qui vous permet de :</p><ul><li><strong>Créer des pipelines visuellement</strong> — Pas de fichiers YAML. Cliquez, glissez et configurez.</li><li><strong>Lancer des builds en temps réel</strong> — Suivez l’exécution de vos builds en direct grâce aux logs diffusés via WebSocket.</li><li><strong>Gérer vos identifiants en toute sécurité</strong> — Stockez vos clés SSH, jetons (tokens) et accès de notification de manière sécurisée.</li><li><strong>Être notifié automatiquement</strong> — Envoyez le statut des builds vers Telegram, Slack, Teams ou par Email.</li><li><strong>Déclencher des builds de n’importe où</strong> — Webhooks, scrutation (polling) de push, ou déclenchement manuel.</li></ul><h2 id="Screenshots"><a href="#Screenshots" class="headerlink" title="Screenshots"></a>Screenshots</h2><table><thead><tr><th>Landing</th><th>Login</th></tr></thead><tbody><tr><td><img src="https://raw.githubusercontent.com/Giwi/giwi-cd/refs/heads/main/docs/images/landing.png" alt="Landing"></td><td><img src="https://raw.githubusercontent.com/Giwi/giwi-cd/refs/heads/main/docs/images/login.png" alt="Login"></td></tr></tbody></table><table><thead><tr><th>Dashboard</th><th>Pipelines</th></tr></thead><tbody><tr><td><img src="https://raw.githubusercontent.com/Giwi/giwi-cd/refs/heads/main/docs/images/dashboard.png" alt="Dashboard"></td><td><img src="https://raw.githubusercontent.com/Giwi/giwi-cd/refs/heads/main/docs/images/pipelines.png" alt="Pipelines"></td></tr></tbody></table><table><thead><tr><th>Builds</th><th>Credentials</th></tr></thead><tbody><tr><td><img src="https://raw.githubusercontent.com/Giwi/giwi-cd/refs/heads/main/docs/images/builds.png" alt="Builds"></td><td><img src="https://raw.githubusercontent.com/Giwi/giwi-cd/refs/heads/main/docs/images/credentials.png" alt="Credentials"></td></tr></tbody></table><hr><h2 id="Caracteristiques-principales"><a href="#Caracteristiques-principales" class="headerlink" title="Caractéristiques principales"></a>Caractéristiques principales</h2><h3 id="Constructeur-de-Pipeline-Visuel"><a href="#Constructeur-de-Pipeline-Visuel" class="headerlink" title="Constructeur de Pipeline Visuel"></a>Constructeur de Pipeline Visuel</h3><p>Créez des pipelines complexes avec plusieurs étapes. Glissez-déposez pour réordonner, ajoutez des étapes de notification et configurez des déclencheurs — le tout via une interface utilisateur intuitive.</p><h3 id="Logs-de-Build-en-temps-reel"><a href="#Logs-de-Build-en-temps-reel" class="headerlink" title="Logs de Build en temps réel"></a>Logs de Build en temps réel</h3><p>Plus besoin de rafraîchir la page. Les logs de build sont transmis à votre navigateur en temps réel via WebSocket. Filtrez par niveau, effectuez des recherches dans la sortie et regardez les étapes se valider en direct.</p><h3 id="Integration-Git"><a href="#Integration-Git" class="headerlink" title="Intégration Git"></a>Intégration Git</h3><p>Fonctionne avec GitHub, GitLab, Bitbucket et n’importe quel serveur Git. Supporte l’authentification HTTPS et SSH. Détection automatique des commits via push polling.</p><h3 id="Notifications-Multi-plateformes"><a href="#Notifications-Multi-plateformes" class="headerlink" title="Notifications Multi-plateformes"></a>Notifications Multi-plateformes</h3><p>Gardez votre équipe informée grâce aux notifications de build vers :</p><ul><li><strong>Telegram</strong> (Bot)</li><li><strong>Slack</strong> (Webhooks)</li><li><strong>Microsoft Teams</strong> (Connecteurs Webhook)</li><li><strong>Email</strong> (SMTP)</li></ul><hr><h2 id="Pile-Technique"><a href="#Pile-Technique" class="headerlink" title="Pile Technique"></a>Pile Technique</h2><table><thead><tr><th align="left">Couche</th><th align="left">Technologie</th></tr></thead><tbody><tr><td align="left"><strong>Backend</strong></td><td align="left">Node.js, Express, TypeScript</td></tr><tr><td align="left"><strong>Base de données</strong></td><td align="left">SQLite (better-sqlite3)</td></tr><tr><td align="left"><strong>Frontend</strong></td><td align="left">Angular 21, Bootstrap 5</td></tr><tr><td align="left"><strong>Temps réel</strong></td><td align="left">WebSocket</td></tr><tr><td align="left"><strong>Testing</strong></td><td align="left">Jest, Supertest, Jasmine</td></tr><tr><td align="left"><strong>CI&#x2F;CD</strong></td><td align="left">GitHub Actions, Docker</td></tr></tbody></table><hr><h2 id="Prise-en-main"><a href="#Prise-en-main" class="headerlink" title="Prise en main"></a>Prise en main</h2><h3 id="Demarrage-rapide-avec-Docker"><a href="#Demarrage-rapide-avec-Docker" class="headerlink" title="Démarrage rapide avec Docker"></a>Démarrage rapide avec Docker</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Cloner le dépôt</span></span><br><span class="line">git <span class="built_in">clone</span> [https://github.com/Giwi/giwi-cd.git](https://github.com/Giwi/giwi-cd.git)</span><br><span class="line"><span class="built_in">cd</span> giwi-cd</span><br><span class="line"></span><br><span class="line"><span class="comment"># Lancer avec Docker Compose</span></span><br><span class="line">docker-compose up --build</span><br></pre></td></tr></table></figure><p>Ouvrez <a href="http://localhost:4200/">http://localhost:4200</a> et connectez-vous avec :</p><ul><li>Email : <a href="mailto:&#97;&#100;&#x6d;&#x69;&#x6e;&#x40;&#103;&#105;&#119;&#x69;&#x63;&#100;&#x2e;&#x6c;&#x6f;&#x63;&#x61;&#108;">admin@giwicd.local</a></li><li>Mot de passe : admin123</li></ul><h3 id="Installation-manuelle"><a href="#Installation-manuelle" class="headerlink" title="Installation manuelle"></a>Installation manuelle</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Installer le backend</span></span><br><span class="line"><span class="built_in">cd</span> backend &amp;&amp; npm install &amp;&amp; npm run build</span><br><span class="line"></span><br><span class="line"><span class="comment"># Installer le frontend</span></span><br><span class="line"><span class="built_in">cd</span> ../frontend &amp;&amp; npm install</span><br><span class="line"></span><br><span class="line"><span class="comment"># Lancer les deux services</span></span><br><span class="line"><span class="built_in">cd</span> backend &amp;&amp; npm start  <span class="comment"># Terminal 1</span></span><br><span class="line"><span class="built_in">cd</span> frontend &amp;&amp; npm start  <span class="comment"># Terminal 2</span></span><br></pre></td></tr></table></figure><h2 id="Prise-en-main-1"><a href="#Prise-en-main-1" class="headerlink" title="Prise en main"></a>Prise en main</h2><h3 id="Demarrage-rapide-avec-Docker-1"><a href="#Demarrage-rapide-avec-Docker-1" class="headerlink" title="Démarrage rapide avec Docker"></a>Démarrage rapide avec Docker</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Cloner le dépôt</span></span><br><span class="line">git <span class="built_in">clone</span> [https://github.com/Giwi/giwi-cd.git](https://github.com/Giwi/giwi-cd.git)</span><br><span class="line"><span class="built_in">cd</span> giwi-cd</span><br><span class="line"></span><br><span class="line"><span class="comment"># Lancer avec Docker Compose</span></span><br><span class="line">docker-compose up --build</span><br><span class="line">Ouvrez https://www.google.com/search?q=http://localhost:4200 et connectez-vous avec :</span><br><span class="line"></span><br><span class="line">Email : admin@giwicd.local</span><br><span class="line"></span><br><span class="line">Mot de passe : admin123</span><br><span class="line"></span><br><span class="line">Installation manuelle</span><br><span class="line">Bash</span><br><span class="line"><span class="comment"># Installer le backend</span></span><br><span class="line"><span class="built_in">cd</span> backend &amp;&amp; npm install &amp;&amp; npm run build</span><br><span class="line"></span><br><span class="line"><span class="comment"># Installer le frontend</span></span><br><span class="line"><span class="built_in">cd</span> ../frontend &amp;&amp; npm install</span><br><span class="line"></span><br><span class="line"><span class="comment"># Lancer les deux services</span></span><br><span class="line"><span class="built_in">cd</span> backend &amp;&amp; npm start  <span class="comment"># Terminal 1</span></span><br><span class="line"><span class="built_in">cd</span> frontend &amp;&amp; npm start  <span class="comment"># Terminal 2</span></span><br></pre></td></tr></table></figure><h2 id="Cas-d’utilisation"><a href="#Cas-d’utilisation" class="headerlink" title="Cas d’utilisation"></a>Cas d’utilisation</h2><ul><li><strong>Projets personnels</strong> : Déployez vos “side projects” sans payer pour du CI cloud. GiwiCD tourne sur un VPS à 5 $ et gère tout, des tests au déploiement.</li><li><strong>Petites équipes</strong> : Offrez à votre équipe une plateforme CI&#x2F;CD partagée sans la complexité de Jenkins.</li><li><strong>Environnements éducatifs</strong> : Parfait pour enseigner les concepts de CI&#x2F;CD grâce à son interface visuelle.</li><li><strong>Déploiements Edge&#x2F;IoT</strong> : Assez léger pour fonctionner sur des appareils “edge” et déployer directement sur du matériel IoT.</li></ul><h2 id="Feuille-de-route-Roadmap"><a href="#Feuille-de-route-Roadmap" class="headerlink" title="Feuille de route (Roadmap)"></a>Feuille de route (Roadmap)</h2><p>GiwiCD est activement développé. Les fonctionnalités à venir incluent :</p><ul><li>Exécution d’étapes en parallèle</li><li>Mise en cache des artefacts de build</li><li>Support Docker-in-Docker</li><li>Système de plugins pour des runners personnalisés</li><li>Déploiements multi-environnements</li></ul><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>GiwiCD comble un vide dans l’écosystème CI&#x2F;CD. Ce n’est pas un remplaçant pour les monolithes d’entreprise, mais une alternative simple, rapide et auto-hébergée qui respecte votre temps et votre infrastructure.</p><p><a href="https://github.com/Giwi/giwi-cd">Voir sur GitHub</a> | <a href="https://github.com/Giwi/giwi-cd/tree/main/docs">Lire la Doc</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>OpenCode.ai, L'Agent IA Open Source qui révolutionne votre Terminal</title>
      <link>https://giwi.fr/2026-04-03-opencode/</link>
      <description>
        <![CDATA[<p>Si vous suivez l’actualité de l’IA, vous avez sûrement entendu parler de Claude Code. Mais il existe une alternative open source, plus fl]]>
      </description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/DevOps/">DevOps</category>
      <category domain="https://giwi.fr/categories/IA/">IA</category>
      <category domain="https://giwi.fr/tags/IA/">IA</category>
      <category domain="https://giwi.fr/tags/Dev/">Dev</category>
      <pubDate>Fri, 03 Apr 2026 19:16:14 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Si vous suivez l’actualité de l’IA, vous avez sûrement entendu parler de Claude Code. Mais il existe une alternative open source, plus flexible et agnostique en termes de modèles : <a href="https://opencode.ai/">OpenCode.ai.</a></p><p>En tant que développeur, OpenCode est devenu mon outil de prédilection pour automatiser les tâches ingrates directement depuis mon terminal ou mon IDE. Décryptage.</p><h2 id="Qu’est-ce-qu’OpenCode"><a href="#Qu’est-ce-qu’OpenCode" class="headerlink" title="Qu’est-ce qu’OpenCode ?"></a>Qu’est-ce qu’OpenCode ?</h2><p>OpenCode est un agent de codage autonome qui ne se contente pas de suggérer du code : il l’écrit, le teste et le déploie. Contrairement aux solutions verrouillées, OpenCode est agnostique. Vous pouvez y brancher n’importe quel LLM (Claude 3.5 Sonnet, GPT-4o, Gemini 1.5 Pro) ou même des modèles locaux via Ollama.</p><p>Pourquoi c’est un “Game Changer” :</p><ul><li><strong>Multi-modèles</strong> : Changez de cerveau selon vos besoins.</li><li><strong>Mode Planification</strong> : Il propose une stratégie avant d’altérer vos fichiers.</li><li><strong>Support LSP (Language Server Protocol)</strong> : Il comprend la structure réelle de votre projet, pas seulement le texte.</li><li><strong>Vision</strong> : Vous pouvez lui envoyer des screenshots de bugs ou de maquettes.</li></ul><h2 id="Cas-pratique-Creer-une-fonctionnalite-de-A-a-Z"><a href="#Cas-pratique-Creer-une-fonctionnalite-de-A-a-Z" class="headerlink" title="Cas pratique : Créer une fonctionnalité de A à Z"></a>Cas pratique : Créer une fonctionnalité de A à Z</h2><p>Imaginons que je veuille ajouter un système de “Soft Delete” sur mon API Node.js. Au lieu de tout taper à la main, je lance OpenCode dans mon terminal.</p><p><img src="/images/2026/04/02.png" alt="terminal"></p><h3 id="Etape-1-Le-Mode-Planification-Tab"><a href="#Etape-1-Le-Mode-Planification-Tab" class="headerlink" title="Étape 1 : Le Mode Planification (Tab)"></a>Étape 1 : Le Mode Planification (<code>Tab</code>)</h3><p>Je lui demande : “Ajoute une colonne ‘deletedAt’ à ma table Users et modifie le middleware de suppression pour faire un soft delete.”</p><p>OpenCode passe en mode Plan :</p><ul><li>Il analyse mon fichier <code>schema.prisma</code>.</li><li>Il identifie le contrôleur <code>userController.js</code>.</li><li>Il m’affiche une liste de modifications prévues sans toucher au code.</li></ul><h3 id="Etape-2-L’execution-Build"><a href="#Etape-2-L’execution-Build" class="headerlink" title="Étape 2 : L’exécution (Build)"></a>Étape 2 : L’exécution (Build)</h3><p>Une fois le plan validé, OpenCode écrit le code. Voici ce qu’il génère par exemple :</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Avant : Modification automatique par OpenCode</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">deleteUser</span> = <span class="keyword">async</span> (<span class="params">id</span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// L&#x27;agent a remplacé &#x27;delete&#x27; par &#x27;update&#x27; intelligemment</span></span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">await</span> prisma.<span class="property">user</span>.<span class="title function_">update</span>(&#123;</span><br><span class="line">    <span class="attr">where</span>: &#123; id &#125;,</span><br><span class="line">    <span class="attr">data</span>: &#123; <span class="attr">deletedAt</span>: <span class="keyword">new</span> <span class="title class_">Date</span>() &#125;</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="Les-fonctionnalites-“Power-User”"><a href="#Les-fonctionnalites-“Power-User”" class="headerlink" title="Les fonctionnalités “Power User”"></a>Les fonctionnalités “Power User”</h2><h3 id="Le-fichier-AGENTS-md-ou-rules"><a href="#Le-fichier-AGENTS-md-ou-rules" class="headerlink" title="Le fichier AGENTS.md (ou rules)"></a>Le fichier <code>AGENTS.md</code> (ou rules)</h3><p>Comme pour Cursor, vous pouvez créer un fichier .opencode&#x2F;AGENTS.md pour donner des instructions persistantes.</p><blockquote><p>Exemple : “Utilise toujours TypeScript”, “Préfère les fonctions fléchées”, “Génère des tests unitaires avec Vitest pour chaque nouvelle fonction”.</p></blockquote><h3 id="Le-partage-de-session-share"><a href="#Le-partage-de-session-share" class="headerlink" title="Le partage de session (/share)"></a>Le partage de session (<code>/share</code>)</h3><p>Un bug vous résiste ? Tapez <code>/share</code> dans le terminal OpenCode. Il génère une URL unique (<code>opencode.ai/s/...</code>) contenant toute la conversation et le contexte du code. Envoyez-le à un collègue pour qu’il reprenne là où vous en êtes.</p><h3 id="OpenCode-Zen"><a href="#OpenCode-Zen" class="headerlink" title="OpenCode Zen"></a>OpenCode Zen</h3><p>Si vous ne voulez pas gérer 50 clés API différentes, OpenCode propose <strong>Zen</strong>, une passerelle unifiée qui donne accès aux meilleurs modèles optimisés pour le code avec une facturation centralisée.</p><h2 id="Installation-rapide"><a href="#Installation-rapide" class="headerlink" title="Installation rapide"></a>Installation rapide</h2><p>Pour tester l’outil, une simple commande suffit (si vous avez Node installé) :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Installation globale</span></span><br><span class="line">npm install -g opencode-ai</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Ou via le script officiel</span></span><br><span class="line">curl -fsSL https://opencode.ai/install | bash</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Lancement dans votre projet</span></span><br><span class="line">opencode</span><br></pre></td></tr></table></figure><h2 id="Le-meilleur-des-deux-mondes-L’extension-VS-Code"><a href="#Le-meilleur-des-deux-mondes-L’extension-VS-Code" class="headerlink" title="Le meilleur des deux mondes : L’extension VS Code"></a>Le meilleur des deux mondes : L’extension VS Code</h2><p>Bien qu’OpenCode brille dans le terminal par sa vélocité, son extension VS Code transforme l’éditeur en une véritable station de programmation assistée. Contrairement à d’autres extensions qui se contentent d’un simple chat latéral, l’intégration OpenCode est profondément ancrée dans l’éditeur.</p><p>Ce qui change dans votre flux de travail :</p><ul><li><strong>Édition en ligne (Inline Diff)</strong> : Appuyez sur Cmd+K (ou Ctrl+K) et demandez une modification. OpenCode affiche les changements directement dans votre fichier avec un code couleur (rouge&#x2F;vert), vous permettant d’accepter ou de refuser chaque ligne d’un clic.</li><li><strong>Contexte de fichier intelligent</strong> : Plus besoin de copier-coller votre code dans une fenêtre de chat. L’extension “voit” vos onglets ouverts et comprend les liens entre vos fichiers grâce au graphe de symboles de VS Code.</li><li><strong>Terminal intégré</strong> : L’extension peut lire la sortie de votre terminal VS Code. Si votre application crash avec une erreur de stack trace, OpenCode peut l’analyser automatiquement et vous proposer un patch.</li></ul><p><img src="/images/2026/04/03.png" alt="vscode"></p><h3 id="Comment-l’installer"><a href="#Comment-l’installer" class="headerlink" title="Comment l’installer ?"></a>Comment l’installer ?</h3><ol><li>Rendez-vous dans le Marketplace VS Code.</li><li>Recherchez “OpenCode”.</li><li>Une fois installée, cliquez sur l’icône OpenCode dans la barre latérale.</li><li>Connectez votre clé API ou utilisez votre session OpenCode Zen pour commencer à coder.</li></ol><blockquote><p><strong>Astuce d’expert</strong> : Vous pouvez indexer l’intégralité de votre codebase via l’extension. Cela permet à l’IA de répondre à des questions complexes du type : “Où est gérée la logique de calcul de la TVA dans tout ce projet ?”</p></blockquote><h2 id="Conclusion-Pourquoi-choisir-OpenCode"><a href="#Conclusion-Pourquoi-choisir-OpenCode" class="headerlink" title="Conclusion : Pourquoi choisir OpenCode ?"></a>Conclusion : Pourquoi choisir OpenCode ?</h2><p>Le choix est simple : si vous voulez le contrôle total sur votre contexte de données et la liberté de choisir votre modèle d’IA (sans être enfermé dans l’écosystème GitHub ou Anthropic), <a href="https://opencode.ai/">OpenCode.ai</a> est l’outil qu’il vous faut.</p><p>C’est un pont entre la puissance brute des LLM et la réalité technique de nos dépôts Git.</p><p>Et vous, quel modèle allez-vous connecter à votre terminal ?</p><p>Des questions sur l’intégration ? N’hésitez pas à consulter la <a href="https://opencode.ai/docs">documentation officielle</a>.</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Introduction à HAProxy</title>
      <link>https://giwi.fr/2026-03-28-HaProxy/</link>
      <description>
        <![CDATA[<p><a href="https://www.haproxy.org/">Haproxy</a> (High Availability Proxy) est le standard de l’industrie pour l’équilibrage de charge TCP&]]>
      </description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Architecture/">Architecture</category>
      <category domain="https://giwi.fr/categories/DevOps/">DevOps</category>
      <category domain="https://giwi.fr/tags/Web/">Web</category>
      <category domain="https://giwi.fr/tags/Proxy/">Proxy</category>
      <pubDate>Sat, 28 Mar 2026 20:16:14 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p><a href="https://www.haproxy.org/">Haproxy</a> (High Availability Proxy) est le standard de l’industrie pour l’équilibrage de charge TCP&#x2F;HTTP. Réputé pour son extrême rapidité et sa stabilité, il est utilisé par des géants comme GitHub, Reddit et Twitter pour gérer des flux massifs de données.</p><h2 id="Pourquoi-choisir-HAProxy"><a href="#Pourquoi-choisir-HAProxy" class="headerlink" title="Pourquoi choisir HAProxy ?"></a>Pourquoi choisir HAProxy ?</h2><table><thead><tr><th>Fonctionnalité</th><th>Description</th></tr></thead><tbody><tr><td><strong>Performance</strong></td><td>Architecture event-driven capable de gérer des milliers de connexions avec une RAM minimale.</td></tr><tr><td><strong>Flexibilité</strong></td><td>Support complet des protocoles de la <strong>Couche 4</strong> (TCP) à la <strong>Couche 7</strong> (HTTP&#x2F;SNI).</td></tr><tr><td><strong>Observabilité</strong></td><td>Dashboard de statistiques en temps réel et logs détaillés pour le debugging.</td></tr><tr><td><strong>Résilience</strong></td><td>Vérification de l’état des serveurs (health checks) avec retrait automatique des nœuds défaillants.</td></tr></tbody></table><h2 id="Fonctionnement-Technique"><a href="#Fonctionnement-Technique" class="headerlink" title="Fonctionnement Technique"></a>Fonctionnement Technique</h2><p>HAProxy repose sur un modèle de traitement non-bloquant. Contrairement à d’autres serveurs qui créent un processus ou un “thread” par connexion, HAProxy utilise une boucle d’événements unique, ce qui lui permet de rester performant même sous une charge CPU intense.</p><blockquote><p><strong>Note : Couche 4 vs Couche 7</strong></p><ul><li><strong>L4 (Transport)</strong> : L’aiguillage se fait sur l’IP et le Port. C’est ultra-rapide mais “aveugle” au contenu.</li><li><strong>L7 (Application)</strong> : HAProxy analyse les headers HTTP, les cookies ou l’URL pour décider du routage.</li></ul></blockquote><h2 id="Configuration-Anatomie-d’un-fichier-cfg"><a href="#Configuration-Anatomie-d’un-fichier-cfg" class="headerlink" title="Configuration : Anatomie d’un fichier .cfg"></a>Configuration : Anatomie d’un fichier <code>.cfg</code></h2><p>La configuration de HAProxy ne se fait pas en YAML, mais via un format propriétaire structuré en quatre sections : <code>global</code>, <code>defaults</code>, <code>frontend</code> et <code>backend</code>.</p><p>Exemple : Routage Intelligent (Statique vs Dynamique)</p><figure class="highlight yml"><table><tr><td class="code"><pre><span class="line"><span class="string">global</span></span><br><span class="line">    <span class="string">log</span> <span class="string">/dev/log</span> <span class="string">local0</span></span><br><span class="line">    <span class="string">maxconn</span> <span class="number">4096</span></span><br><span class="line">    <span class="string">user</span> <span class="string">haproxy</span></span><br><span class="line">    <span class="string">group</span> <span class="string">haproxy</span></span><br><span class="line">    <span class="string">daemon</span></span><br><span class="line"></span><br><span class="line"><span class="string">defaults</span></span><br><span class="line">    <span class="string">log</span>     <span class="string">global</span></span><br><span class="line">    <span class="string">mode</span>    <span class="string">http</span></span><br><span class="line">    <span class="string">option</span>  <span class="string">httplog</span></span><br><span class="line">    <span class="string">timeout</span> <span class="string">connect</span> <span class="string">5s</span></span><br><span class="line">    <span class="string">timeout</span> <span class="string">client</span>  <span class="string">50s</span></span><br><span class="line">    <span class="string">timeout</span> <span class="string">server</span>  <span class="string">50s</span></span><br><span class="line"></span><br><span class="line"><span class="string">frontend</span> <span class="string">http_in</span></span><br><span class="line">    <span class="string">bind</span> <span class="string">*:80</span></span><br><span class="line">    <span class="comment"># Définition des ACL (Access Control Lists)</span></span><br><span class="line">    <span class="string">acl</span> <span class="string">is_static</span> <span class="string">path_beg</span> <span class="string">/static</span> <span class="string">/images</span></span><br><span class="line">    <span class="string">acl</span> <span class="string">is_api</span>    <span class="string">path_beg</span> <span class="string">/api</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># Décisions de routage basées sur les ACL</span></span><br><span class="line">    <span class="string">use_backend</span> <span class="string">static_servers</span> <span class="string">if</span> <span class="string">is_static</span></span><br><span class="line">    <span class="string">use_backend</span> <span class="string">api_servers</span>    <span class="string">if</span> <span class="string">is_api</span></span><br><span class="line">    <span class="string">default_backend</span> <span class="string">app_servers</span></span><br><span class="line"></span><br><span class="line"><span class="string">backend</span> <span class="string">app_servers</span></span><br><span class="line">    <span class="string">balance</span> <span class="string">roundrobin</span></span><br><span class="line">    <span class="comment"># Persistence de session via cookie</span></span><br><span class="line">    <span class="string">cookie</span> <span class="string">SERVERID</span> <span class="string">insert</span> <span class="string">indirect</span> <span class="string">nocache</span></span><br><span class="line">    <span class="string">server</span> <span class="string">node1</span> <span class="number">192.168</span><span class="number">.1</span><span class="number">.10</span><span class="string">:80</span> <span class="string">check</span> <span class="string">cookie</span> <span class="string">node1</span></span><br><span class="line">    <span class="string">server</span> <span class="string">node2</span> <span class="number">192.168</span><span class="number">.1</span><span class="number">.11</span><span class="string">:80</span> <span class="string">check</span> <span class="string">cookie</span> <span class="string">node2</span></span><br></pre></td></tr></table></figure><h2 id="Securisation-avec-SSL-TLS-Termination"><a href="#Securisation-avec-SSL-TLS-Termination" class="headerlink" title="Sécurisation avec SSL&#x2F;TLS Termination"></a>Sécurisation avec SSL&#x2F;TLS Termination</h2><p>La <strong>SSL Termination</strong> permet à HAProxy de déchiffrer le trafic HTTPS. Cela libère vos serveurs backend de cette tâche lourde en calcul, leur permettant de se concentrer sur la logique applicative en HTTP simple.</p><h3 id="Mise-en-place-du-HTTPS"><a href="#Mise-en-place-du-HTTPS" class="headerlink" title="Mise en place du HTTPS"></a>Mise en place du HTTPS</h3><ul><li><strong>Préparation</strong> : HAProxy nécessite souvent que le certificat et la clé privée soient combinés dans un seul fichier .pem.</li><li><strong>Configuration du Frontend</strong> :</li></ul><figure class="highlight yml"><table><tr><td class="code"><pre><span class="line"><span class="string">frontend</span> <span class="string">https_in</span></span><br><span class="line">    <span class="comment"># On spécifie le certificat et on active le support HTTP/2 (alpn)</span></span><br><span class="line">    <span class="string">bind</span> <span class="string">*:443</span> <span class="string">ssl</span> <span class="string">crt</span> <span class="string">/etc/ssl/certs/mon-site.pem</span> <span class="string">alpn</span> <span class="string">h2,http/1.1</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Redirection automatique du HTTP vers le HTTPS</span></span><br><span class="line">    <span class="string">http-request</span> <span class="string">redirect</span> <span class="string">scheme</span> <span class="string">https</span> <span class="string">unless</span> &#123; <span class="string">ssl_fc</span> &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="string">default_backend</span> <span class="string">app_servers</span></span><br></pre></td></tr></table></figure><h2 id="Mise-en-place-de-la-Persistence-de-Session-Cookie"><a href="#Mise-en-place-de-la-Persistence-de-Session-Cookie" class="headerlink" title="Mise en place de la Persistence de Session (Cookie)"></a>Mise en place de la Persistence de Session (Cookie)</h2><p>La méthode la plus propre consiste à demander à HAProxy d’insérer un cookie spécifique dans la réponse HTTP.</p><p>Configuration du Backend :</p><figure class="highlight yml"><table><tr><td class="code"><pre><span class="line"><span class="string">backend</span> <span class="string">app_servers</span></span><br><span class="line">    <span class="string">balance</span> <span class="string">roundrobin</span></span><br><span class="line">    <span class="comment"># &#x27;SERVERID&#x27; est le nom du cookie qui sera créé</span></span><br><span class="line">    <span class="comment"># &#x27;insert&#x27; : HAProxy ajoute le cookie si absent</span></span><br><span class="line">    <span class="comment"># &#x27;indirect&#x27; : Le cookie n&#x27;est pas envoyé au serveur backend</span></span><br><span class="line">    <span class="comment"># &#x27;nocache&#x27; : Empêche les caches intermédiaires de stocker ce cookie</span></span><br><span class="line">    <span class="string">cookie</span> <span class="string">SERVERID</span> <span class="string">insert</span> <span class="string">indirect</span> <span class="string">nocache</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># On ajoute &#x27;cookie &lt;nom&gt;&#x27; à la fin de chaque ligne serveur</span></span><br><span class="line">    <span class="string">server</span> <span class="string">node1</span> <span class="number">192.168</span><span class="number">.1</span><span class="number">.10</span><span class="string">:80</span> <span class="string">check</span> <span class="string">cookie</span> <span class="string">node1</span></span><br><span class="line">    <span class="string">server</span> <span class="string">node2</span> <span class="number">192.168</span><span class="number">.1</span><span class="number">.11</span><span class="string">:80</span> <span class="string">check</span> <span class="string">cookie</span> <span class="string">node2</span></span><br><span class="line">    <span class="string">server</span> <span class="string">node3</span> <span class="number">192.168</span><span class="number">.1</span><span class="number">.12</span><span class="string">:80</span> <span class="string">check</span> <span class="string">cookie</span> <span class="string">node3</span></span><br></pre></td></tr></table></figure><p>Comment ça marche ?</p><ul><li>Lors de la première visite, HAProxy choisit un serveur (ex: <code>node1</code>).</li><li>Il ajoute un header <code>Set-Cookie: SERVERID=node1</code> dans la réponse.</li><li>Au prochain clic, le navigateur renvoie ce cookie.</li><li>HAProxy lit <code>SERVERID=node1</code> et sait qu’il doit renvoyer l’utilisateur précisément sur ce serveur.</li></ul><h2 id="Filtrage-d’IP-et-Securite-White-Blacklisting"><a href="#Filtrage-d’IP-et-Securite-White-Blacklisting" class="headerlink" title="Filtrage d’IP et Sécurité (White&#x2F;Blacklisting)"></a>Filtrage d’IP et Sécurité (White&#x2F;Blacklisting)</h2><p>HAProxy peut servir de première ligne de défense contre des adresses IP malveillantes ou pour restreindre l’accès à une interface d’administration.</p><h3 id="Exemple-Restreindre-l’acces-a-l’Admin"><a href="#Exemple-Restreindre-l’acces-a-l’Admin" class="headerlink" title="Exemple : Restreindre l’accès à l’Admin"></a>Exemple : Restreindre l’accès à l’Admin</h3><figure class="highlight yml"><table><tr><td class="code"><pre><span class="line"><span class="string">frontend</span> <span class="string">http_in</span></span><br><span class="line">    <span class="string">bind</span> <span class="string">*:80</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 1. Définition des ACLs</span></span><br><span class="line">    <span class="string">acl</span> <span class="string">is_admin_path</span> <span class="string">path_beg</span> <span class="string">/admin</span></span><br><span class="line">    <span class="string">acl</span> <span class="string">is_allowed_ip</span> <span class="string">src</span> <span class="number">123.123</span><span class="number">.123</span><span class="number">.123</span>  <span class="comment"># Votre IP fixe</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 2. Action : Bloquer si c&#x27;est l&#x27;admin MAIS PAS la bonne IP</span></span><br><span class="line">    <span class="string">http-request</span> <span class="string">deny</span> <span class="string">if</span> <span class="string">is_admin_path</span> <span class="type">!is_allowed_ip</span></span><br><span class="line">    </span><br><span class="line">    <span class="string">default_backend</span> <span class="string">app_servers</span></span><br></pre></td></tr></table></figure><h3 id="Exemple-Liste-noire-d’IP-IP-Blacklist"><a href="#Exemple-Liste-noire-d’IP-IP-Blacklist" class="headerlink" title="Exemple : Liste noire d’IP (IP Blacklist)"></a>Exemple : Liste noire d’IP (IP Blacklist)</h3><p>Si vous avez une liste massive d’IP à bloquer, utilisez un fichier externe pour ne pas alourdir votre config :</p><figure class="highlight yml"><table><tr><td class="code"><pre><span class="line"><span class="string">frontend</span> <span class="string">http_in</span></span><br><span class="line">    <span class="comment"># Bloquer toutes les IP présentes dans le fichier &#x27;blacklist.lst&#x27;</span></span><br><span class="line">    <span class="string">acl</span> <span class="string">restricted_ip</span> <span class="string">src</span> <span class="string">-f</span> <span class="string">/etc/haproxy/blacklist.lst</span></span><br><span class="line">    <span class="string">http-request</span> <span class="string">deny</span> <span class="string">if</span> <span class="string">restricted_ip</span></span><br></pre></td></tr></table></figure><p>(Le fichier <code>blacklist.lst</code> contient simplement une IP ou un réseau par ligne, ex: <code>1.2.3.4</code> ou <code>192.168.1.0/24</code>).</p><h2 id="Bonus-Le-“Drain-Mode”-pour-la-maintenance"><a href="#Bonus-Le-“Drain-Mode”-pour-la-maintenance" class="headerlink" title="Bonus : Le “Drain Mode” pour la maintenance"></a>Bonus : Le “Drain Mode” pour la maintenance</h2><p>Un avantage énorme des Sticky Sessions est de pouvoir éteindre un serveur sans déconnecter personne.</p><ul><li>Vous passez le serveur en mode <code>DRAIN</code> via l’interface de stats ou la ligne de commande.</li><li>HAProxy arrête d’envoyer de nouveaux clients sur ce serveur.</li><li>Il laisse les clients existants (ceux qui ont déjà le cookie) finir leur session.</li><li>Une fois que le compteur de connexions tombe à zéro, vous pouvez couper le serveur en toute sécurité.</li></ul><h2 id="Administration-et-Diagnostic"><a href="#Administration-et-Diagnostic" class="headerlink" title="Administration et Diagnostic"></a>Administration et Diagnostic</h2><p>Avant de redémarrer le service, validez toujours votre syntaxe pour éviter de “casser” la production :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Vérifier la syntaxe <span class="built_in">du</span> fichier de configuration</span></span><br><span class="line">haproxy -c -f /etc/haproxy/haproxy.cfg</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Recharger la configuration sans couper les connexions en cours</span></span><br><span class="line">sudo systemctl reload haproxy</span><br></pre></td></tr></table></figure><h2 id="Le-Dashboard-de-Statistiques"><a href="#Le-Dashboard-de-Statistiques" class="headerlink" title="Le Dashboard de Statistiques"></a>Le Dashboard de Statistiques</h2><p>HAProxy inclut une interface web native pour surveiller vos flux. Ajoutez ceci à votre config pour l’activer :</p><figure class="highlight yml"><table><tr><td class="code"><pre><span class="line"><span class="string">listen</span> <span class="string">stats</span></span><br><span class="line">    <span class="string">bind</span> <span class="string">*:8404</span></span><br><span class="line">    <span class="string">stats</span> <span class="string">enable</span></span><br><span class="line">    <span class="string">stats</span> <span class="string">uri</span> <span class="string">/monitor</span></span><br><span class="line">    <span class="string">stats</span> <span class="string">refresh</span> <span class="string">5s</span></span><br><span class="line">    <span class="string">stats</span> <span class="string">auth</span> <span class="string">admin:password123</span></span><br></pre></td></tr></table></figure><h2 id="L’architecture-“Keepalived-VIP”"><a href="#L’architecture-“Keepalived-VIP”" class="headerlink" title="L’architecture “Keepalived + VIP”"></a>L’architecture “Keepalived + VIP”</h2><p>Le <strong>mode cluster</strong> (ou Haute Disponibilité - HA) pour HAProxy répond à une problématique simple : si votre équilibreur de charge tombe, toute votre infrastructure devient inaccessible, même si vos serveurs backend fonctionnent parfaitement. C’est ce qu’on appelle un <strong>SPOF</strong> (Single Point of Failure).</p><p>Pour éviter cela, on utilise généralement un couple de deux serveurs HAProxy travaillant de concert.</p><p>La méthode la plus courante pour “clustériser” HAProxy n’est pas une fonctionnalité native interne au binaire HAProxy lui-même, mais l’utilisation d’un outil complémentaire nommé Keepalived.</p><h3 id="Le-concept-de-l’IP-Virtuelle-VIP"><a href="#Le-concept-de-l’IP-Virtuelle-VIP" class="headerlink" title="Le concept de l’IP Virtuelle (VIP)"></a>Le concept de l’IP Virtuelle (VIP)</h3><ul><li>Vous avez deux serveurs HAProxy avec leurs propres adresses IP (ex: <code>10.0.0.1</code> et <code>10.0.0.2</code>).</li><li>Vous créez une IP Virtuelle (VIP) (ex: <code>10.0.0.100</code>) qui “flotte” entre les deux.</li><li>Vos clients (ou votre DNS) pointent uniquement vers cette VIP.</li></ul><h3 id="Fonctionnement-Actif-Passif"><a href="#Fonctionnement-Actif-Passif" class="headerlink" title="Fonctionnement : Actif &#x2F; Passif"></a>Fonctionnement : Actif &#x2F; Passif</h3><ul><li>Le Maître (Master) : Il détient la VIP et traite 100% du trafic.</li><li>L’Esclave (Backup) : Il surveille le Maître via un protocole appelé VRRP (sorte de “battement de cœur”).</li><li>Le Basculement (Failover) : Si le Maître ne répond plus, l’Esclave s’aperçoit de l’absence de battement de cœur et “arrache” la VIP pour l’assigner à sa propre interface réseau. Le trafic reprend en quelques millisecondes.</li></ul><h3 id="La-synchronisation-des-sessions-Peers"><a href="#La-synchronisation-des-sessions-Peers" class="headerlink" title="La synchronisation des sessions (Peers)"></a>La synchronisation des sessions (Peers)</h3><p>Un problème survient lors du basculement : si HAProxy gère des Sticky Sessions (via table de persistance en mémoire), l’Esclave ne connaît pas les sessions du Maître. L’utilisateur devra se reconnecter.</p><p>Pour corriger cela, on configure la section <code>peers</code> dans HAProxy pour que les nœuds partagent leurs tables d’états en temps réel.</p><figure class="highlight yml"><table><tr><td class="code"><pre><span class="line"><span class="string">peers</span> <span class="string">mypeers</span></span><br><span class="line">    <span class="string">peer</span> <span class="string">haproxy1</span> <span class="number">10.0</span><span class="number">.0</span><span class="number">.1</span><span class="string">:1024</span></span><br><span class="line">    <span class="string">peer</span> <span class="string">haproxy2</span> <span class="number">10.0</span><span class="number">.0</span><span class="number">.2</span><span class="string">:1024</span></span><br><span class="line"></span><br><span class="line"><span class="string">backend</span> <span class="string">app_servers</span></span><br><span class="line">    <span class="string">stick-table</span> <span class="string">type</span> <span class="string">ip</span> <span class="string">size</span> <span class="string">1m</span> <span class="string">expire</span> <span class="string">1h</span> <span class="string">peers</span> <span class="string">mypeers</span></span><br><span class="line">    <span class="string">stick</span> <span class="string">on</span> <span class="string">src</span></span><br><span class="line">    <span class="string">server</span> <span class="string">node1</span> <span class="number">192.168</span><span class="number">.1</span><span class="number">.10</span><span class="string">:80</span> <span class="string">check</span></span><br></pre></td></tr></table></figure><h3 id="Configuration-type-de-Keepalived"><a href="#Configuration-type-de-Keepalived" class="headerlink" title="Configuration type de Keepalived"></a>Configuration type de Keepalived</h3><p>Voici à quoi ressemble la configuration sur le serveur Maître (<code>/etc/keepalived/keepalived.conf</code>) :</p><figure class="highlight js"><table><tr><td class="code"><pre><span class="line">vrrp_script check_haproxy &#123;</span><br><span class="line">    script <span class="string">&quot;killall -0 haproxy&quot;</span> # Vérifie si le processus <span class="title class_">HAProxy</span> tourne</span><br><span class="line">    interval <span class="number">2</span></span><br><span class="line">    weight <span class="number">2</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">vrrp_instance <span class="variable constant_">VI_1</span> &#123;</span><br><span class="line">    state <span class="variable constant_">MASTER</span></span><br><span class="line">    interface eth0</span><br><span class="line">    virtual_router_id <span class="number">51</span></span><br><span class="line">    priority <span class="number">101</span> # <span class="title class_">Priorit</span>é plus haute que le backup</span><br><span class="line">    advert_int <span class="number">1</span></span><br><span class="line">    authentication &#123;</span><br><span class="line">        auth_type <span class="variable constant_">PASS</span></span><br><span class="line">        auth_pass secret</span><br><span class="line">    &#125;</span><br><span class="line">    virtual_ipaddress &#123;</span><br><span class="line">        <span class="number">10.0</span><span class="number">.0</span><span class="number">.100</span> # L<span class="string">&#x27;IP Virtuelle</span></span><br><span class="line"><span class="string">    &#125;</span></span><br><span class="line"><span class="string">    track_script &#123;</span></span><br><span class="line"><span class="string">        check_haproxy</span></span><br><span class="line"><span class="string">    &#125;</span></span><br><span class="line"><span class="string">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="Actif-Actif-Pour-les-tres-grosses-charges"><a href="#Actif-Actif-Pour-les-tres-grosses-charges" class="headerlink" title="Actif &#x2F; Actif (Pour les très grosses charges)"></a>Actif &#x2F; Actif (Pour les très grosses charges)</h3><p>Dans certains cas extrêmes, on veut que les deux HAProxy travaillent en même temps. On utilise alors :</p><ul><li>DNS Round Robin : Le nom de domaine pointe vers deux VIP différentes (une sur chaque nœud).</li><li>Anycast BGP : Une solution réseau plus complexe où les routeurs distribuent le trafic vers le nœud HAProxy le plus “proche”.</li></ul><h3 id="Resume-des-benefices"><a href="#Resume-des-benefices" class="headerlink" title="Résumé des bénéfices"></a>Résumé des bénéfices</h3><ul><li><strong>Zéro interruption</strong> : Maintenance transparente (on coupe le maître, le backup prend le relais).</li><li><strong>Mise à jour sans risque</strong> : On met à jour le nœud passif, on bascule, puis on met à jour l’ancien maître.</li><li><strong>Intégrité des données</strong> : Grâce aux peers, les informations de bannissement (Security) ou de session sont préservées.</li></ul><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>HAProxy n’est pas seulement un outil de répartition de charge ; c’est un véritable couteau suisse pour la haute disponibilité. Sa rigueur et sa légèreté en font un choix incontournable pour toute architecture microservices ou web moderne.</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Apache NiFi - un apperçu</title>
      <link>https://giwi.fr/2026-03-26-presentation-nifi/</link>
      <description>
        <![CDATA[<h1 id="Presentation-Technique-de-Apache-NiFi"><a href="#Presentation-Technique-de-Apache-NiFi" class="headerlink" title="Présentation Techn]]>
      </description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Java/">Java</category>
      <category domain="https://giwi.fr/categories/Architecture/">Architecture</category>
      <category domain="https://giwi.fr/tags/bus/">bus</category>
      <category domain="https://giwi.fr/tags/ESB/">ESB</category>
      <category domain="https://giwi.fr/tags/ETL/">ETL</category>
      <pubDate>Thu, 26 Mar 2026 20:16:14 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h1 id="Presentation-Technique-de-Apache-NiFi"><a href="#Presentation-Technique-de-Apache-NiFi" class="headerlink" title="Présentation Technique de Apache NiFi"></a>Présentation Technique de Apache NiFi</h1><p> <a href="https://nifi.apache.org/">Apache NiFi</a> est une plateforme de gestion de flux de données open-source qui permet de concevoir, contrôler, exécuter et surveiller des flux de données en temps réel. Développé initialement par la National Security Agency (NSA) et maintenant maintenu par la Fondation Apache, NiFi est devenu un outil incontournable pour les entreprises cherchant à automatiser et à gérer efficacement leurs flux de données.</p><h2 id="Introduction-a-Apache-NiFi"><a href="#Introduction-a-Apache-NiFi" class="headerlink" title="Introduction à Apache NiFi"></a>Introduction à Apache NiFi</h2><p>Apache NiFi est conçu pour être une plateforme de gestion de flux de données facile à utiliser, puissante et fiable. Elle permet aux utilisateurs de créer des flux de données complexes sans avoir besoin de compétences en programmation avancées. NiFi est particulièrement utile pour les entreprises qui doivent traiter de grandes quantités de données en temps réel, telles que les données de capteurs, les journaux d’applications, les données de réseaux sociaux, et bien d’autres.</p><h2 id="Architecture-de-NiFi"><a href="#Architecture-de-NiFi" class="headerlink" title="Architecture de NiFi"></a>Architecture de NiFi</h2><p>L’architecture de NiFi est conçue pour être modulaire et extensible, ce qui permet aux utilisateurs de l’adapter à leurs besoins spécifiques. Voici les principaux composants de l’architecture de NiFi :</p><ul><li><strong>NiFi FlowController</strong> : C’est le cœur de NiFi. Il est responsable de la gestion des flux de données, de l’exécution des processeurs et de la gestion des ressources.</li><li><strong>Processeurs</strong> : Ce sont les unités de base de traitement des données dans NiFi. Chaque processeur effectue une tâche spécifique, comme la lecture de données à partir d’une source, la transformation des données, ou l’écriture de données dans une destination.</li><li><strong>FlowFiles</strong> : Ce sont les unités de données que NiFi traite. Chaque FlowFile contient des données et des attributs métadonnées.</li><li><strong>Connections</strong> : Ce sont les canaux par lesquels les FlowFiles sont transférés entre les processeurs.</li><li><strong>Repositories</strong> : NiFi utilise plusieurs types de dépôts pour stocker les données et les métadonnées, tels que le Provenance Repository, le Content Repository, et le FlowFile Repository.</li></ul><h2 id="Fonctionnalites-Cles-de-NiFi"><a href="#Fonctionnalites-Cles-de-NiFi" class="headerlink" title="Fonctionnalités Clés de NiFi"></a>Fonctionnalités Clés de NiFi</h2><p>Apache NiFi présente plusieurs avantages par rapport à d’autres outils de gestion de flux de données. Voici quelques-uns des principaux avantages :</p><ul><li><strong>Interface Utilisateur Intuitive</strong> : NiFi offre une interface utilisateur web intuitive qui permet aux utilisateurs de concevoir, contrôler et surveiller les flux de données en temps réel sans nécessiter de compétences en programmation avancées. Cela facilite la création et la gestion des flux de données complexes.</li><li><strong>Automatisation</strong> des Flux de Données : NiFi permet d’automatiser les flux de données complexes, réduisant ainsi la nécessité d’interventions manuelles. Cela améliore l’efficacité et réduit les erreurs humaines.</li><li><strong>Scalabilité</strong> : NiFi est conçu pour être hautement scalable, ce qui permet de traiter de grandes quantités de données en temps réel. Cela est particulièrement utile pour les entreprises qui doivent gérer des volumes de données croissants.</li><li><strong>Fiabilité</strong> : NiFi garantit la fiabilité des flux de données grâce à des mécanismes de reprise après sinistre et de gestion des erreurs. Cela assure que les données sont traitées de manière fiable et cohérente.</li><li><strong>Extensibilité</strong> : NiFi est extensible, ce qui permet aux utilisateurs d’ajouter de nouveaux processeurs et de nouvelles fonctionnalités selon leurs besoins spécifiques. Cela permet de personnaliser NiFi pour répondre à des exigences particulières.</li><li><strong>Surveillance et Gestion</strong> : NiFi offre des outils de surveillance et de gestion des flux de données pour garantir leur bon fonctionnement. Le tableau de bord de NiFi fournit une vue d’ensemble des flux de données, y compris les statistiques de performance, les erreurs, et les alertes.</li><li><strong>Intégration Facile</strong> : NiFi peut être facilement intégré avec d’autres outils et systèmes, ce qui permet de créer des flux de données complexes et intégrés. Cela facilite l’intégration de NiFi dans des environnements existants.</li><li><strong>Gestion des Métadonnées</strong> : NiFi permet de gérer les métadonnées associées aux données, ce qui facilite la recherche, la filtration et la transformation des données. Cela est particulièrement utile pour les entreprises qui doivent gérer des données hétérogènes.</li><li><strong>Sécurité</strong> : NiFi offre des fonctionnalités de sécurité robustes, telles que l’authentification et l’autorisation, pour protéger les données et les flux de données. Cela est crucial pour les entreprises qui doivent respecter des normes de sécurité strictes.</li><li><strong>Communauté et Support</strong> : NiFi bénéficie d’une communauté active et d’un support robuste, ce qui facilite la résolution des problèmes et l’obtention d’aide pour l’utilisation de NiFi. Cela est particulièrement utile pour les entreprises qui cherchent à adopter NiFi pour la première fois.</li></ul><p><img src="/images/2026/03/nifi-3.png" alt="Nifi Clustering"></p><p>En résumé, NiFi offre une combinaison unique de fonctionnalités qui en font un outil puissant et flexible pour la gestion des flux de données. Ses avantages en termes d’automatisation, de scalabilité, de fiabilité, et d’extensibilité en font un choix idéal pour les entreprises cherchant à optimiser leurs processus de gestion des données.</p><h2 id="Quelques-cas-d’usage"><a href="#Quelques-cas-d’usage" class="headerlink" title="Quelques cas d’usage"></a>Quelques cas d’usage</h2><p>Les cas d’utilisation typiques de NiFi dans les entreprises sont variés et couvrent un large éventail de besoins en gestion de flux de données. Voici quelques-uns des cas d’utilisation les plus courants :</p><ul><li><strong>Intégration de Données</strong> : NiFi est souvent utilisé pour intégrer des données provenant de différentes sources. Par exemple, une entreprise peut utiliser NiFi pour collecter des données de capteurs, des journaux d’applications, et des bases de données, puis les intégrer dans un entrepôt de données pour une analyse ultérieure.</li><li><strong>Transformation de Données</strong> : NiFi permet de transformer des données en temps réel. Par exemple, une entreprise peut utiliser NiFi pour convertir des données JSON en XML, ou pour nettoyer et normaliser des données avant de les stocker dans une base de données.</li><li><strong>Surveillance et Alertes</strong> : NiFi peut être utilisé pour surveiller les flux de données et générer des alertes en temps réel. Par exemple, une entreprise peut utiliser NiFi pour surveiller les journaux d’applications et générer des alertes en cas de détection d’anomalies ou d’erreurs.</li><li><strong>Gestion des Flux de Données en Temps Réel</strong> : NiFi est particulièrement utile pour les entreprises qui doivent traiter des flux de données en temps réel. Par exemple, une entreprise peut utiliser NiFi pour traiter des données de capteurs en temps réel et prendre des décisions basées sur ces données.</li><li><strong>Automatisation des Processus</strong> : NiFi permet d’automatiser les processus de gestion des données, réduisant ainsi la nécessité d’interventions manuelles. Par exemple, une entreprise peut utiliser NiFi pour automatiser le processus de collecte, de transformation et de stockage des données.</li><li><strong>Gestion des Métadonnées</strong> : NiFi permet de gérer les métadonnées associées aux données, ce qui facilite la recherche, la filtration et la transformation des données. Par exemple, une entreprise peut utiliser NiFi pour gérer les métadonnées des fichiers de données et faciliter leur recherche et leur analyse.</li><li><strong>Sécurité et Conformité</strong> : NiFi offre des fonctionnalités de sécurité robustes, telles que l’authentification et l’autorisation, pour protéger les données et les flux de données. Par exemple, une entreprise peut utiliser NiFi pour garantir que les données sensibles sont traitées de manière sécurisée et conforme aux réglementations.</li><li><strong>Optimisation des Ressources</strong> : NiFi permet de surveiller et d’optimiser l’utilisation des ressources. Par exemple, une entreprise peut utiliser NiFi pour surveiller l’utilisation des ressources de calcul et de stockage, et optimiser leur utilisation pour améliorer l’efficacité.</li><li><strong>Gestion des Données de Surveillance</strong> : NiFi peut être utilisé pour collecter et analyser les données de surveillance des performances. Par exemple, une entreprise peut utiliser NiFi pour collecter des données de surveillance des performances, des données de gestion des événements (incidents, logs), et des prévisions de la demande pour générer des plans de gestion de la capacité et des rapports de performance.</li><li><strong>Amélioration de la Qualité de Service</strong> : NiFi peut aider à améliorer la qualité de service en fournissant des données et des rapports détaillés sur les performances et la capacité. Par exemple, une entreprise peut utiliser NiFi pour générer des rapports de capacité et de performance, des modèles de prévision de capacité, et des recommandations pour l’optimisation des ressources.</li></ul><p>En résumé, NiFi est un outil polyvalent qui peut être utilisé pour une variété de cas d’utilisation dans les entreprises, allant de l’intégration et de la transformation des données à la surveillance et à l’optimisation des flux de données.</p><h2 id="Utilisation-de-NiFi"><a href="#Utilisation-de-NiFi" class="headerlink" title="Utilisation de NiFi"></a>Utilisation de NiFi</h2><p><img src="/images/2026/03/nifi-2.png" alt="Nifi GUI"></p><p>Pour utiliser NiFi, les utilisateurs doivent d’abord installer NiFi sur leur système. Une fois installé, ils peuvent accéder à l’interface utilisateur web pour commencer à concevoir leurs flux de données. Voici les étapes de base pour créer un flux de données dans NiFi :</p><ul><li><strong>Connexion à NiFi</strong> : Accédez à l’interface utilisateur web de NiFi en utilisant un navigateur web.</li><li><strong>Création d’un Nouveau Flux</strong> : Cliquez sur “Create” pour créer un nouveau flux de données.</li><li><strong>Ajout de Processeurs</strong> : Ajoutez les processeurs nécessaires à votre flux de données. Par exemple, vous pouvez ajouter un processeur - <code>GetFile</code> pour lire des fichiers à partir d’un répertoire, un processeur “TransformJSON” pour transformer les données JSON, et un processeur  <code>PutFile</code> pour écrire les données transformées dans un autre répertoire.</li><li><strong>Connexion des Processeurs</strong> : Connectez les processeurs en utilisant des connexions pour définir le flux de données.</li><li><strong>Configuration des Processeurs</strong> : Configurez chaque processeur en fonction de vos besoins spécifiques.</li><li><strong>Démarrage du Flux</strong> : Démarrez le flux de données en cliquant sur le bouton “Start”.</li></ul><h2 id="Exemple-de-Flux-de-Donnees"><a href="#Exemple-de-Flux-de-Donnees" class="headerlink" title="Exemple de Flux de Données"></a>Exemple de Flux de Données</h2><p>Pour illustrer l’utilisation de NiFi, prenons un exemple simple de flux de données. Supposons que nous voulons lire des fichiers JSON à partir d’un répertoire, transformer les données JSON en XML, et écrire les données transformées dans un autre répertoire.</p><p><img src="/images/2026/03/nifi-4.png" alt="Nifi GUI"></p><ul><li><strong>Ajout des Processeurs</strong> :<ul><li>Ajoutez un processeur <code>GetFile</code> pour lire les fichiers JSON à partir d’un répertoire.</li><li>Ajoutez un processeur <code>TransformJSON</code> pour transformer les données JSON en XML.</li><li>Ajoutez un processeur <code>PutFile</code> pour écrire les données transformées dans un autre répertoire.</li></ul></li></ul><p><img src="/images/2026/03/nifi-6.png" alt="Nifi processor"></p><ul><li><strong>Connexion des Processeurs</strong> :<ul><li>Connectez la sortie du processeur <code>GetFile</code> à l’entrée du processeur <code>TransformJSON</code>.</li><li>Connectez la sortie du processeur <code>TransformJSON</code> à l’entrée du processeur <code>PutFile</code>.</li></ul></li></ul><p><img src="/images/2026/03/nifi-5.png" alt="Nifi link"></p><ul><li><strong>Configuration des Processeurs</strong> :<ul><li>Configurez le processeur <code>GetFile</code> pour lire les fichiers JSON à partir du répertoire source.</li><li>Configurez le processeur <code>TransformJSON</code> pour transformer les données JSON en XML.</li><li>Configurez le processeur <code>PutFile</code> pour écrire les données transformées dans le répertoire de destination.</li></ul></li><li><strong>Démarrage du Flux</strong> :<ul><li>Démarrez le flux de données en cliquant sur le bouton “Start”.</li></ul></li></ul><h2 id="Surveillance-et-Gestion-des-Flux-de-Donnees"><a href="#Surveillance-et-Gestion-des-Flux-de-Donnees" class="headerlink" title="Surveillance et Gestion des Flux de Données"></a>Surveillance et Gestion des Flux de Données</h2><p><img src="/images/2026/03/nifi-7.png" alt="Nifi back pressure"></p><p>NiFi offre des outils de surveillance et de gestion des flux de données pour garantir leur bon fonctionnement. Voici quelques-unes des fonctionnalités de surveillance et de gestion disponibles dans NiFi :</p><ul><li><strong>Tableau de Bord</strong> : Le tableau de bord de NiFi fournit une vue d’ensemble des flux de données, y compris les statistiques de performance, les erreurs, et les alertes.</li><li><strong>Provenance Repository</strong> : Le Provenance Repository stocke les informations sur les FlowFiles, ce qui permet de suivre leur parcours à travers le flux de données.</li><li><strong>Alertes et Notifications</strong> : NiFi permet de configurer des alertes et des notifications pour signaler les erreurs et les problèmes de performance.</li><li><strong>Gestion des Erreurs</strong> : NiFi offre des mécanismes de gestion des erreurs pour garantir la fiabilité des flux de données.</li></ul><p><img src="/images/2026/03/nifi-8.png" alt="Nifi flow monitoring"></p><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>Apache NiFi est une plateforme de gestion de flux de données puissante et flexible qui permet aux entreprises de traiter de grandes quantités de données en temps réel. Avec son interface utilisateur intuitive, ses fonctionnalités de surveillance et de gestion, et sa capacité à automatiser les flux de données complexes, NiFi est un outil incontournable pour les entreprises cherchant à optimiser leurs processus de gestion des données. Que vous soyez un développeur, un administrateur système, ou un analyste de données, NiFi offre les outils nécessaires pour gérer efficacement vos flux de données. </p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Psychodrop. Comment j'ai ressuscité l'esprit de Winamp</title>
      <link>https://giwi.fr/2026-03-25-psychodrop/</link>
      <description>
        <![CDATA[<h1 id="Comment-j’ai-ressuscite-l’esprit-de-Winamp-pour-mon-groupe-Wattson"><a href="#Comment-j’ai-ressuscite-l’esprit-de-Winamp-pour-mon-gr]]>
      </description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Realisations/">Réalisations</category>
      <pubDate>Wed, 25 Mar 2026 20:16:14 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h1 id="Comment-j’ai-ressuscite-l’esprit-de-Winamp-pour-mon-groupe-Wattson"><a href="#Comment-j’ai-ressuscite-l’esprit-de-Winamp-pour-mon-groupe-Wattson" class="headerlink" title="Comment j’ai ressuscité l’esprit de Winamp pour mon groupe Wattson."></a>Comment j’ai ressuscité l’esprit de Winamp pour mon groupe Wattson.</h1><p> Récemment, avec mon groupe de musique <a href="https://www.facebook.com/WattSon.band">Wattson</a> nous avons eu la chance de fouler une scène équipée d’un dispositif de vidéoprojection complet. Au-delà de la simple diffusion d’un logo statique, j’ai tout de suite vu l’opportunité de créer une véritable expérience immersive pour notre public.</p><hr><p>C’est alors qu’une bouffée de nostalgie m’a frappé : je me suis revu, adolescent, fasciné par les <a href="https://winampheritage.com/visualizations/Visualizations-1">visualisations psychédéliques</a> de l’inoxydable <a href="https://winamp.com/">Winamp</a>, ce lecteur audio culte qui transformait chaque morceau en une explosion de fractales et de couleurs. Je me suis dit : “Et si on projetait ce chaos organisé derrière nous, en plein set ?”</p><p><img src="/images/2026/03/5or6s55or6s55or6.png" alt="WinAmp legacy"></p><h2 id="Le-defi-technique-du-WebAssembly-a-la-scene"><a href="#Le-defi-technique-du-WebAssembly-a-la-scene" class="headerlink" title="Le défi technique : du WebAssembly à la scène"></a>Le défi technique : du WebAssembly à la scène</h2><p>Le déclic est venu lorsque je suis tombé sur <a href="https://butterchurnviz.com/">Butter Churn</a>. Un développeur talentueux (<a href="https://github.com/jberg">jberg</a>) a réussi l’exploit de porter ces visualisations légendaires en WebAssembly, les rendant accessibles via les technologies web modernes. <a href="https://github.com/jberg/butterchurn">Son projet GitHub</a> était la brique de base parfaite, mais il restait à en faire un outil de scène performant.</p><p>Ni une ni deux, je me suis retroussé les manches pour concevoir une application dédiée. L’objectif était clair :</p><ul><li><strong>Analyse temps réel</strong> : Le logiciel, installé sur un laptop en bord de scène, capte le flux audio brut du groupe via le micro ou la console.</li><li><strong>Dynamisme automatique</strong> : Les visuels réagissent instantanément aux fréquences et au rythme de notre musique, évoluant de manière organique sans intervention humaine.</li><li><strong>Identité visuelle</strong> : J’ai intégré une couche de personnalisation pour que notre logo reste présent au cœur de cette tornade visuelle.</li></ul><h2 id="Decouvrez-Psychodrop"><a href="#Decouvrez-Psychodrop" class="headerlink" title="Découvrez Psychodrop"></a>Découvrez Psychodrop</h2><p><img src="/images/2026/03/screenshot01.png" alt="Psychodrop"></p><p>Le résultat de ce travail acharné s’appelle <a href="https://psychodrop.giwi.fr/">Psychodrop</a>. C’est un pont entre l’esthétique “Legacy” des années 2000 et l’énergie brute d’un concert de rock. Le rendu est hypnotique, dynamique, et apporte une dimension supplémentaire à nos morceaux.</p><p>Curieux de voir ce que ça donne ? Vous pouvez tester l’expérience interactive directement sur la <a href="https://psychodrop.giwi.fr/demo">demo ici</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Plongée dans l’Architecture Client-Serveur MCP</title>
      <link>https://giwi.fr/2026-03-23-Plongee-dans-lArchitecture-Client-Serveur-MCP/</link>
      <description>
        <![CDATA[<h1 id="MCP-Plongee-dans-l’Architecture-Client-Serveur-MCP"><a href="#MCP-Plongee-dans-l’Architecture-Client-Serveur-MCP" class="headerlink"]]>
      </description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/IA/">IA</category>
      <category domain="https://giwi.fr/tags/IA/">IA</category>
      <category domain="https://giwi.fr/tags/MCP/">MCP</category>
      <pubDate>Mon, 23 Mar 2026 19:24:17 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h1 id="MCP-Plongee-dans-l’Architecture-Client-Serveur-MCP"><a href="#MCP-Plongee-dans-l’Architecture-Client-Serveur-MCP" class="headerlink" title="MCP : Plongée dans l’Architecture Client-Serveur MCP"></a>MCP : Plongée dans l’Architecture Client-Serveur MCP</h1><p>Le <strong>Model Context Protocol (MCP)</strong>, introduit par <a href="https://www.anthropic.com/engineering/code-execution-with-mcp">Anthropic</a> et désormais adopté par un écosystème croissant (Cursor, Zed, Cloudflare, Google Cloud), s’impose comme le “USB-C” de l’intelligence artificielle.</p><hr><p>Pour les ingénieurs, MCP n’est pas simplement une couche de <em>tool calling</em>. C’est un protocole de communication <strong>stateful</strong> (à état) qui normalise la manière dont un LLM accède aux données et aux outils.</p><h2 id="1-L’Architecture-Tripartite-Host-Client-et-Serveur"><a href="#1-L’Architecture-Tripartite-Host-Client-et-Serveur" class="headerlink" title="1. L’Architecture Tripartite : Host, Client et Serveur"></a>1. L’Architecture Tripartite : Host, Client et Serveur</h2><p>L’écosystème MCP repose sur une séparation stricte des responsabilités. Comprendre cette hiérarchie est crucial pour concevoir des intégrations robustes.</p><table><thead><tr><th align="left">Rôle</th><th align="left">Composant</th><th align="left">Responsabilité</th></tr></thead><tbody><tr><td align="left"><strong>Hôte (Host)</strong></td><td align="left">IDE, App Desktop</td><td align="left">Orchestre l’UX et détient le contexte global de la session.</td></tr><tr><td align="left"><strong>Client MCP</strong></td><td align="left">Couche Protocolaire</td><td align="left">Maintient la connexion, découvre les capacités et traduit les requêtes.</td></tr><tr><td align="left"><strong>Serveur MCP</strong></td><td align="left">Processus local&#x2F;distant</td><td align="left">Expose des ressources spécifiques (BBDD, API, Fichiers).</td></tr></tbody></table><h2 id=""><a href="#" class="headerlink" title=""></a><img src="/images/2026/03/image_okoy4jokoy4jokoy.png" alt="schéma"></h2><h2 id="2-Le-Transport-JSON-RPC-2-0-et-Canaux-de-Communication"><a href="#2-Le-Transport-JSON-RPC-2-0-et-Canaux-de-Communication" class="headerlink" title="2. Le Transport : JSON-RPC 2.0 et Canaux de Communication"></a>2. Le Transport : JSON-RPC 2.0 et Canaux de Communication</h2><p>Le MCP utilise <strong>JSON-RPC 2.0</strong> comme format d’échange. Ce choix permet une sérialisation simple et une interopérabilité maximale. Le protocole supporte principalement deux types de transport :</p><ol><li><strong>Stdio (Standard Input&#x2F;Output) :</strong> Utilisé pour les serveurs locaux. L’Hôte lance le serveur comme un processus fils et communique via les flux <code>stdin</code> et <code>stdout</code>.</li><li><strong>SSE (Server-Sent Events) :</strong> Utilisé pour les serveurs distants. Le client envoie des requêtes via HTTP POST, et le serveur répond via un flux d’événements persistants.</li></ol><h3 id="Exemple-d’une-requete-de-decouverte-tools-list"><a href="#Exemple-d’une-requete-de-decouverte-tools-list" class="headerlink" title="Exemple d’une requête de découverte (tools/list)"></a>Exemple d’une requête de découverte (<code>tools/list</code>)</h3><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;jsonrpc&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;method&quot;</span><span class="punctuation">:</span> <span class="string">&quot;tools/list&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;params&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span><span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h1 id="Les-Primitives-Fondamentales"><a href="#Les-Primitives-Fondamentales" class="headerlink" title="Les Primitives Fondamentales"></a>Les Primitives Fondamentales</h1><p>Un serveur MCP expose trois types de capacités via des points de terminaison standardisés :</p><h2 id="Resources-Donnees-Passives"><a href="#Resources-Donnees-Passives" class="headerlink" title="Resources (Données Passives)"></a>Resources (Données Passives)</h2><p>Les ressources sont des sources de données en lecture seule. Elles permettent au modèle de “lire” un contexte.</p><p>Exemple : Un log de serveur ou le contenu d’un fichier .csv.</p><p>Identification : Les ressources utilisent des URI (ex: <code>postgres://db/table/schema</code>).</p><h2 id="Tools-Actions-Actives"><a href="#Tools-Actions-Actives" class="headerlink" title="Tools (Actions Actives)"></a>Tools (Actions Actives)</h2><p>Contrairement aux ressources, les outils permettent au modèle d’effectuer des actions.</p><p>Exemple : Exécuter une requête SQL ou envoyer un email.</p><p>Validation : Chaque outil définit un schéma JSON (paramsSchema) pour valider les arguments.</p><h2 id="Prompts-Modeles-Reutilisables"><a href="#Prompts-Modeles-Reutilisables" class="headerlink" title="Prompts (Modèles Réutilisables)"></a>Prompts (Modèles Réutilisables)</h2><p>Les serveurs peuvent exposer des “Prompts” pré-configurés (templates) qui aident l’utilisateur à structurer ses interactions avec les données du serveur.</p><h2 id="Le-Cycle-de-Vie-d’une-Connexion-Handshake"><a href="#Le-Cycle-de-Vie-d’une-Connexion-Handshake" class="headerlink" title="Le Cycle de Vie d’une Connexion (Handshake)"></a>Le Cycle de Vie d’une Connexion (Handshake)</h2><p>L’établissement d’une connexion MCP suit une séquence rigoureuse de négociation :</p><p>Initialize : Le client envoie ses capacités (version du protocole, fonctionnalités supportées).</p><p>Response : Le serveur répond avec ses propres métadonnées.</p><p>Initialized Notification : Le client confirme la fin de la négociation. Le flux est ouvert.</p><h1 id="Implementation-Creer-un-Serveur-FastMCP-Python"><a href="#Implementation-Creer-un-Serveur-FastMCP-Python" class="headerlink" title="Implémentation : Créer un Serveur FastMCP (Python)"></a>Implémentation : Créer un Serveur FastMCP (Python)</h1><p>Grâce au SDK de <a href="https://gofastmcp.com/getting-started/welcome">FastMCP</a>, la création d’un serveur est devenue triviale. Voici un exemple d’un serveur exposant un outil météo :</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> mcp.server.fastmcp <span class="keyword">import</span> FastMCP</span><br><span class="line"></span><br><span class="line"><span class="comment"># Initialisation du serveur</span></span><br><span class="line">mcp = FastMCP(<span class="string">&quot;WeatherService&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta">@mcp.tool()</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">get_temperature</span>(<span class="params">city: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;Récupère la température actuelle pour une ville donnée.&quot;&quot;&quot;</span></span><br><span class="line">    <span class="comment"># Logique d&#x27;appel API réelle ici</span></span><br><span class="line">    <span class="keyword">return</span> <span class="string">f&quot;La température à <span class="subst">&#123;city&#125;</span> est de 22°C&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    mcp.run()</span><br></pre></td></tr></table></figure><p>Conclusion : Pourquoi MCP change la donne ?<br>Avant les MCP, chaque développeur devait écrire des connecteurs personnalisés pour chaque plateforme. Avec un MCP, vous écrivez votre serveur une seule fois, et il devient instantanément compatible avec n’importe quel client (Claude Desktop, Cursor, etc.).</p><p>Note technique : L’abstraction déplace l’intelligence de l’intégration vers la donnée elle-même, rendant les agents IA véritablement portables.</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Favicon avec thème clair/sombre</title>
      <link>https://giwi.fr/2023-02-07-favicon-avec-theme-clair-sombre/</link>
      <description>Voici une technique pour que le favicon de votre site s'adapte automatiquement au thème de votre navigateur.</description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Web/">Web</category>
      <category domain="https://giwi.fr/categories/JavaScript/">JavaScript</category>
      <category domain="https://giwi.fr/tags/Web/">Web</category>
      <category domain="https://giwi.fr/tags/HTML/">HTML</category>
      <pubDate>Tue, 07 Feb 2023 20:26:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Tout le monde n’utilise pas son OS ou son navigateur en thème sombre, tout le monde n’a pas son côté gothique twighlight.</p><p>Cependant, des fois, le <a href="https://fr.wikipedia.org/wiki/Favicon">favicon</a> de notre site Web se marie mal avec un thème light ou un thème dark (voire les 2, mais là, je ne peux rien faire).</p><p><img src="/images/2023/02/image.png" alt="Favicon wikipedia"></p><p>L’idée est de d’avoir un favicon dédié mode sombre et un dédié mode clair et d’utiliser celui qui est en phase avec le thème utilisé par notre internaute.</p><p>Ce se fait avec l’attribut <a href="https://www.w3schools.com/tags/att_link_media.asp">média</a> de la balise <a href="https://www.w3schools.com/tags/tag_link.asp">link</a> :</p><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;icon&quot;</span> <span class="attr">type</span>=<span class="string">&quot;image/png&quot;</span> <span class="attr">href</span>=<span class="string">&quot;/favicon-light.png&quot;</span> <span class="attr">media</span>=<span class="string">&quot;(prefers-color-scheme:light)&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;icon&quot;</span> <span class="attr">type</span>=<span class="string">&quot;image/png&quot;</span> <span class="attr">href</span>=<span class="string">&quot;/favicon-dark.png&quot;</span> <span class="attr">media</span>=<span class="string">&quot;(prefers-color-scheme:dark)&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;icon&quot;</span> <span class="attr">type</span>=<span class="string">&quot;image/png&quot;</span> <span class="attr">href</span>=<span class="string">&quot;/favicon-light.png&quot;</span> <span class="attr">media</span>=<span class="string">&quot;(prefers-color-scheme:no-preference)&quot;</span>&gt;</span></span><br></pre></td></tr></table></figure><p>C’est aussi simple que ça.</p><p>Pour générer des favicons, vous avez l’embarras du choix :</p><ul><li><a href="https://realfavicongenerator.net/">https://realfavicongenerator.net/</a></li><li><a href="https://favicon.io/">https://favicon.io/</a></li><li><a href="https://www.favicon-generator.org/">https://www.favicon-generator.org/</a></li><li><a href="https://www.favicon.cc/">https://www.favicon.cc/</a></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Discovery Explorer + NginX reverse proxy + HTTP basic auth</title>
      <link>https://giwi.fr/2022-07-14-discovery-explorer-nginx-reverse-proxy-http-basic-auth/</link>
      <description>Tutorial pour installer Discovery Explorer avec une authentification Basic avec NGinX</description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Linux/">Linux</category>
      <category domain="https://giwi.fr/categories/Linux/Ubuntu/">Ubuntu</category>
      <category domain="https://giwi.fr/categories/Warp-10/">Warp 10</category>
      <category domain="https://giwi.fr/tags/Tutorial/">Tutorial</category>
      <category domain="https://giwi.fr/tags/warp-10/">warp 10</category>
      <category domain="https://giwi.fr/tags/Dicovery/">Dicovery</category>
      <category domain="https://giwi.fr/tags/Docker/">Docker</category>
      <pubDate>Thu, 14 Jul 2022 16:08:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p><a href="https://warp10.io/content/05_Ecosystem/02_Visualization/03_Discovery_Explorer">Discovery Explorer</a> est un site web permettant d’exposer des dashboars <a href="https://warp10.io/content/05_Ecosystem/02_Visualization/02_Discovery/00_Overview">Discovery</a>.<br>Voici une façon simple d’en sécuriser l’accès via du HTTPBasic Auth.</p><p>Nous allons exposer Discovery Explorer sous le path &#x2F;dashboards.</p><p>Prérequis, Docker, DockerCompose et :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> apt install apache2-utils</span></span><br></pre></td></tr></table></figure><p>D’abord, créons un fichier avec les users :</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> htpasswd -c .htpasswd user1</span><br><span class="line"><span class="built_in">sudo</span> htpasswd .htpasswd user2</span><br></pre></td></tr></table></figure><p>Cela vous crée un fichier <code>.htpasswd</code> dont il faudra retenir le path.</p><p>Créez un fichier de conf pour NginX, <code>nginx.conf</code> :</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="attribute">user</span> www-data;</span><br><span class="line"><span class="attribute">worker_processes</span> auto;</span><br><span class="line"><span class="attribute">pid</span> /run/nginx.pid;</span><br><span class="line"><span class="attribute">include</span> /etc/nginx/modules-enabled/<span class="regexp">*.conf</span>;</span><br><span class="line"></span><br><span class="line"><span class="section">events</span> &#123;</span><br><span class="line"><span class="attribute">worker_connections</span> <span class="number">768</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="section">http</span> &#123;</span><br><span class="line">  <span class="attribute">sendfile</span> <span class="literal">on</span>;</span><br><span class="line">  <span class="attribute">tcp_nopush</span> <span class="literal">on</span>;</span><br><span class="line">  <span class="attribute">types_hash_max_size</span> <span class="number">2048</span>;</span><br><span class="line">  <span class="attribute">include</span> /etc/nginx/mime.types;</span><br><span class="line">  <span class="attribute">default_type</span> application/octet-stream;</span><br><span class="line">  <span class="attribute">ssl_protocols</span> TLSv1 TLSv1.<span class="number">1</span> TLSv1.<span class="number">2</span> TLSv1.<span class="number">3</span>; <span class="comment"># Dropping SSLv3, ref: POODLE</span></span><br><span class="line">  <span class="attribute">ssl_prefer_server_ciphers</span> <span class="literal">on</span>;</span><br><span class="line">  <span class="attribute">access_log</span> /var/log/nginx/access.log;</span><br><span class="line">  <span class="attribute">error_log</span> /var/log/nginx/<span class="literal">error</span>.log;</span><br><span class="line">  <span class="attribute">gzip</span> <span class="literal">on</span>;</span><br><span class="line"></span><br><span class="line">  <span class="section">server</span> &#123;</span><br><span class="line">    <span class="attribute">listen</span> <span class="number">80</span> default_server;</span><br><span class="line">    <span class="attribute">listen</span> [::]:<span class="number">80</span> default_server;</span><br><span class="line">    <span class="attribute">root</span> /var/www/html;</span><br><span class="line">    <span class="attribute">index</span> index.html index.htm index.nginx-debian.html;</span><br><span class="line">    <span class="attribute">server_name</span> _;</span><br><span class="line">    <span class="section">location</span> / &#123;</span><br><span class="line">      <span class="attribute">try_files</span> <span class="variable">$uri</span> <span class="variable">$uri</span>/ =<span class="number">404</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="section">location</span> /dashboards &#123;</span><br><span class="line">      <span class="attribute">auth_basic</span> <span class="string">&quot;Restricted Content&quot;</span>; </span><br><span class="line">      <span class="attribute">auth_basic_user_file</span> /etc/nginx/.htpasswd;</span><br><span class="line">      <span class="attribute">proxy_pass</span> http://discovery-explorer:3000;</span><br><span class="line">      <span class="attribute">rewrite</span> /dashboards/api/(.*) /api/<span class="variable">$1</span>  <span class="literal">break</span>;</span><br><span class="line">      <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">      <span class="attribute">proxy_redirect</span> <span class="literal">off</span>;</span><br><span class="line">      <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">      <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Enfin créez un fichier <code>docker-compose.yml</code> :</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&quot;3.9&quot;</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">discovery-explorer:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">warp10io/discovery-explorer:latest</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">/home/me/myDashboards:/data</span>  <span class="comment"># pointe vers l&#x27;endroit où vous avez vos dashboards Discovery</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">BASE_HREF=/dashboards/</span></span><br><span class="line">  <span class="attr">nginx:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">nginx</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;80:80&quot;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">/home/me/nginx.conf:/etc/nginx/nginx.conf:ro</span> <span class="comment"># Fichier de conf créé plus haut</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">/home/me/.htpasswd:/etc/nginx/.htpasswd</span> <span class="comment"># fichiers des users créé plus haut</span></span><br><span class="line">    <span class="attr">links:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">discovery-explorer:discovery-explorer</span></span><br></pre></td></tr></table></figure><p>Et y’a plus qu’a :</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ docker-compose up</span><br></pre></td></tr></table></figure><p>Vous pouvez admirer votre oeuvre sur <a href="http://localhost/dashboards">http://localhost/dashboards</a>.</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Générer des Hash 128 bits sous Linux</title>
      <link>https://giwi.fr/2022-06-16-generer-des-hash-128-bits-sous-linux/</link>
      <description>Il ya plusieurs façon de générer des Hash hexadécimaux sous linux.</description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Linux/">Linux</category>
      <category domain="https://giwi.fr/tags/linux/">linux</category>
      <category domain="https://giwi.fr/tags/bash/">bash</category>
      <pubDate>Thu, 16 Jun 2022 18:24:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Il ya plusieurs façon de générer des Hash hexadécimaux sous linux.</p><h2 id="dev-urandom"><a href="#dev-urandom" class="headerlink" title="&#x2F;dev&#x2F;urandom"></a>&#x2F;dev&#x2F;urandom</h2><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/urandom hexdump -Cv</span></span><br><span class="line">0028cb40  17 9b d8 db 72 41 56 9a  d9 a2 9d 55 e9 5b e3 cf  ....rAV....U.[..</span><br><span class="line">0028cb50  96 a4 48 b8 2d fe cc b4  bd 10 29 3a 74 8d aa 64  ..H.-.....):t..d</span><br><span class="line">0028cb60  32 b3 35 f8 cd 4c 35 de  c9 df 3f b7 44 9e cc 51  2.5..L5...?.D..Q</span><br><span class="line">0028cb70  67 86 b5 8a df ef 53 66  86 06 18 41 d9 5f a1 ca  g.....Sf...A._..</span><br><span class="line">0028cb80  7e 4f 67 71 08 b2 21 47  85 f4 5c 2a d5 3b 8d 3a  ~Ogq..!G..\*.;.:</span><br><span class="line">0028cb90  df b3 7f 29 79 75 75 dc  16 f3 3e b4 c8 78 41 e7  ...)yuu...&gt;..xA.</span><br><span class="line">0028cba0  54 d6 da 6a e5 8a 90 46  9f dd 7f 61 36 0c 81 bd  T..j...F...a6...</span><br><span class="line">0028cbb0  a0 9e 6b 98 03 a0 c8 4d  31 27 19 4a be 23 93 05  ..k....M14.J..^C</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">128 : a0 9e 6b 98 03 a0 c8 4d  31 27 19 4a be 23 93 05 &lt;- dernière ligne <span class="built_in">du</span> dump par exemple</span></span><br></pre></td></tr></table></figure><h2 id="proc-sys-kernel-random-uuid"><a href="#proc-sys-kernel-random-uuid" class="headerlink" title="&#x2F;proc&#x2F;sys&#x2F;kernel&#x2F;random&#x2F;uuid"></a>&#x2F;proc&#x2F;sys&#x2F;kernel&#x2F;random&#x2F;uuid</h2><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cat</span> /proc/sys/kernel/random/uuid  hexdump -Cv</span></span><br><span class="line">00000000  37 64 61 30 64 61 64 38  2d 30 64 36 61 2d 34 65  7da0dad8-0d6a-4e</span><br><span class="line">00000010  35 39 2d 62 63 62 38 2d  33 64 38 66 39 36 34 33  59-bcb8-3d8f9643</span><br><span class="line">00000020  64 63 33 34 0a                                    dc34.</span><br><span class="line">00000025</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">128 : 37 64 61 30 64 61 64 38  2d 30 64 36 61 2d 34 65 &lt;- ici la première ligne</span></span><br></pre></td></tr></table></figure><h2 id="md5sum"><a href="#md5sum" class="headerlink" title="md5sum"></a>md5sum</h2><p>Voici pour finir, la version la plus simple :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$</span><span class="language-bash"><span class="built_in">date</span>  <span class="built_in">md5sum</span>  <span class="built_in">cut</span> -d<span class="string">&#x27;-&#x27;</span> -f 1</span></span><br><span class="line">f332f2806519d8d0d3c37cead8997932</span><br></pre></td></tr></table></figure><p>Si vous en connaissez d’autres, dites le moi.</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Appelez un programme C depuis java avec JNA</title>
      <link>https://giwi.fr/2022-01-24-appelez-un-programme-c-depuis-java-avec-jna/</link>
      <description>Et oui, on peut appeler nativement une lib C, C++ Rust ou Go directement en Java</description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Java/">Java</category>
      <category domain="https://giwi.fr/tags/Java/">Java</category>
      <category domain="https://giwi.fr/tags/Tutorial/">Tutorial</category>
      <category domain="https://giwi.fr/tags/JNA/">JNA</category>
      <pubDate>Mon, 24 Jan 2022 19:34:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Et oui, on peut appeler nativement une lib C, C++ Rust ou Go directement en Java grâce à <a href="https://github.com/java-native-access/jna">JNA</a>.</p><p>En fait c’est facile.</p><p>D’abord, il faut une lib en C. Par exemple en voici une :</p><p><code>helloworld.</code>h</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">char</span> *<span class="title function_">hello</span><span class="params">(<span class="type">char</span>* ch)</span>;</span><br></pre></td></tr></table></figure><p><code>helloworld.</code>c</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;helloworld.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">char</span>* <span class="title function_">hello</span><span class="params">(<span class="type">char</span>* ch)</span> &#123;</span><br><span class="line">    <span class="type">char</span> prefix[] = <span class="string">&quot;Hello &quot;</span>;</span><br><span class="line">    <span class="type">char</span> *res;</span><br><span class="line">    res = <span class="built_in">malloc</span>(<span class="built_in">strlen</span>(prefix) + <span class="built_in">strlen</span>(ch));</span><br><span class="line">    <span class="built_in">strcpy</span>(res, prefix);</span><br><span class="line">    <span class="built_in">strcat</span>(res,ch);</span><br><span class="line">    <span class="keyword">return</span> res;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Hey, mais, il y a un <code>malloc</code> sans <code>free</code>, ça craint. Bon, pas de soucis, on verra plus loi comment faire un <code>free</code> après l’appel.</p><p>Ok, maintenant, on compile :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">gcc -c helloworld.c</span> </span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">gcc -shared -dynamiclib helloworld.o -o ./helloworld.so</span></span><br></pre></td></tr></table></figure><p>Maintenant, la partie Java. Il faut avoir la lib JNA dans son classpath, bien évidement.</p><p><code>CHelloWorld.java</code></p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> io.warp10.ext.test;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.sun.jna.Library;</span><br><span class="line"><span class="keyword">import</span> com.sun.jna.Native;</span><br><span class="line"><span class="keyword">import</span> com.sun.jna.Pointer;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">CHelloWorld</span> <span class="keyword">extends</span> <span class="title class_">Library</span> &#123;</span><br><span class="line">  <span class="comment">// On charge la lib</span></span><br><span class="line">  <span class="type">CHelloWorld</span> <span class="variable">INSTANCE</span> <span class="operator">=</span> Native.load(<span class="string">&quot;helloworld.so&quot;</span>, CHelloWorld.class);</span><br><span class="line">  <span class="comment">// on décrit l&#x27;interface utilisée</span></span><br><span class="line">  Pointer <span class="title function_">hello</span><span class="params">(String g)</span>;</span><br><span class="line">  <span class="comment">// Youpi, on pourra appeler free</span></span><br><span class="line">  <span class="keyword">void</span> <span class="title function_">free</span><span class="params">(Pointer p)</span>;  <span class="comment">// JNA le fournit (et pas que ça)</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Test.java</code></p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test</span> &#123;</span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    <span class="type">Pointer</span> <span class="variable">ptr</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="comment">// on appelle notre fonction C</span></span><br><span class="line">      ptr = CHelloWorld.INSTANCE.hello(args[<span class="number">0</span>]);</span><br><span class="line">      <span class="comment">// on parse le résultat</span></span><br><span class="line">      System.out.println(ptr.getString(<span class="number">0</span>));</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">      <span class="keyword">throw</span> e;</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">      <span class="comment">// On libère la mémoire</span></span><br><span class="line">      <span class="keyword">if</span> (<span class="literal">null</span> != ptr) &#123;</span><br><span class="line">        CHelloWorld.INSTANCE.free(ptr); </span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>On compile :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">javac -<span class="built_in">cp</span> ./jna-5.10.0.jar ./*.java</span></span><br></pre></td></tr></table></figure><p>On exécute :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">java -<span class="built_in">cp</span> /opt/jna-5.10.0.jar:./* mon.package.Test Kitty</span></span><br><span class="line">Hello Kitty</span><br></pre></td></tr></table></figure><p>Et voilà.</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Mon Linky dans Warp 10 avec un joli dashboard</title>
      <link>https://giwi.fr/2021-05-05-mon-linky-dans-warp-10-avec-un-joli-dashboard/</link>
      <description>Depuis longtemps, je cherchais un moyen simple de récupérer mes données de conso Linky et de les afficher dans un dashboard.</description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/JavaScript/">JavaScript</category>
      <category domain="https://giwi.fr/categories/Warp-10/">Warp 10</category>
      <category domain="https://giwi.fr/tags/warp-10/">warp 10</category>
      <category domain="https://giwi.fr/tags/WarpScript/">WarpScript</category>
      <category domain="https://giwi.fr/tags/JavaScript/">JavaScript</category>
      <category domain="https://giwi.fr/tags/dashboard/">dashboard</category>
      <category domain="https://giwi.fr/tags/Linky/">Linky</category>
      <pubDate>Wed, 05 May 2021 20:11:02 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Depuis longtemps, je cherchais un moyen simple de récupérer mes données de conso Linky et de les afficher dans un dashboard.</p><p>Pour accéder aux données du Linky collectées par Enedis, il n’y a pas 36 solutions :</p><ul><li>Aller sur votre espace perso et cliquer sur un bouton pour télécharger un CSV avec ses données (pas pratique)</li><li>Utiliser une lib qui scrappe ce site pour récupérer ces données au format CSV. Sauf que depuis des mois, ils ont changé la façon de se loguer et ont ajouté un captcha. Il n’y a plus aucune lib de fonctionnelle (<a href="https://blog.senx.io/analyze-your-electrical-consumption/">dommage, ça marchait bien</a>)</li><li>Avoir un numéro de Siret, contractualiser avec Enedis, recueillir son propre consentement (oui, je sais, c’est con) et suivre une procédure très lourde pour se <a href="https://www.enedis.fr/acceder-aux-donnees-de-mesure">connecter à leur SGE</a>.</li><li>Avoir un numéro de Siret, contractualiser avec Enedis, bâtir une app déclarée et se connecter sur leur <a href="https://datahub-enedis.fr/data-connect/">DataHub</a>.</li><li>Utiliser la connexion TéléInfo directement sur le Linky, ce qui fera l’objet d’un prochain post.</li></ul><p>Bref, pour les particuliers qui, comme moi, veulent geeker un peu avec leurs propres données, c’est pas possible. Cependant, autour du DataHub, il y a quelques initiatives de tiers permettant de s’y connecter (via le tiers) comme :</p><ul><li><a href="https://gitlab.com/aeneria/enedis-data-connect">https://gitlab.com/aeneria/enedis-data-connect</a></li><li><a href="https://wiki.consometers.org/projets:dataconnect">https://wiki.consometers.org/projets:dataconnect</a></li></ul><p>C’est sur cet axe, à défaut d’avoir accès à une vraie API en direct, que je suis parti.</p><blockquote><p>Attention, pensez à vous autoriser à collecter vos données horaires dans votre espace client!</p></blockquote><p>Pour commencer, il vous faut un <a href="https://warp.senx.io/">Warp 10</a>. Facile à <a href="https://warp10.io/download">installer en local ou sur un serveur</a> (il y a même un <a href="https://hub.docker.com/r/warp10io/warp10">Docker</a>), vous pouvez même tester dans le cloud avec leur <a href="https://sandbox.senx.io/">SandBox</a>. Ici, on va le faire avec la SandBox (mais dans la vraie vie, j’utilise une instance hébergée sur un Pi, oui, oui, ça tourne bien sur un Pi).</p><h2 id="Preparation-de-Warp-10"><a href="#Preparation-de-Warp-10" class="headerlink" title="Préparation de Warp 10"></a>Préparation de Warp 10</h2><p>Direction <a href="https://sandbox.senx.io/">https://sandbox.senx.io/</a> pour se créer son espace et ses tokens. C’est pas compliqué, un clic suffit :</p><p><img src="/images/2021/05/Peek-05-05-2021-20-27.gif" alt="SandBox Warp 10"></p><p>La puissance à portée d’un clic</p><p>Bon, ça nous laisse soit 2 jours (délai de rétention des données), soit 14 jours (délai des tokens mais il faut ré-uploader les données) pour jouer avec. (<a href="https://blog.senx.io/introducing-the-warp-10-sandbox/">Plus d’info ici</a>)</p><p>Copiez les 3 tokens quelque part et gardez les précieusement.</p><h2 id="Recuperation-des-donnees"><a href="#Recuperation-des-donnees" class="headerlink" title="Récupération des données"></a>Récupération des données</h2><p>Pour se faire, je vais m’appuyer sur cette librairie NodeJS : <a href="https://github.com/bokub/linky">bokub&#x2F;linky</a></p><p>Rien de bien sorcier, si ce n’est qu’on va devoir récupérer des jetons d’OAuth via <a href="https://conso.vercel.app/">https://conso.vercel.app/</a>, un fameux tiers qui se connecte au DataHub Enedis. Suivez ce qui est écrit sur ce site, rien de bien sorcier. Vous aurez à autoriser cette app à se connecter à votre espace perso. Notez également quelque part, les jetons générés et gardez les au chaud.</p><p>Ouvrez un terminal, on va créer un nouveau projet javascript monstrueux :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">mkdir</span> linky2warp10</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cd</span> linky2warp10</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm init</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm install @senx/warp10 dayjs linky node-cron</span></span><br></pre></td></tr></table></figure><p>Créez un fichier <code>conf.json</code> dans lequel vous renseignerez les jetons Linky, votre n° de PDL (n° de compteur Linky, quoi, dispo dans votre espace client) et les tokens Warp 10:</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;accessToken&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xxxxxxxxxxxxxxxxxxx&quot;</span><span class="punctuation">,</span> </span><br><span class="line">  <span class="attr">&quot;refreshToken&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xxxxxxxxxxxxxxxxxxx&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;usagePointId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;n° de PDL&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;warp10&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;w10URL&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://sandbox.senx.io/api/v0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;rt&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lK8M3_ixxxxxxxxxxxx3AsTc2ojYk&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;wt&quot;</span><span class="punctuation">:</span> <span class="string">&quot;hHo5qxxxxxxxxxc7PRUeHa&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;dt&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ttIb7TxxxxxxxxxxxxxxxWir4YynF&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><ul><li><code>w10URL.rt</code> : Read Token</li><li><code>w10URL.wt</code> : Write Token</li><li><code>w10URL.dt</code> : Delete Token (on sait jamais, ça peut servir)</li></ul><p>Créez un fichier <code>index.js</code> :</p><p>D’abord les imports</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> linky = <span class="built_in">require</span>(<span class="string">&#x27;linky&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">&#x27;fs&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> dayjs = <span class="built_in">require</span>(<span class="string">&#x27;dayjs&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Warp10</span> = <span class="built_in">require</span>(<span class="string">&#x27;@senx/warp10&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> cron = <span class="built_in">require</span>(<span class="string">&#x27;node-cron&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> conf = <span class="title class_">JSON</span>.<span class="title function_">parse</span>(fs.<span class="title function_">readFileSync</span>(<span class="string">&#x27;conf.json&#x27;</span>, <span class="string">&#x27;utf-8&#x27;</span>));</span><br><span class="line"><span class="keyword">const</span> w10 = <span class="keyword">new</span> <span class="title class_">Warp10</span>.<span class="title class_">Warp10</span>(conf.<span class="property">warp10</span>.<span class="property">w10URL</span>)</span><br></pre></td></tr></table></figure><p>Ensuite, il faut créer une session Linky avec la gestion du refresh token :</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> session = <span class="keyword">new</span> linky.<span class="title class_">Session</span>(&#123;</span><br><span class="line">  <span class="attr">accessToken</span>: conf.<span class="property">accessToken</span>,</span><br><span class="line">  <span class="attr">refreshToken</span>: conf.<span class="property">refreshToken</span>,</span><br><span class="line">  <span class="attr">usagePointId</span>: conf.<span class="property">usagePointId</span>,</span><br><span class="line">  <span class="attr">onTokenRefresh</span>: <span class="function">(<span class="params">accessToken, refreshToken</span>) =&gt;</span> &#123;</span><br><span class="line">    fs.<span class="title function_">writeFileSync</span>(<span class="string">&#x27;conf.json-bak-&#x27;</span> + <span class="title function_">dayjs</span>().<span class="title function_">valueOf</span>(), <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(conf), <span class="string">&#x27;utf-8&#x27;</span>)</span><br><span class="line">    conf.<span class="property">accessToken</span> = accessToken;</span><br><span class="line">    conf.<span class="property">refreshToken</span> = refreshToken;</span><br><span class="line">    <span class="comment">// Cette fonction sera appelée si les tokens sont renouvelés</span></span><br><span class="line">    <span class="comment">// Les tokens précédents ne seront plus valides</span></span><br><span class="line">    <span class="comment">// Il faudra utiliser ces nouveaux tokens à la prochaine création de session</span></span><br><span class="line">    fs.<span class="title function_">writeFileSync</span>(<span class="string">&#x27;conf.json&#x27;</span>, <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(conf), <span class="string">&#x27;utf-8&#x27;</span>)</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>Ensuite on va se coder la fonction de récupération de l’historique :</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// à partir d&#x27;une date dans le passé et de step en step</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">updateHistory</span>(<span class="params">startDate, step</span>) &#123; </span><br><span class="line">  <span class="keyword">const</span> now = <span class="title function_">dayjs</span>().<span class="title function_">subtract</span>(<span class="number">1</span>, <span class="string">&#x27;day&#x27;</span>);</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">getBetween2Dates</span>(startDate, now.<span class="title function_">format</span>(<span class="string">&#x27;YYYY-MM-DD&#x27;</span>), step)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// entre 2 dates et de step en step</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">getBetween2Dates</span>(<span class="params">startDate, endDate, step = <span class="number">1</span></span>) &#123;</span><br><span class="line">  <span class="keyword">let</span> start = <span class="title function_">dayjs</span>(startDate, <span class="string">&#x27;YYYY-MM-DD&#x27;</span>); </span><br><span class="line">  <span class="keyword">let</span> end = <span class="title function_">dayjs</span>(endDate, <span class="string">&#x27;YYYY-MM-DD&#x27;</span>);</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="title function_">async</span> (resolve, reject) =&gt; &#123;</span><br><span class="line">    <span class="keyword">let</span> sum = <span class="number">0</span>; <span class="comment">// le nombre de données que l&#x27;on va récupérer</span></span><br><span class="line">    <span class="comment">// Zou! on boucle de step en step pour aller du début à la fin de la période temporelle</span></span><br><span class="line">    <span class="keyword">while</span> (end.<span class="title function_">isAfter</span>(start) &amp;&amp; !!session) &#123; </span><br><span class="line">      <span class="keyword">const</span> next = end.<span class="title function_">subtract</span>(step, <span class="string">&#x27;day&#x27;</span>);</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">       <span class="comment">// On récupère notre courbe de charge qui offre un point toutes les 30 minutes</span></span><br><span class="line">        <span class="keyword">const</span> data = <span class="keyword">await</span> session.<span class="title function_">getLoadCurve</span>(next.<span class="title function_">format</span>(<span class="string">&#x27;YYYY-MM-DD&#x27;</span>), end.<span class="title function_">format</span>(<span class="string">&#x27;YYYY-MM-DD&#x27;</span>));</span><br><span class="line">        <span class="keyword">let</span> inputFormat = [];</span><br><span class="line">        data.<span class="property">data</span>.<span class="title function_">forEach</span>(<span class="function"><span class="params">d</span> =&gt;</span> &#123;</span><br><span class="line">          <span class="comment">// On converti au format Warp 10</span></span><br><span class="line">          <span class="keyword">const</span> ts = <span class="title function_">dayjs</span>(d.<span class="property">date</span>, <span class="string">&#x27;YYYY-MM-DD HH:mm:ss&#x27;</span>).<span class="title function_">valueOf</span>() * <span class="number">1000</span>;</span><br><span class="line">          <span class="keyword">if</span> (d.<span class="property">value</span>) &#123;</span><br><span class="line">            <span class="comment">// Pensez à modifier subscribed avec votre puissance souscrite</span></span><br><span class="line">            inputFormat.<span class="title function_">push</span>(<span class="string">`<span class="subst">$&#123;ts&#125;</span>// enedis.linky&#123;unit=W,subscribed=9,pdl=<span class="subst">$&#123;conf.usagePointId&#125;</span>&#125; <span class="subst">$&#123;d.value&#125;</span>`</span>);</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">        <span class="comment">// on pousse notre buffer dans Warp 10</span></span><br><span class="line">        <span class="keyword">await</span> w10.<span class="title function_">update</span>(conf.<span class="property">warp10</span>.<span class="property">wt</span>, inputFormat);</span><br><span class="line">        sum +=  data.<span class="property">data</span>.<span class="property">length</span>;</span><br><span class="line">      &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">error</span>(start.<span class="title function_">format</span>(<span class="string">&#x27;YYYY-MM-DD&#x27;</span>), e);</span><br><span class="line">      &#125;</span><br><span class="line">      end = next;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="title function_">resolve</span>(sum);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Parfait, maintenant on peut récupérer notre historique complet (ou presque) entre le premier Janvier 2015 et maintenant.</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="title function_">updateHistory</span>(<span class="string">&#x27;2015-01-01&#x27;</span>, <span class="number">1</span>).<span class="title function_">then</span>(<span class="function"><span class="params">r</span> =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(r))</span><br></pre></td></tr></table></figure><p>Il arrive que certains jours, Enedis n’ait pas collecté de données, (il y aura des 404 et pas des Peugeots) et si vous tapez trop loin dans le passé, vous aurez un 500 (de mémoire) pour vous dire qu’Enedis n’a pas collecté de données avant que vous ayez activé votre collecte de votre conso horaire, donc soyez raisonnable quant au choix dans la date (arf).</p><p>Une fois fini, vous avez un historique quasi complet. Mais il faut le faire tourner tous les jours :</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line">cron.<span class="title function_">schedule</span>(<span class="string">&#x27;0 1 * * *&#x27;</span>, <span class="function">() =&gt;</span> &#123; <span class="comment">// 1 h du mat tous les jours</span></span><br><span class="line">  <span class="keyword">const</span> start = <span class="title function_">dayjs</span>().<span class="title function_">subtract</span>(<span class="number">5</span>, <span class="string">&#x27;day&#x27;</span>); <span class="comment">// les 5 derniers jours</span></span><br><span class="line">  <span class="title function_">updateHistory</span>(start.<span class="title function_">format</span>(<span class="string">&#x27;YYYY-MM-DD&#x27;</span>), <span class="number">1</span>).<span class="title function_">then</span>(<span class="function"><span class="params">r</span> =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(r));</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>Y’a plus qu’à déployé ce script sur un Raspberry (ou ce que vous voulez), le lancer et il collectera quotidiennement vos données.</p><h2 id="Afficher-mes-donnees"><a href="#Afficher-mes-donnees" class="headerlink" title="Afficher mes données"></a>Afficher mes données</h2><p>Pour ce faire, allez sur <a href="http://studio.senx.io/">http://studio.senx.io/</a> et sélectionnez la Sandbox dans la liste des endpoints.</p><p><img src="/images/2021/05/screenshot-studio.senx_.io-2021.05.05-21_00_56.png" alt="WarpStudio"></p><p>WarpStudio</p><p>Codons notre premier scripte pour afficher 30 jours d’historique. (remplacez bien les textes par vos infos ;) )</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;votre token de lecture&#x27;</span> <span class="string">&#x27;token&#x27;</span> STORE</span><br><span class="line"><span class="string">&#x27;n° de PDL&#x27;</span> <span class="string">&#x27;pdl&#x27;</span> STORE</span><br><span class="line">[ $token <span class="string">&#x27;enedis.linky&#x27;</span> &#123; <span class="string">&#x27;pdl&#x27;</span> $pdl &#125; NOW <span class="number">30</span> d ] FETCH</span><br></pre></td></tr></table></figure><p>Cliquez sur l’onglet ‘Dataviz’ et admirez en bavant votre courbe de charge exprimée en W :</p><p><img src="/images/2021/05/screenshot-studio.senx_.io-2021.05.05-21_04_35-1024x287.png" alt="Courbe de charge"></p><p>Ma conso à moi</p><p>Déjà, c’est cool, mais j’en veux plus, je veux un vrai dashboard qui claque!</p><p>On va utiliser Discovery (<a href="/monitorer-son-infra-avec-warp-10-partie-3/">décrit ici</a>). D’abord, le squelette. Créez un fichier <code>index.html</code> :</p><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">title</span>&gt;</span>Linky dashboard<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">discovery-dashboard</span> <span class="attr">url</span>=<span class="string">&quot;https://sandbox.senx.io/api/v0/exec&quot;</span> <span class="attr">cols</span>=<span class="string">&quot;12&quot;</span> <span class="attr">cell-height</span>=<span class="string">&quot;160&quot;</span>&gt;</span></span><br><span class="line">// Le WarpScript va ici</span><br><span class="line">  <span class="tag">&lt;/<span class="name">discovery-dashboard</span>&gt;</span></span><br><span class="line"><span class="comment">&lt;!-- Les imports --&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">script</span> <span class="attr">nomodule</span> <span class="attr">src</span>=<span class="string">&quot;https://unpkg.com/@senx/discovery-widgets/dist/discovery/discovery.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">&quot;module&quot;</span> <span class="attr">src</span>=<span class="string">&quot;https://unpkg.com/@senx/discovery-widgets/dist/discovery/discovery.esm.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure><p>Et maintenant le code WarpScript :</p><p>Pour les tuiles (à ajouter dans <code>tiles</code>), on va récupérer souvent un an, un mois et 24 heures d’historique, la dernière conso et la dernière date de synchro. Plutôt que de faire un accès à la BDD à chaque tuile, on va mutualiser cette récupération et on va présenter l’info différemment par tuile.</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;n° de PDL&#x27;</span> <span class="string">&#x27;pdl&#x27;</span> STORE</span><br><span class="line"><span class="string">&#x27;votre read token&#x27;</span> <span class="string">&#x27;token&#x27;</span> STORE</span><br><span class="line"><span class="comment">// on récupère 1 an qu&#x27;on met dans &#x27;1y&#x27;</span></span><br><span class="line">[ $token <span class="string">&#x27;enedis.linky&#x27;</span> &#123; <span class="string">&#x27;pdl&#x27;</span> $pdl &#125; NOW <span class="number">365</span> d ] FETCH <span class="string">&#x27;1y&#x27;</span> STORE </span><br><span class="line"><span class="comment">// on coupe pour ne garder que 30 jours, stoké dans &#x27;30d&#x27;</span></span><br><span class="line">$<span class="number">1</span>y NOW <span class="number">30</span> d TIMECLIP <span class="string">&#x27;30d&#x27;</span> STORE </span><br><span class="line"><span class="comment">// on récupère la dernière date connue</span></span><br><span class="line">$<span class="number">30</span>d <span class="number">0</span> GET LASTTICK <span class="string">&#x27;lasttick&#x27;</span> STORE </span><br><span class="line"><span class="comment">// la dernière valeur connue</span></span><br><span class="line">$<span class="number">30</span>d $lasttick ATTICK <span class="number">0</span> GET REVERSE <span class="number">0</span> GET <span class="string">&#x27;lastvalue&#x27;</span> STORE</span><br><span class="line"><span class="comment">// et les dernières 24 heures connues que l&#x27;on place dans &#x27;24h&#x27;</span></span><br><span class="line">$<span class="number">30</span>d $lasttick <span class="number">24</span> h TIMECLIP <span class="string">&#x27;24h&#x27;</span> STORE </span><br></pre></td></tr></table></figure><p>Ensuite la coquille du dashboard :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Linky&#x27;</span></span><br><span class="line">  <span class="string">&#x27;description&#x27;</span> <span class="string">&#x27;Personal electric consumption&#x27;</span></span><br><span class="line">  <span class="string">&#x27;options&#x27;</span> &#123; <span class="string">&#x27;scheme&#x27;</span> <span class="string">&#x27;CHARTANA&#x27;</span> &#125;</span><br><span class="line">  <span class="string">&#x27;tiles&#x27;</span> [</span><br><span class="line"><span class="comment">// ... les tuiles ici</span></span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Enfin, les tuiles.</p><h3 id="La-moyenne-annuelle"><a href="#La-moyenne-annuelle" class="headerlink" title="La moyenne annuelle"></a>La moyenne annuelle</h3><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">   <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;1 year average&#x27;</span></span><br><span class="line">   <span class="string">&#x27;x&#x27;</span> <span class="number">2</span> <span class="string">&#x27;y&#x27;</span> <span class="number">0</span> <span class="string">&#x27;w&#x27;</span> <span class="number">2</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span></span><br><span class="line">   <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;display&#x27;</span> <span class="string">&#x27;unit&#x27;</span> <span class="string">&#x27;kW&#x27;</span></span><br><span class="line">   <span class="string">&#x27;data&#x27;</span> [ $<span class="number">1</span>y bucketizer.mean <span class="number">0</span> <span class="number">0</span> <span class="number">1</span> ] BUCKETIZE <span class="number">0</span> GET <span class="string">&#x27;gts&#x27;</span> STORE <span class="comment">// calcul de la moyenne</span></span><br><span class="line">                <span class="comment">// récupération de la valeur et conversion/arrondi en kW</span></span><br><span class="line">                $gts VALUES <span class="number">0</span> GET <span class="number">1000.0</span> / <span class="number">1000.0</span> * ROUND <span class="number">1000.0</span> / </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="La-derniere-conso-connue"><a href="#La-derniere-conso-connue" class="headerlink" title="La dernière conso connue"></a>La dernière conso connue</h3><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Last known consumption&#x27;</span></span><br><span class="line">  <span class="string">&#x27;x&#x27;</span> <span class="number">4</span> <span class="string">&#x27;y&#x27;</span> <span class="number">0</span> <span class="string">&#x27;w&#x27;</span> <span class="number">2</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span></span><br><span class="line">  <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;display&#x27;</span> <span class="string">&#x27;unit&#x27;</span> <span class="string">&#x27;kW&#x27;</span></span><br><span class="line">  <span class="string">&#x27;data&#x27;</span> $lastvalue <span class="number">1000.0</span> / <span class="number">1000.0</span> * ROUND <span class="number">1000.0</span> / <span class="comment">// conversion/arrondi en kW</span></span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><h3 id="La-date-de-derniere-synchro"><a href="#La-date-de-derniere-synchro" class="headerlink" title="La date de dernière synchro"></a>La date de dernière synchro</h3><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Last sync date&#x27;</span></span><br><span class="line">  <span class="string">&#x27;x&#x27;</span> <span class="number">0</span> <span class="string">&#x27;y&#x27;</span> <span class="number">0</span> <span class="string">&#x27;w&#x27;</span> <span class="number">2</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span></span><br><span class="line">  <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;display&#x27;</span></span><br><span class="line">  <span class="string">&#x27;data&#x27;</span>  &#123; <span class="string">&#x27;data&#x27;</span> $lasttick <span class="string">&#x27;globalParams&#x27;</span> &#123; <span class="string">&#x27;timeMode&#x27;</span> <span class="string">&#x27;duration&#x27;</span> &#125; &#125;</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><h3 id="Les-derniere-24-heures-connues"><a href="#Les-derniere-24-heures-connues" class="headerlink" title="Les dernière 24 heures connues"></a>Les dernière 24 heures connues</h3><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Last 24h of data &#x27;</span></span><br><span class="line">  <span class="string">&#x27;x&#x27;</span> <span class="number">6</span> <span class="string">&#x27;y&#x27;</span> <span class="number">0</span> <span class="string">&#x27;w&#x27;</span> <span class="number">4</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span></span><br><span class="line">  <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;bar&#x27;</span></span><br><span class="line">   <span class="comment">// On ne garde que le max horaire</span></span><br><span class="line">   <span class="string">&#x27;data&#x27;</span> [ $<span class="number">24</span>h bucketizer.max <span class="number">0</span> <span class="number">1</span> h <span class="number">0</span> ] BUCKETIZE </span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><h3 id="Le-Max-sur-les-derniers-30-jours"><a href="#Le-Max-sur-les-derniers-30-jours" class="headerlink" title="Le Max sur les derniers 30 jours"></a>Le Max sur les derniers 30 jours</h3><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Max 1 month&#x27;</span></span><br><span class="line">  <span class="string">&#x27;x&#x27;</span> <span class="number">10</span> <span class="string">&#x27;y&#x27;</span> <span class="number">2</span> <span class="string">&#x27;w&#x27;</span> <span class="number">2</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span></span><br><span class="line">  <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;display&#x27;</span> <span class="string">&#x27;unit&#x27;</span> <span class="string">&#x27;kW&#x27;</span></span><br><span class="line">  <span class="string">&#x27;data&#x27;</span> [ $<span class="number">30</span>d bucketizer.max <span class="number">0</span> <span class="number">0</span> <span class="number">1</span> ] BUCKETIZE <span class="number">0</span> GET <span class="string">&#x27;gts&#x27;</span> STORE <span class="comment">// recherche du max</span></span><br><span class="line">     <span class="comment">// récupération de la valeur et conversion/arrondi en kW</span></span><br><span class="line">    $gts VALUES <span class="number">0</span> GET <span class="number">1000.0</span> / <span class="number">1000.0</span> * ROUND <span class="number">1000.0</span> / <span class="string">&#x27;data&#x27;</span> STORE </span><br><span class="line">    <span class="comment">// récupération de la puissance souscrite (en kW)</span></span><br><span class="line">    $gts LABELS <span class="string">&#x27;subscribed&#x27;</span> GET TODOUBLE <span class="string">&#x27;subscribed&#x27;</span> STORE </span><br><span class="line">      &#123;</span><br><span class="line">        <span class="string">&#x27;data&#x27;</span> $data</span><br><span class="line">        <span class="string">&#x27;globalParams&#x27;</span> &#123;</span><br><span class="line">          <span class="comment">// Si votre conso est &gt; à la puissance souscrite alors la tuile aura un fond rouge. </span></span><br><span class="line">          <span class="comment">// elle sera verte sinon</span></span><br><span class="line">          <span class="string">&#x27;bgColor&#x27;</span> &lt;% $data $subscribed &lt;= %&gt; &lt;% <span class="string">&#x27;#32cb0099&#x27;</span> %&gt; &lt;% <span class="string">&#x27;#ff616f99&#x27;</span> %&gt; IFTE</span><br><span class="line">          <span class="string">&#x27;fontColor&#x27;</span> <span class="string">&#x27;#ffffff&#x27;</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="La-max-sur-un-an"><a href="#La-max-sur-un-an" class="headerlink" title="La max sur un an"></a>La max sur un an</h3><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Max in 1 year&#x27;</span></span><br><span class="line">  <span class="string">&#x27;x&#x27;</span> <span class="number">10</span> <span class="string">&#x27;y&#x27;</span> <span class="number">1</span> <span class="string">&#x27;w&#x27;</span> <span class="number">2</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span></span><br><span class="line">  <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;display&#x27;</span> <span class="string">&#x27;unit&#x27;</span> <span class="string">&#x27;kW&#x27;</span></span><br><span class="line">  <span class="string">&#x27;data&#x27;</span> [ $<span class="number">1</span>y bucketizer.max <span class="number">0</span> <span class="number">0</span> <span class="number">1</span> ] BUCKETIZE <span class="number">0</span> GET <span class="string">&#x27;gts&#x27;</span> STORE</span><br><span class="line">    <span class="comment">// récupération de la valeur et conversion/arrondi en kW</span></span><br><span class="line">    $gts VALUES <span class="number">0</span> GET <span class="number">1000.0</span> / <span class="number">1000.0</span> * ROUND <span class="number">1000.0</span> / <span class="string">&#x27;data&#x27;</span> STORE</span><br><span class="line">    $gts LABELS <span class="string">&#x27;subscribed&#x27;</span> GET <span class="string">&#x27;subscribed&#x27;</span> STORE</span><br><span class="line">    &#123; <span class="string">&#x27;data&#x27;</span> $data <span class="string">&#x27;globalParams&#x27;</span> &#123;</span><br><span class="line">      <span class="string">&#x27;bgColor&#x27;</span> &lt;% $data $subscribed TODOUBLE &lt;= %&gt; &lt;% <span class="string">&#x27;#32cb0099&#x27;</span> %&gt; &lt;% <span class="string">&#x27;#ff616f99&#x27;</span> %&gt; IFTE</span><br><span class="line">      <span class="string">&#x27;fontColor&#x27;</span> <span class="string">&#x27;#ffffff&#x27;</span></span><br><span class="line">      &#125;</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Le-max-sur-24-heures"><a href="#Le-max-sur-24-heures" class="headerlink" title="Le max sur 24 heures"></a>Le max sur 24 heures</h3><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Max in 24 h&#x27;</span></span><br><span class="line">  <span class="string">&#x27;x&#x27;</span> <span class="number">10</span> <span class="string">&#x27;y&#x27;</span> <span class="number">0</span> <span class="string">&#x27;w&#x27;</span> <span class="number">2</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span></span><br><span class="line">  <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;display&#x27;</span> <span class="string">&#x27;unit&#x27;</span> <span class="string">&#x27;kW&#x27;</span></span><br><span class="line">  <span class="string">&#x27;data&#x27;</span> [ $<span class="number">24</span>h bucketizer.max <span class="number">0</span> <span class="number">0</span> <span class="number">1</span> ] BUCKETIZE <span class="number">0</span> GET <span class="string">&#x27;gts&#x27;</span> STORE</span><br><span class="line">   <span class="comment">// récupération de la valeur et conversion/arrondi en kW</span></span><br><span class="line">  $gts VALUES <span class="number">0</span> GET <span class="number">1000.0</span> / <span class="number">1000.0</span> * ROUND <span class="number">1000.0</span> / <span class="string">&#x27;data&#x27;</span> STORE</span><br><span class="line">  $gts LABELS <span class="string">&#x27;subscribed&#x27;</span> GET <span class="string">&#x27;subscribed&#x27;</span> STORE</span><br><span class="line">  &#123; <span class="string">&#x27;data&#x27;</span> $data <span class="string">&#x27;globalParams&#x27;</span> &#123;</span><br><span class="line">    <span class="string">&#x27;bgColor&#x27;</span> &lt;% $data $subscribed TODOUBLE &lt;= %&gt; &lt;% <span class="string">&#x27;#32cb0099&#x27;</span> %&gt; &lt;% <span class="string">&#x27;#ff616f99&#x27;</span> %&gt; IFTE</span><br><span class="line">    <span class="string">&#x27;fontColor&#x27;</span> <span class="string">&#x27;#ffffff&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="La-consommation-sur-les-30-derniers-jours"><a href="#La-consommation-sur-les-30-derniers-jours" class="headerlink" title="La consommation sur les 30 derniers jours"></a>La consommation sur les 30 derniers jours</h3><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Last month consumption&#x27;</span></span><br><span class="line">  <span class="string">&#x27;x&#x27;</span> <span class="number">0</span> <span class="string">&#x27;y&#x27;</span> <span class="number">1</span> <span class="string">&#x27;w&#x27;</span> <span class="number">5</span> <span class="string">&#x27;h&#x27;</span> <span class="number">2</span></span><br><span class="line">  <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;line&#x27;</span></span><br><span class="line">  <span class="comment">// on calcule le max par tranche de 3 heures</span></span><br><span class="line">  <span class="string">&#x27;data&#x27;</span> [ $<span class="number">30</span>d bucketizer.max NOW <span class="number">3</span> h <span class="number">0</span> ] BUCKETIZE <span class="number">0</span> GET <span class="string">&#x27;conso&#x27;</span> STORE</span><br><span class="line">  <span class="comment">// on récupère la puissance souscrite </span></span><br><span class="line">  $conso LABELS <span class="string">&#x27;subscribed&#x27;</span> GET TOLONG <span class="number">1000</span> * <span class="string">&#x27;subscribed&#x27;</span> STORE</span><br><span class="line">  <span class="comment">// On récupère les bornes temporelles min et max</span></span><br><span class="line">  $conso TICKLIST REVERSE <span class="number">0</span> GET <span class="string">&#x27;first&#x27;</span> STORE</span><br><span class="line">  $conso LASTTICK <span class="string">&#x27;last&#x27;</span> STORE</span><br><span class="line">  <span class="comment">// on se crée une courbe pour afficher une ligne correspondant à la puissance souscrite</span></span><br><span class="line">  NEWGTS <span class="string">&#x27;subscribed&#x27;</span> RENAME <span class="string">&#x27;psGTS&#x27;</span> STORE</span><br><span class="line">  <span class="comment">// min</span></span><br><span class="line">  $psGTS $first NaN NaN NaN $subscribed ADDVALUE DROP</span><br><span class="line">  <span class="comment">// max</span></span><br><span class="line">  $psGTS $last NaN NaN NaN $subscribed ADDVALUE DROP</span><br><span class="line">  <span class="comment">// On met en forme avec des couleurs</span></span><br><span class="line">  &#123; <span class="string">&#x27;data&#x27;</span> [ $conso $psGTS  ] <span class="string">&#x27;params&#x27;</span> [ &#123; <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;area&#x27;</span> &#125; &#123; <span class="string">&#x27;datasetColor&#x27;</span> <span class="string">&#x27;#ef5350&#x27;</span> &#125; ] &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="La-consommation-annuelle"><a href="#La-consommation-annuelle" class="headerlink" title="La consommation annuelle"></a>La consommation annuelle</h3><p>Pareil que précédemment.</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Last year consumption&#x27;</span></span><br><span class="line">  <span class="string">&#x27;x&#x27;</span> <span class="number">5</span> <span class="string">&#x27;y&#x27;</span> <span class="number">1</span> <span class="string">&#x27;w&#x27;</span> <span class="number">5</span> <span class="string">&#x27;h&#x27;</span> <span class="number">2</span></span><br><span class="line">  <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;line&#x27;</span></span><br><span class="line">  <span class="string">&#x27;data&#x27;</span> [ $<span class="number">1</span>y bucketizer.max NOW <span class="number">1</span> d <span class="number">0</span> ] BUCKETIZE <span class="number">0</span> GET <span class="string">&#x27;conso&#x27;</span> STORE</span><br><span class="line">    $conso</span><br><span class="line">    $conso LABELS <span class="string">&#x27;subscribed&#x27;</span> GET TOLONG <span class="number">1000</span> * <span class="string">&#x27;subscribed&#x27;</span> STORE</span><br><span class="line">    $conso TICKLIST REVERSE <span class="number">0</span> GET <span class="string">&#x27;first&#x27;</span> STORE</span><br><span class="line">    $conso LASTTICK <span class="string">&#x27;last&#x27;</span> STORE</span><br><span class="line">    NEWGTS <span class="string">&#x27;subscribed&#x27;</span> RENAME <span class="string">&#x27;psGTS&#x27;</span> STORE</span><br><span class="line">    $psGTS $first NaN NaN NaN $subscribed ADDVALUE DROP</span><br><span class="line">    $psGTS $last NaN NaN NaN $subscribed ADDVALUE</span><br><span class="line">    <span class="number">2</span> -&gt;LIST  <span class="string">&#x27;data&#x27;</span> STORE</span><br><span class="line">    &#123; <span class="string">&#x27;data&#x27;</span> [ $conso $psGTS  ] <span class="string">&#x27;params&#x27;</span> [ &#123; <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;area&#x27;</span> &#125; &#123; <span class="string">&#x27;datasetColor&#x27;</span> <span class="string">&#x27;#ef5350&#x27;</span> &#125; ] &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Le-resultat"><a href="#Le-resultat" class="headerlink" title="Le résultat"></a>Le résultat</h2><p>Et donc, en ajoutant un peu de CSS à notre page Web :</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="keyword">@import</span> url(<span class="string">&#x27;https://fonts.googleapis.com/css2?family=Kanit:wght@200&amp;display=swap&#x27;</span>);</span><br><span class="line"></span><br><span class="line">* &#123;</span><br><span class="line">    <span class="attribute">box-sizing</span>: border-box;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-pseudo">:root</span> &#123;</span><br><span class="line">    <span class="attr">--wc-split-gutter-color</span>: <span class="number">#404040</span>;</span><br><span class="line">    <span class="attr">--warp-view-pagination-bg-color</span>: <span class="number">#343a40</span> <span class="meta">!important</span>;</span><br><span class="line">    <span class="attr">--warp-view-pagination-border-color</span>: <span class="number">#6c757d</span>;</span><br><span class="line">    <span class="attr">--warp-view-datagrid-odd-bg-color</span>: <span class="built_in">rgba</span>(<span class="number">255</span>, <span class="number">255</span>, <span class="number">255</span>, .<span class="number">05</span>);</span><br><span class="line">    <span class="attr">--warp-view-datagrid-odd-color</span>: <span class="number">#FFFFFF</span>;</span><br><span class="line">    <span class="attr">--warp-view-datagrid-even-bg-color</span>: <span class="number">#212529</span>;</span><br><span class="line">    <span class="attr">--warp-view-datagrid-even-color</span>: <span class="number">#FFFFFF</span>;</span><br><span class="line">    <span class="attr">--warp-view-font-color</span>: <span class="number">#FFFFFF</span>;</span><br><span class="line">    <span class="attr">--warp-view-chart-label-color</span>: <span class="number">#FFFFFF</span>;</span><br><span class="line">    <span class="attr">--gts-stack-font-color</span>: <span class="number">#FFFFFF</span>;</span><br><span class="line">    <span class="attr">--warp-view-resize-handle-color</span>: <span class="number">#111111</span>;</span><br><span class="line">    <span class="attr">--warp-view-chart-legend-bg</span>: <span class="number">#000</span>;</span><br><span class="line">    <span class="attr">--gts-labelvalue-font-color</span>: <span class="number">#ccc</span>;</span><br><span class="line">    <span class="attr">--gts-separator-font-color</span>: <span class="number">#FFFFFF</span>;</span><br><span class="line">    <span class="attr">--gts-labelname-font-color</span>: <span class="built_in">rgb</span>(<span class="number">105</span>, <span class="number">223</span>, <span class="number">184</span>);</span><br><span class="line">    <span class="attr">--gts-classname-font-color</span>: <span class="built_in">rgb</span>(<span class="number">126</span>, <span class="number">189</span>, <span class="number">245</span>);</span><br><span class="line">    <span class="attr">--warp-view-chart-legend-color</span>: <span class="number">#FFFFFF</span>;</span><br><span class="line">    <span class="attr">--wc-tab-header-color</span>: <span class="number">#FFFFFF</span>;</span><br><span class="line">    <span class="attr">--wc-tab-header-selected-color</span>: <span class="number">#404040</span>;</span><br><span class="line">    <span class="attr">--warp-view-tile-background</span>: <span class="number">#40404066</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-tag">body</span> &#123;</span><br><span class="line">    <span class="attribute">font-family</span>: <span class="string">&#x27;Kanit&#x27;</span>, sans-serif;</span><br><span class="line">    <span class="attribute">font-size</span>: <span class="number">12px</span>;</span><br><span class="line">    <span class="attribute">line-height</span>: <span class="number">1.52</span>;</span><br><span class="line">    <span class="attribute">color</span>: <span class="number">#1b1b1b</span>;</span><br><span class="line">    <span class="attribute">background</span>: <span class="number">#FAFBFF</span> <span class="built_in">linear-gradient</span>(<span class="number">40deg</span>, <span class="number">#3BBC7D</span>, <span class="number">#1D434C</span>) fixed;</span><br><span class="line">    <span class="attribute">padding</span>: <span class="number">1rem</span>;</span><br><span class="line">    <span class="attribute">height</span>: <span class="built_in">calc</span>(<span class="number">100vh</span> - <span class="number">2rem</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">discovery-dashboard &#123;</span><br><span class="line">    <span class="attribute">color</span>: transparent;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>On obtient :</p><p><img src="/images/2021/05/screenshot-localhost_63342-2021.05.05-22_03_16-1024x336.png" alt="le résultat"></p><p>et hop</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Monitorer son infra avec Warp 10 – partie 3</title>
      <link>https://giwi.fr/2021-05-03-monitorer-son-infra-avec-warp-10-partie-3/</link>
      <description>
        <![CDATA[<p>Monitoring de raspberry, la suite.</p>
<p>Maintenant que l’on a <a href="/2020-12-21-monitorer-son-infra-avec-warp-10-partie-1">instrumen]]>
      </description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Linux/">Linux</category>
      <category domain="https://giwi.fr/categories/DevOps/">DevOps</category>
      <category domain="https://giwi.fr/categories/Warp-10/">Warp 10</category>
      <category domain="https://giwi.fr/tags/monitoring/">monitoring</category>
      <category domain="https://giwi.fr/tags/warp-10/">warp 10</category>
      <category domain="https://giwi.fr/tags/WarpScript/">WarpScript</category>
      <pubDate>Mon, 03 May 2021 21:19:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Monitoring de raspberry, la suite.</p><p>Maintenant que l’on a <a href="/2020-12-21-monitorer-son-infra-avec-warp-10-partie-1">instrumenté</a> et <a href="/2021-03-16-monitorer-son-infra-avec-warp-10-partie-2">analysé</a> son infra, il est temps d’afficher tout ça dans un dashboard.</p><p>Il y a plusieurs façon d’e faire de la dataviz avec <a href="https://warp10.io/">Warp 10</a>.</p><ul><li>Coder <a href="https://warp10.io/content/03_Documentation/03_Interacting_with_Warp_10/09_Analysing_data">sois-même</a> son interface</li><li>Utiliser un outil tout fait comme <a href="https://grafana.com/grafana/plugins/ovh-warp10-datasource/">Grafana</a></li><li>Utiliser <a href="https://senx.github.io/warpview/getting-started">WarpView</a> pour se construire son dashboard</li><li>Utiliser Discovery pour un dashboard as code</li></ul><p>Nous utiliserons ici Discovery. Un dashboard as code a de multiples intérêts :</p><ul><li>versionner son dashboard</li><li>le rendre dynamique en fonction de certains paramètres<ul><li>afficher certaines info quand on dépasse un seuil par exemple</li><li>passer un graphique en rouge en fonction d’un KPI</li><li>…</li></ul></li><li>le réutiliser simplement pour d’autres usages</li><li>le modulariser</li></ul><h2 id="Mode-d’emploi"><a href="#Mode-d’emploi" class="headerlink" title="Mode d’emploi"></a>Mode d’emploi</h2><p>Discovery est très simple à prendre en main. Il s’agit de passer une structure de donnée (que je vais détailler) en <a href="https://warp10.io/content/03_Documentation/04_WarpScript">WarpScript</a> entre 2 balises HTML.</p><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">html</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">title</span>&gt;</span>Raspi-1 dashboard<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">discovery-dashboard</span> <span class="attr">url</span>=<span class="string">&quot;https://your warp 10/api/v0/exec&quot;</span>&gt;</span></span><br><span class="line">// Le WarpScript va ici</span><br><span class="line">  <span class="tag">&lt;/<span class="name">discovery-dashboard</span>&gt;</span></span><br><span class="line"><span class="comment">&lt;!-- Les imports --&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">script</span> <span class="attr">nomodule</span> <span class="attr">src</span>=<span class="string">&quot;https://unpkg.com/@senx/discovery-widgets/dist/discovery/discovery.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">&quot;module&quot;</span> <span class="attr">src</span>=<span class="string">&quot;https://unpkg.com/@senx/discovery-widgets/dist/discovery/discovery.esm.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure><p>Le WarpScript doit produire la structure suivante :</p><ul><li><p>&#x2F;&#x2F; dashboard</p></li><li><p><code>title</code> : Titre du dashboard (optionnel)</p></li><li><p><code>description</code> : Description du dashboard (optionnel)</p></li><li><p><code>options</code> : les options communes pour le dashboard (ce sont les mêmes que pour <a href="https://github.com/senx/warpview/wiki/common-options">WarpView</a> et <a href="https://github.com/senx/warpview/wiki/common-params">ici aussi</a>) (optionnel)</p><ul><li><code>autoRefresh</code> : période en seconde entre 2 refresh des données, -1 pour désactiver (par défaut) (optionnel)</li></ul></li><li><p><code>tiles</code> : Liste des “tuiles” à afficher</p><ul><li><p>&#x2F;&#x2F; tuile</p></li><li><p><code>title</code> : Titre de la tuile (optionnel)</p></li><li><p><code>x, y</code> : position x et y sur la grille (commencent à 0 et la grille fait 12 cases de large)</p></li><li><p><code>w, h</code> : hauteur et largeur en nombre de cases sur la grille (la largeur max est de 12)</p></li><li><p>type : type de graphique (‘line’, ‘area’, ‘scatter’, ‘spline-area’, ‘spline’, ‘step’, ‘step-after’, ‘step-before’, ‘annotation’, ‘bar’, ‘display’, ‘image’, ‘map’, ‘gauge’, ‘circle’, ‘pie’, ‘plot’, ‘doughnut’, ‘rose’, ‘tabular’, ‘button’)</p></li><li><p><code>unit</code> : unité à afficher (optionnel)</p></li><li><p><code>options</code> : les options communes pour le dashboard (ce sont les mêmes que pour <a href="https://github.com/senx/warpview/wiki/common-options">WarpView</a> et <a href="https://github.com/senx/warpview/wiki/common-params">ici aussi</a>) (optionnel)</p><ul><li><code>autoRefresh</code> : période en seconde entre 2 refresh des données, -1 pour désactiver (par défaut) (optionnel et ne concerne que ‘macro’)</li></ul></li><li><p><code>data</code> &#x2F; <code>macro</code></p><ul><li><code>data</code> : le code sera exécuté au moment de l’exécution de la structure du dashboard</li><li><code>macro</code> : le code sera exécuté par la tuile une fois affichée</li></ul></li></ul></li></ul><p>Bon, un exemple sera plus parlant :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Test&#x27;</span></span><br><span class="line">  <span class="string">&#x27;options&#x27;</span> &#123; <span class="string">&#x27;autoRefresh&#x27;</span> <span class="number">10</span> &#125;  <span class="comment">// l&#x27;ensemble du dashboard se rafraîchira toutes les 10 secondes</span></span><br><span class="line">  <span class="string">&#x27;tiles&#x27;</span> [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Utilisation de &quot;macro&quot;&#x27;</span> </span><br><span class="line">      <span class="string">&#x27;x&#x27;</span> <span class="number">0</span> <span class="string">&#x27;y&#x27;</span> <span class="number">0</span> <span class="string">&#x27;w&#x27;</span> <span class="number">6</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span></span><br><span class="line">      <span class="string">&#x27;options&#x27;</span> &#123; <span class="string">&#x27;autoRefresh&#x27;</span> <span class="number">2</span> &#125; <span class="comment">// cette tuile se rafraîchira toutes les 2 secondes</span></span><br><span class="line">      <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;display&#x27;</span></span><br><span class="line">      <span class="string">&#x27;macro&#x27;</span> &lt;% <span class="comment">// C&#x27;est une macro WarpScript</span></span><br><span class="line">        RAND <span class="number">100.0</span> * ROUND</span><br><span class="line">      %&gt;</span><br><span class="line">    &#125;</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Utilisation de &quot;data&quot;&#x27;</span> </span><br><span class="line">      <span class="string">&#x27;x&#x27;</span> <span class="number">6</span> <span class="string">&#x27;y&#x27;</span> <span class="number">0</span> <span class="string">&#x27;w&#x27;</span> <span class="number">6</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span></span><br><span class="line">      <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;display&#x27;</span></span><br><span class="line">      <span class="string">&#x27;data&#x27;</span> RAND <span class="number">100.0</span> * ROUND</span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><img src="/images/2021/05/Peek-03-05-2021-18-39-1.gif" alt="Exemple du dynamisme du dashboard"></p><p>regardez, ça bouge oO</p><h2 id="Notre-dashboard"><a href="#Notre-dashboard" class="headerlink" title="Notre dashboard"></a>Notre dashboard</h2><p>En utilisant ‘data’ on peut calculer au moment de la génération (ou du refresh) du dashboard, avec ‘macro’ c’est en ajax depuis la tuile.</p><p>Bon, cool, passons à nos métriques <a href="/2021-03-16-monitorer-son-infra-avec-warp-10-partie-2">réalisés la dernière fois</a>. On va utiliser ‘data’ car, ça nous permet d’utiliser des variables (genre, le token ou le host name) et on va rafraîchir le dashboard périodiquement.</p><p>Commençons par les variables :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Variables</span></span><br><span class="line"><span class="string">&#x27;your read token&#x27;</span> <span class="string">&#x27;token&#x27;</span> STORE</span><br><span class="line"><span class="string">&#x27;2592000000000&#x27;</span> <span class="string">&#x27;duration&#x27;</span> STORE  <span class="comment">// 1 mois</span></span><br><span class="line"><span class="string">&#x27;rasp1-1&#x27;</span> <span class="string">&#x27;hname&#x27;</span> STORE</span><br></pre></td></tr></table></figure><p>Puis le dashboad (sans les tuiles) :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Raspi-1&#x27;</span></span><br><span class="line">  <span class="string">&#x27;options&#x27;</span> &#123; <span class="string">&#x27;autoRefresh&#x27;</span> <span class="number">30</span> &#125; <span class="comment">// auto refresh de 30 secondes</span></span><br><span class="line">  <span class="string">&#x27;tiles&#x27;</span> [</span><br><span class="line">    <span class="comment">// On va ajouter les tuiles là !</span></span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Tuile d’affichage de l’espace disque :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Disks&#x27;</span></span><br><span class="line">  <span class="string">&#x27;x&#x27;</span> <span class="number">0</span> <span class="string">&#x27;y&#x27;</span> <span class="number">0</span> <span class="string">&#x27;w&#x27;</span> <span class="number">3</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span> <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;gauge&#x27;</span> <span class="string">&#x27;unit&#x27;</span> <span class="string">&#x27;Gb&#x27;</span></span><br><span class="line">  <span class="string">&#x27;data&#x27;</span> [ $token <span class="string">&#x27;~linux.df.bytes.(capacityfree)&#x27;</span> &#123; <span class="string">&#x27;hname&#x27;</span> $hname <span class="string">&#x27;device&#x27;</span> <span class="string">&#x27;~/dev/.*&#x27;</span> &#125; NOW <span class="number">-1</span> ] FETCH  <span class="string">&#x27;gts&#x27;</span> STORE</span><br><span class="line">    [ $gts bucketizer.last NOW <span class="number">0</span> <span class="number">1</span> ] BUCKETIZE [ <span class="string">&#x27;device&#x27;</span> <span class="string">&#x27;mountpoint&#x27;</span> ] PARTITION  <span class="string">&#x27;gts&#x27;</span> STORE</span><br><span class="line">    [] <span class="string">&#x27;data&#x27;</span> STORE</span><br><span class="line">    [] <span class="string">&#x27;params&#x27;</span> STORE</span><br><span class="line">    $gts KEYLIST &lt;%</span><br><span class="line">      <span class="string">&#x27;k&#x27;</span> STORE</span><br><span class="line">      &#123;</span><br><span class="line">        $gts $k GET</span><br><span class="line">        &lt;% <span class="string">&#x27;g&#x27;</span> STORE</span><br><span class="line">          $g NAME <span class="string">&#x27;linux.df.bytes.&#x27;</span> <span class="string">&#x27;&#x27; REPLACE</span></span><br><span class="line"><span class="string">          $g VALUES REVERSE 0 GET</span></span><br><span class="line"><span class="string">        %&gt; FOREACH</span></span><br><span class="line"><span class="string">      &#125; &#x27;</span>vals<span class="string">&#x27; STORE</span></span><br><span class="line"><span class="string">      $data &#123;</span></span><br><span class="line"><span class="string">        &#x27;</span>key<span class="string">&#x27;  $k &#x27;</span>device<span class="string">&#x27; GET &#x27;</span> (<span class="string">&#x27; + $k &#x27;</span>mountpoint<span class="string">&#x27; GET + &#x27;</span>)<span class="string">&#x27; +</span></span><br><span class="line"><span class="string">        &#x27;</span>value<span class="string">&#x27;  $vals &#x27;</span>capacity<span class="string">&#x27; GET $vals &#x27;</span><span class="built_in">free</span><span class="string">&#x27; GET - 1024 / 1024 / 1024.0 / 100 * ROUND 100.0 /</span></span><br><span class="line"><span class="string">      &#125; +! DROP</span></span><br><span class="line"><span class="string">      $params &#123; &#x27;</span>maxValue<span class="string">&#x27; $vals &#x27;</span>capacity<span class="string">&#x27; GET  1024 / 1024 / 1024 /  &#125; +! DROP</span></span><br><span class="line"><span class="string">  %&gt; ASREGS FOREACH</span></span><br><span class="line"><span class="string">  &#123; &#x27;</span>data<span class="string">&#x27; $data &#x27;</span>params<span class="string">&#x27; $params &#125;</span></span><br><span class="line"><span class="string"> &#125;</span></span><br></pre></td></tr></table></figure><p>La tuile pour le réseau :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Network&#x27;</span></span><br><span class="line">  <span class="string">&#x27;x&#x27;</span> <span class="number">0</span> <span class="string">&#x27;y&#x27;</span> <span class="number">1</span> <span class="string">&#x27;w&#x27;</span> <span class="number">3</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span> <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;area&#x27;</span></span><br><span class="line">  <span class="string">&#x27;data&#x27;</span> [ $token <span class="string">&#x27;~linux.proc.net.dev.(receivetransmit).bytes&#x27;</span> &#123; <span class="string">&#x27;hname&#x27;</span> $hname <span class="string">&#x27;iface&#x27;</span> <span class="string">&#x27;eth0&#x27;</span> &#125; NOW $duration TOLONG ] FETCH</span><br><span class="line">    <span class="literal">false</span> RESETS <span class="string">&#x27;gts&#x27;</span> STORE</span><br><span class="line">    [</span><br><span class="line">      [ $gts [] <span class="string">&#x27;linux.proc.net.dev.receive.bytes&#x27;</span> filter.byclass ] FILTER</span><br><span class="line">      [ $gts [] <span class="string">&#x27;linux.proc.net.dev.transmit.bytes&#x27;</span> filter.byclass ] FILTER</span><br><span class="line">    ] FLATTEN [ SWAP bucketizer.mean NOW <span class="number">1</span> h <span class="number">0</span> ] BUCKETIZE  <span class="string">&#x27;gts&#x27;</span> STORE</span><br><span class="line">    [ $gts mapper.delta <span class="number">1</span> <span class="number">0</span> <span class="number">0</span> ] MAP</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>La tuile pour la charge CPU :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Load&#x27;</span></span><br><span class="line">  <span class="string">&#x27;x&#x27;</span> <span class="number">3</span> <span class="string">&#x27;y&#x27;</span> <span class="number">1</span> <span class="string">&#x27;w&#x27;</span> <span class="number">9</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span> <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;area&#x27;</span></span><br><span class="line">  <span class="string">&#x27;data&#x27;</span> &lt;%</span><br><span class="line">    [ $token <span class="string">&#x27;~linux.proc.stat.userhz.(usernicesystemidleiowait)&#x27;</span> &#123; <span class="string">&#x27;hname&#x27;</span> $hname <span class="string">&#x27;cpu&#x27;</span> <span class="string">&#x27;cpu&#x27;</span> &#125; NOW $duration TOLONG ] FETCH <span class="string">&#x27;gts&#x27;</span> STORE</span><br><span class="line">    [ $gts bucketizer.mean NOW <span class="number">1</span> h <span class="number">0</span> ] BUCKETIZE <span class="string">&#x27;gts&#x27;</span> STORE</span><br><span class="line">    [ $gts [] <span class="string">&#x27;~linux.proc.stat.userhz.(usernicesystemidleiowait)&#x27;</span> filter.byclass ] FILTER [ SWAP [] reducer.sum ] REDUCE <span class="number">0</span> GET <span class="string">&#x27;cpuGTS&#x27;</span> STORE</span><br><span class="line">    [ $gts [] <span class="string">&#x27;linux.proc.stat.userhz.idle&#x27;</span> filter.byclass ] FILTER <span class="number">0</span> GET <span class="string">&#x27;idleGTS&#x27;</span> STORE</span><br><span class="line">    [ $gts [] <span class="string">&#x27;~linux.proc.stat.userhz.(iowaitidle)&#x27;</span> filter.byclass ] FILTER [ SWAP [] reducer.sum ] REDUCE <span class="number">0</span> GET <span class="string">&#x27;iowaitGTS&#x27;</span> STORE</span><br><span class="line">    [ $gts [] <span class="string">&#x27;~linux.proc.stat.userhz.(systemidle)&#x27;</span> filter.byclass ] FILTER [ SWAP [] reducer.sum ] REDUCE <span class="number">0</span> GET <span class="string">&#x27;systemGTS&#x27;</span> STORE</span><br><span class="line">    [</span><br><span class="line">      [ [ $cpuGTS $idleGTS ] [] &lt;%</span><br><span class="line">       <span class="string">&#x27;d&#x27;</span> STORE</span><br><span class="line">       $d <span class="number">7</span> GET <span class="number">0</span> GET <span class="string">&#x27;cpu&#x27;</span> STORE</span><br><span class="line">       $d <span class="number">7</span> GET <span class="number">1</span> GET <span class="string">&#x27;idle&#x27;</span> STORE</span><br><span class="line">        <span class="number">1000.0</span> $cpu $idle - * $cpu <span class="number">5</span>  + / <span class="number">10</span> / <span class="string">&#x27;v&#x27;</span> STORE</span><br><span class="line">        [ $d <span class="number">0</span> GET NaN NaN NaN $v ]</span><br><span class="line">      %&gt; ASREGS MACROREDUCER ] REDUCE <span class="number">0</span> GET <span class="string">&#x27;CPU&#x27;</span> RENAME</span><br><span class="line">    [ [ $iowaitGTS $idleGTS ] [] &lt;%</span><br><span class="line">      <span class="string">&#x27;d&#x27;</span> STORE</span><br><span class="line">      $d <span class="number">7</span> GET <span class="number">0</span> GET <span class="string">&#x27;cpu&#x27;</span> STORE</span><br><span class="line">      $d <span class="number">7</span> GET <span class="number">1</span> GET <span class="string">&#x27;idle&#x27;</span> STORE</span><br><span class="line">      <span class="number">1000.0</span> $cpu $idle - * $cpu <span class="number">5</span>  + / <span class="number">10</span> / <span class="string">&#x27;v&#x27;</span> STORE</span><br><span class="line">      [ $d <span class="number">0</span> GET NaN NaN NaN $v ]</span><br><span class="line">   %&gt; ASREGS MACROREDUCER ] REDUCE <span class="number">0</span> GET <span class="string">&#x27;Disk IO&#x27;</span> RENAME</span><br><span class="line">   [ [ $systemGTS $idleGTS ] [] &lt;%</span><br><span class="line">    <span class="string">&#x27;d&#x27;</span> STORE</span><br><span class="line">     $d <span class="number">7</span> GET <span class="number">0</span> GET <span class="string">&#x27;cpu&#x27;</span> STORE</span><br><span class="line">     $d <span class="number">7</span> GET <span class="number">1</span> GET <span class="string">&#x27;idle&#x27;</span> STORE</span><br><span class="line">     <span class="number">1000.0</span> $cpu $idle - * $cpu <span class="number">5</span>  + / <span class="number">10</span> / <span class="string">&#x27;v&#x27;</span> STORE</span><br><span class="line">     [ $d <span class="number">0</span> GET NaN NaN NaN $v ]</span><br><span class="line">   %&gt; ASREGS MACROREDUCER ] REDUCE <span class="number">0</span> GET <span class="string">&#x27;System&#x27;</span> RENAME</span><br><span class="line">  ] %&gt;  &lt;% [] %&gt; &lt;% %&gt; TRY</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><p>Et enfin, la tuile pour la RAM&#x2F;Swap :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;RAM/Swap&#x27;</span></span><br><span class="line">  <span class="string">&#x27;x&#x27;</span> <span class="number">3</span> <span class="string">&#x27;y&#x27;</span> <span class="number">0</span> <span class="string">&#x27;w&#x27;</span> <span class="number">9</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span> <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;area&#x27;</span></span><br><span class="line">  <span class="string">&#x27;data&#x27;</span> [ $token <span class="string">&#x27;~linux.proc.meminfo.(MemFreeSwapFree)&#x27;</span> &#123; <span class="string">&#x27;hname&#x27;</span> $hname &#125; NOW  $duration TOLONG ] FETCH <span class="string">&#x27;gts&#x27;</span> STORE</span><br><span class="line">  [ $gts bucketizer.mean NOW <span class="number">1</span> h <span class="number">0</span> ] BUCKETIZE <span class="string">&#x27;gts&#x27;</span> STORE</span><br><span class="line">  [ $gts <span class="number">1.0</span> <span class="number">1024.0</span> <span class="number">1024.0</span> * <span class="number">1024.0</span> * / mapper.mul <span class="number">0</span> <span class="number">0</span> <span class="number">0</span> ] MAP</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Vous trouverez l’ensemble dans ce <a href="https://gist.github.com/Giwi/022bdcb5a13be390e4bedd1492f909ad">Gist</a>. Et voici le résultat :</p><p><img src="/images/2021/05/Capture-decran-du-2021-05-03-19-12-00-1024x522.png" alt="Mon premier dashboard"></p><p>c’est beau</p><h2 id="Pimp-my-dashboard"><a href="#Pimp-my-dashboard" class="headerlink" title="Pimp my dashboard"></a>Pimp my dashboard</h2><p>Bon, les goûts et les couleurs… On va skiner un peut tout ça.</p><p>D’abord, on peut utiliser un des <a href="https://github.com/senx/warpview/blob/master/projects/warpview-ng/src/lib/utils/color-lib.ts#L39">thèmes de couleurs</a> proposé par la lib, il y en a quelques uns. Choisissons ‘CHARTANA’ au pif.</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;title&#x27;</span> <span class="string">&#x27;Raspi-1&#x27;</span></span><br><span class="line">  <span class="string">&#x27;options&#x27;</span> &#123; <span class="string">&#x27;scheme&#x27;</span> <span class="string">&#x27;CHARTANA&#x27;</span> <span class="string">&#x27;autoRefresh&#x27;</span> <span class="number">30</span> &#125;</span><br><span class="line">  <span class="string">&#x27;tiles&#x27;</span> [ ... ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Ensuite, on peut utiliser quelques variables CSS :</p><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">title</span>&gt;</span>Raspi-1 dashboard<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">style</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">* &#123;</span></span><br><span class="line"><span class="language-css">    <span class="attribute">box-sizing</span>: border-box;</span></span><br><span class="line"><span class="language-css">&#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css"><span class="selector-pseudo">:root</span> &#123;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--wc-split-gutter-color</span>            : <span class="number">#404040</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--warp-view-pagination-bg-color</span>    : <span class="number">#343a40</span> <span class="meta">!important</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--warp-view-pagination-border-color</span>: <span class="number">#6c757d</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--warp-view-datagrid-odd-bg-color</span>  : <span class="built_in">rgba</span>(<span class="number">255</span>, <span class="number">255</span>, <span class="number">255</span>, .<span class="number">05</span>);</span></span><br><span class="line"><span class="language-css">    <span class="attr">--warp-view-datagrid-odd-color</span>     : <span class="number">#FFFFFF</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--warp-view-datagrid-even-bg-color</span> : <span class="number">#212529</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--warp-view-datagrid-even-color</span>    : <span class="number">#FFFFFF</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--warp-view-font-color</span>             : <span class="number">#FFFFFF</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--warp-view-chart-label-color</span>      : <span class="number">#FFFFFF</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--gts-stack-font-color</span>             : <span class="number">#FFFFFF</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--warp-view-resize-handle-color</span>    : <span class="number">#111111</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--warp-view-chart-legend-bg</span>        : <span class="number">#000</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--gts-labelvalue-font-color</span>        : <span class="number">#ccc</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--gts-separator-font-color</span>         : <span class="number">#FFFFFF</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--gts-labelname-font-color</span>         : <span class="built_in">rgb</span>(<span class="number">105</span>, <span class="number">223</span>, <span class="number">184</span>);</span></span><br><span class="line"><span class="language-css">    <span class="attr">--gts-classname-font-color</span>         : <span class="built_in">rgb</span>(<span class="number">126</span>, <span class="number">189</span>, <span class="number">245</span>);</span></span><br><span class="line"><span class="language-css">    <span class="attr">--warp-view-chart-legend-color</span>     : <span class="number">#FFFFFF</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--wc-tab-header-color</span>              : <span class="number">#FFFFFF</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--wc-tab-header-selected-color</span>     : <span class="number">#404040</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attr">--warp-view-tile-background</span>        : <span class="number">#3A3C46</span>;</span></span><br><span class="line"><span class="language-css">&#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css"><span class="selector-tag">body</span> &#123;</span></span><br><span class="line"><span class="language-css">    <span class="attribute">font-size</span>       : <span class="number">12px</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attribute">line-height</span>     : <span class="number">1.52</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attribute">background-color</span>: <span class="number">#333540</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attribute">color</span>           : <span class="number">#FFFFFF</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attribute">padding</span>         : <span class="number">1rem</span>;</span></span><br><span class="line"><span class="language-css">    <span class="attribute">height</span>          : <span class="built_in">calc</span>(<span class="number">100vh</span> - <span class="number">2rem</span>);</span></span><br><span class="line"><span class="language-css">&#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">discovery-dashboard &#123;</span></span><br><span class="line"><span class="language-css">    <span class="attribute">color</span>: transparent;</span></span><br><span class="line"><span class="language-css">&#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">  </span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br></pre></td></tr></table></figure><p>Et voilà :</p><p><img src="/images/2021/05/Capture-decran-du-2021-05-03-19-20-53-1024x536.png" alt="Résultat dark mode du dashboard"></p><p>C’est beau</p><p>On peut aller beaucoup plus loin, on peut par exemple changer la couleur de fond d’une tuile en fonction d’une valeur :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">RAND <span class="number">100.0</span> * ROUND <span class="string">&#x27;value&#x27;</span> STORE <span class="comment">// ce qu&#x27;on surveille, ici du random entre 0 et 100</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;type&#x27;</span> <span class="string">&#x27;display&#x27;</span> <span class="string">&#x27;unit&#x27;</span> <span class="string">&#x27;%25&#x27;</span> <span class="comment">// %</span></span><br><span class="line">   <span class="string">&#x27;w&#x27;</span> <span class="number">1</span> <span class="string">&#x27;h&#x27;</span> <span class="number">1</span> <span class="string">&#x27;x&#x27;</span> <span class="number">0</span> <span class="string">&#x27;y&#x27;</span> <span class="number">1</span></span><br><span class="line">   <span class="string">&#x27;data&#x27;</span> &#123;</span><br><span class="line">     <span class="string">&#x27;data&#x27;</span> $value</span><br><span class="line">    <span class="string">&#x27;globalParams&#x27;</span> &#123;</span><br><span class="line">      <span class="string">&#x27;bgColor&#x27;</span></span><br><span class="line">      &lt;% $value <span class="number">33</span> &lt; %&gt; &lt;% <span class="string">&#x27;#77BE69&#x27;</span> %&gt; <span class="comment">// value &lt; 33 ? -&gt; vert</span></span><br><span class="line">      &lt;% $value <span class="number">66</span> &lt; %&gt; &lt;% <span class="string">&#x27;#FF9830&#x27;</span> %&gt; <span class="comment">// value &lt; 66 ? -&gt; orange</span></span><br><span class="line">      &lt;% <span class="string">&#x27;#F24865&#x27;</span> %&gt; <span class="number">2</span> SWITCH <span class="comment">// sinon rouge</span></span><br><span class="line">      <span class="string">&#x27;fontColor&#x27;</span> <span class="string">&#x27;white&#x27;</span> <span class="comment">// affichage de &#x27;value&#x27; en blanc</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>Bref, faites vous plaisir :)</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Télémétrie de vol d'un drone</title>
      <link>https://giwi.fr/2021-03-22-telemetrie-de-vol-dun-drone/</link>
      <description>J'ai décidé de le &quot;hacker&quot; mon drone et de regarder les données de télémétrie de vol qu'il émet.</description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/JavaScript/">JavaScript</category>
      <category domain="https://giwi.fr/categories/Realisations/">Réalisations</category>
      <category domain="https://giwi.fr/categories/Warp-10/">Warp 10</category>
      <category domain="https://giwi.fr/tags/warp-10/">warp 10</category>
      <category domain="https://giwi.fr/tags/WarpScript/">WarpScript</category>
      <category domain="https://giwi.fr/tags/drone/">drone</category>
      <category domain="https://giwi.fr/tags/JavaScript/">JavaScript</category>
      <category domain="https://giwi.fr/tags/TypeScript/">TypeScript</category>
      <category domain="https://giwi.fr/tags/UDP/">UDP</category>
      <pubDate>Mon, 22 Mar 2021 22:06:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Il y a quelques années, j’ai acheté un petit drone pas cher : <a href="https://www.ryzerobotics.com/fr/tello">Tello</a>. On peut le piloter avec son smartphone au travers d’une communication UDP sur du WiFi. J’ai décidé de le “hacker” et de regarder les données de télémétrie de vol qu’il émet.</p><p>Prenant mon courage à deux bras, j’ai développé une petite librairie NodeJS pour pouvoir le piloter depuis un bout de code, récupérer les données de vol en temps réel et stream la vidéo de la caméra. Après plusieurs heure et litres de café, j’y suis parvenu, vous <a href="https://github.com/Giwi/ryze-tello-sdk">la trouverez là</a>. J’ai des enfants et le but principal était de les faire développer en Javascript en s’amusant. En fait, mes enfant n’y ont prêté aucun intérêt et le seul enfant qui joue avec, c’est moi.</p><p>Si vous possédez ce drone, vous pouvez bien sûr utiliser cette lib. Ce petit drone communique en UDP. Il expose un serveur pour recevoir des commandes et a un client pour transmettre ses données de vol à un serveur UDP tiers.</p><p>Les données de télémétrie disponibles sont :</p><table><thead><tr><th>Name</th><th>Unit</th><th>Description</th></tr></thead><tbody><tr><td><code>h</code></td><td>cm</td><td>altitude</td></tr><tr><td><code>baro</code></td><td>cm</td><td>baromètre</td></tr><tr><td><code>tof</code></td><td>cm</td><td>distance au sol</td></tr><tr><td><code>templ</code></td><td>°C</td><td>temperature mini de la batterie</td></tr><tr><td><code>temph</code></td><td>°C</td><td>temperature maxi de la batterie</td></tr><tr><td><code>pitch</code></td><td>°</td><td>tangage</td></tr><tr><td><code>roll</code></td><td>°</td><td>roulis</td></tr><tr><td><code>yaw</code></td><td>°</td><td>lacet</td></tr><tr><td><code>agx</code></td><td>0.001g</td><td>accéleration x</td></tr><tr><td><code>agy</code></td><td>0.001g</td><td>accéleration y</td></tr><tr><td><code>agz</code></td><td>0.001g</td><td>accelération z</td></tr><tr><td><code>vgx</code></td><td>cm&#x2F;s</td><td>vitesse x</td></tr><tr><td><code>vgy</code></td><td>cm&#x2F;s</td><td>vitesse y</td></tr><tr><td><code>vgz</code></td><td>cm&#x2F;s</td><td>vitesse z</td></tr><tr><td><code>bat</code></td><td>%</td><td>batterie</td></tr></tbody></table><p>données de télémétrie de vol</p><p><img src="https://upload.wikimedia.org/wikipedia/commons/1/1b/A%C3%A9ronef_%28Beech_bimoteur%29_et_ses_axes.png" alt="axes d&#39;un avion"></p><p><a href="https://fr.wikipedia.org/wiki/Pilotage_d%27un_avion">https://fr.wikipedia.org/wiki/Pilotage_d%27un_avion</a></p><h2 id="Warp-10"><a href="#Warp-10" class="headerlink" title="Warp 10"></a>Warp 10</h2><p><a href="https://warp10.io/">Warp 10</a> est une plateforme formidable, à la fois une base de données de <a href="/2021-02-04-tout-sur-le-modele-geo-time-series">séries temporelles</a> (<a href="https://blog.senx.io/what-in-the-world-is-a-time-series-database/">TSDB</a>), un moteur d’analyse avec son langage de script (on verra plus tard) mais surtout une plateforme avec plein de plugins dont un permettant d’instancier un serveur UDP.</p><h3 id="Plugin-UDP"><a href="#Plugin-UDP" class="headerlink" title="Plugin UDP"></a>Plugin UDP</h3><p>Je pars du postulat que vous avez installé Warp 10 dans <code>/opt/warp10</code> (pas dans une image docker hein ;o) ), on va activer le <a href="https://www.warp10.io/content/03_Documentation/07_Extending_Warp_10/15_Native_Plugins/13_UDP_Plugin">plugin UDP</a>. Dans <code>/opt/warp10/etc/conf.d/80-udp-plugin.conf</code> :</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line">// activate the UDP plugin</span><br><span class="line"><span class="attr">warp10.plugin.udp</span> = io.warp10.plugins.udp.UDPWarp10Plugin</span><br><span class="line"><span class="attr">udp.dir</span> = <span class="variable">$&#123;standalone.home&#125;</span>/udp</span><br><span class="line"><span class="attr">udp.period</span> = <span class="number">5000</span></span><br></pre></td></tr></table></figure><p>Il serait bien d’activer l’<a href="https://www.warp10.io/content/03_Documentation/07_Extending_Warp_10/10_Native_Extensions/01_Debug_Extension">extension Debug</a> :</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="attr">warpscript.extension.debug</span> = io.warp10.script.ext.debug.DebugWarpScriptExtension</span><br></pre></td></tr></table></figure><p>Ensuite, il faut créer un répertoire <code>/opt/warp10/udp</code> et y placer un fichier <code>tello.mc2</code> :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;Loading Tello telemetry&#x27;</span> LOGMSG</span><br><span class="line">&#123;</span><br><span class="line">  <span class="string">&#x27;mode&#x27;</span> <span class="string">&#x27;server&#x27;</span></span><br><span class="line">  <span class="string">&#x27;host&#x27;</span> <span class="string">&#x27;0.0.0.0&#x27;</span></span><br><span class="line">  <span class="string">&#x27;port&#x27;</span> <span class="number">8890</span></span><br><span class="line">  <span class="string">&#x27;timeout&#x27;</span> <span class="number">5000</span></span><br><span class="line">  <span class="string">&#x27;macro&#x27;</span> &lt;% </span><br><span class="line">      <span class="string">&#x27;datagramLIST&#x27;</span> STORE</span><br><span class="line">    &lt;% $datagramLIST ISNULL %&gt;</span><br><span class="line">    &lt;%</span><br><span class="line">      <span class="string">&#x27;timeout reached, nothing received&#x27;</span> LOGMSG</span><br><span class="line">    %&gt;</span><br><span class="line">    &lt;%</span><br><span class="line">      $datagramLIST</span><br><span class="line">      &lt;%</span><br><span class="line">        <span class="string">&#x27;datagram&#x27;</span> STORE</span><br><span class="line">        $datagram <span class="number">2</span> GET <span class="string">&#x27;utf-8&#x27;</span> BYTES-&gt; <span class="string">&#x27;dataFrame&#x27;</span> STORE</span><br><span class="line">        $dataFrame LOGMSG</span><br><span class="line">        <span class="string">&#x27;Your write token&#x27;</span> <span class="string">&#x27;token&#x27;</span> STORE <span class="comment">// Insert your write token here</span></span><br><span class="line">        $dataFrame <span class="string">&#x27;;&#x27;</span> SPLIT &lt;%</span><br><span class="line">        <span class="comment">// For each data in the dataframe </span></span><br><span class="line">        TRIM <span class="string">&#x27;data&#x27;</span> STORE</span><br><span class="line">        &lt;% $data <span class="string">&#x27;&#x27; != %&gt; // manage the trailing &#x27;</span>;&#x27;</span><br><span class="line">        &lt;% </span><br><span class="line">            <span class="comment">// extract key and value</span></span><br><span class="line">            $data <span class="string">&#x27;:&#x27;</span> SPLIT LIST-&gt; DROP [ <span class="string">&#x27;key&#x27;</span> <span class="string">&#x27;value&#x27;</span> ] STORE</span><br><span class="line">            NEWGTS <span class="string">&#x27;tello.telemetry.&#x27;</span> $key + RENAME <span class="comment">// create a new GTS</span></span><br><span class="line">            NOW NaN NaN NaN $value TODOUBLE ADDVALUE <span class="comment">// Add the new datapoint</span></span><br><span class="line">            $token UPDATE</span><br><span class="line">        %&gt; IFT</span><br><span class="line">        %&gt; FOREACH</span><br><span class="line">      %&gt; FOREACH</span><br><span class="line">    %&gt; IFTE</span><br><span class="line">  %&gt;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Script-de-pilotage"><a href="#Script-de-pilotage" class="headerlink" title="Script de pilotage"></a>Script de pilotage</h3><p>Ensuite vous pouvez redémarer Warp 10. Connectez le drone via le WiFi et utilisez ce petit javascript pour le piloter :</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">Tello</span>&#125; <span class="keyword">from</span> <span class="string">&quot;@giwisoft/ryze-tello-sdk&quot;</span>;</span><br><span class="line"><span class="keyword">const</span> tello = <span class="keyword">new</span> <span class="title class_">Tello</span>();</span><br><span class="line">(<span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="comment">// Start the engine and play &quot;Ride Of The Valkyries&quot;</span></span><br><span class="line">  <span class="keyword">await</span> tello.<span class="title function_">start</span>();</span><br><span class="line">  <span class="keyword">await</span> tello.<span class="title function_">takeoff</span>();</span><br><span class="line">  <span class="comment">// Go up</span></span><br><span class="line">  <span class="keyword">await</span> tello.<span class="title function_">up</span>(<span class="number">50</span>);</span><br><span class="line">  <span class="comment">// Perform a forward flip</span></span><br><span class="line">  <span class="keyword">await</span> tello.<span class="title function_">flip</span>(<span class="string">&#x27;f&#x27;</span>);</span><br><span class="line">  <span class="comment">// Go forward</span></span><br><span class="line">  <span class="keyword">await</span> tello.<span class="title function_">forward</span>(<span class="number">50</span>);</span><br><span class="line">  <span class="keyword">await</span> tello.<span class="title function_">right</span>(<span class="number">20</span>);</span><br><span class="line">  <span class="comment">// Go backward</span></span><br><span class="line">  <span class="keyword">await</span> tello.<span class="title function_">backward</span>(<span class="number">100</span>);</span><br><span class="line">  <span class="keyword">await</span> tello.<span class="title function_">rotateCW</span>(<span class="number">360</span>);</span><br><span class="line">  <span class="comment">// Finally land</span></span><br><span class="line">  <span class="keyword">await</span> tello.<span class="title function_">land</span>();</span><br><span class="line">  <span class="comment">// And then shut down the engine</span></span><br><span class="line">&#125;)().<span class="title function_">then</span>(<span class="function">() =&gt;</span> tello.<span class="title function_">stop</span>());</span><br></pre></td></tr></table></figure><p>Vous pouvez également streamer la vidéo via mplayer :</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">await</span> tello.<span class="title function_">startStream</span>();</span><br><span class="line">[...]</span><br><span class="line"><span class="keyword">await</span> tello.<span class="title function_">stopStream</span>();</span><br></pre></td></tr></table></figure><p>Balancer la télémétrie en direct sur Warp 10 :</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">await</span> tello.<span class="title function_">startTelemetry</span>(&#123;</span><br><span class="line">  <span class="attr">withWarp10</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">warp10Params</span>: &#123;</span><br><span class="line">    <span class="attr">url</span>: <span class="string">&#x27;http://localhost:8080/api/v0/exec&#x27;</span>,</span><br><span class="line">    <span class="attr">writeToken</span>: <span class="string">&#x27;Token d\&#x27;écriture&#x27;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>Balancer la télémétrie en direct sur du MQTT</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">await</span> tello.<span class="title function_">startTelemetry</span>(&#123;</span><br><span class="line">  <span class="attr">withMqtt</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">mqttParams</span>: &#123;</span><br><span class="line">    <span class="attr">url</span>: <span class="string">&#x27;http://localhost:1883&#x27;</span>,</span><br><span class="line">    <span class="attr">clientId</span>: <span class="string">&#x27;tello&#x27;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>Voici un petit serveur MQTT en JavaScript pour tester :</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> aedes = <span class="built_in">require</span>(<span class="string">&#x27;aedes&#x27;</span>)();</span><br><span class="line"><span class="keyword">const</span> server = <span class="built_in">require</span>(<span class="string">&#x27;net&#x27;</span>).<span class="title function_">createServer</span>(aedes.<span class="property">handle</span>);</span><br><span class="line">server.<span class="title function_">listen</span>(<span class="number">1883</span>, <span class="function">() =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;server started and listening&#x27;</span>));</span><br></pre></td></tr></table></figure><p>Et un client pour afficher les traces :</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> mqtt = <span class="built_in">require</span>(<span class="string">&#x27;mqtt&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> client = mqtt.<span class="title function_">connect</span>(<span class="string">&#x27;mqtt://127.0.0.1:1883&#x27;</span>, &#123; <span class="string">&#x27;clientId&#x27;</span>: <span class="string">&#x27;PC&#x27;</span> &#125;);</span><br><span class="line"><span class="keyword">let</span> connected = <span class="literal">false</span>;</span><br><span class="line">client.<span class="title function_">on</span>(<span class="string">&#x27;connect&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;connected&quot;</span>)</span><br><span class="line">    client.<span class="title function_">subscribe</span>(<span class="string">&#x27;ryze.tello&#x27;</span>);</span><br><span class="line">    connected = <span class="literal">true</span>;</span><br><span class="line">&#125;);</span><br><span class="line">client.<span class="title function_">on</span>(<span class="string">&#x27;message&#x27;</span>, <span class="function">(<span class="params">topic, message</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (topic === <span class="string">&#x27;ryze.tello&#x27;</span>) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(message.<span class="title function_">toString</span>());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>Mais bon, le plus fun c’est quand même d’utiliser la com UDP directement avec Warp 10.</p><p>Normalement, dès que le drone démarre ses moteurs, les données sont transmises à Warp 10 et en faisant <code>tail -f /opt/warp10/logs/warp10.log</code>, vous devriez voir :</p><figure class="highlight cs"><table><tr><td class="code"><pre><span class="line">[<span class="meta">1616403713307</span>] pitch:<span class="number">0</span>;roll:<span class="number">0</span>;yaw:<span class="number">-156</span>;vgx:<span class="number">0</span>;vgy:<span class="number">0</span>;vgz:<span class="number">0</span>;templ:<span class="number">79</span>;temph:<span class="number">81</span>;tof:<span class="number">10</span>;h:<span class="number">0</span>;bat:<span class="number">82</span>;baro:<span class="number">-99.61</span>;time:<span class="number">31</span>;agx:<span class="number">1.00</span>;agy:<span class="number">-10.00</span>;agz:<span class="number">-998.00</span>;[<span class="number">1616403713409</span>] pitch:<span class="number">0</span>;roll:<span class="number">0</span>;yaw:<span class="number">-156</span>;vgx:<span class="number">0</span>;vgy:<span class="number">0</span>;vgz:<span class="number">0</span>;templ:<span class="number">79</span>;temph:<span class="number">81</span>;tof:<span class="number">10</span>;h:<span class="number">0</span>;bat:<span class="number">82</span>;baro:<span class="number">-99.83</span>;time:<span class="number">31</span>;agx:<span class="number">1.00</span>;agy:<span class="number">-10.00</span>;agz:<span class="number">-1002.00</span>;[<span class="number">1616403713511</span>] pitch:<span class="number">0</span>;roll:<span class="number">0</span>;yaw:<span class="number">-156</span>;vgx:<span class="number">0</span>;vgy:<span class="number">0</span>;vgz:<span class="number">0</span>;templ:<span class="number">79</span>;temph:<span class="number">81</span>;tof:<span class="number">10</span>;h:<span class="number">0</span>;bat:<span class="number">82</span>;baro:<span class="number">-99.87</span>;time:<span class="number">31</span>;agx:<span class="number">1.00</span>;agy:<span class="number">-11.00</span>;agz:<span class="number">-1003.00</span>;</span><br></pre></td></tr></table></figure><h2 id="Premiere-analyse-de-la-telemetrie"><a href="#Premiere-analyse-de-la-telemetrie" class="headerlink" title="Première analyse de la télémétrie"></a>Première analyse de la télémétrie</h2><p>Et pour visualiser les données, allez sur <a href="https://studio.senx.io/">WarpStudio</a> et affichons la distance au sol (par exemple) :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">[ <span class="number">2021</span> <span class="number">03</span> <span class="number">22</span> <span class="number">10</span> <span class="number">18</span> ] TSELEMENTS-&gt; <span class="string">&#x27;start&#x27;</span> STORE</span><br><span class="line">[ <span class="number">2021</span> <span class="number">03</span> <span class="number">22</span> <span class="number">10</span> <span class="number">15</span> ] TSELEMENTS-&gt; <span class="string">&#x27;end&#x27;</span> STORE</span><br><span class="line">[</span><br><span class="line">  <span class="string">&quot;your read token&quot;</span></span><br><span class="line">  <span class="string">&#x27;tello.telemetry.tof&#x27;</span> &#123;&#125;</span><br><span class="line">  $start ISO8601</span><br><span class="line">  $end ISO8601</span><br><span class="line">] FETCH <span class="number">500</span> LTTB</span><br></pre></td></tr></table></figure><p><img src="/images/2021/03/telechargement-2.png" alt="flip"></p><p>Le flip</p><p>Et hop, je vous laisse vous amuser à explorer les données.</p><h2 id="Aller-plus-haut-aller-plus-hauuuuuauuut"><a href="#Aller-plus-haut-aller-plus-hauuuuuauuut" class="headerlink" title="Aller plus haut, aller plus hauuuuuauuut"></a>Aller plus haut, aller plus hauuuuuauuut</h2><p>Après avoir niqué votre journée en vous ayant collé une chanson dans la tête, je vous encourage à aller lire <a href="https://blog.senx.io/w-files-conspiracy-vol-2-spy-drones-over-udp/">l’article original</a> qui vous permettra de faire de la détection d’anomalie ainsi que réaliser un dashboard.</p><p> <a href="https://blog.senx.io/w-files-conspiracy-vol-2-spy-drones-over-udp/">Article original</a></p><p> <a href="https://github.com/Giwi/ryze-tello-sdk">Code source</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Comment copier un disque dur distant sur un disque dur local</title>
      <link>https://giwi.fr/2021-03-17-comment-copier-un-disque-dur-distant-sur-un-disque-dur-local/</link>
      <description>
        <![CDATA[<p>Si comme moi, vous souhaitez migrer le disque dur de votre media player sur un nouveau disque, voici une ligne de commande bien pratique.]]>
      </description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Linux/">Linux</category>
      <category domain="https://giwi.fr/tags/linux/">linux</category>
      <category domain="https://giwi.fr/tags/bash/">bash</category>
      <category domain="https://giwi.fr/tags/command-line/">command line</category>
      <pubDate>Wed, 17 Mar 2021 19:47:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Si comme moi, vous souhaitez migrer le disque dur de votre media player sur un nouveau disque, voici une ligne de commande bien pratique.</p><p>J’accède à mon media center en ssh, sur mon media center, mon vieux disque est en <code>/dev/sda</code> (<code>fdisk -l</code> pour vous en assurer).</p><p>Mon nouveau disque est branché en USB sur mon laptop en <code>/dev/sdb</code> (faites attention, je ne veux pas être tenu pour responsable d’une fausse manip!)</p><p>Pour copier le distant sur le local :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">ssh user@&lt;serveur distant&gt; &quot;sudo dd if=/dev/sda&quot;  sudo dd of=/dev/sdb</span><br></pre></td></tr></table></figure><p>Et pour connaître la progression, logguez vous en ssh sur le serveur distant dans un autre terminal et tapez :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">sudo pkill -USR1 dd</span><br></pre></td></tr></table></figure><p>Vous verrez la progression dans le premier terminal.</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Monitorer son infra avec Warp 10 – partie 2</title>
      <link>https://giwi.fr/2021-03-16-monitorer-son-infra-avec-warp-10-partie-2/</link>
      <description>
        <![CDATA[<p>Suite de l’article précédent <a href="/2020-12-21-monitorer-son-infra-avec-warp-10-partie-1">Monitorer son infra avec Warp 10 – partie 1<]]>
      </description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Linux/">Linux</category>
      <category domain="https://giwi.fr/categories/DevOps/">DevOps</category>
      <category domain="https://giwi.fr/categories/Warp-10/">Warp 10</category>
      <category domain="https://giwi.fr/tags/monitoring/">monitoring</category>
      <category domain="https://giwi.fr/tags/warp-10/">warp 10</category>
      <category domain="https://giwi.fr/tags/WarpScript/">WarpScript</category>
      <pubDate>Tue, 16 Mar 2021 21:26:38 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Suite de l’article précédent <a href="/2020-12-21-monitorer-son-infra-avec-warp-10-partie-1">Monitorer son infra avec Warp 10 – partie 1</a>.</p><p>Nous avons vu comment installer <a href="https://github.com/senx/sensision">sensision</a> et remonter des métriques dans <a href="https://warp10.io/">Warp 10</a>. Maintenant, explorons ces métriques.</p><p>Pour avoir la liste complète des métriques, vous pouvez requêter votre instance Warp 10 avec <a href="http://studio.senx.io/">WarpStudio</a> en y ajoutant les infos de connexion.</p><blockquote><p>Retrouvez plus d’info sur WarpStudio ici : <a href="https://blog.senx.io/warpstudio-unleashed/">https://blog.senx.io/warpstudio-unleashed/</a></p></blockquote><p>Puis exécutez ce <a href="https://warp10.io/content/03_Documentation/04_WarpScript">WarpScript</a> :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">[ </span><br><span class="line">  <span class="string">&#x27;votre token de lecture&#x27;</span> </span><br><span class="line">  <span class="string">&#x27;~.*&#x27;</span> &#123;&#125; </span><br><span class="line">] FIND</span><br></pre></td></tr></table></figure><p>Retrouvez la documentation de <a href="https://warp10.io/doc/FIND">FIND</a>.</p><p>Je vous encourage donc à tester différents scripts (référez vous à la donc Warp 10) pour explorer un peu ces données.</p><p>Par exemple, récupérons 24 heures de données sur le trafic réseau en réception :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">[</span><br><span class="line">   <span class="string">&#x27;votre token de lecture&#x27;</span> </span><br><span class="line">   <span class="string">&#x27;linux.proc.net.dev.receive.bytes&#x27;</span></span><br><span class="line">  &#123; <span class="string">&#x27;hname&#x27;</span> <span class="string">&#x27;votre serveur&#x27;</span> &#125; NOW  <span class="number">24</span> h</span><br><span class="line">] FETCH</span><br></pre></td></tr></table></figure><p>Retrouvez la documentation de <a href="https://warp10.io/doc/FETCH">FETCH</a>.</p><p>Vous devriez avoir un graphique du genre :</p><p><img src="/images/2021/03/01.png"></p><p>24 h de trafic réseau</p><p>Ok, c’est rigolo, mais allons plus loin. Mettons, je veux 1 mois de données. Ca risque de faire beaucoup. On peut avoir 2 stratégies.</p><p>Soit on découpe en tranches avec <a href="https://warp10.io/doc/BUCKETIZE">BUCKETIZE</a>, soit on calcule une courbe representative avec moins de points avec <a href="https://warp10.io/doc/LTTB">LTTB</a>.</p><h2 id="BUCKETIZE"><a href="#BUCKETIZE" class="headerlink" title="BUCKETIZE"></a>BUCKETIZE</h2><p>Le principe du bucketize est simple : comment on se partage un saucisson aux noix à plusieurs. Il y a deux façons de le faire, soit on détermine un nombre de tranche (si on est 4, ça fait une grosse tranche chacun), soit on choisis une épaisseur de tranche (des tranches fines donnent l’impression d’en avoir plus). On commence à découper à partir d’un endroit et on remonte dans le passé. Ensuite, on choisis ce que l’on veut faire des noix (quand il y en a, j’y reviendrai) que l’on trouve dans une tranche.</p><p>Prenons donc un saucisson :</p><p><img src="/images/2021/03/Capture-decran-du-2021-03-16-20-54-07.png"></p><p>Il n’est pas beau mon saucisson? Comment ça non, c’est vexant!</p><p>Bref, je commence à découper ma série de valeurs sur 30 jours à un instant T et disons que je choisis une épaisseur de tranche de 1 journée. Je vais avoir 30 tranches de 1 jour de données. Ensuite, j’applique une opération sur les données (mes noix) qui se trouvent dans cette tranche pour calculer une nouvelle donnée (la nouvelle noix) qui aura pour timestamp le début de la tranche.</p><p>Par exemple, je commence ma découpe le 30 juin à minuit, ma première tranche à droite contient 3 noix, je décide de calculer la valeur moyenne. Une nouvelle série est créée et son “premier” point sera daté du 30 juin minuit avec comme valeur, la moyenne que j’ai calculé. Ma seconde tranche contient deux noix, même punition, je calcule la moyenne et mon second point sera daté du 29 juin minuit. Troisième tranche, diantre, elle est vide. Ma nouvelle série va contenir un trou (et c’est pas grave, on peut boucher les trous après) daté du 28 juin minuit. Et ainsi de suite.</p><p>Ce qui est cool avec le bucketize, c’est qu’il peut prendre un ensemble de séries et produire un nouvel ensemble de séries dont tous les points seront alignés (si l’on choisit une épaisseur de tranche, si on choisi une quantité, ça va dépendre de quand se place le plus vieux point de chaque série). Par exemple, je veux calculer la charge moyenne de mon datacenter, mais mes serveurs ne remontent pas les données au même instant à la micro seconde près. Ce qui est également cool, c’est qu’il y a plein de <a href="https://warp10.io/tags/bucketizer">fonctions sur étagère</a>.</p><p>Et pour boucher les trous, il y a FILLPREVIOUS ou FILLNEXT mais encore mieux : <a href="https://warp10.io/doc/FILL">FILL</a></p><p>Revenons à notre trafic réseau, ça donnerait un truc du genre :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">[</span><br><span class="line">   <span class="string">&#x27;votre token de lecture&#x27;</span> </span><br><span class="line">   <span class="string">&#x27;linux.proc.net.dev.receive.bytes&#x27;</span></span><br><span class="line">  &#123; <span class="string">&#x27;hname&#x27;</span> <span class="string">&#x27;votre serveur&#x27;</span> &#125; NOW  <span class="number">30</span> d</span><br><span class="line">] FETCH <span class="string">&#x27;data&#x27;</span> STORE</span><br><span class="line">[ $data bucketizer.mean NOW <span class="number">1</span> d <span class="number">0</span> ] BUCKETIZE</span><br></pre></td></tr></table></figure><p>Retrouvez la documentation de <a href="https://warp10.io/doc/BUCKETIZE">BUCKETIZE</a>.</p><p>Ca nous donne un truc du genre :</p><p><img src="/images/2021/03/02.png"></p><p>1 mois de trafic</p><h2 id="LTTB"><a href="#LTTB" class="headerlink" title="LTTB"></a>LTTB</h2><p>Cette fonction est puissante et sympa, elle permet de recalculer une courbe représentative en ne spécifiant qu’on nombre de points par courbe (et LTTB accepte une liste de séries en entrée). L’avantage par rapport au bucketize, c’est que l’on ne masque pas des pics ou des valeurs abérantes.</p><p>Et pour notre trafic réseau, je veux 1000 points par courbe :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">[</span><br><span class="line">   <span class="string">&#x27;votre token de lecture&#x27;</span> </span><br><span class="line">   <span class="string">&#x27;linux.proc.net.dev.receive.bytes&#x27;</span></span><br><span class="line">  &#123; <span class="string">&#x27;hname&#x27;</span> <span class="string">&#x27;votre serveur&#x27;</span> &#125; NOW  <span class="number">30</span> d</span><br><span class="line">] FETCH </span><br><span class="line"><span class="number">1000</span> LTTB</span><br></pre></td></tr></table></figure><p><img src="/images/2021/03/03.png"></p><p>toujours 1 mois d’histo</p><h2 id="Des-metriques"><a href="#Des-metriques" class="headerlink" title="Des métriques!"></a>Des métriques!</h2><p>Ok, les présentations sont faites, maintenant sortons des métriques de tout ça :</p><h3 id="Trafic-entrant-sortant"><a href="#Trafic-entrant-sortant" class="headerlink" title="Trafic entrant&#x2F;sortant"></a>Trafic entrant&#x2F;sortant</h3><p>Ah ben oui, tiens, on en parlait. Ben modifions notre script pour y ajouter le trafic sortant avec une expression régulière parce qu’on est des geudins. Filtrons sur eth0 pour le fun et calculons plutôt la variation (le delta entre la valeur n et n-1).</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">[</span><br><span class="line">  <span class="string">&#x27;votre token de lecture&#x27;</span> </span><br><span class="line">  <span class="string">&#x27;~linux.proc.net.dev.(receivetransmit).bytes&#x27;</span></span><br><span class="line">  &#123; <span class="string">&#x27;hname&#x27;</span> <span class="string">&#x27;votre serveur&#x27;</span> <span class="string">&#x27;iface&#x27;</span> <span class="string">&#x27;eth0&#x27;</span> &#125; NOW <span class="number">30</span> d</span><br><span class="line">] FETCH  <span class="string">&#x27;data&#x27;</span> STORE</span><br><span class="line">[ $data mapper.delta <span class="number">1</span> <span class="number">0</span> <span class="number">0</span> ] MAP <span class="string">&#x27;data&#x27;</span> STORE</span><br><span class="line">[ $data bucketizer.mean $NOW <span class="number">1</span> d <span class="number">0</span> ] BUCKETIZE</span><br></pre></td></tr></table></figure><p>Alors, <a href="https://warp10.io/doc/MAP">MAP</a> permet de transformer les données d’une série au travers d’une fenêtre glissante (pratique pour calculer une moyenne glissante par exemple) .</p><h3 id="RAM-SWAP"><a href="#RAM-SWAP" class="headerlink" title="RAM&#x2F;SWAP"></a>RAM&#x2F;SWAP</h3><p>C’est pas tellement plus compliqué, on ramène en méga-octets :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">[</span><br><span class="line">   <span class="string">&#x27;votre token de lecture&#x27;</span>  </span><br><span class="line">   <span class="string">&#x27;~linux.proc.meminfo.(MemFreeSwapFree)&#x27;</span></span><br><span class="line">   &#123; <span class="string">&#x27;hname&#x27;</span> <span class="string">&#x27;votre serveur&#x27;</span> &#125; NOW  <span class="number">30</span> d</span><br><span class="line">] FETCH <span class="string">&#x27;data&#x27;</span> STORE</span><br><span class="line">[ $data bucketizer.mean NOW <span class="number">1</span> d <span class="number">0</span> ] BUCKETIZE <span class="string">&#x27;data&#x27;</span> STORE</span><br><span class="line">[ $data <span class="number">1.0</span> <span class="number">1024.0</span> <span class="number">1024.0</span> * <span class="number">1024.0</span> * / mapper.mul <span class="number">0</span> <span class="number">0</span> <span class="number">0</span> ] MAP</span><br></pre></td></tr></table></figure><p>Alors oui, pour convertir les octets en méga, c’est un peu casse pied : il faut diviser, donc multiplier par l’inverse. Le <a href="https://warp10.io/tags/mapper">mappeur sur étagère</a> de la division n’existe pas, et alors, on a été au collège, non?</p><p><code>1.0 1024.0 1024.0 * 1024.0 * /</code><br>se traduit par :<br><code>1 / ( 1024 * 1024 * 1024)</code></p><p>Ok, ce n’est pas naturel, mais on a plus de 30 de QI et on a peut être eu une calculette HP dans sa prime jeunesse.</p><h3 id="Espace-disque-utilise"><a href="#Espace-disque-utilise" class="headerlink" title="Espace disque utilisé"></a>Espace disque utilisé</h3><p>On monte d’un cran :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">[</span><br><span class="line">   <span class="string">&#x27;votre token de lecture&#x27;</span>  </span><br><span class="line">   <span class="string">&#x27;~linux.df.bytes.(capacityfree)&#x27;</span></span><br><span class="line">   &#123; <span class="string">&#x27;hname&#x27;</span> <span class="string">&#x27;votre serveur&#x27;</span> &#125; NOW  <span class="number">30</span> d</span><br><span class="line">] FETCH <span class="string">&#x27;data&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// la moyenne quotidienne</span></span><br><span class="line">[ $data bucketizer.mean NOW <span class="number">1</span> d <span class="number">0</span> ] BUCKETIZE <span class="string">&#x27;data&#x27;</span> STORE  </span><br><span class="line"></span><br><span class="line"><span class="comment">// On extraie capacity pour avoir le total tout montage confondu</span></span><br><span class="line">[ $data [] <span class="string">&#x27;linux.df.bytes.capacity&#x27;</span> filter.byclass ] FILTER <span class="string">&#x27;capacity&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// On calcul donc l&#x27;espace disque total cumulé</span></span><br><span class="line">[ $capacity [] reducer.sum ] REDUCE <span class="string">&#x27;capacity&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// On fait pareil avec l&#x27;espace libre</span></span><br><span class="line">[ $data [] <span class="string">&#x27;linux.df.bytes.free&#x27;</span> filter.byclass ] FILTER <span class="string">&#x27;free&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// On calcul donc l&#x27;espace disque libre cumulé</span></span><br><span class="line">[ $<span class="built_in">free</span> [] reducer.sum ] REDUCE <span class="string">&#x27;free&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// On multiplie par -1 (c&#x27;est pour soustraire tout à l&#x27;heure)</span></span><br><span class="line">[ $<span class="built_in">free</span> <span class="number">-1.0</span> mapper.mul <span class="number">0</span> <span class="number">0</span> <span class="number">0</span> ] MAP  <span class="string">&#x27;free&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// On se crée une liste de listes de listes que l&#x27;on aplatit </span></span><br><span class="line">[ $capacity $<span class="built_in">free</span> ] FLATTEN <span class="string">&#x27;list&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// On fait la soustraction, oui c&#x27;est comme additionner le négatif</span></span><br><span class="line">[ $<span class="built_in">list</span> [] reducer.sum ] REDUCE <span class="string">&#x27;result&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// On converti en méga</span></span><br><span class="line">[ $result <span class="number">1.0</span> <span class="number">1024.0</span> <span class="number">1024.0</span> * <span class="number">1024.0</span> * / mapper.mul <span class="number">0</span> <span class="number">0</span> <span class="number">0</span> ] MAP</span><br></pre></td></tr></table></figure><p>Ici on a plusieurs disques, donc on somme le libre (free) et le total (capacity), on multiplie le libre par -1 et on additionne ( - ) le libre et le total (oui on soustraie les 2, 5ème b, madame Grimbert en Math, rappelez vous). Retrouvez la doc de <a href="https://warp10.io/doc/REDUCE">REDUCE</a>.</p><p>REDUCE prends une liste de séries, et pour chaque timestamp, prend tous les points correspondant à ce timestamp dans chacune des série et applique une opération à l’ensemble de ces points. A la sortie, on a une série (ou plusieurs si vous faites l’équivalent d’un GROUPBY).</p><h3 id="Charge-CPU"><a href="#Charge-CPU" class="headerlink" title="Charge CPU"></a>Charge CPU</h3><p>Là ok, l’algo de calcul n’est pas simple. D’abord, le script :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">[</span><br><span class="line">   <span class="string">&#x27;votre token de lecture&#x27;</span>  </span><br><span class="line">   <span class="string">&#x27;~linux.proc.stat.userhz.(usernicesystemidleiowait)&#x27;</span></span><br><span class="line">  &#123; <span class="string">&#x27;hname&#x27;</span> <span class="string">&#x27;votre serveur&#x27;</span> <span class="string">&#x27;cpu&#x27;</span> <span class="string">&#x27;cpu&#x27;</span> &#125; NOW  <span class="number">30</span> d</span><br><span class="line">] FETCH <span class="string">&#x27;data&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// Moyenne quotidienne</span></span><br><span class="line">[ $data bucketizer.mean NOW <span class="number">0</span> <span class="number">1</span> d ] BUCKETIZE <span class="string">&#x27;data&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// On extraie usernicesystemidleiowait pour les additionner entre eux</span></span><br><span class="line">[ $data [] <span class="string">&#x27;~linux.proc.stat.userhz.(usernicesystemidleiowait)&#x27;</span> filter.byclass ] FILTER <span class="string">&#x27;cpuGTS&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// à la sortie du REDUCE, j&#x27;ai une liste d&#x27;une série, je la récupère</span></span><br><span class="line">[ $cpuGTS [] reducer.sum ] REDUCE <span class="number">0</span> GET <span class="string">&#x27;cpuGTS&#x27;</span> STORE </span><br><span class="line"></span><br><span class="line"><span class="comment">// je récupère l&#x27;idle</span></span><br><span class="line">[ $gts [] <span class="string">&#x27;linux.proc.stat.userhz.idle&#x27;</span> filter.byclass ] FILTER <span class="number">0</span> GET <span class="string">&#x27;idleGTS&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// J&#x27;additionne iowait et idle</span></span><br><span class="line">[ $gts [] <span class="string">&#x27;~linux.proc.stat.userhz.(iowaitidle)&#x27;</span> filter.byclass ] FILTER <span class="string">&#x27;iowaitGTS&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line">[ $iowaitGTS [] reducer.sum ] REDUCE <span class="number">0</span> GET <span class="string">&#x27;iowaitGTS&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// Pareil pour system et idle</span></span><br><span class="line">[ $gts [] <span class="string">&#x27;~linux.proc.stat.userhz.(systemidle)&#x27;</span> filter.byclass ] FILTER <span class="string">&#x27;systemGTS&#x27;</span> STORE</span><br><span class="line">[ $systemGTS [] reducer.sum ] REDUCE <span class="number">0</span> GET <span class="string">&#x27;systemGTS&#x27;</span> STORE</span><br><span class="line"></span><br><span class="line"><span class="comment">// Etape 1 on applique une opération custom entre 2 série pour calculer la charge CPU</span></span><br><span class="line">[ [ $cpuGTS $idleGTS ] [] &lt;%</span><br><span class="line">  <span class="string">&#x27;d&#x27;</span> STORE</span><br><span class="line">  $d <span class="number">7</span> GET <span class="number">0</span> GET <span class="string">&#x27;cpu&#x27;</span> STORE</span><br><span class="line">  $d <span class="number">7</span> GET <span class="number">1</span> GET <span class="string">&#x27;idle&#x27;</span> STORE</span><br><span class="line">  <span class="comment">// Le calcule est là</span></span><br><span class="line">  <span class="number">1000.0</span> $cpu $idle - * $cpu <span class="number">5</span>  + / <span class="number">10</span> / <span class="string">&#x27;v&#x27;</span> STORE</span><br><span class="line">  [ $d <span class="number">0</span> GET NaN NaN NaN $v ]</span><br><span class="line">%&gt; MACROREDUCER ] REDUCE <span class="number">0</span> GET <span class="string">&#x27;CPU&#x27;</span> RENAME <span class="comment">// On renomme la série de résultat</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Etape 2 on calcule l&#x27;IO Disk</span></span><br><span class="line">[ [ $iowaitGTS $idleGTS ] [] &lt;%</span><br><span class="line">  <span class="string">&#x27;d&#x27;</span> STORE</span><br><span class="line">  $d <span class="number">7</span> GET <span class="number">0</span> GET <span class="string">&#x27;cpu&#x27;</span> STORE</span><br><span class="line">  $d <span class="number">7</span> GET <span class="number">1</span> GET <span class="string">&#x27;idle&#x27;</span> STORE</span><br><span class="line">  <span class="comment">// Avec ce calcul savant</span></span><br><span class="line">  <span class="number">1000.0</span> $cpu $idle - * $cpu <span class="number">5</span>  + / <span class="number">10</span> / <span class="string">&#x27;v&#x27;</span> STORE</span><br><span class="line">  [ $d <span class="number">0</span> GET NaN NaN NaN $v ]</span><br><span class="line">%&gt; MACROREDUCER ] REDUCE <span class="number">0</span> GET <span class="string">&#x27;Disk IO&#x27;</span> RENAME <span class="comment">// Et on renomme</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Et enfin on a la charge système</span></span><br><span class="line">[ [ $systemGTS $idleGTS ] [] &lt;%</span><br><span class="line">  <span class="string">&#x27;d&#x27;</span> STORE</span><br><span class="line">  $d <span class="number">7</span> GET <span class="number">0</span> GET <span class="string">&#x27;cpu&#x27;</span> STORE</span><br><span class="line">  $d <span class="number">7</span> GET <span class="number">1</span> GET <span class="string">&#x27;idle&#x27;</span> STORE</span><br><span class="line">  <span class="comment">// Avec ce calcul tout aussi savant</span></span><br><span class="line">  <span class="number">1000.0</span> $cpu $idle - * $cpu <span class="number">5</span>  + / <span class="number">10</span> / <span class="string">&#x27;v&#x27;</span> STORE</span><br><span class="line">  [ $d <span class="number">0</span> GET NaN NaN NaN $v ]</span><br><span class="line">%&gt; MACROREDUCER ] REDUCE <span class="number">0</span> GET <span class="string">&#x27;System&#x27;</span> RENAME</span><br><span class="line"></span><br><span class="line"><span class="comment">// A la sortie, 3 séries pour les charges CPU, Système et IO/Disque</span></span><br></pre></td></tr></table></figure><p>Ok, ça pique les yeux. Ici on utilise des <a href="https://warp10.io/doc/MACROREDUCER">MACROREDUCER</a> pour effectuer un calcul custom au sein du reducer. Pour info, on peut faire des opérations custom également pour le bucketize et le map (je dis ça, je ne dis rien).</p><p>Je ne vais pas détailler le calcul en lui même, comme dirait Kâ, ayez confianssssssssse.</p><p>Toujours est-il qu’on a récupéré un paquet de séries, qu’on les a ventilé en fonction de leur petit nom pour en faire un calcul sur des sous ensembles et forgé de nouvelles séries correspondant à nos KPIs (oui, ça sonne bien hein?) avec.</p><h2 id="Pour-aller-plus-loin"><a href="#Pour-aller-plus-loin" class="headerlink" title="Pour aller plus loin"></a>Pour aller plus loin</h2><p>Je vous encourage à aller voir <a href="https://warp10.io/">warp10.io</a> pour découvrir tout la puissance du langage WarpScript (un peu tordu de prime abord) qui permet d’exprimer en peu de code des algos puissants. Il y a une fonction pour presque tout (oui, pas la division ni la soustraction, mais on a tous notre brevet des collèges alors).</p><p>Concernant la division ou la soustraction de 2 séries, il y a plus simple :</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">serie1 serie2 /</span><br><span class="line">serie1 serie2 -</span><br></pre></td></tr></table></figure><p>Mais quand on a des listes de séries, il faut faire autrement.</p><p>Dans les prochains articles, on va voir comment on peut ajouter de nouveaux métriques (process en cours par exemple) et se faire un dashboard de hipster joli tout plein sans tomber dans le mainstream moutonnier de Grafana (même si c’est possible).</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Tout sur le modèle Geo Time Series</title>
      <link>https://giwi.fr/2021-02-04-tout-sur-le-modele-geo-time-series/</link>
      <description>
        <![CDATA[<p>Les Geo Time Series (GTS) sont le cœur de la plateforme <a href="https://warp.denx.io/">Warp 10</a>. Seules les GTS qui peuvent être stoc]]>
      </description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Architecture/">Architecture</category>
      <category domain="https://giwi.fr/categories/Warp-10/">Warp 10</category>
      <category domain="https://giwi.fr/tags/warp-10/">warp 10</category>
      <category domain="https://giwi.fr/tags/GTS/">GTS</category>
      <category domain="https://giwi.fr/tags/Serie-temporelle/">Série temporelle</category>
      <pubDate>Thu, 04 Feb 2021 10:20:06 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Les Geo Time Series (GTS) sont le cœur de la plateforme <a href="https://warp.denx.io/">Warp 10</a>. Seules les GTS qui peuvent être stockées dans la base de données Warp 10 et ce sont des citoyens de première classe à la fois dans <a href="https://warp10.io/content/03_Documentation/04_FLoWS">FLoWS</a> et <a href="https://warp10.io/content/03_Documentation/04_WarpScript">WarpScript</a>. C’est pourquoi il y a tant de <a href="https://warp10.io/tags/gts">fonctions WarpLib</a> pour leur manipulation et <a href="https://blog.senx.io/?s=geo+time+series">tant de billets de blog</a> faisant mention de GTS!</p><p>Par conséquent, comprendre le modèle GTS est la première étape de la maîtrise de la plateforme Warp 10. Heureusement, ce modèle est assez simple, mais puissant, et cet article passera en revue la plupart de ses aspects.</p><h2 id="Que-sont-les-Geo-Time-Series"><a href="#Que-sont-les-Geo-Time-Series" class="headerlink" title="Que sont les Geo Time Series?"></a>Que sont les Geo Time Series?</h2><p>Geo Time Series est un modèle pour représenter des données de séries temporelle avec des informations de localisation facultatives pour chaque point.</p><p>Si vous préférez un exemple, imaginez un capteur de pollution atmosphérique sur un bus couplé à un GPS. Ce capteur mesure des niveaux d’ozone de temps en temps pendant que le bus se déplace. La séquence de lectures, chacune composée d’un horodatage, d’une latitude, d’une longitude, d’une élévation et d’un niveau d’ozone, forme une série.</p><p>Comme nous allons indexer le temps, nous les appelons séries temporelle et comme nous aurons non seulement la valeur du capteur mais aussi sa géolocalisation, nous les appelons Geo Time Series.</p><p><img src="/images/2021/02/blog_gts.png" alt="Séquence des enregistrements: horodatage en noir, latitude en bleu, longitude en vert, élévation en jaune et valeur en rouge."></p><p>Séquence des enregistrements: horodatage en noir, latitude en bleu, longitude en vert, élévation en jaune et valeur en rouge.</p><p>Les GTS sont indexés par le temps afin que vous puissiez facilement sélectionner une plage de temps et obtenir les valeurs et les positions associés. Vous pouvez également <a href="https://blog.senx.io/spatio-temporal-indexing-in-warp-10/">indexer des positions</a>, mais cela n’est pas fait par défaut. L’index au temps est utilisé lors de l’obtention des données d’un stockage (<a href="https://blog.senx.io/demystifying-leveldb/">LevelDB</a>, HBase, <a href="https://blog.senx.io/warp-10-accelerator/">mémoire</a>, HFile, <a href="https://blog.senx.io/archiving-time-series-data-into-s3/">S3</a>, etc.) mais aussi lors de la manipulation du GTS, notamment avec des <a href="https://warp10.io/tags/framework">frameworks</a>.</p><h2 id="Valeur-en-fonction-de-l’espace-et-du-temps"><a href="#Valeur-en-fonction-de-l’espace-et-du-temps" class="headerlink" title="Valeur en fonction de l’espace et du temps"></a>Valeur en fonction de l’espace et du temps</h2><p>Vous devez vous demander: pourquoi regrouper les positions et les valeurs dans un seul modèle? Il est en effet possible d’avoir une série temporelle de latitudes, une de longitudes, une d’élévations et une de valeurs. L’avantage du modèle GTS est triple.</p><p><strong>Premièrement</strong>, cela contextualise la valeur mesurée. Si l’on garde le même exemple de pollution de l’air que ci-dessus, il est évident que le niveau d’ozone est fortement dépendant du temps, mais aussi de l’emplacement du capteur. Mathématiquement, nous pouvons voir les enregistrements comme des échantillons d’une fonction <em>f (temps, lieu) &#x3D; ozone</em>, qui sont tous, commodément, dans le même modèle.</p><p><strong>Deuxièmement</strong>, cela rend les données plus compactes. Le fractionnement des données en plusieurs séries temporelles nécessiterait la duplication des horodatages. Si vous multipliez cela par des milliards ou des billions d’enregistrements, le gain peut être important. Attention cependant, vous ne pouvez généraliser cela à aucune valeur. Par exemple, si nous avions un modèle tabulaire, factorisant l’horodatage pour plusieurs capteurs, les capteurs pourraient avoir des fréquences d’échantillonnage très différents, disons 1Hz et 100Hz, cela conduirait à des tables presque vides. Même si une solution pour éviter de perdre de l’espace peut être trouvée, la manipulation d’une telle table ne serait pas pratique.</p><p><strong>Troisièmement</strong>, cela rend l’analyse beaucoup plus facile. Avoir toutes ces informations dans le même modèle est bien plus pratique que d’avoir à combiner plusieurs structures. Vous pouvez facilement analyser l’influence de l’altitude sur la pollution ou restreindre votre analyse à une zone spécifique.</p><h2 id="Les-Geo-Time-Series-sont-un-sur-ensemble-de-series-temporelles"><a href="#Les-Geo-Time-Series-sont-un-sur-ensemble-de-series-temporelles" class="headerlink" title="Les Geo Time Series sont un sur-ensemble de séries temporelles"></a>Les Geo Time Series sont un sur-ensemble de séries temporelles</h2><p>Pour les capteurs fixes, ce n’est évidemment pas une bonne idée de stocker leurs positions pour chaque valeur. Bonne nouvelle, le couple latitude &#x2F; longitude et l’élévation sont tous deux optionnels! Ce cas est bien géré par Warp 10 et stocker les GTS sans emplacement et les charger en mémoire ne prend pas de place pour représenter les emplacements «manquants». Il n’y a aucun inconvénient à utiliser Geo Time Series sans emplacement.</p><p>En ce sens, les Geo Time Series sont un sur-ensemble de séries temporelles, car une GTS peut à la fois associer des horodatages pour représenter des valeurs ou des couples valeurs &#x2F; position. Très souvent, un bon modèle de GTS mélange les deux types de GTS. Par exemple, lorsque vous avez un capteur à très haute fréquence sur un objet à déplacement lent, comme la <a href="https://www.epsilonoptics.com/marine.html">fibre optique dans un yacht</a>, stocker la position est une mauvaise idée car l’emplacement sera à peu près le même pour des milliers d’enregistrements consécutifs. Si vous avez besoin de combiner ces capteurs haute fréquence avec des données de localisation pendant l’analyse, c’est en effet possible.</p><blockquote><p>Les Geo Time Series sont le modèle de données principal de Warp 10. Il répond à la plupart des besoins de modélisation de séries chronologiques, avec des informations de localisation facultatives.</p></blockquote><h2 id="Valeurs-dans-les-Geo-Time-Series"><a href="#Valeurs-dans-les-Geo-Time-Series" class="headerlink" title="Valeurs dans les Geo Time Series"></a>Valeurs dans les Geo Time Series</h2><p>Jusqu’à présent, nous nous sommes concentrés sur l’aspect spatial et temporel d’une GTS, mais il est temps de se concentrer sur les valeurs. Une GTS peut stocker 4 types principaux de valeurs: booléens, entiers longs, doubles et chaînes de caractères. Ces types suffisent à couvrir n’importe quel cas d’utilisation.</p><p>Les booléens sont les valeurs les plus simples qu’une GTS puisse stocker: vrai ou faux, c’est tout. Même si elles sont très simples, elles sont extrêmement utiles: <a href="https://blog.senx.io/alerts-are-real-time-series/">une alerte est-elle déclenchée</a>? Les trains d’atterrissage sont-ils au sol? Ils ont également un très faible encombrement disque et mémoire, ce qui les rend parfaits pour optimiser certains calculs.</p><p>Ensuite, probablement les types les plus courants: les GTS numérique. Un grand nombre de capteurs donnent des nombres à virgule flottante ou entiers, comme la température, les <a href="https://simpleflying.com/squawk-codes/">codes de squawk</a>, le nombre d’écriture, etc…. Ils prennent plus de place que les GTS booléennes, mais le stockage est optimisé pour les entiers longs et les doubles.</p><p>Enfin, les chaînes de caractères sont le type le plus polyvalent mais n’est pas optimisé comme les autres types. Les valeurs de chaîne peuvent stocker des annotations ou des messages de journal par exemple. Elles peuvent également être utilisées pour stocker des structures plus complexes comme les chaînes JSON ou base64 qui rendent possible la sérialisation et la désérialisation des objets stockés dans GTS.</p><p>Pour aller plus loin, Warp 10 permet le stockage de valeurs binaires dans les GTS, qui sont représentées par GTS de chaînes de caractères codées <a href="https://en.wikipedia.org/wiki/ISO/IEC_8859-1">ISO8859-1</a>. Pour aller encore plus loin, Warp 10 permet le stockage de GTS encodées sous forme de valeurs binaires dans une GTS: le <a href="https://www.warp10.io/content/03_Documentation/03_Interacting_with_Warp_10/03_Ingesting_data/02_GTS_input_format#multi-value">multi-valeurs</a>. Cela rend possible des modèles de données très complexes qui ont à la fois d’excellentes performances et sont faciles à utiliser.</p><h2 id="Organisation-des-Geo-Time-Series"><a href="#Organisation-des-Geo-Time-Series" class="headerlink" title="Organisation des Geo Time Series"></a>Organisation des Geo Time Series</h2><h3 id="Nommez-votre-Geo-Time-Series"><a href="#Nommez-votre-Geo-Time-Series" class="headerlink" title="Nommez votre Geo Time Series"></a>Nommez votre Geo Time Series</h3><p>Maintenant que nous avons défini précisément le contenu d’une GTS, il est temps de voir comment nous identifions une seule GTS ou un sous-ensemble spécifique de GTS. Chaque GTS est identifiée de manière unique par un <strong>nom de classe</strong> et des paires clé-valeur facultatives appelées <strong>label</strong>. C’est exactement le même principe que <a href="https://prometheus.io/docs/concepts/data_model/">Prometheus</a> avec son nom de métrique et ses <em>labels</em> et <a href="http://opentsdb.net/docs/build/html/user_guide/query/timeseries.html">OpenTSDB</a> avec son nom de métrique et ses <em>tags</em>, ce n’est pas une surprise puisque tous ces outils ont été inspirés par le modèle utilisé dans le <a href="https://www.oreilly.com/radar/google-infrastructure-for-everyone-else/">Borgmon</a> de Google. Il est très important de comprendre que le nom de la classe et les labels forment un identifiant unique. Changez quoi que ce soit, une valeur de label par exemple, et vous faites référence à une autre GTS.</p><p>Prenons un exemple. Imaginez que vous ayez installé 2 capteurs de vitesse du vent sur un pétrolier. Le nom de classe définit généralement la valeur mesurée. Dans notre cas, pour les deux GTS, le nom de classe serait <em>com.company.fleet.aws</em> car AWS signifie Vitesse du vent apparent. Ensuite, nous utilisons les labels pour définir précisément le capteur. Nous pouvons utiliser presque autant de labels que nous le souhaitons, mais deux suffiront: le nom du navire et le côté. Enfin, nous sélectionnons des valeurs pour les labels, <em>mybigtanker</em> sera associé à <em>shipname</em> et un capteur aura le port associé au côté tandis que l’autre aura le tribord.</p><p>Vous pouvez voir comment <em>com.company.fleet.aws {shipname &#x3D; mybigtanker, side &#x3D; port}</em> identifie de manière unique un seul capteur. Vous devez faire très attention à ne pas ajouter de label qui pourrait changer pendant la durée de vie de la métrique. Dans ce cas, ajouter quelque chose comme la marque du capteur ou la destination du bateau est probablement une mauvaise idée car le capteur pourrait être remplacé et la destination est appelée à changer. Les GTS sont déjà indexées au temps, il n’y a donc guère d’avantages à ajouter un label dépendante du temps.</p><h3 id="Selectionnez-votre-Geo-Time-Series"><a href="#Selectionnez-votre-Geo-Time-Series" class="headerlink" title="Sélectionnez votre Geo Time Series"></a>Sélectionnez votre Geo Time Series</h3><p>La façon dont les GTS sont nommées permet la définition d’ensembles de GTS à l’aide de sélecteurs. Vous voulez tous les GTS AWS sur le navire? Interrogez <code>com.company.fleet.aws {shipname = mybigtanker}</code>, les labels non spécifiés peuvent prendre n’importe quelle valeur. Vous voulez tous les GTS sur votre pétrolier? Requête <code>~com.company.fleet.*{Shipname=mybigtanker}</code>, <code>~</code> signifie que le nom de classe suivant est une expression régulière. Vous pouvez également utiliser la même logique sur les valeurs de label pour sélectionner plusieurs bateaux avec <code>com.company.fleet.aws{shipname~my(bigsmall)tanker}</code>.</p><p>Auriez-vous besoin d’ajouter des informations supplémentaires à une GTS qui ne sont pas essentielles à son identification? Vous pouvez ajouter des attributs. Les attributs sont également des paires clé-valeur mais sont modifiables. Vous pouvez ajouter l’attribut {class &#x3D; tanker} à votre GTS, ce qui vous permettrait de sélectionner facilement tous les pétroliers de la même manière que vous utilisez les labels dans les sélecteurs.</p><p><img src="/images/2021/02/blog_gts_all-1024x289-1.png" alt="Une GTS complete avec nom de classe, étiquette et valeur d&#39;étiquette en haut, attributs en bas et points de données au milieu."></p><p>Une GTS complete avec nom de classe, étiquette et valeur d’étiquette en haut, attributs en bas et points de données au milieu. Notez que l’élévation est manquante car elle est probablement inutile pour un navire.</p><h3 id="Manipulation-des-Geo-Time-Series"><a href="#Manipulation-des-Geo-Time-Series" class="headerlink" title="Manipulation des Geo Time Series"></a>Manipulation des Geo Time Series</h3><p>Il serait trop long de lister toutes les manipulations possibles qui peuvent être faites sur des GTS, mais nous allons vous donner quelques pointeurs vers des ressources utiles. Comme nous manipulons des séries temporelles, le temps est de la plus haute importance dans presque toutes les fonctions liées aux GTS. Par exemple, le framework <a href="https://www.warp10.io/doc/MAP">MAP</a> vous permet d’appliquer une fonction sur une fenêtre glissante sur une GTS. <a href="https://www.warp10.io/doc/REDUCE">REDUCE</a> combine des points de différentes GTS partageant le même horodatage, etc.</p><p>Ces deux fonctions font partie des frameworks qui sont presque toujours au cœur même de chaque script que vous allez écrire. Ce sont des fonctionnalités très puissantes et polyvalentes qui peuvent, par exemple, <a href="https://blog.senx.io/time-synchronization/">synchroniser les horodatages</a> entre GTS ou rendre les horodatages de données irrégulières, <a href="https://blog.senx.io/aggregate-by-calendar-duration-in-warpscript/">uniformément espacés</a>.</p><blockquote><p>En savoir plus sur <a href="https://blog.senx.io/thinking-in-warpscript-essential-frameworks/">tous les frameworks disponibles dans WarpScript</a></p></blockquote><p>Outre les frameworks, d’autres fonctions plus spécialisées facilitent considérablement l’analyse des données de séries temporelles. Certaines fonctions permettent le <a href="https://blog.senx.io/motion-split/">découpage d’une GTS</a> selon différents paramètres: temps ou distance entre points consécutifs ou détection d’arrêt plus complexe. Il est également possible de <a href="https://blog.senx.io/thinking-in-warpscript-detecting-anomalies/">détecter des anomalies</a> dans des GTS à l’aide de différents algorithmes.</p><p><a href="https://www.warp10.io/tags/gts">Toutes ces fonctions</a> évitent d’avoir à réécrire du code complexe chaque fois que vous devez concevoir un script pour l’analyse de séries temporelle. Cela vous fait gagner du temps de développement et vous permet de vous concentrer sur la logique de votre analyse.</p><h2 id="Plier-les-Geo-Time-Series-a-votre-volonte"><a href="#Plier-les-Geo-Time-Series-a-votre-volonte" class="headerlink" title="Plier les Geo Time Series à votre volonté"></a>Plier les Geo Time Series à votre volonté</h2><p>Cet article ne serait pas complet si nous ne parlions pas de la manière de corrompre ce modèle GTS. Attention, cette section concerne la modélisation et la manipulation très avancées des GTS.</p><p>Premièrement, les positions dans une GTS sont <a href="https://blog.senx.io/working-with-geo-data-in-warp-10/">codées en tant que codes HHC 64 bits</a>. Donc, pour le dire franchement, une GTS est une séquence d’enregistrements, chacun composé de 1 entier long, 2 entiers longs optionnels et un Boolean, un entier Long, un Double ou une chaîne de caractères. Vous utilisez normalement les 3 Longs pour représenter l’horodatage, l’emplacement HHCode et l’élévation, mais on peut en fait représenter n’importe quoi.</p><p>Si vous souhaitez stocker une GTS, les horodatages doivent être uniques dans une GTS. Vous pouvez stocker un identifiant unique dans des horodatages, par exemple, un MMSI, identifiant unique d’un navire, <a href="https://wiki.openstreetmap.org/wiki/Way">un ID de voie OSM</a> ou même un HHCode. Cela vous permet de récupérer rapidement les informations dont vous avez besoin sans avoir besoin d’une base de données supplémentaire pour stocker ce type d’informations.</p><p>Si vous avez besoin que votre GTS stocke 3 Longs par point de données, vous pouvez les stocker dans la position, l’élévation et la valeur. Par défaut, l’entier long de la position est interprété comme un HHCode 64 bits et converti en latitude &#x2F; longitude. Vous pouvez facilement récupérer la représentation Long avec la fonction <code>[-&gt;GTSHHCODELONG](https://www.warp10.io/doc/AIt6K4C7H3CEG3KBIot6)</code>. Attention, le HHCode <code>91480763316633925</code> est réservé et signifie «pas de position», vous ne devez donc pas l’utiliser.</p><h2 id="En-resume"><a href="#En-resume" class="headerlink" title="En résumé"></a>En résumé</h2><p>Une Geo Time Series est un modèle polyvalent pour représenter des données de séries temporelles, bien adapté aux données provenant de capteurs et de métriques d’un large éventail de domaines. Les Geo Time Series peuvent éventuellement stocker un emplacement et une altitude pour chaque point, mais il n’y a pas d’inconvénient à utiliser une Geo Time Series sans localisation.</p><p>Elles peuvent stocker des booléens, des longs, des doubles et des chaînes de caractères, ce qui offre à la fois des performances et une compatibilité avec presque tous les types de données. Pour interroger les Geo Time Series, elles sont identifiées de manière unique par un nom de classe et des paires clé-valeur facultatives appelées labels. Cela permet de sélectionner une série unique ou un groupe d’entre elles pour les combiner et les analyser.</p><p>Enfin, les fonctions WarpLib offrent un large éventail de fonctionnalités pour manipuler les Geo Time Series. Le but de ces fonctions est de vous faire gagner du temps de développement et vous permettre de vous concentrer sur la logique de votre analyse.</p><blockquote><p>Article Original : <a href="https://blog.senx.io/all-about-geo-time-series-model/">https://blog.senx.io/all-about-geo-time-series-model/</a></p></blockquote>]]>
      </content:encoded>
    </item>
    <item>
      <title>Monitorer son infra avec Warp 10 - partie 1</title>
      <link>https://giwi.fr/2020-12-21-monitorer-son-infra-avec-warp-10-partie-1/</link>
      <description>
        <![CDATA[<h2 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h2><p>Sous ce titre pompeux et aguic]]>
      </description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Linux/">Linux</category>
      <category domain="https://giwi.fr/categories/DevOps/">DevOps</category>
      <category domain="https://giwi.fr/categories/Warp-10/">Warp 10</category>
      <category domain="https://giwi.fr/tags/monitoring/">monitoring</category>
      <category domain="https://giwi.fr/tags/warp-10/">warp 10</category>
      <pubDate>Mon, 21 Dec 2020 19:51:36 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h2><p>Sous ce titre pompeux et aguicheur, je vais vous expliquer avec une série de posts comment je monitore mon infrastructure IT domestique (comprenez 3 Raspberry Pi).</p><p>Ok, ça fait un peu tromperie sur la marchandise, mais le principe est le même sur une infra plus classique (sauf pour la partie alerting, mais on y reviendra).</p><p>Comme beaucoup de geek, j’ai quelques Raspberry Pi à la maison et celui qui gère la domotique est un tantinet important. Il y a quelques années, j’utilisais Nodequery, mais leur blog et leur Github sont sans vie depuis 2014… bref rien de bien encourageant quant à la pérennité du service. De plus, je souhaitais avoir la main sur les données et remonter bien plus que ce que ne fait Nodequery.</p><p>Alors oui, plein de gens vous expliqueront que c’est trop cool de le faire avec Influx+Grafana ou Prometheus mais c’est surtout trop limité.</p><p>Mon choix s’est porté sur <a href="https://warp10.io/">Warp 10</a> qui, en plus de gérer bien plus efficacement que Influx ou Prometheus les séries temporelles (oui, les données de monitoring sont des séries temporelles), il offre un langage puissant de manipulation de cette typologie de données (nous y reviendrons dans un prochain post) que ne proposent pas les autres (non, le <code>select *</code> n’est pas un langage de manipulation évolué).</p><p>Warp 10 est assez versatile et non limité au monitoring (contrairement aux autres que j’ai cité), on peut bâtir bien des choses avec. Cependant, dans le domaine du monitoring, il vient avec un petit agent qui s’appelle <a href="https://warp10.io/content/03_Documentation/06_Operations/04_Monitor_Warp_10">Sensision</a>. Cet agent n’est pas super documenté, mais l’éditeur de Warp 10 (<a href="https://senx.io/">SenX</a>) ne mets pas forcément l’accent sur le use case du monitoring. Et puis je peux me sortir les doigts du slip et creuser un peu quand même.</p><p>Dans ce premier post je vais vous expliquer l’architecture et l’installation de Warp 10 et de Sensision.</p><h2 id="Architecture-generale"><a href="#Architecture-generale" class="headerlink" title="Architecture générale"></a>Architecture générale</h2><p>L’idée est de déployer un agent Sensision sur chacun de mes Raspberry. Chacun de ces agents va remonter des <a href="https://github.com/senx/warp10-platform/blob/master/warp10/src/main/java/io/warp10/continuum/sensision/SensisionConstants.java">métriques</a> sur une instance centrale de Warp 10. Enfin, on va pouvoir se construire un dashboard et du monitoring (niveau CM1) pour surveiller tout ça.</p><p><img src="/images/2020/12/diag1.png"></p><h2 id="Warp-10"><a href="#Warp-10" class="headerlink" title="Warp 10"></a>Warp 10</h2><p>Installer Warp 10, c’est simple. C’est plus rapide avec <a href="https://store.docker.com/community/images/warp10io/warp10">Docker</a> :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker run -d \</span></span><br><span class="line"><span class="language-bash">  -p 8080:8080 -p 8081:8081 \</span></span><br><span class="line"><span class="language-bash">  --volume=/opt/warp10:/data \</span></span><br><span class="line"><span class="language-bash">  --name=warp10 warp10io/warp10:latest</span></span><br></pre></td></tr></table></figure><p>Je ne vais pas rentrer dans le détail de l’utilisation ou du paramétrage de Warp 10, leur <a href="https://blog.senx.io/">blog</a> couvre beaucoup de choses. Et si vous préférez l’installer hors Docker, <a href="https://warp10.io/content/03_Documentation/02_Installation/01_Standalone">c’est à peine plus compliqué</a>.</p><p>Warp 10 fonctionne parfaitement sur Raspberry et à fortiori également sur un vrai serveur. J’ai choisi de l’installer sur un Raspberry via Docker (crade mais fonctionnel).</p><p>Une fois installé, il va nous falloir des jetons (<a href="https://warp10.io/content/03_Documentation/05_Security/01_Overview">tokens</a>):</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker <span class="built_in">exec</span> -u warp10 -it warp10 warp10-standalone.sh worf monitor 315360000000</span></span><br></pre></td></tr></table></figure><p>Ici je crée une paire de jetons (read&#x2F;write) valable 10 ans pour une application nommée “monitor”.</p><p>Conservez bien ces jetons présents dans les champs read et write de la structure JSON.</p><p>Et hop Warp 10 est opérationnel.</p><h2 id="Sensision"><a href="#Sensision" class="headerlink" title="Sensision"></a>Sensision</h2><p>Il n’y a pas d’image Docker pour Sensision, mais vous pouvez en créer une, c’est pas très complexe.</p><p>Allez zou! Je me logue sur mon Raspberry en ssh, puis:</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cd</span> /opt</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> su</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">wget https://github.com/senx/sensision/releases/download/1.0.24/sensision-service-1.0.24.tar.gz</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">tar xf sensision-service-1.0.24.tar.gz</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">useradd sensision</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">chown</span> -R sensision:sensision sensision-1.0.24</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cd</span> sensision-1.0.24</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">./bin/sensision.init start</span></span><br></pre></td></tr></table></figure><p>Arrivé ici Sensision va créer les fichiers de conf qui vont bien.</p><p>Avant d’aller plus loin, on va modifier <code>/etc/fstab</code> et créer des RAMDisk pour éviter de matraquer la carte SD (et la bousiller trop vite). Ici je présume que le user sensision a comme id 1001 (idem pour son groupe)</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">vi /etc/fstab</span></span><br></pre></td></tr></table></figure><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line">tmpfs /var/tmp tmpfs defaults,noatime,nosuid,<span class="attr">size</span>=<span class="number">10</span>m <span class="number">0</span> <span class="number">0</span></span><br><span class="line">tmpfs /var/log tmpfs defaults,noatime,nosuid,<span class="attr">mode</span>=<span class="number">0755</span>,size=<span class="number">10</span>m <span class="number">0</span> <span class="number">0</span></span><br><span class="line">tmpfs /opt/sensision-1.0.24/logs tmpfs defaults,noatime,nosuid,nodev,noexec,<span class="attr">mode</span>=<span class="number">0777</span> <span class="number">0</span> <span class="number">0</span></span><br><span class="line">tmpfs /opt/sensision-1.0.24/targets tmpfs defaults,noatime,nosuid,nodev,noexec,<span class="attr">mode</span>=<span class="number">0777</span> <span class="number">0</span> <span class="number">0</span></span><br><span class="line">tmpfs /tmp tmpfs defaults,noatime,nosuid,nodev,noexec,<span class="attr">mode</span>=<span class="number">0755</span>,size=<span class="number">10</span>M <span class="number">0</span> <span class="number">0</span></span><br></pre></td></tr></table></figure><p>Bon, on peut faire mieux (je suis ouvert aux propositions).</p><p>L’étape suivante consiste à paramétrer Sensision, ça se fait dans <code>/opt/sensision-1.0.23/etc/sensision.conf</code> (le fichier créer avec le premier start).</p><p>D’abord, des labels dédiés à votre serveur, ici j’ai host qui contient l’adresse IP et hname qui contient son petit nom. Vous pouvez en ajouter d’autres.</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="section">[...]</span></span><br><span class="line"><span class="attr">sensision.default.labels</span>=host=<span class="number">192.168</span>.<span class="number">60</span>.xxx,hname=monPi</span><br><span class="line"><span class="section">[...]</span></span><br></pre></td></tr></table></figure><p>Puis il faut configurer le endpoint de Warp 10:</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="section">[...]</span></span><br><span class="line"><span class="comment">#</span></span><br><span class="line"><span class="comment"># Default queue</span></span><br><span class="line"><span class="comment">#</span></span><br><span class="line"><span class="comment">#</span></span><br><span class="line"><span class="attr">sensision.qf.url.default</span>=http://ip.de.warp10/api/v0/update</span><br><span class="line"><span class="attr">sensision.qf.token.default</span>=C0[xxxxxx]CV</span><br><span class="line"><span class="attr">sensision.qf.topn.default</span>=<span class="number">250</span></span><br><span class="line"><span class="attr">sensision.qf.period.default</span>=<span class="number">1000</span></span><br><span class="line"><span class="attr">sensision.qf.batchsize.default</span>=<span class="number">10000</span></span><br><span class="line"><span class="section">[...]</span></span><br></pre></td></tr></table></figure><ul><li><code>sensision.qf.url.default</code> doit contenir le chemin vers votre instance de Warp 10.</li><li><code>sensision.qf.token.default</code> doit contenir votre token d’écriture (write) défini plus haut.</li></ul><p>Le reste on ne touche pas.</p><p>Démarrons Sensision sous forme de service. Il faut créer le fichier <code>/opt/sensision-1.0.23/bin/sensision.service</code> :</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Sensision</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">Type</span>=forking</span><br><span class="line"><span class="attr">User</span>=root</span><br><span class="line"><span class="attr">ExecStart</span>=/opt/sensision-<span class="number">1.0</span>.<span class="number">24</span>/bin/sensision.init start</span><br><span class="line"><span class="attr">ExecStop</span>=/opt/sensision-<span class="number">1.0</span>.<span class="number">24</span>/bin/sensision.init stop</span><br><span class="line"><span class="attr">SuccessExitStatus</span>=<span class="number">143</span></span><br><span class="line"><span class="attr">RestartSec</span>=<span class="number">10</span>s</span><br><span class="line"><span class="attr">TimeoutStartSec</span>=<span class="number">60</span>s</span><br><span class="line"><span class="comment">#If you do not want systemd to monitor and restart Warp 10 automatically, comment the restart always option:</span></span><br><span class="line"><span class="attr">Restart</span>=always</span><br><span class="line"><span class="attr">ConditionPathIsMountPoint</span>=/opt/sensision-<span class="number">1.0</span>.<span class="number">24</span>/targets</span><br><span class="line"><span class="comment">#You can customize system limits here: see https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Process%20Properties</span></span><br><span class="line"><span class="comment">#LimitNOFILE=1000000</span></span><br><span class="line"><span class="comment">#LimitCORE=infinity</span></span><br><span class="line"><span class="comment">#LimitNPROC=32000</span></span><br><span class="line"><span class="comment">#LimitMEMLOCK=500000</span></span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=multi-user.target</span><br></pre></td></tr></table></figure><p>Puis en tant que root :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">ln</span> -s /opt/sensision-1.0.24/bin/sensision.service /etc/systemd/system/sensision.service</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">systemctl daemon-reload</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">systemctl <span class="built_in">enable</span> sensision</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">systemctl start sensision</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">journalctl -f -u sensision // Pour vérifier que c<span class="string">&#x27;est bien démarré</span></span></span><br></pre></td></tr></table></figure><p>Il n’y a plus qu’à rebooter le Pi (à cause du FSTab) et il est monitoré.</p><p>Dans un prochain chapitre, nous explorerons les métriques de Sensisions et nous construirons notre premier dashboard.</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Connaître le nombre de lignes d’un fichier</title>
      <link>https://giwi.fr/2018-04-19-connaitre-le-nombre-de-lignes-dun-fichier/</link>
      <description>Vous souhaitez savoir le nombre de ligne d’un fichier? Voici plusieurs techniques.</description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/Linux/">Linux</category>
      <category domain="https://giwi.fr/tags/shell/">shell</category>
      <category domain="https://giwi.fr/tags/ubuntu/">ubuntu</category>
      <category domain="https://giwi.fr/tags/Linux/">Linux</category>
      <pubDate>Thu, 19 Apr 2018 10:21:04 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Vous souhaitez savoir le nombre de ligne d’un fichier?</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">cat monfichier.txt | wc -l</span><br><span class="line">wc -l monfichier.txt</span><br></pre></td></tr></table></figure><p>Mais quand le fichier est monstrueux, c’est la misère, voici un script qui permet de faire une estimation :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">wcle – word count line estimate</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Fast line-count estimate <span class="keyword">for</span> huge files</span></span><br><span class="line">file=$1</span><br><span class="line">nsample=1000</span><br><span class="line">headbytes=`head -q -n $nsample $file | wc -c`</span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash">tailbytes=`<span class="built_in">tail</span> -q -n <span class="variable">$nsample</span> <span class="variable">$file</span> | <span class="built_in">wc</span> -c`</span></span><br><span class="line">filesize=`ls -sH --block-size=1 $file | cut -f1 -d&quot; &quot;`</span><br><span class="line">echo -n $((filesize / (headbytes) * $nsample))</span><br><span class="line">echo &quot; (&quot; $((filesize / headbytes )) &quot;K;&quot; $((filesize / headbytes /1000 )) &quot;M )&quot;</span><br></pre></td></tr></table></figure><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">./wcle.sh mon-gros-fichier.txt</span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>Générer un changelog Github</title>
      <link>https://giwi.fr/2018-04-17-changelog/</link>
      <description>Un script NodeJS pour vous générer un changelog tout beau tout propre avec les commits entre 2 tags Github</description>
      <author>Giwi</author>
      <category domain="https://giwi.fr/categories/JavaScript/">JavaScript</category>
      <category domain="https://giwi.fr/tags/Github/">Github</category>
      <category domain="https://giwi.fr/tags/changelog/">changelog</category>
      <pubDate>Tue, 17 Apr 2018 10:21:04 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Bon, c’est cadeau, voici un script NodeJS pour vous générer un changelog tout beau tout propre avec les commits (hors merge) entre 2 tags Github :</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="meta">#!/usr/bin/env node</span></span><br><span class="line"><span class="keyword">const</span> &#123; execSync &#125; = <span class="built_in">require</span>(<span class="string">&#x27;child_process&#x27;</span>);</span><br><span class="line"><span class="keyword">let</span> repo = <span class="string">&#x27;&#x27;</span>;</span><br><span class="line"><span class="keyword">let</span> md =</span><br><span class="line">  <span class="string">`TITRE DE VOTRE CHANGELOG</span></span><br><span class="line"><span class="string">---</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">`</span>;</span><br><span class="line"><span class="comment">// changez &#x27;head -n 10&#x27; pour avoir autre chose que les 10 derniers tags</span></span><br><span class="line"><span class="keyword">let</span> tagList = <span class="title function_">execSync</span>(<span class="string">&#x27;git tag --sort=-committerdate | head -n 10&#x27;</span>).<span class="title function_">toString</span>().<span class="title function_">split</span>(<span class="string">&#x27;\n&#x27;</span>); </span><br><span class="line"><span class="keyword">let</span> lastTag = tagList[<span class="number">0</span>];</span><br><span class="line">tagList = tagList.<span class="title function_">slice</span>(<span class="number">1</span>, -<span class="number">1</span>);</span><br><span class="line">tagList.<span class="title function_">forEach</span>(<span class="function"><span class="params">tag</span> =&gt;</span> &#123;</span><br><span class="line">  md += <span class="string">`## <span class="subst">$&#123;lastTag&#125;</span></span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">`</span>;</span><br><span class="line"> <span class="title function_">execSync</span>(<span class="string">`git log --no-merges --date=iso --format=&quot;&gt; +  ts%ct  | %s %N (*[%cN](%ce) | [view commit](<span class="subst">$&#123;repo&#125;</span>/commit/%H)*)&quot; <span class="subst">$&#123;tag&#125;</span>..<span class="subst">$&#123;lastTag&#125;</span>`</span>)</span><br><span class="line">    .<span class="title function_">toString</span>().<span class="title function_">split</span>(<span class="string">&#x27;\n&#x27;</span>).<span class="title function_">forEach</span>(<span class="function"><span class="params">l</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> timestamp = <span class="regexp">/ts([0-9]+)/</span>.<span class="title function_">exec</span>(l);</span><br><span class="line">    <span class="keyword">if</span> (timestamp) &#123;</span><br><span class="line">      l = l.<span class="title function_">replace</span>(<span class="string">&#x27;ts&#x27;</span> + timestamp[<span class="number">1</span>], <span class="keyword">new</span> <span class="title class_">Date</span>(timestamp[<span class="number">1</span>] * <span class="number">1000</span>).<span class="title function_">toISOString</span>().<span class="title function_">split</span>(<span class="string">&#x27;T&#x27;</span>)[<span class="number">0</span>].<span class="title function_">replace</span>(<span class="regexp">/\-/gi</span>, <span class="string">&#x27;/&#x27;</span>));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">let</span> issue = <span class="regexp">/#([0-9]+)/</span>.<span class="title function_">exec</span>(l);</span><br><span class="line">    <span class="keyword">if</span> (issue) &#123;</span><br><span class="line">      l = l.<span class="title function_">replace</span>(<span class="string">&#x27;#&#x27;</span> + issue[<span class="number">1</span>], <span class="string">`[#<span class="subst">$&#123;issue[<span class="number">1</span>]&#125;</span>](<span class="subst">$&#123;repo&#125;</span>/issues/<span class="subst">$&#123;issue[<span class="number">1</span>]&#125;</span>)`</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    md += l + <span class="string">&#x27;\n&#x27;</span>;</span><br><span class="line">  &#125;);</span><br><span class="line">  lastTag = tag;</span><br><span class="line"></span><br><span class="line">&#125;);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(md);</span><br></pre></td></tr></table></figure><p>Usage :</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">node changelog.js &gt; CHANGELOG.md</span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
  </channel>
</rss>
