<?xml version='1.0' encoding='UTF-8'?>
<?xml-stylesheet type="text/xsl" href="/fr/atom.xslt"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr" xml:base="https://vincent.bernat.ch">
  <title>Vincent Bernat</title>
  <link href="https://vincent.bernat.ch/fr/blog/atom.xml" rel="self"/>
  <link href="https://vincent.bernat.ch/fr" rel="alternate"/>
  <id>http://www.luffy.cx/fr/blog/atom.xml/</id>
  <updated>2026-07-04T20:27:01Z</updated>

  <entry>
    <title type="html">L'usine à clous soviétique ou l'échec des KPI</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-kpi-goodhart" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-kpi-goodhart#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-06-16T06:26:27Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-kpi-goodhart.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p>En 2008, j’ai décroché mon second emploi, dans l’équipe réseau d’<em>Orange
Portails</em><sup id="fnref:hebex"><a class="footnote-ref" href="#fn:hebex">1</a></sup>, la division derrière les sites web et le moteur de recherche de
l’opérateur télécom français Orange. Tout y fonctionnait comme sur des
roulettes : un environnement technique complet, une équipe dédiée pour chaque
pan de l’activité et la liberté de me concentrer sur ce que je faisais de mieux.
Quelques années plus tard, plus rien n’allait : minés par une obsession du
chiffre, nous n’arrivions plus à livrer les nouveaux services dans les temps.</p>
<div class="admonition">
<p class="admonition-title">Avertissement</p>
<p>C’est une histoire que j’aime raconter pour mettre en garde
contre la <a href="https://fr.wikipedia.org/wiki/Loi_de_Goodhart" title="La loi de Goodhart sur Wikipédia">loi de Goodhart</a><sup id="fnref:campbell"><a class="footnote-ref" href="#fn:campbell">2</a></sup>. Comme ces événements
remontent à près de 15 ans, mes souvenirs sont un peu flous. Je suis parti en
2012.</p>
</div>
<h1 id="les-premieres-annees">Les premières années</h1>
<p>À mes débuts, le département fonctionnait comme une startup. Son origine était
l’entreprise française Echo. Elle avait bâti un moteur de recherche. France
Télécom l’a racheté et rebaptisé <a href="https://fr.wikipedia.org/wiki/Voila">Voila</a>. C’était le moteur de recherche le
plus consulté en France au début des années 2000. France Télécom a regroupé les
activités de portail au sein de la division <em>Wanadoo Portails</em>, renommée plus
tard <em>Orange Portails</em>.</p>
<p>L’environnement technique était excellent. Nous disposions de nombreux outils
internes<sup id="fnref:nocloud"><a class="footnote-ref" href="#fn:nocloud">3</a></sup> : un système de tickets, un outil de graphes RRD, un IPAM, un
outil de reporting et un outil d’alerte basé sur SNMP<sup id="fnref:snalert"><a class="footnote-ref" href="#fn:snalert">4</a></sup>. Nous déployions
nos serveurs Linux avec <a href="https://cfengine.com/">CFEngine</a>. Nous installions les systèmes et les
applications depuis des dépôts Debian internes. Nous documentions tout dans une
instance <a href="https://www.mediawiki.org/wiki/MediaWiki" title="MediaWiki est une plateforme de collaboration et de documentation">MediaWiki</a> privée. La supervision était basée sur un ancêtre de
<a href="https://www.xymon.com/servers/servers.html" title="The Xymon Monitor">XYmon</a>. L’architecture réseau était propre et évolutive, avec peu de dette
technique. Un nouvel arrivant était opérationnel en une journée.</p>
<p>C’était un environnement épanouissant pour moi. J’ai développé plusieurs
outils : <a href="https://lldpd.github.io/" title="lldpd : implémentation de IEEE 802.1AB">lldpd</a>, une implémentation de 802.1AB, <a href="/fr/blog/2013-snimpy" title="Snimpy : SNMP &amp; Python">Snimpy</a>, une interface
pythonique pour Net-SNMP, <a href="https://github.com/vincentbernat/wiremaps">Wiremaps</a>, un outil de découverte de niveau 2 doté
d’une machine à remonter le temps pour savoir quel équipement est connecté où,
<a href="https://github.com/vincentbernat/Kitero">Kitérő</a>, un outil pour simuler des conditions réseau, <a href="https://github.com/vincentbernat/QCss-3/">QCSS-3</a>, un
contrôleur pour répartiteurs de charge, et <a href="https://github.com/vincentbernat/ipoo">ipoo</a>, un service accessible via
un bot Jabber et un script Greasemonkey pour exposer des informations liées aux
adresses IP. J’ai ajouté le <a href="/fr/blog/2011-keepalived-snmp-ipv6" title="Support SNMP pour Keepalived">support SNMP pour Keepalived</a> et <a href="https://github.com/search?q=repo%3AFRRouting%2Ffrr+author%3Avincentbernat+snmp&amp;type=commits" title="Commits liés à SNMP pour Quagga/FRR">Quagga</a>. J’ai aussi lancé ce blog, avec des articles comme
« <a href="/fr/blog/2011-dns-anycast" title="DNS anycast">DNS anycast</a> », des articles sur TLS comme « <a href="/fr/blog/2011-dos-ssl-solutions" title="Déni de service TLS : quelles solutions ?">Déni de service
TLS : quelles solutions ?</a> », des articles sur
SNMP comme « <a href="/fr/blog/2012-snmp-boucle-evenements" title="Intégration de Net-⁠SNMP dans une boucle d'évènements">Intégration de Net-SNMP dans une boucle d’évènements</a> », des articles sur Linux comme « <a href="/fr/blog/2011-ipv4-route-cache-linux" title="Comprendre la mise en cache des routes IPv4 sous Linux">Comprendre la
mise en cache des routes IPv4 sous Linux</a> », et
un <a href="/fr/blog/2012-multicast-vxlan" title="Réseaux virtuels avec VXLAN">article sur VXLAN</a> bien avant que ce soit à la mode.</p>
<h1 id="la-chute">La chute</h1>
<p>Quand nous avions besoin de nouveaux serveurs, l’équipe de proximité en prenait
un lot dans l’inventaire, y installait notre distribution Linux, les déplaçait dans le
datacenter et les câblait au réseau. Nous ouvrions un ticket décrivant les
serveurs dont nous avions besoin et, une semaine plus tard, nos serveurs étaient
disponibles. 💫</p>
<p>Orange voulait savoir si l’équipe de proximité était performante : elle a donc
réclamé des <abbr title="Indicateur clé de performance">KPI</abbr>. Le choix s’est porté sur le nombre de tickets traités dans
l’année. Orange a demandé à doubler ce nombre. Au lieu d’un seul ticket pour un
nouveau service, nous en ouvrions six, un par serveur. À la fin de l’année, les
<abbr title="Indicateur clé de performance">KPI</abbr> avaient plus que doublé.</p>
<p>Tout le monde y a vu une réussite du pilotage de la performance. Il a donc été
demandé de récidiver l’année suivante. Cette fois, nous devions ouvrir un ticket
par serveur et par étape. De nouveau, les <abbr title="Indicateur clé de performance">KPI</abbr> ont doublé. En coulisses, les
tickets partaient à différentes personnes et n’étaient plus traités dans
l’ordre. Aussi, pour l’année suivante, il a été décidé de créer des méta-tickets
et de tenir des réunions pour suivre l’avancement de ces tickets. Bien sûr,
toutes ces étapes supplémentaires faisaient encore grimper le <abbr title="Indicateur clé de performance">KPI</abbr>.</p>
<p>Cette méthode de pilotage de la performance s’est étendue aux autres
équipes<sup id="fnref:firewall"><a class="footnote-ref" href="#fn:firewall">5</a></sup>. Tout est devenu plus lent. Au lieu de quelques semaines, il
nous fallait six mois pour déployer un nouveau service. Nous avions bâti une
<a href="https://fronterabrands.com/goodharts-law/" title="Goodhart’s Law: Soviet Nail Factories &amp; The Power of Incentives">usine à clous soviétique</a>. Mais les <abbr title="Indicateur clé de performance">KPI</abbr> étaient bons et
nous avons lâché l’affaire.</p>
<p>Prenons un autre exemple. Nous devions estimer l’impact de chaque opération de
nuit. Nous n’étions pas mauvais : l’essentiel des opérations étaient déclarées
« sans impact ». La plupart du temps, c’était le cas. Parfois, il y avait un
impact de 5 secondes. On nous a demandé de faire plus d’efforts pour tenir
l’impact annoncé. Qu’avons-nous fait ? Nous avons commencé à déclarer un impact
attendu de 5 secondes. Un jour, nous avons eu un impact de 30 secondes et il
nous a été reproché de ne pas avoir respecté l’impact annoncé. Au final, la
plupart des opérations étaient déclarées avec un impact attendu de 10 minutes,
et nous avons cessé de nous en préoccuper : au lieu de basculer le trafic
avec soin, nous nous autorisions un impact de 5 minutes. Et nos <abbr title="Indicateur clé de performance">KPI</abbr> n’avaient
jamais été aussi bons.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 630px"><span class="lf-media-inner" style="padding-bottom: 38.889%"><img alt="Graphique montrant l'impact des opérations de nuit. Année après année, la&#10;tolérance de l'impact est augmentée. La dernière année, l'impact attendu est de&#10;10 minutes et toutes les opérations sont en dessous de ce seuil. Toutefois, les&#10;impacts sont beaucoup plus importants que la première&#10;année." src="https://d2pzklc15kok91.cloudfront.net/images/orange-impacts.03b883aacf07da.svg" width="630" height="245" class="lf-media"/></span></div><figcaption>Vue d'artiste de l'évolution des impacts au fil des années.</figcaption></figure>
<hr/>
<p>Les <abbr title="Indicateur clé de performance">KPI</abbr> ne sont pas inutiles, mais ils peuvent avoir des conséquences négatives.
Utilisez-les avec précaution : laissez les personnes opérationnelles participer
au choix des indicateurs et reliez ces indicateurs à la qualité du service
rendu, par exemple avec des <a href="https://sre.google/sre-book/service-level-objectives/" title="Google SRE book: Service Level Objectives">objectifs de niveau de service</a>. Sinon, même les
plus consciencieux finissent par lâcher prise et détourner le système avant de
partir<sup id="fnref:google"><a class="footnote-ref" href="#fn:google">6</a></sup>. 📊</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:hebex">
<p>En interne, cette entité s’appelait « Hebex » (hébergement et
exploitation) et était située sur les sites de Bagnolet et de
Sophia-Antipolis. <a class="footnote-backref" href="#fnref:hebex" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:campbell">
<p>La loi de Goodhart est souvent citée, mais la <a href="https://fr.wikipedia.org/wiki/Loi_de_Campbell" title="La loi de Campbell sur Wikipédia">loi de
Campbell</a> décrit encore mieux mon expérience : plus on
s’appuie sur un chiffre pour prendre des décisions, plus vite les gens le
corrompent. <a class="footnote-backref" href="#fnref:campbell" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:nocloud">
<p>À l’époque, le <abbr title="Software as a Service">SaaS</abbr> n’était pas vraiment répandu. Je me souviens
avoir envisagé, avec deux collègues, de vendre <a href="https://github.com/vincentbernat/wiremaps">Wiremaps</a> en <abbr title="Software as a Service">SaaS</abbr>, avec un
chiffrement homomorphe pour la base de données. Mais qui aurait accepté
d’externaliser sa pile d’observabilité ? <a class="footnote-backref" href="#fnref:nocloud" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:snalert">
<p><em>Snalert</em> était un outil d’alerte métacirculaire écrit en Perl. Il
pouvait interroger un très grand nombre de cibles SNMP en peu de temps.
Toute notre supervision reposait sur SNMP, y compris la supervision système. <a class="footnote-backref" href="#fnref:snalert" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:firewall">
<p>Mon équipe gérait aussi les règles de nombreux pare-feux sous Linux.
Pour gonfler nos <abbr title="Indicateur clé de performance">KPI</abbr>, nous appliquions la même méthode : plutôt que d’accepter
un seul ticket avec une matrice de flux, nous exigions un ticket par flux. <a class="footnote-backref" href="#fnref:firewall" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:google">
<p>Orange n’est pas un cas isolé. Le processus de promotion de Google
est un autre exemple bien connu de <abbr title="Indicateur clé de performance">KPI</abbr> défaillant. Michael Lynch le raconte
dans « <a href="https://mtlynch.io/why-i-quit-google/" title="Why I Quit Google to Work for Myself">Why I Quit Google to Work for Myself</a> ». <a class="footnote-backref" href="#fnref:google" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
</ol>
</div>
      </div></content>
  </entry>
  <entry>
    <title type="html">Tenir un blog bilingue avec l'aide d'une IA</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-blog-ia" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-blog-ia#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-06-09T20:15:13Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-blog-ia.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p>L’<abbr title="Intelligence artificielle">IA</abbr> inonde le web de contenus insipides. Les articles sur <a href="https://www.linkedin.com/">LinkedIn</a> avec une
image générée par <abbr title="Intelligence artificielle">IA</abbr>, remplis d’émojis, publiés par des soi-disant experts sur
un sujet auquel ils ne portent pas assez d’intérêt pour écrire eux-mêmes, sont
les pires spécimens. Bien que je sois très contrarié de cette situation, je
m’appuie sur des <abbr title="Large Language Models">LLMs</abbr> pour la <strong>grammaire</strong>, les <strong>révisions linguistiques</strong> et
la <strong>traduction</strong>. Je ne trouve pas cela incohérent.</p>
<p>Ma langue maternelle est le français mais j’écris la plupart des articles en
anglais et en français. Quand j’ai commencé à écrire ce blog en 2011, je
rédigeais d’abord en <a href="/fr/blog/2011-migration-vers-github" title="Migration de Trac vers GitHub">français</a> avant de traduire en <a href="/en/blog/2011-migrating-to-github" title="Migrating from Trac to GitHub">anglais</a>.
J’ai fini par comprendre qu’il était <a href="https://sci-hub.fr/10.1016/j.jslw.2009.06.003" title="L1 use during L2 writing: An empirical study of a complex phenomenon">préférable de le faire dans l’autre
sens</a> afin de rendre le texte plus naturel
et idiomatique. J’essaie d’écrire un anglais « correct » mais cela n’a jamais
été mon point fort<sup id="fnref:book"><a class="footnote-ref" href="#fn:book">1</a></sup>. Par exemple, j’ai toujours eu des difficultés sur
l’accord des temps. J’apprends les règles et je les oublie immédiatement. Je ne
me vois pas non plus <a href="https://mtlynch.io/editor/" title="How I Hired a Freelance Editor for My Blog">embaucher un éditeur</a> pour quelque chose
que je considère comme un loisir.</p>
<p>À titre d’exemple, j’ai gardé l’historique des modifications successives lors de
l’écriture de « <em><a href="/en/blog/2026-akvorado-rib-sharding" title="Scaling Akvorado BMP RIB with sharding">Scaling Akvorado BMP RIB with sharding</a></em> » :</p>
<ol>
<li>la <a href="https://github.com/vincentbernat/vincent.bernat.ch/commit/aad263c20c7b021b1069952d1487374ff559d3b3" title="article: Akvorado BMP RIB with sharding">première ébauche</a> rédigée à l’aide d’un dictionnaire de synonymes<sup id="fnref:kagi"><a class="footnote-ref" href="#fn:kagi">2</a></sup> ;</li>
<li>la <a href="https://github.com/vincentbernat/vincent.bernat.ch/commit/11231af4be15160c1dd48e45c9b4dc7c042cf191" title="content: copyediting of Akvorado BMP RIB article">version modifiée</a> par la compétence « <a href="https://github.com/vincentbernat/vincent.bernat.ch/blob/latest/.claude/skills/edit/SKILL.md">révision linguistique</a> » ;</li>
<li>la <a href="https://github.com/vincentbernat/vincent.bernat.ch/commit/0435ae3a8450132abb89507e856a012831457e49" title="article: Akvorado BMP RIB in French">traduction en français</a> générée par la compétence
   « <a href="https://github.com/vincentbernat/vincent.bernat.ch/blob/latest/.claude/skills/translate/SKILL.md">traduction</a> » ;</li>
<li>la <a href="https://github.com/vincentbernat/vincent.bernat.ch/commit/c0629b1d9fe450335fcd30c071fb44301615dd15" title="content: human proofread of the French translation">version révisée par un humain</a> avec quelques modifications
   sur la version anglaise.</li>
</ol>
<p>Je sais que les <abbr title="Intelligence artificielle">IA</abbr> peuvent <a href="https://sites.google.com/view/llmwritingdistortion/home" title="How LLMs Distort Our Written Language">dénaturer la voix de l’auteur pendant
l’édition</a>, mais les corrections
apportées lors de la seconde étape sont mineures. Les instructions sont « d’appliquer de légères modifications de style » tout en demandant d’éviter les
formes passives, les phrases trop longues, les verbes peu expressifs et les mots
inutiles. Elles définissent également l’audience cible : technique avec un
niveau d’anglais B2.</p>
<p>Dans l’extrait suivant, j’ai utilisé « <em>long time</em> » au lieu de « <em>long-standing</em> ». Outre le trait d’union manquant, le premier s’applique à des
personnes (<em>a <a href="https://dictionary.cambridge.org/us/dictionary/english/long-time" title="Definition of long-time in the Cambridge Dictionary">long-time</a> friend</em>), tandis que le second concerne des
situations (<em>a <a href="https://dictionary.cambridge.org/us/dictionary/english/long-standing" title="Definition of long-standing in the Cambridge Dictionary">long-standing</a> agreement</em>). J’ai mis pas mal de temps à
comprendre la seconde modification : l’<abbr title="Intelligence artificielle">IA</abbr> préfère <a href="https://dictionary.cambridge.org/us/grammar/british-grammar/relative-clauses-defining-and-non-defining" title="Relative clauses: defining and non-defining">cette forme
grammaticale</a> pour introduire la définition de « <em>RIB
sharding</em> ».</p>
<blockquote>
<p>As the Internet routing table contains more than 1 million routes, Akvorado
needs to scale to tens of millions of routes. This has been a <del>long
time</del> <ins>long-standing</ins> challenge, but I expect this issue is now
fixed by using RIB sharding, a method <del>to split</del> <ins>that
splits</ins> the routing database into several parts to enable concurrent
updates.</p>
</blockquote>
<p>Dans la modification ci-dessous, l’<abbr title="Intelligence artificielle">IA</abbr> utilise le mot « <em>device</em> » plutôt que le
mot « <em>équipement</em> » car ce dernier est indénombrable. Je le sais mais je tombe
régulièrement dans ce piège.</p>
<blockquote>
<p>When Akvorado does not find a route from a specific device, it falls back to a
route sent by another <del>equipment</del> <ins>device</ins>.</p>
</blockquote>
<p>J’encourage l’<abbr title="Intelligence artificielle">IA</abbr> à utiliser des verbes descriptifs et elle s’exécute en
remplaçant une périphrase par un verbe précis :</p>
<blockquote>
<p>The benchmarks demonstrate it <del>has better performance than</del>
<ins>outperforms</ins> other <del>packages, both</del> <ins>packages</ins> for
lookups, insertions, and memory usage.</p>
</blockquote>
<p>Elle corrige aussi les erreurs de grammaire. Dans le passage suivant, « <em>list of
routes</em> » est au singulier. De plus, « <em>stored</em> » représente un état et il ne
faut pas le faire suivre de « <em>into</em> » qui indique un changement.</p>
<blockquote>
<p>The list of routes for each prefix <del>are</del> <ins>is</ins> not stored
directly <del>into</del> <ins>in</ins> the prefix tree.</p>
</blockquote>
<p>Voici un dernier exemple : le verbe « <em><a href="https://dictionary.cambridge.org/dictionary/english/require" title="Definition of require in the Cambridge Dictionary">require</a></em> » doit être suivi d’un nom
ou d’un objet suivi d’un infinitif. Il n’est pas possible de le faire suivre
directement par un infinitif.</p>
<blockquote>
<p>An alternative would be to have one prefix tree for each peer but it would
require <del>to configure</del> <ins>configuring</ins> all routers to export
their routes.</p>
</blockquote>
<p>Même si je lis beaucoup d’anglais<sup id="fnref:monkey"><a class="footnote-ref" href="#fn:monkey">3</a></sup>, je bute toujours sur ces règles de
grammaire. Le français est une langue relativement simple une fois les bases
acquises alors que l’anglais est rempli d’irrégularités.</p>
<hr/>
<p>J’indique dans chaque pied de page si une <abbr title="Intelligence artificielle">IA</abbr> a été utilisée pour modifier le
contenu. Il y a 3 niveaux :</p>
<ul>
<li>🧠: pas d’<abbr title="Intelligence artificielle">IA</abbr> ou pratiquement pas (par exemple, des corrections grammaticales)</li>
<li>✨: amélioré par l’<abbr title="Intelligence artificielle">IA</abbr> (par exemple, des révisions linguistiques)</li>
<li>🤖: généré par <abbr title="Intelligence artificielle">IA</abbr> (par exemple, traduit depuis une autre langue)</li>
</ul>
<p>Survolez avec la souris ou tapotez sur l’icône pour révéler le nom de l’<abbr title="Intelligence artificielle">IA</abbr> et
son rôle dans la modification du document.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 264px"><span class="lf-media-inner" style="padding-bottom: 45.833%"><img alt="Capture d'écran montrant le pied de page avec l'émoji&#10;« étincelles »" src="https://d2pzklc15kok91.cloudfront.net/images/ai-usage@1x.03aee3edade4f4.jpg" srcset="https://d2pzklc15kok91.cloudfront.net/images/ai-usage@1x.03aee3edade4f4.jpg 332w,https://d2pzklc15kok91.cloudfront.net/images/ai-usage@2x.84457fd4153330.jpg 528w" sizes="auto, (max-width: 264px) 100vw, 264px" width="264" height="121" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAhEAAADyAQMAAADJHwVUAAAABlBMVEUiIiIAAABeOQYjAAAAJklEQVR42u3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAPBiQEgAAWwH/fsAAAAASUVORK5CYII=)"/></span></div><figcaption>Exemple d'utilisation de transparence sur l'usage de l'IA : Claude Sonnet 4.5 a édité cet article.</figcaption></figure>
<p>Le graphique ci-dessous représente par année les outils utilisés pour modifier
chaque article. J’ai récemment demandé à une <abbr title="Intelligence artificielle">IA</abbr> des <a href="https://github.com/vincentbernat/vincent.bernat.ch/blob/latest/.claude/skills/grammar/SKILL.md">corrections
grammaticales</a> sur les anciens articles, aussi bien en
<a href="https://github.com/vincentbernat/vincent.bernat.ch/commit/7baf2c8f18b57e351cc25e7e62f4aa6611362c8c" title="content: fix English grammar">anglais</a> qu’en <a href="https://github.com/vincentbernat/vincent.bernat.ch/commit/0713a8800321da710c81ed10a731da5da34e686d" title="content: fix French grammar">français</a>. Depuis 2018, les
articles sont traduits en français à l’aide de <a href="https://www.deepl.com">DeepL</a> puis ensuite d’une <abbr title="Intelligence artificielle">IA</abbr>.
Depuis 2024, je m’appuie sur une <abbr title="Intelligence artificielle">IA</abbr> pour les révisions linguistiques des
articles en anglais.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 733px"><span class="lf-media-inner" style="padding-bottom: 68.349%"><object width="733" height="501" type="image/svg+xml" data="https://d2pzklc15kok91.cloudfront.net/images/ai-usage.46a97b79787591.svg" class="lf-media">&amp;#128444; Graphique montrant l'utilisation de l'IA année par année. Chaque niveau a sa
propre teinte.</object></span></div><figcaption>Utilisation de l'IA année par année. Passez la souris sur une bande pour plus de détails.</figcaption></figure>
<hr/>
<p>Si vous êtes fermement opposé à l’utilisation des <abbr title="Large Language Model">LLM</abbr> dans l’écriture, j’espère
que vous acceptez mon point de vue plus nuancé comme une contrepartie pour des
articles en français, l’<abbr title="Intelligence artificielle">IA</abbr> mâchant énormément le travail de traduction. En ce
qui concerne l’anglais, la littérature sur le sujet s’accorde sur l’importance
de choisir le bon mot pour capturer l’attention du lecteur jusqu’à la fin.</p>
<blockquote>
<p>[…] Bien écrire consiste à maîtriser un certain nombre de choses
fondamentales, à savoir le vocabulaire, la grammaire et le style, et à remplir
le troisième compartiment de sa boîte à outils avec les bons instruments.</p>
<p>― <em>Stephen King</em>, On Writing</p>
</blockquote>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>Contrairement à d’autres articles récents, je n’ai pas utilisé d’<abbr title="Intelligence artificielle">IA</abbr>,
ni pour traduire en français, ni pour relire la <a href="/en/blog/2026-blogging-llm" title="Blogging with LLMs as a non-native speaker">version anglaise</a>. Pour cette
dernière, une personne anonyme a accepté de corriger ma copie.</p>
</div>
<div class="admonition">
<p class="admonition-title">Mise à jour (06.2026)</p>
<p>Voir la <a href="https://lobste.rs/s/tdvu7a/blogging_with_llm_assistant">discussion associée à cet article sur
Lobsters</a>, ainsi que l’article « <a href="https://writethatblog.substack.com/p/dev-reaction-to-ai-blog-posts" title="Report: How developers react to AI-scented blog posts">How developers react to AI-scented blog
posts</a> » de Cynthia Dunlop, l’une des coautrices de « <a href="https://www.manning.com/books/writing-for-developers" title="“Writing for Developers” by Piotr Sarna and Cynthia Dunlop">Writing for
Developers</a> ».</p>
</div>
<div class="footnote">
<hr/>
<ol>
<li id="fn:book">
<p>J’ai récemment lu « <a href="https://www.manning.com/books/writing-for-developers" title="“Writing for Developers” by Piotr Sarna and Cynthia Dunlop">Writing for Developers</a> » du début à la fin et
je l’ai trouvé motivant. <a href="https://mtlynch.io/about/">Michael Lynch</a> est en train d’écrire
« <a href="https://refactoringenglish.com/" title="“Refactoring English: Effective Writing for Software Developers” by Michael Lynch">Refactoring English</a> » sur le même sujet et je me suis inscrit à
l’accès anticipé. <a class="footnote-backref" href="#fnref:book" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:kagi">
<p>Je suis très satisfait des outils d’aide à l’écriture proposés par
<a href="https://www.kagi.com">Kagi</a>. L’<a href="https://translate.kagi.com">outil de traduction</a> et le
<a href="https://translate.kagi.com/dictionary">dictionnaire</a> me sont très utiles pour trouver le bon mot. Je
recours aussi à l’<a href="https://help.kagi.com/kagi/ai/kagi-research.html" title="Kagi Research Assistants">assistant de recherche de Kagi</a> lors de la phase de documentation. <a class="footnote-backref" href="#fnref:kagi" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:monkey">
<p>Dès le CM2, je jouais à <em>Monkey Island 2</em> en anglais sans l’avoir
appris à l’école. J’utilisais un dictionnaire et je restais perplexe à la
vue des verbes irréguliers. <a class="footnote-backref" href="#fnref:monkey" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
</ol>
</div>
      </div></content>
  </entry>
  <entry>
    <title type="html">Partitionner une table de routage pour des lectures sans verrou en Go</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-akvorado-rib-sharding" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-akvorado-rib-sharding#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-05-24T19:00:00Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-akvorado-rib-sharding.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p>Pour associer des informations de routage, telles que les chemins d’AS ou les
communautés <abbr title="Border Gateway Protocol">BGP</abbr>, aux flux, <a href="https://github.com/akvorado/akvorado" title="Akvorado: flow collector, enricher and visualizer">Akvorado</a> peut importer des routes via le <a href="https://www.rfc-editor.org/rfc/rfc7854" title="BGP Monitoring Protocol (BMP)"><abbr title="Border Gateway Protocol">BGP</abbr>
Monitoring Protocol</a> (<abbr title="BGP Monitoring Protocol">BMP</abbr>). Comme la table de routage Internet contient plus
d’<a href="https://bgp.potaroo.net" title="BGP Routing Table Analysis Reports">un million de routes</a>, Akvorado doit <strong>gérer des dizaines
de millions de routes</strong><sup id="fnref:optimize"><a class="footnote-ref" href="#fn:optimize">1</a></sup>. C’est un défi de longue date<sup id="fnref:past"><a class="footnote-ref" href="#fn:past">2</a></sup>, mais je
pense que ce problème est désormais résolu grâce au <strong>sharding de la <abbr title="Routing Information Base">RIB</abbr></strong>, une
méthode qui découpe la base de routage en plusieurs parties afin de permettre
les mises à jour concurrentes.</p>
<div class="toc">
<ul>
<li><a href="#implementation-precedente">Implémentation précédente</a><ul>
<li><a href="#stocker-les-routes-dans-une-table-associative">Stocker les routes dans une table associative</a></li>
<li><a href="#canonisation-des-routes">Canonisation des routes</a></li>
<li><a href="#quest-ce-qui-pose-probleme">Qu’est-ce qui pose problème ?</a></li>
</ul>
</li>
<li><a href="#sharding-de-la-rib">Sharding de la RIB</a><ul>
<li><a href="#premiere-etape">Première étape</a></li>
<li><a href="#deuxieme-etape">Deuxième étape</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="implementation-precedente">Implémentation précédente</h1>
<p>Akvorado associe deux éléments pour construire sa <abbr title="Routing Information Base">RIB</abbr> :</p>
<ol>
<li>un <strong>arbre de préfixes</strong> et</li>
<li>une <strong>liste de routes</strong> attachée à chaque préfixe.</li>
</ol>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 711px"><span class="lf-media-inner" style="padding-bottom: 105.626%"><img alt="Implémentation de la RIB BMP d'Akvorado avant le sharding, avec la disposition&#10;mémoire de chaque structure et un unique verrou." src="https://d2pzklc15kok91.cloudfront.net/images/akvorado/sharding-before.6657877be051e0.svg" width="711" height="751" class="lf-media"/></span></div><figcaption>Implémentation de la RIB BMP d'Akvorado sans sharding. Un unique verrou en lecture/écriture.</figcaption></figure>
<p>Dans le diagramme ci-dessus, la <abbr title="Routing Information Base">RIB</abbr> stocke cinq préfixes IPv4 et deux préfixes
IPv6. L’un d’eux, <code>2001:db8:1::/48</code>, contient trois routes :</p>
<ul>
<li>depuis le pair 3, via <code>2001:db8::3:1</code>, AS 65402, chemin d’AS <code>65402</code>, communauté
  <code>65402:31</code> ;</li>
<li>depuis le pair 4, via <code>2001:db8::4:1</code>, mêmes <abbr title="Autonomous System Number">ASN</abbr>, chemin d’AS et communauté ;</li>
<li>depuis le pair 5, via <code>2001:db8::5:1</code>, AS 65402, chemin d’AS <code>65401 65402</code>,
  communauté <code>65402:31</code>.</li>
</ul>
<p>La structure <code>rib</code> est définie en Go de la manière suivante :</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">rib</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">tree</span><span class="w">          </span><span class="o">*</span><span class="nx">bart</span><span class="p">.</span><span class="nx">Table</span><span class="p">[</span><span class="nx">prefixIndex</span><span class="p">]</span>
<span class="w">    </span><span class="nx">routes</span><span class="w">        </span><span class="kd">map</span><span class="p">[</span><span class="nx">routeKey</span><span class="p">]</span><span class="nx">route</span>
<span class="w">    </span><span class="nx">nlris</span><span class="w">         </span><span class="o">*</span><span class="nx">intern</span><span class="p">.</span><span class="nx">Pool</span><span class="p">[</span><span class="nx">nlri</span><span class="p">]</span>
<span class="w">    </span><span class="nx">nextHops</span><span class="w">      </span><span class="o">*</span><span class="nx">intern</span><span class="p">.</span><span class="nx">Pool</span><span class="p">[</span><span class="nx">nextHop</span><span class="p">]</span>
<span class="w">    </span><span class="nx">rtas</span><span class="w">          </span><span class="o">*</span><span class="nx">intern</span><span class="p">.</span><span class="nx">Pool</span><span class="p">[</span><span class="nx">routeAttributes</span><span class="p">]</span>
<span class="w">    </span><span class="nx">nextPrefixID</span><span class="w">  </span><span class="nx">prefixIndex</span>
<span class="w">    </span><span class="nx">freePrefixIDs</span><span class="w"> </span><span class="p">[]</span><span class="nx">prefixIndex</span>
<span class="p">}</span>
</code></pre></div>

<p>L’arbre de préfixes s’appuie sur le paquet <a href="https://github.com/gaissmai/bart/" title="Balanced Routing Tables (BART) in Go">bart</a>, une adaptation de
l’<a href="https://www.hariguchi.org/art/art.pdf" title="ART: A Fast Free Multibit Trie Based Routing Table">algorithme ART</a> de Donald Knuth. Les <a href="https://github.com/gaissmai/iprbench">benchmarks</a> montrent
qu’il surpasse les autres implémentations pour les recherches, les insertions et
la consommation mémoire<sup id="fnref:performance"><a class="footnote-ref" href="#fn:performance">3</a></sup>. De plus, l’auteur est d’une grande aide.</p>
<h2 id="stocker-les-routes-dans-une-table-associative">Stocker les routes dans une table associative</h2>
<p>La liste des routes de chaque préfixe n’est pas stockée directement dans
l’arbre de préfixes : cela mettrait trop de pression sur le ramasse-miettes en
allouant un tableau par préfixe.</p>
<p>À la place, la <abbr title="Routing Information Base">RIB</abbr> attribue un identifiant de 32 bits unique à chaque préfixe,
soit en prenant le dernier identifiant disponible dans le tableau
<code>freePrefixIDs</code>, soit en utilisant la valeur <code>nextPrefixID</code> avant de
l’incrémenter. Les routes sont ensuite stockées dans la table associative
<code>routes</code>, qui s’appuie en Go sur une <a href="https://go.dev/blog/swisstable" title="Faster Go maps with Swiss Tables">structure particulièrement
optimisée</a>. Pour récupérer les routes attachées à un préfixe, nous les
cherchons un par un dans la table <code>routes</code> via une clef de 64 bits qui combine
l’index de 32 bits du préfixe avec l’index de 32 bits représentant la position
de la route dans la liste. Akvorado parcourt les routes de la première à la
dernière pour trouver la meilleure<sup id="fnref:scan"><a class="footnote-ref" href="#fn:scan">4</a></sup>. Il sait qu’il n’y a plus de route
lorsque la clé ne renvoie aucun résultat.</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">prefixIndex</span><span class="w"> </span><span class="kt">uint32</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">routeIndex</span><span class="w"> </span><span class="kt">uint32</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">routeKey</span><span class="w"> </span><span class="kt">uint64</span>
</code></pre></div>

<h2 id="canonisation-des-routes">Canonisation des routes</h2>
<p>Une route contient un identifiant de pair <abbr title="Border Gateway Protocol">BGP</abbr>, un <abbr title="Network Layer Reachability Information">NLRI</abbr> partiel<sup id="fnref:nlri"><a class="footnote-ref" href="#fn:nlri">5</a></sup>, le prochain
saut et les attributs.</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">route</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">peer</span><span class="w">       </span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">nlri</span><span class="w">       </span><span class="nx">intern</span><span class="p">.</span><span class="nx">Reference</span><span class="p">[</span><span class="nx">nlri</span><span class="p">]</span>
<span class="w">    </span><span class="nx">nextHop</span><span class="w">    </span><span class="nx">intern</span><span class="p">.</span><span class="nx">Reference</span><span class="p">[</span><span class="nx">nextHop</span><span class="p">]</span>
<span class="w">    </span><span class="nx">attributes</span><span class="w"> </span><span class="nx">intern</span><span class="p">.</span><span class="nx">Reference</span><span class="p">[</span><span class="nx">routeAttributes</span><span class="p">]</span>
<span class="w">    </span><span class="nx">prefixLen</span><span class="w">  </span><span class="kt">uint8</span>
<span class="p">}</span>

<span class="kd">type</span><span class="w"> </span><span class="nx">nlri</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">family</span><span class="w"> </span><span class="nx">bgp</span><span class="p">.</span><span class="nx">Family</span>
<span class="w">    </span><span class="nx">path</span><span class="w">   </span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">rd</span><span class="w">     </span><span class="nx">RD</span>
<span class="p">}</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">nextHop</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">routeAttributes</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">asn</span><span class="w">              </span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">asPath</span><span class="w">           </span><span class="p">[]</span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">communities</span><span class="w">      </span><span class="p">[]</span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">largeCommunities</span><span class="w"> </span><span class="p">[]</span><span class="nx">bgp</span><span class="p">.</span><span class="nx">LargeCommunity</span>
<span class="p">}</span>
</code></pre></div>

<p>Pour économiser la mémoire et les allocations, les <abbr title="Network Layer Reachability Information">NLRI</abbr>, les prochains sauts et
les attributs de route sont canonisés : un entier de 32 bits remplace la valeur
réelle. Ce mécanisme est antérieur au <a href="https://go.dev/blog/unique" title="New unique package">paquet <code>unique</code></a> introduit dans
Go 1.23. Nous le conservons car ses compromis sont différents :</p>
<ul>
<li>il utilise un <strong>comptage de références explicite</strong> plutôt que des pointeurs
  faibles ;</li>
<li>il fonctionne avec des <strong>valeurs non comparables</strong> implémentant les méthodes
  <code>Hash()</code> et <code>Equal()</code><sup id="fnref:hash"><a class="footnote-ref" href="#fn:hash">6</a></sup> ;</li>
<li>il utilise des <strong>instances explicites pour la canonisation</strong>, ce qui se révèlera utile pour
  le sharding ;</li>
<li>il offre de <strong>meilleures performances</strong>. Voir par exemple ce <a href="https://github.com/akvorado/akvorado/pull/2244/changes/682b6063af50780dcd64e46b39d5e66c5074d9ab" title="outlet/routing: use Go's unique instead of intern">benchmark</a> ;</li>
<li>il consomme <strong>deux fois moins de mémoire</strong> grâce à des références 32 bits
  plutôt que des pointeurs ;</li>
<li>mais il <strong>n’est pas utilisable de manière concurrente</strong>.</li>
</ul>
<h2 id="quest-ce-qui-pose-probleme">Qu’est-ce qui pose problème ?</h2>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>Pour l’<a href="https://www.free.fr/freebox">AS 12322</a>, nous n’utilisons pas encore <abbr title="BGP Monitoring Protocol">BMP</abbr><sup id="fnref:cisco"><a class="footnote-ref" href="#fn:cisco">7</a></sup>. Mais
<a href="https://github.com/bogi788" title="bogi788 on GitHub">Gerhard Bogner</a> a eu la patience, la disponibilité et les compétences
techniques pour m’aider à <a href="https://github.com/akvorado/akvorado/discussions/2287" title="Using go's unique package">déboguer ce problème</a>.</p>
</div>
<p>Le verrou global en lecture/écriture est le goulot d’étranglement de cette
implémentation. Mais pourquoi ? Plusieurs composants utilisent la <abbr title="Routing Information Base">RIB</abbr>, chacun
avec ses propres contraintes :</p>
<ul>
<li>
<p>Les <strong>consommateurs Kafka</strong> consultent la <abbr title="Routing Information Base">RIB</abbr> pour enrichir les flux avec des
  informations de routage. Leur nombre est bornée par la somme des partitions
  Kafka<sup id="fnref:KIP-932"><a class="footnote-ref" href="#fn:KIP-932">8</a></sup>. Akvorado ajuste également ce nombre afin de garantir un
  export groupé efficace des flux vers ClickHouse. Sur notre installation, la
  quantité de consommateurs oscille entre 8 et 16. Comme nous voulons observer les
  données récentes le plus tôt possible, nous ne pouvons pas nous permettre que
  les consommateurs Kafka prennent trop de retard.</p>
</li>
<li>
<p>Les <strong>routeurs surveillés</strong> envoient leurs routes via le protocole <abbr title="BGP Monitoring Protocol">BMP</abbr>. À la
  connexion, ils peuvent envoyer des millions de routes<sup id="fnref:bmpconfig"><a class="footnote-ref" href="#fn:bmpconfig">9</a></sup>. Après la
  synchronisation initiale, les mises à jour arrivent en continu et peuvent
  connaître des pics ponctuels. Le routeur détecte une station <abbr title="BGP Monitoring Protocol">BMP</abbr> bloquée
  lorsque sa fenêtre TCP est pleine. Dans ce cas, il réinitialise la session.
  Bien qu’Akvorado dispose d’un important tampon en entrée, il doit appliquer
  les routes reçues suffisamment vite pour ne pas être considéré comme bloqué.</p>
</li>
<li>
<p>Quand un <strong>pair <abbr title="Border Gateway Protocol">BGP</abbr> distant tombe</strong>, Akvorado purge les routes associées en
  parcourant la <abbr title="Routing Information Base">RIB</abbr>. Quand un <strong>routeur surveillé tombe</strong>, Akvorado patiente un
  peu, mais finit par purger toutes les routes associées.</p>
</li>
</ul>
<p>En résumé : sur une installation chargée, la contention sur le verrou est
forte autant pour les lecteurs que pour les écrivains, et aucun des deux ne
peut prendre trop de retard.</p>
<h1 id="sharding-de-la-rib">Sharding de la <abbr title="Routing Information Base">RIB</abbr></h1>
<h2 id="premiere-etape">Première étape</h2>
<p>Pour supprimer le verrou global, la <abbr title="Routing Information Base">RIB</abbr> est découpée en plusieurs « shards »,
chacun gérant un sous-ensemble des préfixes :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 876px"><span class="lf-media-inner" style="padding-bottom: 72.603%"><img alt="Implémentation de la RIB BMP d'Akvorado après le sharding, avec la disposition&#10;mémoire de chaque structure et un verrou par partition." src="https://d2pzklc15kok91.cloudfront.net/images/akvorado/sharding-step1.90ee5154ac6b88.svg" width="876" height="636" class="lf-media"/></span></div><figcaption>Implémentation de la RIB BMP d'Akvorado avec sharding.</figcaption></figure>
<p>L’arbre de préfixes reste global et protégé par un unique verrou. Chaque
partition dispose de son propre verrou en lecture/écriture, de sa table de
routes et de ses blocs de canonisation pour stocker les <abbr title="Network Layer Reachability Information">NLRI</abbr>, les prochains
sauts et les attributs de route, ce qui n’aurait pas été possible avec le
<a href="https://go.dev/blog/unique" title="New unique package">paquet <code>unique</code> de Go</a>. Les index de préfixes sont eux aussi
partitionnés : les 8 bits de poids fort correspondent à l’index de la partition
et les 24 bits de poids faible à l’index local du préfixe.</p>
<p>Gerhard <a href="https://github.com/akvorado/akvorado/discussions/2287#discussioncomment-16020731">a confirmé</a> que suite à <a href="https://github.com/akvorado/akvorado/commit/7e6bbf2210fdf7116d2ee168b307b9906cc223c0" title="outlet/routing: implement RIB sharding for BMP">ce changement à
l’aveugle</a>, le composant <abbr title="BGP Monitoring Protocol">BMP</abbr> a commencé à ronronner. 🎉</p>
<p>J’ai ensuite écrit un <a href="https://github.com/akvorado/akvorado/blob/0811c40cc2065380e3a6230c2796312838e57850/outlet/routing/provider/bmp/concurrent_test.go">benchmark</a> portant sur un
demi-million de routes synthétiques mais plausibles<sup id="fnref:plausible"><a class="footnote-ref" href="#fn:plausible">10</a></sup>, réparties entre
0 et 8 écrivains qui modifient les routes aussi vite que possible, tandis que 1
à 16 lecteurs consultent en boucle un ensemble de 10 000 routes. J’ignore si ce
benchmark est réaliste, mais il confirme les améliorations à la fois sur les
latences en lecture et en écriture :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 970px"><span class="lf-media-inner" style="padding-bottom: 40.928%"><img alt="Deux cartes de chaleur. L'une pour le ratio de latence en lecture, l'autre&#10;pour celui de la latence en écriture. Toutes deux comparent l'accélération&#10;entre le code avant et après le sharding via des cases colorées. La plupart des&#10;cases sont vertes." src="https://d2pzklc15kok91.cloudfront.net/images/akvorado/sharding-heatmap.38c6059c3585ee.svg" width="970" height="397" class="lf-media"/></span></div><figcaption>Amélioration des performances de latence en lecture et en écriture après le sharding.</figcaption></figure>
<p>Il montre également qu’un grand nombre d’écrivains dégrade la latence en
lecture.</p>
<h2 id="deuxieme-etape">Deuxième étape</h2>
<p>L’unique verrou en lecture/écriture protégeant l’arbre de préfixes constitue la
cible suivante. Le paquet <a href="https://github.com/gaissmai/bart/" title="Balanced Routing Tables (BART) in Go">bart</a> propose des méthodes alternatives pour les
mutations. Elles renvoient un arbre mis à jour via une copie partielle. Les
lecteurs n’ont plus besoin du verrou global, qui ne sert plus qu’à synchroniser
les écrivains. L’arbre de préfixes est encapsulé dans un pointeur atomique.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 876px"><span class="lf-media-inner" style="padding-bottom: 72.603%"><img alt="Implémentation de la RIB BMP d'Akvorado pour le sharding avec lectures sans&#10;verrou, avec la disposition mémoire de chaque structure." src="https://d2pzklc15kok91.cloudfront.net/images/akvorado/sharding-step2.1c48d3f740d4d0.svg" width="876" height="636" class="lf-media"/></span></div><figcaption>Implémentation de la RIB BMP d'Akvorado avec sharding et lectures sans verrou.</figcaption></figure>
<p>Sans verrou, un lecteur peut désormais récupérer un index de préfixe périmé en
parcourant sa copie de l’arbre, lorsqu’un écrivain concurrent supprime la dernière
route attachée à cet index et le recycle pour un autre préfixe. Pour éviter ce
problème, nous combinons l’index de préfixe avec un numéro de génération que
nous stockons dans l’arbre :</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">generation</span><span class="w"> </span><span class="kt">uint32</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">prefixRef</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">idx</span><span class="w"> </span><span class="nx">prefixIndex</span>
<span class="w">    </span><span class="nx">gen</span><span class="w"> </span><span class="nx">generation</span>
<span class="p">}</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">rib</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">mu</span><span class="w">     </span><span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span>
<span class="w">    </span><span class="nx">tree</span><span class="w">   </span><span class="nx">atomic</span><span class="p">.</span><span class="nx">Pointer</span><span class="p">[</span><span class="nx">bart</span><span class="p">.</span><span class="nx">Table</span><span class="p">[</span><span class="nx">prefixRef</span><span class="p">]]</span>
<span class="w">    </span><span class="nx">shards</span><span class="w"> </span><span class="p">[]</span><span class="o">*</span><span class="nx">ribShard</span>
<span class="p">}</span>
</code></pre></div>

<p>Chaque partition stocke le numéro de génération de chaque index de préfixe
local. Ce numéro est incrémenté quand l’index de préfixe associé est libéré.
Lors de la recherche des routes attachées à un index de préfixe, le lecteur
vérifie que le numéro de génération correspond. Sinon, il considère que l’index
a été recyclé et que la liste de routes est vide<sup id="fnref:retry"><a class="footnote-ref" href="#fn:retry">11</a></sup>. Le schéma précédent
illustre ce cas pour l’index de préfixe 5, stocké avec un numéro de génération
de 3, alors que la valeur actuelle dans le tableau <code>[]generations</code> est 4. Le
numéro de génération peut boucler, mais ce n’est pas un problème car les
recherches sont rapides.</p>
<p>L’exécution du benchmark sur cette nouvelle implémentation montre les
améliorations sur la latence en lecture ainsi qu’en écriture dès que le coût de
la copie partielle de l’arbre de préfixes est amorti.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 1256px"><span class="lf-media-inner" style="padding-bottom: 46.815%"><img alt="Six cartes de chaleur. Trois pour le ratio de latence en lecture, trois&#10;autres pour celui de la latence en écriture. Elles comparent par paires les&#10;valeurs sans sharding, avec sharding, et avec lectures sans verrou. Pour la&#10;latence en lecture, la plupart des cases sont vertes, ce qui traduit&#10;l'amélioration apportée par la seconde étape. Pour la latence en écriture,&#10;l'accélération est négative pour un faible nombre de&#10;lecteurs." src="https://d2pzklc15kok91.cloudfront.net/images/akvorado/sharding-heatmap2.b13190eb7548a2.svg" width="1256" height="588" class="lf-media"/></span></div><figcaption>Amélioration des performances de latence en lecture et en écriture après les lectures sans verrou. La colonne centrale montre les améliorations cumulées des deux étapes.</figcaption></figure>
<hr/>
<p>Parmi les nombreuses tentatives d’optimisation du composant <abbr title="BGP Monitoring Protocol">BMP</abbr>, le sharding
de la <abbr title="Routing Information Base">RIB</abbr> est l’une des plus satisfaisantes. <a href="https://github.com/akvorado/akvorado/releases/tag/v2.2.0">Akvorado 2.2</a> implémente la
première étape. La <a href="https://github.com/akvorado/akvorado/pull/2433" title="More efficient RIB tree for BMP">PR #2433</a>, rédigée pendant l’écriture de ce
billet, implémente la seconde et est sortie avec <a href="https://github.com/akvorado/akvorado/releases/tag/v2.4.0">Akvorado 2.4</a>. 🪓</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:optimize">
<p>Chaque routeur exportant des flux n’a pas besoin d’envoyer ses
routes. Quand Akvorado ne trouve pas de route pour un équipement donné, il
se rabat sur une route envoyée par un autre équipement. C’est à l’opérateur
de juger si cette approximation est acceptable. <a class="footnote-backref" href="#fnref:optimize" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:past">
<p>J’ai effectué de nombreuses tentatives pour augmenter l’efficacité du
composant <abbr title="BGP Monitoring Protocol">BMP</abbr>. Voir par exemple les <a href="https://github.com/akvorado/akvorado/pull/254" title="inlet/bmp: avoid long lock times when flushing peers">PR #254</a>, <a href="https://github.com/akvorado/akvorado/pull/255" title="“Lockless” RIB design">PR #255</a>, <a href="https://github.com/akvorado/akvorado/pull/278" title="inlet/bmp: new lockless design">PR #278</a>,
<a href="https://github.com/akvorado/akvorado/pull/2244" title="outlet/routing: read from RIB without any locks">PR #2244</a> et <a href="https://github.com/akvorado/akvorado/pull/2245" title="outlet/routing: buffer BMP messages to avoid being flagged as “stuck”">PR #2245</a>. Malgré ces efforts, ce composant est resté
problématique pour certains utilisateurs. Voir la
<a href="https://github.com/akvorado/akvorado/discussions/2287" title="Using go's unique package">discussion #2287</a> comme exemple le plus récent. <a class="footnote-backref" href="#fnref:past" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:performance">
<p>Cela continue de s’améliorer : <a href="https://github.com/gaissmai/bart/releases/tag/v0.28.0" title="bart release v0.28.0: New FastNode implementation">bart 0.28.0</a> introduit une
implémentation alternative qui troque un peu de mémoire contre de meilleures
performances en recherche. Je ne l’ai pas encore essayée car cela fait déjà
quelques mois que je prépare ce billet. <a class="footnote-backref" href="#fnref:performance" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:scan">
<p>Akvorado préfère la route dont le prochain saut correspond exactement.
Sinon, il se rabat sur n’importe quelle autre route. C’est une
approximation. Une alternative consisterait à maintenir un arbre de
préfixes par pair <abbr title="Border Gateway Protocol">BGP</abbr>, mais il faudrait alors configurer tous les routeurs pour
qu’ils exportent leurs routes. Le démon <abbr title="BGP Monitoring Protocol">BMP</abbr> de <a href="http://www.pmacct.net/">pmacct</a> adopte cette
approche. <a class="footnote-backref" href="#fnref:scan" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:nlri">
<p>Si nous considérons la <abbr title="Routing Information Base">RIB</abbr> <abbr title="Border Gateway Protocol">BGP</abbr> comme une base de données, le <abbr title="Network Layer Reachability Information">NLRI</abbr>
(<em>Network Layer Reachability Information</em>) en constitue la clé primaire.
Son contenu dépend de la famille <abbr title="Border Gateway Protocol">BGP</abbr>. Pour IPv4 ou IPv6 unicast, il s’agit
du préfixe. Pour les familles VPNv4 et VPNv6, il inclut le <em>route
distinguisher</em>. Si l’extension <a href="https://www.rfc-editor.org/rfc/rfc7911" title="Advertisement of Multiple Paths in BGP">ADD-PATH</a> est activée, le <abbr title="Network Layer Reachability Information">NLRI</abbr> contient
également un identifiant de chemin.</p>
<p>Dans notre implémentation, nous ne stockons pas le préfixe : nous le
reconstituons à partir de l’adresse IP recherchée et de la longueur de
préfixe stockée séparément. <a class="footnote-backref" href="#fnref:nlri" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:hash">
<p>Les méthodes <code>Hash()</code> s’appuient sur les paquets
<a href="https://pkg.go.dev/hash/maphash"><code>hash/maphash</code></a> et <a href="https://pkg.go.dev/unsafe"><code>unsafe</code></a> pour une meilleure
performance. Voir par exemple la <a href="https://github.com/akvorado/akvorado/blob/97fced5e855ac220e96148e132f643dc180cd097/outlet/routing/provider/bmp/rib.go#L89-L96">fonction <code>Hash()</code> de la structure
<code>nlri</code></a>. <a class="footnote-backref" href="#fnref:hash" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:cisco">
<p>Bien qu’auteur ou co-auteur des premières RFC liées à <abbr title="BGP Monitoring Protocol">BMP</abbr> depuis 2016
(<a href="https://www.rfc-editor.org/rfc/rfc7854" title="BGP Monitoring Protocol (BMP)">RFC 7854</a>, <a href="https://www.rfc-editor.org/rfc/rfc8671" title="Support for Adj-RIB-Out in the BGP Monitoring Protocol (BMP)">RFC 8671</a>, <a href="https://www.rfc-editor.org/rfc/rfc9069" title="Support for Local RIB in the BGP Monitoring Protocol (BMP)">RFC 9069</a>), Cisco n’a pas fourni
d’implémentation utilisable dans IOS XR avant la version 24.2.1. Il nous
reste encore quelques routeurs à mettre à jour pour activer cette
fonctionnalité. <a class="footnote-backref" href="#fnref:cisco" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:KIP-932">
<p>La <a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-932%3A+Queues+for+Kafka" title="KIP-932: Queues for Kafka">KIP-932</a> introduit, dans Kafka 4.2, la notion de <em>groupes
partagés</em> pour permettre la consommation coopérative d’une même partition.
Akvorado ne la prend pas encore en charge. <a class="footnote-backref" href="#fnref:KIP-932" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:bmpconfig">
<p><abbr title="BGP Monitoring Protocol">BMP</abbr> peut être configuré pour envoyer les routes de chaque pair <abbr title="Border Gateway Protocol">BGP</abbr>
avant ou après l’application des politiques entrantes. Dans ce dernier cas,
plus d’un million de routes peuvent arriver par transit. <abbr title="BGP Monitoring Protocol">BMP</abbr> peut aussi être
configuré pour envoyer la <abbr title="Routing Information Base">RIB</abbr> locale, qui ne contient que le meilleur chemin
pour chaque préfixe. <a class="footnote-backref" href="#fnref:bmpconfig" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:plausible">
<p>Les préfixes sont aléatoires, mais la distribution de leurs tailles
et celle des longueurs de chemin AS suivent les <a href="https://bgp.potaroo.net/as2.0/" title="AS65000 BGP Routing Table Analysis Report">données fournies par
Geoff Huston</a>. <a class="footnote-backref" href="#fnref:plausible" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
<li id="fn:retry">
<p>Une alternative aurait été de réessayer la recherche, mais cela
n’aurait pas de sens : la <abbr title="Routing Information Base">RIB</abbr> est une structure <a href="https://fr.wikipedia.org/wiki/Mod%C3%A8le_de_coh%C3%A9rence#Coh%C3%A9rence_%C3%A0_terme" title="Modèle de coherence sur Wikipedia">cohérente à terme</a> et
une liste de routes vide est bien un état qui s’est produit récemment. <a class="footnote-backref" href="#fnref:retry" title="Jump back to footnote 11 in the text">↩</a></p>
</li>
</ol>
</div>
      </div></content>
  </entry>
  <entry>
    <title type="html">CSS &amp; rythme vertical pour le texte, les images et les tableaux</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-css-rythme-vertical" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-css-rythme-vertical#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-04-22T19:48:10Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-css-rythme-vertical.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p>Le rythme vertical structure les lignes selon un espacement régulier de haut en
bas. Il permet de guider l’œil de manière fluide et prévisible. Grâce à l’unité
CSS <code>rlh</code>, ce rythme est simple à mettre en place pour le texte<sup id="fnref:pawel"><a class="footnote-ref" href="#fn:pawel">1</a></sup>. Mais
les illustrations et les tableaux peuvent perturber la mise en page. Le
typographe amateur en moi aspire à suivre la sagesse de Bringhurst :</p>
<blockquote>
<p>Les titres, intertitres, citations, notes de bas de page, illustrations,
légendes et autres intrusions dans le texte créent des syncopes et des
variations par rapport au rythme de base d’un interlignage régulier. Ces
variations peuvent et doivent animer la page, mais le texte principal doit
également reprendre, après chaque variation, précisément la mesure et la
phase.</p>
<p>― <em>Robert Bringhurst</em>, <a href="https://en.wikipedia.org/wiki/The_Elements_of_Typographic_Style" title="The Elements of Typographic Style sur Wikipédia">The Elements of Typographic Style</a></p>
</blockquote>
<h1 id="pour-le-texte">Pour le texte</h1>
<p>Trois facteurs régissent le rythme vertical : la <strong>taille de police</strong>, la
<strong>hauteur de ligne</strong> et la <strong>marge ou le remplissage</strong>. Posons la base avec une
police de 18 pixels et une hauteur de ligne de 1,5 :</p>
<div class="language-css codehilite"><pre><span/><code><span class="nt">html</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mf">112.5</span><span class="kt">%</span><span class="p">;</span>
<span class="w">  </span><span class="k">line-height</span><span class="p">:</span><span class="w"> </span><span class="mf">1.5</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h1</span><span class="o">,</span><span class="w"> </span><span class="nt">h2</span><span class="o">,</span><span class="w"> </span><span class="nt">h3</span><span class="o">,</span><span class="w"> </span><span class="nt">h4</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="kt">%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">html</span><span class="o">,</span><span class="w"> </span><span class="nt">body</span><span class="o">,</span>
<span class="nt">h1</span><span class="o">,</span><span class="w"> </span><span class="nt">h2</span><span class="o">,</span><span class="w"> </span><span class="nt">h3</span><span class="o">,</span><span class="w"> </span><span class="nt">h4</span><span class="o">,</span>
<span class="nt">p</span><span class="o">,</span><span class="w"> </span><span class="nt">blockquote</span><span class="o">,</span>
<span class="nt">dl</span><span class="o">,</span><span class="w"> </span><span class="nt">dt</span><span class="o">,</span><span class="w"> </span><span class="nt">dd</span><span class="o">,</span><span class="w"> </span><span class="nt">ol</span><span class="o">,</span><span class="w"> </span><span class="nt">ul</span><span class="o">,</span><span class="w"> </span><span class="nt">li</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">margin</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="w">  </span><span class="k">padding</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<p>Le <a href="https://www.w3.org/TR/css-values-4/" title="CSS Values and Units Module Level 4, W3C Working Draft">CSS Values and Units Module Level 4</a> définit l’unité <code>rlh</code>, égale à la
hauteur de ligne de l’élément racine. Tous les navigateurs la gèrent <a href="https://webstatus.dev/features/rlh" title="L'unité rlh sur Web Platform Status">depuis
2023</a><sup id="fnref:postcss"><a class="footnote-ref" href="#fn:postcss">2</a></sup>. Utilisez-la pour insérer des espaces verticaux ou
corriger la hauteur de ligne quand la taille de police change<sup id="fnref:calc"><a class="footnote-ref" href="#fn:calc">3</a></sup> :</p>
<div class="language-css codehilite"><pre><span/><code><span class="nt">h1</span><span class="o">,</span><span class="w"> </span><span class="nt">h2</span><span class="o">,</span><span class="w"> </span><span class="nt">h3</span><span class="o">,</span><span class="w"> </span><span class="nt">h4</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">margin-top</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="n">rlh</span><span class="p">;</span>
<span class="w">  </span><span class="k">margin-bottom</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="n">rlh</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h1</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mf">2.5</span><span class="kt">rem</span><span class="p">;</span>
<span class="w">  </span><span class="k">line-height</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="n">rlh</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h2</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mf">1.5</span><span class="kt">rem</span><span class="p">;</span>
<span class="w">  </span><span class="k">line-height</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="n">rlh</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h3</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mf">1.25</span><span class="kt">rem</span><span class="p">;</span>
<span class="w">  </span><span class="k">line-height</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="n">rlh</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">p</span><span class="o">,</span><span class="w"> </span><span class="nt">blockquote</span><span class="o">,</span><span class="w"> </span><span class="nt">pre</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">margin-top</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="n">rlh</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">aside</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mf">0.875</span><span class="kt">rem</span><span class="p">;</span>
<span class="w">  </span><span class="k">line-height</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="n">rlh</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<p>Vérifions le résultat en superposant une grille<sup id="fnref:grid"><a class="footnote-ref" href="#fn:grid">4</a></sup> au-dessus du texte :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 680px"><span class="lf-media-inner" style="padding-bottom: 104.853%"><img alt="Capture d'écran de mon site avec une grille en superposition et chaque ligne&#10;de texte alignée sur la grille" src="https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/text@1x.17b2e8512acd85.png" srcset="https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/text@1x.17b2e8512acd85.png 855w,https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/text@2x.93207136439be4.png 1360w" sizes="auto, (max-width: 680px) 100vw, 680px" width="680" height="713" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAqgAAALJAQMAAABC3kPgAAAABlBMVEX59O0AAAAOW55mAAAAUklEQVR42u3BAQ0AAADCoPdPbQ43oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+DHvhgABu8TltwAAAABJRU5ErkJggg==)"/></span></div><figcaption>L'unité CSS <code>rlh</code> fonctionne bien pour régler les espaces verticaux du texte. Vous pouvez afficher la grille avec <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd>.</figcaption></figure>
<p>Si un sous-élément utilise une police aux métriques intrinsèques plus hautes, il
peut étirer la boîte de la ligne au-delà de la hauteur configurée<sup id="fnref:line-height"><a class="footnote-ref" href="#fn:line-height">5</a></sup>.
Une astuce consiste à ramener la hauteur de ligne à 1. Les glyphes débordent
mais ne poussent pas la ligne plus haut.</p>
<div class="language-css codehilite"><pre><span/><code><span class="nt">code</span><span class="o">,</span><span class="w"> </span><span class="nt">kbd</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">line-height</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<h1 id="pour-les-images-adaptatives">Pour les images adaptatives</h1>
<p>Les images adaptatives, dont la taille s’ajuste à la largeur de l’affichage,
sont difficiles à aligner sur la grille car leur hauteur nous est inconnue. Le
<a href="https://www.w3.org/TR/css-rhythm-1/" title="CSS Rhythmic Sizing Module Level 1">CSS Rhythmic Sizing Module Level 1</a> introduit la propriété <code>block-step</code> pour
ajuster la hauteur d’un élément à un multiple d’une unité de pas. Cependant, la
plupart des navigateurs ne la gèrent pas encore.</p>
<p>Avec JavaScript, nous pouvons ajouter un remplissage autour de l’image pour
qu’elle ne perturbe pas le rythme vertical :</p>
<div class="language-javascript codehilite"><pre><span/><code><span class="kd">const</span><span class="w"> </span><span class="nx">targets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s2">".lf-media-outer"</span><span class="p">);</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">adjust</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">el</span><span class="p">,</span><span class="w"> </span><span class="nx">height</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">rlh</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">parseFloat</span><span class="p">(</span><span class="nx">getComputedStyle</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">).</span><span class="nx">lineHeight</span><span class="p">);</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">padding</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">ceil</span><span class="p">(</span><span class="nx">height</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="nx">rlh</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nx">rlh</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nx">height</span><span class="p">;</span>
<span class="w">  </span><span class="nx">el</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">padding</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nx">padding</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mf">2</span><span class="si">}</span><span class="sb">px 0`</span><span class="p">;</span>
<span class="p">};</span>

<span class="nx">targets</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">el</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">adjust</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span><span class="w"> </span><span class="nx">el</span><span class="p">.</span><span class="nx">clientHeight</span><span class="p">));</span>
</code></pre></div>

<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 680px"><span class="lf-media-inner" style="padding-bottom: 104.853%"><img alt="Capture d'écran de mon site avec une grille en superposition et une image qui&#10;ne rompt pas le rythme vertical. Un remplissage supplémentaire est visible avant&#10;et après l'image. La hauteur de l'image avec son remplissage est de&#10;216." src="https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/images@1x.08d84fe73e020f.png" srcset="https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/images@1x.08d84fe73e020f.png 855w,https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/images@2x.5e994bde851c27.png 1360w" sizes="auto, (max-width: 680px) 100vw, 680px" width="680" height="713" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAqgAAALJAQMAAABC3kPgAAAABlBMVEX38usAAACeelVqAAAAUklEQVR42u3BAQ0AAADCoPdPbQ43oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+DHvhgABu8TltwAAAABJRU5ErkJggg==)"/></span></div><figcaption>L'image s'aligne sur la grille grâce au remplissage calculé par JavaScript. 216 est divisible par 27, notre hauteur de ligne dans cet exemple.</figcaption></figure>
<p>Comme l’image est adaptative, sa hauteur peut varier. Il faut appeler la
fonction <code>adjust()</code> dans un observateur :</p>
<div class="language-javascript codehilite"><pre><span/><code><span class="kd">const</span><span class="w"> </span><span class="nx">ro</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">ResizeObserver</span><span class="p">((</span><span class="nx">entries</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">entry</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">entries</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="nx">height</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">entry</span><span class="p">.</span><span class="nx">contentBoxSize</span><span class="p">[</span><span class="mf">0</span><span class="p">].</span><span class="nx">blockSize</span><span class="p">;</span>
<span class="w">    </span><span class="nx">adjust</span><span class="p">(</span><span class="nx">entry</span><span class="p">.</span><span class="nx">target</span><span class="p">,</span><span class="w"> </span><span class="nx">height</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">});</span>
<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">target</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">targets</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">ro</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">target</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>

<h1 id="pour-les-tableaux">Pour les tableaux</h1>
<p>Les cellules d’un tableau pourraient fixer leur hauteur à <code>1rlh</code> mais
paraîtraient à l’étroit. Passer à <code>2rlh</code> gaspille trop d’espace. À la place,
nous utilisons l’<a href="https://markboulton.co.uk/journal/incremental-leading/" title="Incremental leading">interligne incrémental</a> : une ligne sur
cinq est alignée sur la grille.</p>
<div class="language-css codehilite"><pre><span/><code><span class="nt">table</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">border-spacing</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="kt">px</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="w">  </span><span class="k">border-collapse</span><span class="p">:</span><span class="w"> </span><span class="kc">separate</span><span class="p">;</span>
<span class="w">  </span><span class="err">th</span><span class="w"> </span><span class="err">{</span>
<span class="w">    </span><span class="k">padding</span><span class="p">:</span><span class="w"> </span><span class="mf">0.4</span><span class="n">rlh</span><span class="w"> </span><span class="mi">1</span><span class="kt">em</span><span class="p">;</span>
<span class="w">  </span><span class="p">}</span>
<span class="w">  </span><span class="nt">td</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">padding</span><span class="p">:</span><span class="w"> </span><span class="mf">0.2</span><span class="n">rlh</span><span class="w"> </span><span class="mf">0.5</span><span class="kt">em</span><span class="p">;</span>
<span class="w">  </span><span class="p">}</span>
<span class="err">}</span>
</code></pre></div>

<p>Pour aligner les éléments après le tableau, il faut ajouter un peu de
remplissage. Nous pouvons soit réutiliser le code JavaScript des images, soit
utiliser quelques lignes de CSS qui comptent les lignes régulières et calculent
le remplissage vertical manquant :</p>
<div class="language-css codehilite"><pre><span/><code><span class="nt">table</span><span class="p">:</span><span class="nd">has</span><span class="o">(</span><span class="nt">tbody</span><span class="w"> </span><span class="nt">tr</span><span class="p">:</span><span class="nd">nth-child</span><span class="o">(</span><span class="nt">5n</span><span class="o">)</span><span class="p">:</span><span class="nd">last-child</span><span class="o">)</span><span class="w">   </span><span class="p">{</span><span class="w"> </span><span class="k">padding-bottom</span><span class="p">:</span><span class="w"> </span><span class="mf">0.2</span><span class="n">rlh</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
<span class="nt">table</span><span class="p">:</span><span class="nd">has</span><span class="o">(</span><span class="nt">tbody</span><span class="w"> </span><span class="nt">tr</span><span class="p">:</span><span class="nd">nth-child</span><span class="o">(</span><span class="nt">5n</span><span class="o">+</span><span class="nt">1</span><span class="o">)</span><span class="p">:</span><span class="nd">last-child</span><span class="o">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">padding-bottom</span><span class="p">:</span><span class="w"> </span><span class="mf">0.8</span><span class="n">rlh</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
<span class="nt">table</span><span class="p">:</span><span class="nd">has</span><span class="o">(</span><span class="nt">tbody</span><span class="w"> </span><span class="nt">tr</span><span class="p">:</span><span class="nd">nth-child</span><span class="o">(</span><span class="nt">5n</span><span class="o">+</span><span class="nt">2</span><span class="o">)</span><span class="p">:</span><span class="nd">last-child</span><span class="o">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">padding-bottom</span><span class="p">:</span><span class="w"> </span><span class="mf">0.4</span><span class="n">rlh</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
<span class="nt">table</span><span class="p">:</span><span class="nd">has</span><span class="o">(</span><span class="nt">tbody</span><span class="w"> </span><span class="nt">tr</span><span class="p">:</span><span class="nd">nth-child</span><span class="o">(</span><span class="nt">5n</span><span class="o">+</span><span class="nt">3</span><span class="o">)</span><span class="p">:</span><span class="nd">last-child</span><span class="o">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">padding-bottom</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">}</span>
<span class="nt">table</span><span class="p">:</span><span class="nd">has</span><span class="o">(</span><span class="nt">tbody</span><span class="w"> </span><span class="nt">tr</span><span class="p">:</span><span class="nd">nth-child</span><span class="o">(</span><span class="nt">5n</span><span class="o">+</span><span class="nt">4</span><span class="o">)</span><span class="p">:</span><span class="nd">last-child</span><span class="o">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">padding-bottom</span><span class="p">:</span><span class="w"> </span><span class="mf">0.6</span><span class="n">rlh</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
</code></pre></div>

<p>Une cellule d’en-tête a deux fois le remplissage d’une cellule régulière. Avec
deux lignes régulières, le remplissage total vaut 2×2×0,2+2×0,4=1,6. Il faut
ajouter <code>0.4rlh</code> pour atteindre <code>2rlh</code> de remplissage vertical supplémentaire
autour du tableau.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 680px"><span class="lf-media-inner" style="padding-bottom: 86.765%"><img alt="Capture d'écran de mon site avec une grille en superposition et un tableau&#10;suivant le rythme vertical. Un remplissage supplémentaire est visible après le&#10;tableau. La hauteur du tableau avec son remplissage est de&#10;405." src="https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/tables@1x.56e678195427d0.png" srcset="https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/tables@1x.56e678195427d0.png 855w,https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/tables@2x.19b9c475806bf5.png 1360w" sizes="auto, (max-width: 680px) 100vw, 680px" width="680" height="590" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEQAAAA7AQMAAAD4qb7lAAAABlBMVEWx3O0AAAAc4XKiAAAAD0lEQVR42mNgGAWjgFoAAAJOAAFGvjzoAAAAAElFTkSuQmCC)"/></span></div><figcaption>Une ligne sur cinq est alignée sur la grille. Un remplissage supplémentaire est ajouté après le tableau pour ne pas rompre le rythme vertical. 405 est divisible par 27, notre hauteur de ligne dans cet exemple.</figcaption></figure>
<hr/>
<p>Rien de tout cela n’est indispensable, mais une fois l’œil entraîné, il devient
difficile de ne plus le remarquer. En attendant que les navigateurs implémentent
le <a href="https://www.w3.org/TR/css-rhythm-1/" title="CSS Rhythmic Sizing Module Level 1">CSS Rhythmic Sizing</a>, un mélange de de
bidouilles CSS et d’un peu de JavaScript suffit. Le texte principal reprend
désormais après chaque intrusion « précisément la mesure et la phase ». 🎼</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:pawel">
<p>Voir « <a href="https://pawelgrzybek.com/vertical-rhythm-using-css-lh-and-rlh-units/" title="Vertical rhythm using CSS lh and rlh units">Vertical rhythm using CSS <code>lh</code> and <code>rlh</code> units</a> » par
Paweł Grzybek. Précédemment, une bonne référence était « <a href="http://lamb.cc/typograph/" title="Scale &amp; Rhythm">Scale &amp;
Rhythm</a> » de Iain Lamb. <a class="footnote-backref" href="#fnref:pawel" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:postcss">
<p>Pour une meilleure compatibilité, remplacez <code>2rlh</code> par
<code>calc(var(--line-height) * 2rem)</code> et définissez la propriété <code>--line-height</code>
dans la pseudo-classe <code>:root</code>. J’ai écrit un <a href="https://github.com/vincentbernat/vincent.bernat.ch/blob/95f29793faad50a94fe4199a1774d59d409d3755/extensions/css.js#L69-L79">petit greffon
PostCSS</a> à cet effet. <a class="footnote-backref" href="#fnref:postcss" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:calc">
<p>Il aurait été plus élégant de calculer la hauteur de ligne avec
<code>calc(round(up, calc(2.4rem / 1rlh), 0) * 1rlh)</code>. Cependant, l’arithmétique
typée n’est <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1264520" title="Support mixed number, unit value, and percent for calc() parser including type checking">pas encore implémentée par Firefox</a>. De plus, les navigateurs ne gèrent <code>round()</code> que <a href="https://webstatus.dev/features/round-mod-rem" title="La fonction round() sur Web Platform Status">depuis 2024</a>. J’ai donc codé un <a href="https://github.com/vincentbernat/vincent.bernat.ch/blob/95f29793faad50a94fe4199a1774d59d409d3755/extensions/css.js#L31-L67">autre greffon PostCSS</a> pour cela. <a class="footnote-backref" href="#fnref:calc" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:grid">
<p>Ce code CSS définit une grille qui suit la hauteur de ligne :</p>
<div class="language-css codehilite"><pre><span/><code><span class="nt">body</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">position</span><span class="p">:</span><span class="w"> </span><span class="kc">relative</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">body</span><span class="p">::</span><span class="nd">after</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">content</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">;</span>
<span class="w">  </span><span class="k">position</span><span class="p">:</span><span class="w"> </span><span class="kc">absolute</span><span class="p">;</span>
<span class="w">  </span><span class="k">inset</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="w">  </span><span class="k">z-index</span><span class="p">:</span><span class="w"> </span><span class="mi">9999</span><span class="p">;</span>
<span class="w">  </span><span class="k">background</span><span class="p">:</span><span class="w"> </span><span class="nb">linear-gradient</span><span class="p">(</span><span class="mi">180</span><span class="kt">deg</span><span class="p">,</span><span class="w"> </span><span class="mh">#c8e1ff</span><span class="mi">99</span><span class="w"> </span><span class="mi">1</span><span class="kt">px</span><span class="p">,</span><span class="w"> </span><span class="kc">transparent</span><span class="w"> </span><span class="mi">1</span><span class="kt">px</span><span class="p">);</span>
<span class="w">  </span><span class="k">background-size</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="kt">px</span><span class="w"> </span><span class="mi">1</span><span class="n">rlh</span><span class="p">;</span>
<span class="w">  </span><span class="k">pointer-events</span><span class="p">:</span><span class="w"> </span><span class="kc">none</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<p><a class="footnote-backref" href="#fnref:grid" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:line-height">
<p>Voir « <a href="https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align">Deep dive CSS: font metrics, line-height and vertical-align</a> » par Vincent De Oliveira. <a class="footnote-backref" href="#fnref:line-height" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
</ol>
</div>
      </div></content>
  </entry>
  <entry>
    <title type="html">Calculer « 1/(40rods/hogshead) to L/100km » depuis l'invite Zsh</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-calculatrice-zsh" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-calculatrice-zsh#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-03-22T13:37:09Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-calculatrice-zsh.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p>J’ai souvent besoin d’un calcul rapide ou de convertir une unité en une autre.
Plutôt que de recourir à un outil séparé, quelques lignes de configuration <em>Zsh</em>
transforment <code>=</code> en calculatrice. En tapant <code>= 660km / (2/3)c * 2 -&gt; ms</code>,
j’obtiens <code>6.60457 ms</code><sup id="fnref:marseille"><a class="footnote-ref" href="#fn:marseille">1</a></sup> sans quitter mon terminal, grâce à l’éditeur
de ligne de Zsh.</p>
<div class="toc">
<ul>
<li><a href="#lalias">L’alias =</a></li>
<li><a href="#le-probleme-des-caracteres-speciaux">Le problème des caractères spéciaux</a><ul>
<li><a href="#proteger-lexpression-automatiquement-avec-zle">Protéger l’expression automatiquement avec ZLE</a></li>
<li><a href="#conserver-lexpression-originale-dans-lhistorique">Conserver l’expression originale dans l’historique</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="lalias">L’alias <code>=</code></h1>
<p>L’idée de base est simple : définir <code>=</code> comme alias vers une calculatrice en
ligne de commande. Je préfère <a href="https://numbat.dev/" title="Numbat: a statically typed programming language for scientific computations">Numbat</a>, une calculatrice scientifique gérant
les conversions d’unités. <a href="https://qalculate.github.io/" title="Qalculate!: the ultimate desktop calculator">Qalculate</a> est une autre option<sup id="fnref:qalc"><a class="footnote-ref" href="#fn:qalc">2</a></sup>. Si aucun
des deux n’est disponible, nous nous rabattons sur le module <em>zcalc</em> intégré à
Zsh.</p>
<p>Comme la commande <code>alias</code> utilise <code>=</code> comme séparateur entre nom et valeur, nous
modifions directement le tableau associatif <code>aliases</code> :</p>
<div class="language-bash codehilite"><pre><span/><code><span class="k">if</span><span class="w"> </span><span class="o">((</span><span class="w"> </span>$+commands<span class="o">[</span>numbat<span class="o">]</span><span class="w"> </span><span class="o">))</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">  </span>aliases<span class="o">[=]=</span><span class="s1">'numbat -e'</span>
<span class="k">elif</span><span class="w"> </span><span class="o">((</span><span class="w"> </span>$+commands<span class="o">[</span>qalc<span class="o">]</span><span class="w"> </span><span class="o">))</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">  </span>aliases<span class="o">[=]=</span><span class="s1">'qalc'</span>
<span class="k">else</span>
<span class="w">  </span>autoload<span class="w"> </span>-Uz<span class="w"> </span>zcalc
<span class="w">  </span>aliases<span class="o">[=]=</span><span class="s1">'zcalc -f -e'</span>
<span class="k">fi</span>
</code></pre></div>

<p>Ainsi, <code>= 847/11</code> devient <code>numbat -e 847/11</code>.</p>
<h1 id="le-probleme-des-caracteres-speciaux">Le problème des caractères spéciaux</h1>
<p>Le premier problème apparaît vite. Taper <code>= 5 * 3</code> échoue : Zsh interprète <code>*</code>
comme un motif pour fichiers avant de le transmettre à la calculatrice. Le même
souci se pose avec d’autres caractères spéciaux pour Zsh, comme <code>&gt;</code> ou <code>|</code>. Il
faut protéger l’expression :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'5 * 3'</span>
<span class="go">15</span>
</code></pre></div>

<p>Nous corrigeons cela en nous appuyant sur l’éditeur de ligne de Zsh pour <strong>placer
l’expression entre guillemets</strong> avant son exécution.</p>
<h2 id="proteger-lexpression-automatiquement-avec-zle">Protéger l’expression automatiquement avec <abbr title="Zsh Line Editor">ZLE</abbr></h2>
<p>Zsh appelle le widget <code>line-finish</code> avant de valider une commande. Nous y
attachons une fonction qui détecte le préfixe <code>=</code> et protège l’expression :</p>
<div class="language-bash codehilite"><pre><span/><code>_vbe_calc_quote<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="w">  </span><span class="k">case</span><span class="w"> </span><span class="nv">$BUFFER</span><span class="w"> </span><span class="k">in</span>
<span class="w">    </span><span class="s2">"="</span>*<span class="o">)</span>
<span class="w">      </span><span class="nb">typeset</span><span class="w"> </span>-g<span class="w"> </span><span class="nv">_vbe_calc_expr</span><span class="o">=</span><span class="nv">$BUFFER</span><span class="w"> </span><span class="c1"># not used yet</span>
<span class="w">      </span><span class="nv">BUFFER</span><span class="o">=</span><span class="s2">"= </span><span class="si">${</span><span class="p">(q-)</span><span class="si">${${</span><span class="nv">BUFFER</span><span class="p">#=</span><span class="si">}</span><span class="p"># </span><span class="si">}}</span><span class="s2">"</span>
<span class="w">      </span><span class="p">;;</span>
<span class="w">  </span><span class="k">esac</span>
<span class="o">}</span>
add-zle-hook-widget<span class="w"> </span>line-finish<span class="w"> </span>_vbe_calc_quote
</code></pre></div>

<p>Quand on tape <code>= 5 * 3</code> puis <kbd>↲</kbd>, <code>_vbe_calc_quote</code> protège
l’expression avec le <a href="https://manpages.debian.org/zshexpn.1.html#q~2" title="zshexpn(1) manual page">drapeau <code>(q-)</code></a> et réécrit la
ligne en <code>= '5 * 3'</code> avant que Zsh ne valide la commande. En bonus, vous
pouvez économiser quelques caractères avec <code>=5*3</code> ! 🚀</p>
<p>Nous pouvons désormais effectuer des calculs et des conversions d’unités
directement depuis le shell. Zsh protège automatiquement les expressions :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'1 + 2'</span>
<span class="go">3</span>
<span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'pi/3 + pi |&gt; cos'</span>
<span class="go">-0.5</span>
<span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'17 USD -&gt; EUR'</span>
<span class="go">14.7122 €</span>
<span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'180*500mg -&gt; g'</span>
<span class="go">90 g</span>
<span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'5 gigabytes / (2 minutes + 17 seconds) -&gt; megabits/s'</span>
<span class="go">291.971 Mbit/s</span>
<span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'now() -&gt; tz("Asia/Tokyo")'</span>
<span class="go">2026-03-22 22:00:03 JST (UTC +09), Asia/Tokyo</span>
<span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'1 / (40 rods / hogshead) -&gt; L / 100km'</span>
<span class="go">118548 × 0.01 l/km</span>
</code></pre></div>

<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 400px"><span class="lf-media-inner" style="padding-bottom: 74.750%"><img alt="« C'est comme ça que j'aime ! » dit Abraham&#10;Simpson" src="https://d2pzklc15kok91.cloudfront.net/images/simpson-s06e18@1x.5105fbe4e02961.jpg" srcset="https://d2pzklc15kok91.cloudfront.net/images/simpson-s06e18@1x.5105fbe4e02961.jpg 503w,https://d2pzklc15kok91.cloudfront.net/images/simpson-s06e18@2x.b9476b906c236f.jpg 800w" sizes="auto, (max-width: 400px) 100vw, 400px" width="400" height="299" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAErAQMAAADKfEc5AAAABlBMVEV7mJsAAABPtU65AAAAJklEQVR42u3BgQAAAADDoPlTn+AGVQEAAAAAAAAAAAAAAAAAANcAO5EAAdUk3zIAAAAASUVORK5CYII=)"/></span></div><figcaption>Le système métrique, c'est une invention du démon ! Ma voiture fait une demi-lieue avec soixante gallons d'essence. Tant pis si ça vous plaît pas ! ― <em>Abraham Simpson</em>, Burns fait son cinéma</figcaption></figure>
<h2 id="conserver-lexpression-originale-dans-lhistorique">Conserver l’expression originale dans l’historique</h2>
<p>En l’état, Zsh enregistre l’expression <em>protégée</em> dans l’historique. Il faut la
déprotéger avant de la soumettre à nouveau, sinon <abbr title="Zsh Line Editor">ZLE</abbr> la protège une seconde
fois. <a href="https://www.zsh.org/mla/users/2026/msg00021.html" title="Re: A ZLE widget for calculator">Bart Schaefer</a> m’a fourni une solution pour
stocker la version originale :</p>
<div class="language-bash codehilite"><pre><span/><code>_vbe_calc_history<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="si">${</span><span class="p">+_vbe_calc_expr</span><span class="si">}</span>
<span class="o">}</span>
add-zsh-hook<span class="w"> </span>zshaddhistory<span class="w"> </span>_vbe_calc_history

_vbe_calc_preexec<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="w">  </span><span class="o">((</span><span class="w"> </span><span class="si">${</span><span class="p">+_vbe_calc_expr</span><span class="si">}</span><span class="w"> </span><span class="o">))</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span>print<span class="w"> </span>-s<span class="w"> </span><span class="nv">$_vbe_calc_expr</span>
<span class="w">  </span><span class="nb">unset</span><span class="w"> </span>_vbe_calc_expr
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="m">0</span>
<span class="o">}</span>
add-zsh-hook<span class="w"> </span>preexec<span class="w"> </span>_vbe_calc_preexec
</code></pre></div>

<p>Le <em>hook</em> <code>zshaddhistory</code> renvoie 1 quand une expression est en cours
d’évaluation, indiquant à <em>Zsh</em> de ne pas enregistrer la commande. Le <em>hook</em>
<code>preexec</code> ajoute ensuite la commande originale, non protégée, avec <code>print -s</code>.</p>
<hr/>
<p>Le code complet est disponible dans mon <a href="https://github.com/vincentbernat/zshrc/blob/9af588820bed37b3b64b2f06777a77d32f4654c4/rc/alias.zsh#L451-L480">zshrc</a>. Une alternative courante est
le modificateur <a href="https://manpages.debian.org/zshmisc.1.html#noglob" title="zshmisc(1) manual page"><code>noglob</code></a>. En utilisant <code>to</code> au lieu de <code>-&gt;</code> pour les
conversions d’unités, il couvre 90 % des cas. Pour une astuce similaire avec
l’éditeur de ligne de Zsh, voyez comment j’utilise les <a href="/fr/blog/2025-zsh-expansion-automatique-alias" title="Expansion automatique des alias dans Zsh">expansions automatiques
des alias</a> pour corriger les fautes de frappe courantes.</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:marseille">
<p>C’est le temps le plus court pour qu’un paquet fasse l’aller-retour
entre Paris et Marseille par fibre optique. <a class="footnote-backref" href="#fnref:marseille" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:qalc">
<p>Qalculate est plus strict avec les unités. Par exemple, il interprète
« Mbps » comme mégabarn par picoseconde : ☢️</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>numbat<span class="w"> </span>-e<span class="w"> </span><span class="s1">'5 MB/s -&gt; Mbps'</span>
<span class="go">40 Mbps</span>
<span class="gp">$ </span>qalc<span class="w"> </span><span class="m">5</span><span class="w"> </span>MB/s<span class="w"> </span>to<span class="w"> </span>Mbps
<span class="go">5 megabytes/second = 0.000005 B/ps</span>
</code></pre></div>

<p><a class="footnote-backref" href="#fnref:qalc" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
</ol>
</div>
      </div></content>
  </entry>
  <entry>
    <title type="html">Découverte automatique de métriques Prometheus avec les labels Docker</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-decouverte-metriques-prometheus-labels-docker" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-decouverte-metriques-prometheus-labels-docker#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-03-05T15:40:24Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-decouverte-metriques-prometheus-labels-docker.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p><a href="/fr/blog/2025-akvorado-2.0" title="Akvorado release 2.0">Akvorado</a>, un collecteur de flux réseau, s’appuie sur <a href="https://traefik.io/traefik" title="Traefik: modern HTTP reverse proxy and load-balancer">Traefik</a>, un proxy
HTTP, pour exposer les services HTTP d’un environnement <a href="https://docs.docker.com/compose/" title="Docker Compose documentation">Docker Compose</a>. Des
<a href="https://docs.docker.com/engine/manage-resources/labels/" title="Docker object labels">labels Docker</a> attachés à chaque service définissent les règles
de routage. Traefik les prend en compte automatiquement au démarrage d’un
conteneur. Plutôt que de maintenir un fichier de configuration statique pour
collecter les <a href="https://prometheus.io/docs/concepts/data_model/" title="Prometheus Data Model">métriques Prometheus</a>, la même approche
s’applique avec <a href="https://grafana.com/docs/alloy/latest/" title="Grafana Alloy documentation">Grafana Alloy</a>, simplifiant sa configuration.</p>
<div class="toc">
<ul>
<li><a href="#traefik-docker">Traefik &amp; Docker</a></li>
<li><a href="#decouverte-de-metriques-avec-alloy">Découverte de métriques avec Alloy</a><ul>
<li><a href="#decouverte-des-conteneurs-docker">Découverte des conteneurs Docker</a></li>
<li><a href="#reetiquetage-des-cibles">Réétiquetage des cibles</a></li>
<li><a href="#collecte-et-transmission">Collecte et transmission</a></li>
</ul>
</li>
<li><a href="#exporteurs-integres">Exporteurs intégrés</a></li>
</ul>
</div>
<h1 id="traefik-docker">Traefik &amp; Docker</h1>
<p>Traefik <a href="https://doc.traefik.io/traefik/reference/install-configuration/providers/docker/" title="Traefik: Docker provider">écoute les événements sur la socket Docker</a>. Chaque service annonce sa configuration via des labels. Par
exemple, voici le service Loki dans Akvorado :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="nt">services</span><span class="p">:</span>
<span class="w">  </span><span class="nt">loki</span><span class="p">:</span>
<span class="w">    </span><span class="c1"># …</span>
<span class="w">    </span><span class="nt">expose</span><span class="p">:</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">3100/tcp</span>
<span class="w">    </span><span class="nt">labels</span><span class="p">:</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">traefik.enable=true</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">traefik.http.routers.loki.rule=PathPrefix(`/loki`)</span>
</code></pre></div>

<p>Dès que le conteneur est opérationnel, Traefik crée un routeur qui redirige les
requêtes correspondant à <code>/loki</code> vers le premier port exposé. Placer la
configuration Traefik au sein même de la définition du service est attirant.
Comment obtenir la même chose pour les métriques Prometheus ?</p>
<h1 id="decouverte-de-metriques-avec-alloy">Découverte de métriques avec Alloy</h1>
<p><a href="https://grafana.com/docs/alloy/latest/" title="Grafana Alloy documentation">Grafana Alloy</a>, un collecteur de métriques capable de collecter des métriques
Prometheus, inclut un composant <a href="https://grafana.com/docs/alloy/latest/reference/components/discovery/discovery.docker/" title="Alloy: discovery.docker"><code>discovery.docker</code></a>. Tout
comme Traefik, il se connecte à la socket Docker<sup id="fnref:socket"><a class="footnote-ref" href="#fn:socket">1</a></sup>. Avec quelques règles
de réétiquetage, on peut lui apprendre à utiliser les labels Docker pour
localiser et collecter les métriques.</p>
<p>On définit trois labels sur chaque service :</p>
<ul>
<li><code>metrics.enable</code> active la collecte de métriques,</li>
<li><code>metrics.port</code> indique le port exposant les métriques Prometheus,</li>
<li><code>metrics.path</code> indique le chemin vers les métriques.</li>
</ul>
<p>Si le service expose plus d’un port, <code>metrics.port</code> est obligatoire, sinon il
prend par défaut la valeur de l’unique port exposé. La valeur par défaut de
<code>metrics.path</code> est <code>/metrics</code>. Le service Loki devient :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="nt">services</span><span class="p">:</span>
<span class="w">  </span><span class="nt">loki</span><span class="p">:</span>
<span class="w">    </span><span class="c1"># …</span>
<span class="w">    </span><span class="nt">expose</span><span class="p">:</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">3100/tcp</span>
<span class="w">    </span><span class="nt">labels</span><span class="p">:</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">traefik.enable=true</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">traefik.http.routers.loki.rule=PathPrefix(`/loki`)</span>
<span class="hll"><span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">metrics.enable=true</span>
</span><span class="hll"><span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">metrics.path=/loki/metrics</span>
</span></code></pre></div>

<p>La configuration d’Alloy se divise en quatre parties :</p>
<ol>
<li><strong>découvrir</strong> les conteneurs via la socket Docker,</li>
<li><strong>filtrer et réétiqueter</strong> les cibles à l’aide des labels Docker,</li>
<li><strong>collecter</strong> les métriques,</li>
<li><strong>transmettre</strong> les métriques à Prometheus.</li>
</ol>
<h2 id="decouverte-des-conteneurs-docker">Découverte des conteneurs Docker</h2>
<p>Le premier bloc découvre les conteneurs en cours d’exécution :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">discovery.docker</span><span class="w"> </span><span class="s2">"docker"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">host</span><span class="w">             </span><span class="o">=</span><span class="w"> </span><span class="s2">"unix:///var/run/docker.sock"</span>
<span class="w">  </span><span class="na">refresh_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"30s"</span>
<span class="w">  </span><span class="nb">filter</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">name</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s2">"label"</span>
<span class="w">    </span><span class="na">values</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"com.docker.compose.project=akvorado"</span><span class="p">]</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p>Alloy se connecte à la socket Docker et liste les conteneurs toutes les 30
secondes<sup id="fnref:events"><a class="footnote-ref" href="#fn:events">2</a></sup>. Le bloc <code>filter</code> restreint la découverte aux conteneurs du
projet <code>akvorado</code>, évitant toute interférence avec d’autres conteneurs sur le
même hôte. Pour chaque conteneur découvert, Alloy produit une cible avec des
labels tels que <code>__meta_docker_container_label_metrics_port</code> pour le label
Docker <code>metrics.port</code>.</p>
<h2 id="reetiquetage-des-cibles">Réétiquetage des cibles</h2>
<p>L’étape de réétiquetage filtre et transforme les cibles brutes issues de la
découverte Docker en cibles exploitables. La première étape ne conserve que les
cibles dont <code>metrics.enable</code> vaut <code>true</code> :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">discovery.relabel</span><span class="w"> </span><span class="s2">"prometheus"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">discovery.docker.docker.targets</span>

<span class="c1">  // Keep only targets with metrics.enable=true</span>
<span class="w">  </span><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__meta_docker_container_label_metrics_enable"</span><span class="p">]</span>
<span class="w">    </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="no">true</span><span class="err">`</span>
<span class="w">    </span><span class="na">action</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="s2">"keep"</span>
<span class="w">  </span><span class="p">}</span>

<span class="c1">  // …</span>
<span class="p">}</span>
</code></pre></div>

<p>La deuxième étape remplace le port découvert lorsque <code>metrics.port</code> est
défini :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="c1">// When metrics.port is set, override __address__.</span>
<span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__address__", "__meta_docker_container_label_metrics_port"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">):</span><span class="err">\d+;</span><span class="p">(.</span><span class="err">+</span><span class="p">)</span><span class="err">`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"__address__"</span>
<span class="w">  </span><span class="na">replacement</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s2">"$1:$2"</span>
<span class="p">}</span>
</code></pre></div>

<p>Ensuite, on gère les conteneurs en mode réseau <code>host</code>. Quand
<code>__meta_docker_network_name</code> vaut <code>host</code>, l’adresse est réécrite en
<code>host.docker.internal</code> au lieu de <code>localhost</code><sup id="fnref:hostdocker"><a class="footnote-ref" href="#fn:hostdocker">3</a></sup> :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="c1">// When host networking, override __address__ to host.docker.internal.</span>
<span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__meta_docker_container_label_metrics_port", "__meta_docker_network_name"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">)</span><span class="err">;host`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"__address__"</span>
<span class="w">  </span><span class="na">replacement</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s2">"host.docker.internal:$1"</span>
<span class="p">}</span>
</code></pre></div>

<p>L’étape suivante dérive le label <code>job</code> à partir du nom du service, en supprimant
tout suffixe numéroté. Le label <code>instance</code> est l’adresse sans le port :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__meta_docker_container_label_com_docker_compose_service"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">)(?:</span><span class="err">-\d+</span><span class="p">)?</span><span class="err">`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"job"</span>
<span class="p">}</span>
<span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__address__"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">):</span><span class="err">\d+`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"instance"</span>
<span class="p">}</span>
</code></pre></div>

<p>Si un conteneur définit <code>metrics.path</code>, Alloy l’utilise comme chemin. Sinon, la
valeur par défaut est <code>/metrics</code> :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__meta_docker_container_label_metrics_path"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">)</span><span class="err">`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"__metrics_path__"</span>
<span class="p">}</span>
<span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__metrics_path__"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="s2">""</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"__metrics_path__"</span>
<span class="w">  </span><span class="na">replacement</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s2">"/metrics"</span>
<span class="p">}</span>
</code></pre></div>

<h2 id="collecte-et-transmission">Collecte et transmission</h2>
<p>Une fois les cibles correctement réétiquetées, la collecte et la transmission
sont sommaires :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">prometheus.scrape</span><span class="w"> </span><span class="s2">"docker"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="nv">discovery.relabel.prometheus.output</span>
<span class="w">  </span><span class="na">forward_to</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="nv">prometheus.remote_write.default.receiver</span><span class="p">]</span>
<span class="w">  </span><span class="na">scrape_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"30s"</span>
<span class="p">}</span>

<span class="nv">prometheus.remote_write</span><span class="w"> </span><span class="s2">"default"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nb">endpoint</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"http://prometheus:9090/api/v1/write"</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p><code>prometheus.scrape</code> récupère périodiquement les métriques des cibles
découvertes. <code>prometheus.remote_write</code> les transmet à Prometheus.</p>
<h1 id="exporteurs-integres">Exporteurs intégrés</h1>
<p>Certains services n’exposent pas de point d’accès Prometheus. Redis et Kafka en
sont des exemples courants. Alloy embarque des <a href="https://grafana.com/docs/alloy/latest/reference/components/prometheus/" title="Alloy: Prometheus components">exporteurs
Prometheus</a> capables d’interroger ces services et d’exposer
les métriques à leur place.</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">prometheus.exporter.redis</span><span class="w"> </span><span class="s2">"docker"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">redis_addr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"redis:6379"</span>
<span class="p">}</span>
<span class="nv">discovery.relabel</span><span class="w"> </span><span class="s2">"redis"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">prometheus.exporter.redis.docker.targets</span>
<span class="w">  </span><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">target_label</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"job"</span>
<span class="w">    </span><span class="na">replacement</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"redis"</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
<span class="nv">prometheus.scrape</span><span class="w"> </span><span class="s2">"redis"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="nv">discovery.relabel.redis.output</span>
<span class="w">  </span><span class="na">forward_to</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="nv">prometheus.remote_write.default.receiver</span><span class="p">]</span>
<span class="w">  </span><span class="na">scrape_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"30s"</span>
<span class="p">}</span>
</code></pre></div>

<p>Le même schéma s’applique à Kafka :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">prometheus.exporter.kafka</span><span class="w"> </span><span class="s2">"docker"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">kafka_uris</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"kafka:9092"</span><span class="p">]</span>
<span class="p">}</span>
<span class="nv">discovery.relabel</span><span class="w"> </span><span class="s2">"kafka"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">prometheus.exporter.kafka.docker.targets</span>
<span class="w">  </span><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">target_label</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"job"</span>
<span class="w">    </span><span class="na">replacement</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"kafka"</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
<span class="nv">prometheus.scrape</span><span class="w"> </span><span class="s2">"kafka"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="nv">discovery.relabel.kafka.output</span>
<span class="w">  </span><span class="na">forward_to</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="nv">prometheus.remote_write.default.receiver</span><span class="p">]</span>
<span class="w">  </span><span class="na">scrape_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"30s"</span>
<span class="p">}</span>
</code></pre></div>

<p>Chaque exporteur est un composant distinct avec sa propre configuration de
réétiquetage et de collecte. Le label <code>job</code> est défini explicitement.</p>
<hr/>
<p>Avec cette configuration, ajouter des métriques à un nouveau service disposant
d’un point d’accès Prometheus se résume à quelques labels dans
<code>docker-compose.yml</code>, tout comme l’ajout d’une route Traefik. Alloy s’en charge
automatiquement. Une approche similaire est possible avec d’autres méthodes de
découverte, comme <a href="https://grafana.com/docs/alloy/latest/reference/components/discovery/discovery.kubernetes/" title="Alloy: discovery.kubernetes"><code>discovery.kubernetes</code></a>,
<a href="https://grafana.com/docs/alloy/latest/reference/components/discovery/discovery.scaleway/" title="Alloy: discovery.scaleway"><code>discovery.scaleway</code></a> ou <a href="https://grafana.com/docs/alloy/latest/reference/components/discovery/discovery.http/" title="Alloy: discovery.http"><code>discovery.http</code></a>. 🩺</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:socket">
<p>Traefik et Alloy nécessitent tous deux l’accès à la socket Docker,
ce qui confère un accès root à la machine hôte. Un <a href="https://github.com/Tecnativa/docker-socket-proxy" title="Docker Socket Proxy: security-enhanced proxy for Docker socket">Docker socket
proxy</a> atténue ce risque en n’exposant que les points d’accès de l’API en
lecture seule nécessaires à la découverte. <a class="footnote-backref" href="#fnref:socket" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:events">
<p>Contrairement à Traefik, qui surveille les événements, Grafana Alloy
interroge la liste des conteneurs à intervalles réguliers — un comportement
hérité de Prometheus. <a class="footnote-backref" href="#fnref:events" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:hostdocker">
<p>Le service Alloy nécessite <code>extra_hosts:
["host.docker.internal:host-gateway"]</code> dans sa définition. <a class="footnote-backref" href="#fnref:hostdocker" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
</ol>
</div>
      </div></content>
  </entry>
</feed>