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

  <entry>
    <title type="html">Building a Soviet Nail Factory: how KPIs killed efficiency</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2026-kpi-goodhart" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2026-kpi-goodhart#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-06-16T06:26:27Z</updated>
    <id>http://www.luffy.cx/en/blog/2026-kpi-goodhart.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p>In 2008, I landed my second job, in the network team at <em>Orange Portails</em>, the
division behind the websites and search engine of the French telecom operator
Orange. The place ran like clockwork: a comprehensive technical setup, a
dedicated team for every part of the business, and room to focus on what I do
best. A few years later, none of that mattered: thanks to an obsession with the
numbers, we could no longer deliver new services on time.</p>
<div class="admonition">
<p class="admonition-title">Disclaimer</p>
<p>This is a story I like to tell to warn people about
<a href="https://en.wikipedia.org/wiki/Goodhart%27s_law" title="Goodhart's law on Wikipedia">Goodhart’s law</a>.<sup id="fnref-campbell"><a class="footnote-ref" href="#fn-campbell">1</a></sup> As these events happened almost 15 years ago, my
recollection is a bit fuzzy. I left in 2012.</p>
</div>
<h1 id="the-first-years">The first years</h1>
<p>During my first years, the department operated like a startup. Its cradle was
the French company Echo. They built a search engine. France Télécom bought it
and renamed it <a href="https://fr.wikipedia.org/wiki/Voila">Voila</a>. It was the most visited search engine in France in the
early 2000s. France Télécom consolidated the portal activities into the <em>Wanadoo
Portails</em> division, later renamed <em>Orange Portails</em>.</p>
<p>The technical environment was excellent. We had many internal tools:<sup id="fnref-nocloud"><a class="footnote-ref" href="#fn-nocloud">2</a></sup> a
ticket system, an RRD-based graphing tool, an IPAM, a reporting tool, and an
SNMP-based alerting tool.<sup id="fnref-snalert"><a class="footnote-ref" href="#fn-snalert">3</a></sup> We deployed our Linux servers with
<a href="https://cfengine.com/">CFEngine</a>. We installed systems and applications from internal Debian
repositories. We documented everything in a private <a href="https://www.mediawiki.org/wiki/MediaWiki" title="MediaWiki is a collaboration and documentation platform">MediaWiki</a> instance.
Supervision was performed with an ancestor of <a href="https://www.xymon.com/servers/servers.html" title="The Xymon Monitor">Xymon</a>. The network
architecture was clean and scalable with little legacy. We onboarded new people
in a day.</p>
<p>It was a nurturing environment for me. I developed several tools:
<a href="https://lldpd.github.io/" title="lldpd: implementation of IEEE 802.1AB">lldpd</a>, an 802.1AB implementation, <a href="/en/blog/2013-snimpy" title="snimpy: SNMP &amp; Python">Snimpy</a>, a pythonic binding for
Net-SNMP, <a href="https://github.com/vincentbernat/wiremaps">Wiremaps</a>, a layer-2 discovery tool with a time machine to know
which device is connected where, <a href="https://github.com/vincentbernat/Kitero">Kitérő</a>, a tool to simulate network
conditions, <a href="https://github.com/vincentbernat/QCss-3/">QCSS-3</a>, a controller for load-balancers, and <a href="https://github.com/vincentbernat/ipoo">ipoo</a>, a service
available through a Jabber chatbot and a Greasemonkey script to expose
IP-related information. I added <a href="/en/blog/2011-keepalived-snmp-ipv6" title="SNMP support for Keepalived">SNMP support for Keepalived</a> and
<a href="https://github.com/search?q=repo%3AFRRouting%2Ffrr+author%3Avincentbernat+snmp&amp;type=commits" title="SNMP-related commits for Quagga/FRR">Quagga</a>. I also started this blog, with articles like
“<a href="/en/blog/2011-dns-anycast" title="Anycast DNS">Anycast DNS</a>,” TLS-related articles like “<a href="/en/blog/2011-ssl-dos-mitigation" title="TLS computational DoS mitigation">TLS computational DoS
mitigation</a>,” SNMP-related articles like “<a href="/en/blog/2012-snmp-event-loop" title="Integration of Net-⁠SNMP into an event loop">Integration of Net-SNMP into an
event loop</a>,” Linux-related articles like “<a href="/en/blog/2011-ipv4-route-cache-linux" title="Tuning Linux IPv4 route cache">Tuning Linux IPv4 route cache</a>,”
and an <a href="/en/blog/2012-multicast-vxlan" title="Network virtualization with VXLAN">article about VXLAN</a> long before it was cool.</p>
<h1 id="the-collapse">The collapse</h1>
<p>When we needed new servers, the on-site team would take a set from the
inventory, install our base Linux distribution on them, put them in the
datacenter, and cable them to the top-of-the-rack switches. We opened a ticket
describing the servers we needed, and one week later, our servers were
available. 💫</p>
<p>Orange wanted to know if this team was performing well, so they asked for <abbr title="Key Performance Indicators">KPIs</abbr>.
They decided to use the number of tickets completed in a year. They asked to
double this number. So instead of one ticket for a new service, we would open
six tickets—one per server. By the end of the year, the <abbr title="Key Performance Indicators">KPIs</abbr> had more than
doubled.</p>
<p>Everybody saw it as a success for performance management. So, they asked to do
the same for the next year. Now, we needed to open a ticket per server and per
step. Again, the <abbr title="Key Performance Indicators">KPIs</abbr> doubled. Behind the scenes, the tickets went to different
people and were no longer handled in order. So, for the next year, it was decided to
have meta-tickets and meetings to follow the progress of these tickets. Of
course, all these extra steps pushed the <abbr title="Key Performance Indicator">KPI</abbr> even higher.</p>
<p>This performance management method spread to the other teams.<sup id="fnref-firewall"><a class="footnote-ref" href="#fn-firewall">4</a></sup>
Everything became slower. Instead of a couple of weeks, a new service now took
six months. We built a <a href="https://fronterabrands.com/goodharts-law/" title="Goodhart’s Law: Soviet Nail Factories &amp; The Power of Incentives">Soviet nail factory</a>. But the <abbr title="Key Performance Indicators">KPIs</abbr> were good, and we
stopped caring.</p>
<p>Let me give you another example. We had to estimate the impact of each night
operation. We weren’t half bad: we declared most operations “without any
expected impact.” Most of the time, there was no impact. One time out of five,
there was a 5-second impact. We were told to try harder to meet our expected
impact. What did we do? We started declaring a 5-second expected impact. One
day, we got a 30-second impact and were told we failed to match the expected
impact. In the end, we declared most operations with a 10-minute expected
impact, and we stopped caring: instead of carefully shifting traffic around, we
allowed ourselves a 5-minute impact. And our <abbr title="Key Performance Indicators">KPIs</abbr> were never better.</p>
<figure><div class="lf-media-outer" style="width: 630px"><span class="lf-media-inner" style="padding-bottom: 38.889%"><img alt="Graph showing the impact of night operations. Year after year, the impact&#10;tolerance has been increased. In the final year, the expected impact is 10&#10;minutes, and all operations remain under this threshold. However, the impacts&#10;are much more significant than they were in the first&#10;year." src="https://d2pzklc15kok91.cloudfront.net/images/orange-impacts.03b883aacf07da.svg" width="630" height="245" class="lf-media"/></span></div><figcaption>An artist's rendering of the evolution of impacts over the years.</figcaption></figure>
<hr/>
<p><abbr title="Key Performance Indicators">KPIs</abbr> are not bad, but they are easy to break. Use them carefully: let the people
doing the work help choose the metrics, and tie those metrics to the quality of
the service—for example, with <a href="https://sre.google/sre-book/service-level-objectives/" title="Google SRE book: Service Level Objectives">service level objectives</a>. Otherwise, even
dedicated people stop caring, game the system, and eventually quit. 📊</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn-campbell">
<p>Goodhart’s law often gets the credit, but <a href="https://en.wikipedia.org/wiki/Campbell%27s_law" title="Campbell's law on Wikipedia">Campbell’s law</a>
describes my experience even better: the more you lean on a number to make
decisions, the faster people corrupt it. <a class="footnote-backref" href="#fnref-campbell" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn-nocloud">
<p>At the time, <abbr title="Software as a Service">SaaS</abbr> was not really a thing. I remember we considered,
with a couple of colleagues, selling <a href="https://github.com/vincentbernat/wiremaps">Wiremaps</a> as a <abbr title="Software as a Service">SaaS</abbr>, with
homomorphic encryption for the database. But who would outsource their
observability stack? <a class="footnote-backref" href="#fnref-nocloud" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn-snalert">
<p><em>Snalert</em> was a metacircular alerting tool in Perl. It was able to
poll a very large number of SNMP targets in a short timespan. All our
monitoring was SNMP-based, including system monitoring. <a class="footnote-backref" href="#fnref-snalert" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn-firewall">
<p>My team also managed the rules of many Linux-based firewalls. To
increase our <abbr title="Key Performance Indicators">KPIs</abbr>, we used the same method: rather than accepting one ticket
with a flow matrix, we requested one ticket per flow. <a class="footnote-backref" href="#fnref-firewall" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
</ol>
</div>
      </div></content>
  </entry>
  <entry>
    <title type="html">Blogging with LLMs as a non-native speaker</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2026-blogging-llm" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2026-blogging-llm#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-06-09T20:15:13Z</updated>
    <id>http://www.luffy.cx/en/blog/2026-blogging-llm.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p><abbr title="Artificial Intelligence">AI</abbr> slop is invading the web. A recent story about <a href="https://lobste.rs/s/29pm2f/llm_generated_submissions_should_be" title="LLM generated submissions should be disallowed">disallowing <abbr title="Large Language Model">LLM</abbr>-generated
submissions</a> on <a href="https://lobste.rs/" title="Lobsters">Lobsters</a> triggered a lot of debate. My personal worst
offenders are <a href="https://www.linkedin.com/">LinkedIn</a> articles with <abbr title="Artificial Intelligence">AI</abbr>-generated images and uninspired
articles filled with emojis from people trying to masquerade as experts on a
subject they don’t care enough to write themselves. While I am unhappy about
this situation, I rely on <abbr title="Large Language Models">LLMs</abbr> for <strong>grammar</strong>, <strong>copyediting</strong>, and
<strong>translation</strong>. I don’t see this as a contradiction.</p>
<p>I am a native French speaker, but I blog in both English and French. When I
started writing this blog in 2011, I was composing in <a href="/fr/blog/2011-migration-vers-github" title="Migration de Trac vers GitHub">French</a> and translating
to <a href="/en/blog/2011-migrating-to-github" title="Migrating from Trac to GitHub">English</a>, but I found it was <a href="https://sci-hub.ee/10.1016/j.jslw.2009.06.003" title="L1 use during L2 writing: An empirical study of a complex phenomenon">better to work in the reverse order</a> to
avoid unnatural and non-idiomatic constructions. One of my goals is to write
“good” English but I never felt it was my strong point.<sup id="fnref-book"><a class="footnote-ref" href="#fn-book">1</a></sup> For example, verb
tenses are often an issue, even if I mostly stick with the present tense. I
learn the rules and forget them right away. I also don’t feel like <a href="https://mtlynch.io/editor/" title="How I Hired a Freelance Editor for My Blog">hiring an
editor</a> for something I see as a hobby.</p>
<p>As an example, I have kept the history of the successive iterations when writing
“<a href="/en/blog/2026-akvorado-rib-sharding" title="Scaling Akvorado BMP RIB with sharding">Scaling Akvorado BMP RIB with sharding</a>”:</p>
<ol>
<li>the <a href="https://github.com/vincentbernat/vincent.bernat.ch/commit/aad263c20c7b021b1069952d1487374ff559d3b3" title="article: Akvorado BMP RIB with sharding">first draft</a>, authored with the help of a thesaurus,<sup id="fnref-kagi"><a class="footnote-ref" href="#fn-kagi">2</a></sup></li>
<li>the <a href="https://github.com/vincentbernat/vincent.bernat.ch/commit/11231af4be15160c1dd48e45c9b4dc7c042cf191" title="content: copyediting of Akvorado BMP RIB article">edited copy</a> revised by the <a href="https://github.com/vincentbernat/vincent.bernat.ch/blob/latest/.claude/skills/edit/SKILL.md">copyediting skill</a>,</li>
<li>the <a href="https://github.com/vincentbernat/vincent.bernat.ch/commit/0435ae3a8450132abb89507e856a012831457e49" title="article: Akvorado BMP RIB in French">translation to French</a> generated with the <a href="https://github.com/vincentbernat/vincent.bernat.ch/blob/latest/.claude/skills/translate/SKILL.md">translation
   skill</a>, and</li>
<li>the <a href="https://github.com/vincentbernat/vincent.bernat.ch/commit/c0629b1d9fe450335fcd30c071fb44301615dd15" title="content: human proofread of the French translation">human proofread of the French translation</a>, with minor
   edits to the English version.</li>
</ol>
<p>I know that <abbr title="Large Language Models">LLMs</abbr> may <a href="https://sites.google.com/view/llmwritingdistortion/home" title="How LLMs Distort Our Written Language">alter the author’s voice when editing</a>, but the
corrections in the second step are minor. The prompt asks to “apply light
stylistic edits,” with some guidance around avoiding passive voice, long
sentences, bland verbs, and filler words. It also defines the target audience:
technical with a B2 level in English.</p>
<p>In the following excerpt, I used “long time” instead of “long-standing.” The
former is missing a hyphen and applies to people—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, while
the later relates to a situation—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. I had a hard
time understanding the reason of the second change: the <abbr title="Large Language Model">LLM</abbr> prefers a
<a href="https://dictionary.cambridge.org/us/grammar/british-grammar/relative-clauses-defining-and-non-defining" title="Relative clauses: defining and non-defining">defining relative clause</a> to provide the definition of “RIB sharding.”</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>In the next modification, the <abbr title="Large Language Model">LLM</abbr> puts “device” instead of “equipment.” This is
correct as “<a href="https://dictionary.cambridge.org/us/dictionary/english/equipment" title="Definition of equipment in the Cambridge Dictionary">equipment</a>” is an uncountable noun. I know that, but I still fall
into this trap.</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>I ask the <abbr title="Large Language Model">LLM</abbr> to use “descriptive verbs” and it complies by replacing a
multi-word predicate with a lexically rich verb:</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>It also fixes grammar errors. In the next excerpt, a “list of routes” is a
singular expression. Moreover, “stored” is a state and I should not use “into”
as it expresses a change.</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>As a last example, consider the following snippet. The “<a href="https://dictionary.cambridge.org/dictionary/english/require" title="Definition of require in the Cambridge Dictionary">require</a>” verb
accepts a noun or an object followed by a to-infinitive. I can’t use it with
just a to-infinitive.</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>As someone who didn’t grow up speaking English, I struggle with these grammar
rules despite reading a lot of English material.<sup id="fnref-monkey2"><a class="footnote-ref" href="#fn-monkey2">3</a></sup> French is more
complex to get started but more systematic. English is full of irregularities.</p>
<hr/>
<p>On each page, I disclose in the footer whether an <abbr title="Artificial Intelligence">AI</abbr> modified the content. There
are three levels:</p>
<ul>
<li>🧠: no <abbr title="Artificial Intelligence">AI</abbr> or almost no <abbr title="Artificial Intelligence">AI</abbr> (e.g., grammar corrections)</li>
<li>✨: enhanced (e.g., copyediting)</li>
<li>🤖: generated (e.g., translated from another language, even if human-edited)</li>
</ul>
<p>Hover or tap the icon to reveal the <abbr title="Artificial Intelligence">AI</abbr>’s name and its role in the document.</p>
<figure><div class="lf-media-outer" style="width: 264px"><span class="lf-media-inner" style="padding-bottom: 45.833%"><img alt="Screenshot of the footer containing the &quot;sparkles&quot;&#10;emoji" src="https://d2pzklc15kok91.cloudfront.net/images/ai-usage@1x.c55381c0eb5251.jpg" srcset="https://d2pzklc15kok91.cloudfront.net/images/ai-usage@1x.c55381c0eb5251.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>Example of AI usage disclosure: Claude Sonnet 4.5 edited this article.</figcaption></figure>
<p>The graph below shows which tool altered each post, year by year. Recently, I
applied the <a href="https://github.com/vincentbernat/vincent.bernat.ch/blob/latest/.claude/skills/grammar/SKILL.md">grammar skill</a> to <a href="https://github.com/vincentbernat/vincent.bernat.ch/commit/7baf2c8f18b57e351cc25e7e62f4aa6611362c8c" title="content: fix English grammar">past articles</a>. Since 2018,
French articles have been translated with the help of <a href="https://www.deepl.com">DeepL</a> first, then of
an <abbr title="Large Language Model">LLM</abbr>. Since 2024, English articles are copyedited.</p>
<figure><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.ec2f37f79c023a.svg" class="lf-media">&amp;#128444; Graph showing the AI usage over the years. Each level get its own
color.</object></span></div><figcaption>AI usage over the years. Hover or tap a band for the details.</figcaption></figure>
<hr/>
<p>If you are strongly against any usage of <abbr title="Large Language Models">LLMs</abbr> specifically for writing, I hope
you accept my more nuanced position on the usage of these tools as a trade-off
to provide clearer and more engaging articles. Years of literature on improving
English told us it is important to choose the right word to keep the reader
engaged.</p>
<blockquote>
<p>[…] Good writing consists of mastering the fundamentals (vocabulary,
grammar, the elements of style) and then filling the third level of your
toolbox with the right instruments.</p>
<p>― <em>Stephen King</em>, On Writing</p>
</blockquote>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>Unlike other recent articles, I did not use an <abbr title="Large Language Model">LLM</abbr> to edit this post:
an unnamed person kindly accepted to proofread it. I translated it to French
without using an <abbr title="Large Language Model">LLM</abbr> either.</p>
</div>
<div class="admonition">
<p class="admonition-title">Update (2026-06)</p>
<p>See the <a href="https://lobste.rs/s/tdvu7a/blogging_with_llm_assistant">story associated to this post on Lobsters</a>,
as well as the 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 <abbr title="Artificial Intelligence">AI</abbr>-scented blog posts</a>” by
Cynthia Dunlop, one of the coauthor of “<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>I recently read cover to cover “<a href="https://www.manning.com/books/writing-for-developers" title="“Writing for Developers” by Piotr Sarna and Cynthia Dunlop">Writing for Developers</a>” and I found
it stimulating. <a href="https://mtlynch.io/about/">Michael Lynch</a> is currently writing “<a href="https://refactoringenglish.com/" title="“Refactoring English: Effective Writing for Software Developers” by Michael Lynch">Refactoring
English</a>” on the same topic and I have subscribed to the early access. <a class="footnote-backref" href="#fnref-book" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn-kagi">
<p>I am quite happy with the writing tools provided by <a href="https://www.kagi.com">Kagi</a>. Both the
<a href="https://translate.kagi.com">translate tool</a> and the <a href="https://translate.kagi.com/dictionary">dictionary</a> are a valuable help to find
different wordings. I also lean on <a href="https://help.kagi.com/kagi/ai/kagi-research.html" title="Kagi Research Assistants">Kagi’s research assistant</a> when
researching a topic. <a class="footnote-backref" href="#fnref-kagi" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn-monkey2">
<p>When I was ten, I played <em>Monkey Island 2</em> in English without having
taken any classes. I used a dictionary to translate word by word and I found
the irregular verbs confusing—and not in the dictionary. <a class="footnote-backref" href="#fnref-monkey2" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
</ol>
</div>
      </div></content>
  </entry>
  <entry>
    <title type="html">Scaling Akvorado BMP RIB with sharding</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2026-akvorado-rib-sharding" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2026-akvorado-rib-sharding#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-05-24T19:00:00Z</updated>
    <id>http://www.luffy.cx/en/blog/2026-akvorado-rib-sharding.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p>To associate routing information—like AS paths or <abbr title="Border Gateway Protocol">BGP</abbr> communities—to flows,
<a href="https://github.com/akvorado/akvorado" title="Akvorado: flow collector, enricher and visualizer">Akvorado</a> can import routes through the <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>). As
the Internet routing table contains more than <a href="https://bgp.potaroo.net" title="BGP Routing Table Analysis Reports">1 million routes</a>, Akvorado
needs to <strong>scale to tens of millions of routes</strong>.<sup id="fnref-optimize"><a class="footnote-ref" href="#fn-optimize">1</a></sup> This has been a
long-standing challenge,<sup id="fnref-past"><a class="footnote-ref" href="#fn-past">2</a></sup> but I expect this issue is now fixed by using
<strong><abbr title="Routing Information Base">RIB</abbr> sharding</strong>, a method that splits the routing database into several parts
to enable concurrent updates.</p>
<div class="toc">
<ul>
<li><a href="#previous-implementation">Previous implementation</a><ul>
<li><a href="#storing-routes-in-a-map">Storing routes in a map</a></li>
<li><a href="#interning-routes">Interning routes</a></li>
<li><a href="#why-does-it-not-scale">Why does it not scale?</a></li>
</ul>
</li>
<li><a href="#rib-sharding">RIB sharding</a><ul>
<li><a href="#first-step-basic-sharding">First step: basic sharding</a></li>
<li><a href="#second-step-lock-free-reads">Second step: lock-free reads</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="previous-implementation">Previous implementation</h1>
<p>Akvorado connects 2 elements to build its <abbr title="Routing Information Base">RIB</abbr>:</p>
<ol>
<li>a <strong>prefix tree</strong>, and</li>
<li>a <strong>list of routes</strong> attached to each prefix.</li>
</ol>
<figure><div class="lf-media-outer" style="width: 711px"><span class="lf-media-inner" style="padding-bottom: 105.626%"><img alt="Akvorado BMP RIB implementation before sharding with the memory layout of each&#10;structure and a single lock." src="https://d2pzklc15kok91.cloudfront.net/images/akvorado/sharding-before.6657877be051e0.svg" width="711" height="751" class="lf-media"/></span></div><figcaption>Akvorado BMP RIB implementation without sharding. One single read/write lock.</figcaption></figure>
<p>In the diagram above, the <abbr title="Routing Information Base">RIB</abbr> stores five IPv4 prefixes and two IPv6 prefixes.
One of them, <code>2001:db8:1::/48</code>, contains three routes:</p>
<ul>
<li>from peer 3, next hop <code>2001:db8::3:1</code>, AS 65402, AS path <code>65402</code>, community
  <code>65402:31</code>,</li>
<li>from peer 4, next hop <code>2001:db8::4:1</code>, same <abbr title="Autonomous System Number">ASN</abbr>, AS path, and community,</li>
<li>from peer 5, next hop <code>2001:db8::5:1</code>, AS 65402, AS path <code>65401 65402</code>,
  community <code>65402:31</code>.</li>
</ul>
<p>The <code>rib</code> structure is defined in Go as follows:</p>
<div class="language-go codehilite"><pre><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">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>
</pre></div>


<p>The prefix tree uses the <a href="https://github.com/gaissmai/bart/" title="Balanced Routing Tables (BART) in Go">bart</a> package, an adaptation of Donald Knuth’s <a href="https://www.hariguchi.org/art/art.pdf" title="ART: A Fast Free Multibit Trie Based Routing Table">ART
algorithm</a>. The <a href="https://github.com/gaissmai/iprbench">benchmarks</a> demonstrate it outperforms other packages for
lookups, insertions, and memory usage.<sup id="fnref-performance"><a class="footnote-ref" href="#fn-performance">3</a></sup> Plus, the author is quite
helpful.</p>
<h2 id="storing-routes-in-a-map">Storing routes in a map</h2>
<p>The list of routes for each prefix is not stored directly in the prefix tree:
it would put too much pressure on the garbage collector by allocating per-prefix
arrays.</p>
<p>Instead, the <abbr title="Routing Information Base">RIB</abbr> assigns a unique 32-bit prefix identifier for each prefix,
either by picking the last available prefix identifier from the <code>freePrefixIDs</code>
array if any, or using the <code>nextPrefixID</code> value before incrementing it. Then,
the routes are stored in the <code>routes</code> map, leveraging the <a href="https://go.dev/blog/swisstable" title="Faster Go maps with Swiss Tables">optimized Swiss
table</a> in Go. To retrieve routes attached to a prefix, we look them up
one by one in the <code>routes</code> map with a 64-bit key combining the 32-bit prefix
index with a 32-bit route index matching the position of the route in the list.
Akvorado scans routes from the first to the last to find the best one.<sup id="fnref-scan"><a class="footnote-ref" href="#fn-scan">4</a></sup> It
knows there is no more route if the route key returns no result.</p>
<div class="language-go codehilite"><pre><span/><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>
</pre></div>


<h2 id="interning-routes">Interning routes</h2>
<p>A route contains a <abbr title="Border Gateway Protocol">BGP</abbr> peer identifier, a partial <abbr title="Network Layer Reachability Information">NLRI</abbr><sup id="fnref-nlri"><a class="footnote-ref" href="#fn-nlri">5</a></sup>, the next hop, and the
attributes.</p>
<div class="language-go codehilite"><pre><span/><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>
</pre></div>


<p>To save memory and allocations, <abbr title="Network Layer Reachability Information">NLRI</abbr>, next hops, and route attributes are
“interned”: a 32-bit integer replaces the real value. The mechanism predates the
<a href="https://go.dev/blog/unique" title="New unique package"><code>unique</code> package</a> introduced in Go 1.23. We keep it because it has
different trade-offs:</p>
<ul>
<li>It uses <strong>explicit reference counting</strong> instead of relying on weak pointers.</li>
<li>It works with <strong>non-comparable values</strong> implementing <code>Hash()</code> and <code>Equal()</code>
  methods.<sup id="fnref-hash"><a class="footnote-ref" href="#fn-hash">6</a></sup></li>
<li>It uses <strong>explicit pool instances</strong>. This will be useful for sharding.</li>
<li>It has <strong>better performance</strong>. See for example this <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>It consumes <strong>half the memory</strong> thanks to unsigned 32-bit references instead
  of pointers.</li>
<li>But it is <strong>not safe for concurrent use</strong>.</li>
</ul>
<h2 id="why-does-it-not-scale">Why does it not scale?</h2>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>At <a href="https://www.free.fr/freebox">AS 12322</a>, we don’t use <abbr title="BGP Monitoring Protocol">BMP</abbr> yet.<sup id="fnref-cisco"><a class="footnote-ref" href="#fn-cisco">7</a></sup> But <a href="https://github.com/bogi788" title="bogi788 on GitHub">Gerhard
Bogner</a> had the patience, availability, and technical skills to help me <a href="https://github.com/akvorado/akvorado/discussions/2287" title="Using go's unique package">debug
this issue</a>.</p>
</div>
<p>The global read/write lock is a bottleneck in this implementation. But how?
There are several users of the <abbr title="Routing Information Base">RIB</abbr>, each with its own set of constraints:</p>
<ul>
<li>
<p>The <strong>Kafka workers</strong> look up the <abbr title="Routing Information Base">RIB</abbr> to enrich flows with routing
  information. They are bound by the number of Kafka partitions.<sup id="fnref-KIP-932"><a class="footnote-ref" href="#fn-KIP-932">8</a></sup>
  Akvorado also adjusts their number to ensure efficient batching to ClickHouse.
  On our setup, the number of workers oscillates between 8 and 16. As we want
  to observe the latest data, we cannot afford for the Kafka workers to lag too
  much.</p>
</li>
<li>
<p>The <strong>monitored routers</strong> send route updates through the <abbr title="BGP Monitoring Protocol">BMP</abbr> protocol. When
  connecting, they can send millions of routes.<sup id="fnref-bmpconfig"><a class="footnote-ref" href="#fn-bmpconfig">9</a></sup> After the initial
  synchronization, updates are sent continuously and may spike from time to
  time. The router detects a stuck <abbr title="BGP Monitoring Protocol">BMP</abbr> station when its TCP window is full and
  resets the session in this case. While Akvorado implements a large incoming
  buffer, it still needs to update the received routes with the write lock held
  fast enough to avoid being detected as stuck.</p>
</li>
<li>
<p>When a <strong>remote <abbr title="Border Gateway Protocol">BGP</abbr> peer goes down</strong>, Akvorado flushes the associated routes by
  walking the <abbr title="Routing Information Base">RIB</abbr> with the write lock held. When a <strong>monitored router goes
  down</strong>, Akvorado waits a bit but eventually flushes all the associated routes.</p>
</li>
</ul>
<p>In short: on a busy setup, lock contention is high for both readers and
writers, and neither can lag too much behind.</p>
<h1 id="rib-sharding"><abbr title="Routing Information Base">RIB</abbr> sharding</h1>
<h2 id="first-step-basic-sharding">First step: basic sharding</h2>
<p>To remove the global lock, the <abbr title="Routing Information Base">RIB</abbr> is split into several “shards,” each one
handling a subset of the prefixes:</p>
<figure><div class="lf-media-outer" style="width: 876px"><span class="lf-media-inner" style="padding-bottom: 72.603%"><img alt="Akvorado BMP RIB implementation after sharding with the memory layout of each&#10;structure and one lock per shard." src="https://d2pzklc15kok91.cloudfront.net/images/akvorado/sharding-step1.90ee5154ac6b88.svg" width="876" height="636" class="lf-media"/></span></div><figcaption>Akvorado BMP RIB implementation with sharding.</figcaption></figure>
<p>The prefix tree stays global and is protected by a single lock. Each shard gets
its read/write lock, its route map, and its intern pools to store NLRIs, next
hops, and route attributes, which would not have been possible with <a href="https://go.dev/blog/unique" title="New unique package">Go’s
<code>unique</code> package</a>. The prefix indexes are also sharded: the 8 most
significant bits are the shard index and the 24 remaining bits are the local
prefix index.</p>
<p>Gerhard <a href="https://github.com/akvorado/akvorado/discussions/2287#discussioncomment-16020731">confirmed</a> that after <a href="https://github.com/akvorado/akvorado/commit/7e6bbf2210fdf7116d2ee168b307b9906cc223c0" title="outlet/routing: implement RIB sharding for BMP">this blind change</a>,
the <abbr title="BGP Monitoring Protocol">BMP</abbr> receiver chugged steadily. 🎉</p>
<p>Later, I wrote a <a href="https://github.com/akvorado/akvorado/blob/0811c40cc2065380e3a6230c2796312838e57850/outlet/routing/provider/bmp/concurrent_test.go">concurrent benchmark</a> over half a million synthetic but
plausible routes<sup id="fnref-plausible"><a class="footnote-ref" href="#fn-plausible">10</a></sup> partitioned over 0 to 8 writers, churning routes as
fast as possible, while 1 to 16 readers continuously look up a set of 10,000
routes. I don’t know if this benchmark is realistic, but it confirms the
improvements for both read and write latencies:</p>
<figure><div class="lf-media-outer" style="width: 970px"><span class="lf-media-inner" style="padding-bottom: 40.928%"><img alt="Two heatmaps. One for read latency ratio, the other for write latency ratio.&#10;Both of them comparing the speedup with colored tiles between the code before&#10;sharding and after sharding. Most tiles are&#10;green." src="https://d2pzklc15kok91.cloudfront.net/images/akvorado/sharding-heatmap.38c6059c3585ee.svg" width="970" height="397" class="lf-media"/></span></div><figcaption>Read and write latency performance improvement after sharding.</figcaption></figure>
<p>It also shows that a high number of writers degrades read latency.</p>
<h2 id="second-step-lock-free-reads">Second step: lock-free reads</h2>
<p>The single read/write lock protecting the prefix tree is the next target. The
<a href="https://github.com/gaissmai/bart/" title="Balanced Routing Tables (BART) in Go">bart</a> package provides alternative mutation methods returning an updated tree
using copy-on-write. Readers don’t need the global lock any more, leaving it
only to synchronize writers. The prefix tree is boxed in an atomic pointer.</p>
<figure><div class="lf-media-outer" style="width: 876px"><span class="lf-media-inner" style="padding-bottom: 72.603%"><img alt="Akvorado BMP RIB implementation for sharding with lock-free reads. It shows&#10;the memory layout of each structure." src="https://d2pzklc15kok91.cloudfront.net/images/akvorado/sharding-step2.1c48d3f740d4d0.svg" width="876" height="636" class="lf-media"/></span></div><figcaption>Akvorado BMP RIB implementation with sharding and lock-free reads.</figcaption></figure>
<p>Without a lock, readers can now fetch a stale prefix index when walking their
copy of the tree if a concurrent writer removes the last route attached to this
prefix index and recycles it for another prefix. To avoid this issue, we combine
the prefix index with a generation number and store them in the tree:</p>
<div class="language-go codehilite"><pre><span/><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>
</pre></div>


<p>Each shard stores the generation number for each local prefix index. The
generation number increases by one if the associated prefix index is freed. When
looking up the routes attached to a prefix index, the reader checks if the
generation number matches. Otherwise, it assumes the index was recycled and the
list of routes is empty.<sup id="fnref-retry"><a class="footnote-ref" href="#fn-retry">11</a></sup> You can see this case in the diagram above for
prefix index 5, stored with a generation index of 3, while the current value in
the <code>[]generations</code> array is 4. The generation number could overflow, but it is
not a problem as lookups are quick.</p>
<p>Running the concurrent benchmark against this new implementation shows the
improvements for the read latency as soon as the cost of the copy-on-write
prefix tree is amortized.</p>
<figure><div class="lf-media-outer" style="width: 1256px"><span class="lf-media-inner" style="padding-bottom: 46.815%"><img alt="Six heatmaps. Three for read latency ratio, three others for write latency&#10;ratio. They compare the numbers without sharding, with sharding, and with&#10;lock-free reads, pair by pair. For read latency, most tiles are green, showing&#10;an improvement of the second step. For write latency, the speedup is negative&#10;for a low number of readers." src="https://d2pzklc15kok91.cloudfront.net/images/akvorado/sharding-heatmap2.b13190eb7548a2.svg" width="1256" height="588" class="lf-media"/></span></div><figcaption>Read and write latency performance improvement after lock-free reads. The middle column shows the cumulative improvements of both steps.</figcaption></figure>
<hr/>
<p>Among the multiple attempts to optimize the <abbr title="BGP Monitoring Protocol">BMP</abbr> component, <abbr title="Routing Information Base">RIB</abbr> sharding is one
of the more satisfying. <a href="https://github.com/akvorado/akvorado/releases/tag/v2.2.0">Akvorado 2.2</a> implements the first step.
<a href="https://github.com/akvorado/akvorado/pull/2433" title="More efficient RIB tree for BMP">PR #2433</a>, drafted while writing this blog post, implements the second step
and was released with <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>Each router exporting flows doesn’t need to send its routes. When
Akvorado does not find a route from a specific device, it falls back to a
route sent by another device. It is up to the operator to decide if this
is a good enough approximation. <a class="footnote-backref" href="#fnref-optimize" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn-past">
<p>I made many attempts to scale the <abbr title="BGP Monitoring Protocol">BMP</abbr> component. See for example
<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>, and <a href="https://github.com/akvorado/akvorado/pull/2245" title="outlet/routing: buffer BMP messages to avoid being flagged as “stuck”">PR #2245</a>.
Despite these efforts, this component remained problematic for some users.
See <a href="https://github.com/akvorado/akvorado/discussions/2287" title="Using go's unique package">discussion #2287</a> as the latest example. <a class="footnote-backref" href="#fnref-past" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn-performance">
<p>It keeps improving: <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> features a new
implementation that trades a bit of memory for greater lookup performance. I
did not test it yet, as I have been preparing this blog post for a couple
of months already. <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 prefers the route matching the exact next hop. Otherwise, it
falls back to any other route. This is an approximation. An alternative
would be to have one prefix tree for each <abbr title="Border Gateway Protocol">BGP</abbr> peer but it would require
configuring all routers to export their routes. <a href="http://www.pmacct.net/">pmacct</a>’s <abbr title="BGP Monitoring Protocol">BMP</abbr> daemon
implements this approach. <a class="footnote-backref" href="#fnref-scan" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn-nlri">
<p>If we consider the <abbr title="Border Gateway Protocol">BGP</abbr> <abbr title="Routing Information Base">RIB</abbr> as a database, the Network Layer
Reachability Information (<abbr title="Network Layer Reachability Information">NLRI</abbr>) is the primary key. Its content depends on
the <abbr title="Border Gateway Protocol">BGP</abbr> family. With IPv4 or IPv6 unicast, this is the prefix. For VPNv4 and
VPNv6 families, it includes the route distinguisher. If you enable the
<a href="https://www.rfc-editor.org/rfc/rfc7911" title="Advertisement of Multiple Paths in BGP">ADD-PATH</a> extension, the <abbr title="Network Layer Reachability Information">NLRI</abbr> also contains a path identifier.</p>
<p>In our implementation, we don’t store the prefix as we get it from the
looked-up IP address using the prefix length stored separately. <a class="footnote-backref" href="#fnref-nlri" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn-hash">
<p>The <code>Hash()</code> methods rely on the <a href="https://pkg.go.dev/hash/maphash"><code>hash/maphash</code></a> package
and on the <a href="https://pkg.go.dev/unsafe"><code>unsafe</code></a> package to avoid memory copies. See for
example the <a href="https://github.com/akvorado/akvorado/blob/97fced5e855ac220e96148e132f643dc180cd097/outlet/routing/provider/bmp/rib.go#L89-L96"><code>Hash()</code> function for the <code>nlri</code> structure</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>Despite being an author or co-author of the first <abbr title="BGP Monitoring Protocol">BMP</abbr>-related RFCs since
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 did not implement it
in a usable way in IOS XR until version 24.2.1. We still need to upgrade a
few routers to enable this feature. <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><a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-932%3A+Queues+for+Kafka" title="KIP-932: Queues for Kafka">KIP-932</a> introduces, in Kafka 4.2, the concept of share groups to
enable cooperative consumption on the same partition. This is not supported
in Akvorado yet. <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>You can configure <abbr title="BGP Monitoring Protocol">BMP</abbr> to send routes for each <abbr title="Border Gateway Protocol">BGP</abbr> peer before or after
applying the incoming policies. In this case, you can get more than one
million routes for each transit peer. You can also tell <abbr title="BGP Monitoring Protocol">BMP</abbr> to send the
local <abbr title="Routing Information Base">RIB</abbr>, which only contains the best path for each prefix. <a class="footnote-backref" href="#fnref-bmpconfig" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn-plausible">
<p>The prefixes are random, but the prefix size distribution and the
AS path length distribution follow the <a href="https://bgp.potaroo.net/as2.0/" title="AS65000 BGP Routing Table Analysis Report">data provided by 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>Alternatively, we could retry the lookup, but it would be pointless:
the <abbr title="Routing Information Base">RIB</abbr> is an <a href="https://en.wikipedia.org/wiki/Eventual_consistency" title="Eventual consistency on Wikipedia">eventually consistent</a> database, and an empty list was a
correct answer at some point in the recent past. <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; vertical rhythm for text, images, and tables</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2026-css-vertical-rhythm" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2026-css-vertical-rhythm#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-04-22T19:48:10Z</updated>
    <id>http://www.luffy.cx/en/blog/2026-css-vertical-rhythm.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p>Vertical rhythm aligns lines to a consistent spacing cadence down the page. It
creates a predictable flow for the eye to follow. Thanks to the <code>rlh</code> CSS unit,
vertical rhythm is now easier to implement for text.<sup id="fnref-pawel"><a class="footnote-ref" href="#fn-pawel">1</a></sup> But illustrations
and tables can disrupt the layout. The amateur typographer in me wants to follow
Bringhurst’s wisdom:</p>
<blockquote>
<p>Headings, subheads, block quotations, footnotes, illustrations, captions and
other intrusions into the text create syncopations and variations against the
base rhythm of regularly leaded lines. These variations can and should add
life to the page, but the main text should also return after each variation
precisely on beat and in 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 on Wikipedia">The Elements of Typographic Style</a></p>
</blockquote>
<h1 id="text">Text</h1>
<p>Three factors govern vertical rhythm: <strong>font size</strong>, <strong>line height</strong> and
<strong>margin or padding</strong>. Let’s set our baseline with an 18-pixel font and a 1.5
line height:</p>
<div class="language-css codehilite"><pre><span/><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>
</pre></div>


<p><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> defines the <code>rlh</code> unit, equal to the
computed line height of the root element. All browsers support it <a href="https://webstatus.dev/features/rlh" title="rlh unit on Web Platform Status">since
2023</a>.<sup id="fnref-postcss"><a class="footnote-ref" href="#fn-postcss">2</a></sup> Use it to insert vertical spaces or to fix the line height
when altering font size:<sup id="fnref-calc"><a class="footnote-ref" href="#fn-calc">3</a></sup></p>
<div class="language-css codehilite"><pre><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">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>
</pre></div>


<p>We can check the result by overlaying a grid<sup id="fnref-grid"><a class="footnote-ref" href="#fn-grid">4</a></sup> on the content:</p>
<figure><div class="lf-media-outer" style="width: 680px"><span class="lf-media-inner" style="padding-bottom: 104.853%"><img alt="Screenshot of my website with a grid as an overlay and each line of text&#10;fitting on the grid" 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>Using CSS <code>rlh</code> unit to set vertical space works well for text. You can display the grid using <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd>.</figcaption></figure>
<p>If a child element uses a font with taller intrinsic metrics, it may stretch
the line’s box beyond the configured line height.<sup id="fnref-line-height"><a class="footnote-ref" href="#fn-line-height">5</a></sup> A workaround
is to reduce the line height to 1. The glyphs overflow but don’t push the line
taller.</p>
<div class="language-css codehilite"><pre><span/><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>
</pre></div>


<h1 id="responsive-images">Responsive images</h1>
<p>Responsive images are difficult to align on the grid because we don’t know their
height. <a href="https://www.w3.org/TR/css-rhythm-1/" title="CSS Rhythmic Sizing Module Level 1">CSS Rhythmic Sizing Module Level 1</a> introduces the <code>block-step</code>
property to adjust the height of an element to a multiple of a step unit. But
most browsers don’t support it yet.</p>
<p>With JavaScript, we can add padding around the image so it does not disturb
the vertical rhythm:</p>
<div class="language-javascript codehilite"><pre><span/><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>
</pre></div>


<figure><div class="lf-media-outer" style="width: 680px"><span class="lf-media-inner" style="padding-bottom: 104.853%"><img alt="Screenshot of my website with a grid as an overlay and an image not breaking&#10;the vertical rhythm. Additional padding is visible before and after the image.&#10;The height of the image with padding is&#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>The image is snapped to the grid thanks to the additional padding computed with JavaScript. 216 is divisible by 27, our line height in this example.</figcaption></figure>
<p>As the image is responsive, its height can change. We need to wrap a resize
observer around the <code>adjust()</code> function:</p>
<div class="language-javascript codehilite"><pre><span/><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>
</pre></div>


<h1 id="tables">Tables</h1>
<p>Table cells could set <code>1rlh</code> as their height but they would feel constricted.
Using <code>2rlh</code> wastes too much space. Instead, we use <a href="https://markboulton.co.uk/journal/incremental-leading/" title="Incremental leading">incremental leading</a>: we
align one in every five lines.</p>
<div class="language-css codehilite"><pre><span/><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>
</pre></div>


<p>To align the elements after the table, we need to add some padding. We can
either reuse the JavaScript code from images or use a few lines of CSS that
count the regular rows and compute the missing vertical padding:</p>
<div class="language-css codehilite"><pre><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="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>
</pre></div>


<p>A header cell has twice the padding of a regular cell. With two regular rows,
the total padding is 2×2×0.2+2×0.4=1.6. We need to add <code>0.4rlh</code> to reach
<code>2rlh</code> of extra vertical padding across the table.</p>
<figure><div class="lf-media-outer" style="width: 680px"><span class="lf-media-inner" style="padding-bottom: 86.765%"><img alt="Screenshot of my website with a grid as an overlay and a table following the&#10;vertical rhythm. Additional padding is visible after the table. The height of&#10;the table with padding is 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>One line out of five is aligned to the grid. Additional padding is added after the table to not break the vertical rhythm. 405 is divisible by 27, our line height in this example.</figcaption></figure>
<hr/>
<p>None of this is necessary. But once you start looking, you can’t unsee it. Until
browsers implement <a href="https://www.w3.org/TR/css-rhythm-1/" title="CSS Rhythmic Sizing Module Level 1">CSS Rhythmic Sizing</a>, a
bit of CSS wizardry and a touch of JavaScript is enough to pull it off. The main
text now returns after each intrusion “precisely on beat and in phase.” 🎼</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn-pawel">
<p>See “<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>” by Paweł
Grzybek. <a class="footnote-backref" href="#fnref-pawel" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn-postcss">
<p>For broader compatibility, you can replace <code>2rlh</code> with
<code>calc(var(--line-height) * 2rem)</code> and set the <code>--line-height</code> custom
property in the <code>:root</code> pseudo-class. I wrote a <a href="https://github.com/vincentbernat/vincent.bernat.ch/blob/95f29793faad50a94fe4199a1774d59d409d3755/extensions/css.js#L69-L79">simple PostCSS
plugin</a> for this purpose. <a class="footnote-backref" href="#fnref-postcss" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn-calc">
<p>It would have been nicer to compute the line height with
<code>calc(round(up, calc(2.4rem / 1rlh), 0) * 1rlh)</code>. Unfortunately, typed
arithmetic is <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">not supported by Firefox yet</a>. Moreover, browsers support
<code>round()</code> only <a href="https://webstatus.dev/features/round-mod-rem" title="round() function on Web Platform Status">since 2024</a>. Instead, I coded a <a href="https://github.com/vincentbernat/vincent.bernat.ch/blob/95f29793faad50a94fe4199a1774d59d409d3755/extensions/css.js#L31-L67">PostCSS
plugin</a> for this as well. <a class="footnote-backref" href="#fnref-calc" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn-grid">
<p>The following CSS code defines a grid tracking the line height:</p>
<div class="language-css codehilite"><pre><span/><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>
</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>See “<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>” by 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">Calculate “1/(40rods/hogshead) to L/100km” from your Zsh prompt</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2026-zsh-calculator" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2026-zsh-calculator#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-03-22T13:37:09Z</updated>
    <id>http://www.luffy.cx/en/blog/2026-zsh-calculator.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p>I often need a quick calculation or a unit conversion. Rather than reaching for
a separate tool, a few lines of <em>Zsh</em> configuration turn <code>=</code> into a calculator.
Typing <code>= 660km / (2/3)c * 2 -&gt; ms</code> gives me <code>6.60457 ms</code><sup id="fnref-marseille"><a class="footnote-ref" href="#fn-marseille">1</a></sup> without
leaving my terminal, thanks to the Zsh line editor.</p>
<div class="toc">
<ul>
<li><a href="#the-equal-alias">The equal alias</a></li>
<li><a href="#the-quoting-problem">The quoting problem</a><ul>
<li><a href="#automatic-quoting-with-zle">Automatic quoting with ZLE</a></li>
<li><a href="#storing-unquoted-history">Storing unquoted history</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="the-equal-alias">The equal alias</h1>
<p>The main idea looks simple: define <code>=</code> as an alias to a calculator command. I
prefer <a href="https://numbat.dev/" title="Numbat: a statically typed programming language for scientific computations">Numbat</a>, a scientific calculator that supports unit conversions.
<a href="https://qalculate.github.io/" title="Qalculate!: the ultimate desktop calculator">Qalculate</a> is a close second.<sup id="fnref-qalc"><a class="footnote-ref" href="#fn-qalc">2</a></sup> If neither is available, we fall back to
Zsh’s built-in <em>zcalc</em> module.</p>
<p>As the <code>alias</code> built-in uses <code>=</code> as a separator for name and value, we need to
alter the <code>aliases</code> associative array:</p>
<div class="language-bash codehilite"><pre><span/><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>
</pre></div>


<p>With this in place, <code>= 847/11</code> becomes <code>numbat -e 847/11</code>.</p>
<h1 id="the-quoting-problem">The quoting problem</h1>
<p>The first problem surfaces quickly. Typing <code>= 5 * 3</code> fails: Zsh expands the <code>*</code>
character as a glob pattern before passing it to the calculator. The same issue
applies to other characters that Zsh treats specially, such as <code>&gt;</code> or <code>|</code>. You
must quote the expression:</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'5 * 3'</span>
<span class="go">15</span>
</pre></div>


<p>We fix this by hooking into the Zsh line editor to <strong>quote the expression</strong>
before executing it.</p>
<h2 id="automatic-quoting-with-zle">Automatic quoting with <abbr title="Zsh Line Editor">ZLE</abbr></h2>
<p>Zsh calls the <code>line-finish</code> widget before submitting a command. We hook a
function that detects the <code>=</code> prefix and quotes the expression:</p>
<div class="language-bash codehilite"><pre><span/>_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
</pre></div>


<p>When you type <code>= 5 * 3</code> and press <kbd>↲</kbd>, <code>_vbe_calc_quote</code> strips the <code>=</code>
prefix, quotes the remainder with the <a href="https://manpages.debian.org/zshexpn.1.html#q~2" title="zshexpn(1) manual page"><code>(q-)</code> parameter expansion flag</a>,
and rewrites the buffer to <code>= '5 * 3'</code> before Zsh submits the command. As a
bonus, you can save a few keystrokes with <code>=5*3</code>! 🚀</p>
<p>You can now compute math expressions and convert units directly from your shell.
Zsh automatically quotes your expressions:</p>
<div class="language-bash-session codehilite"><pre><span/><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>
</pre></div>


<figure><div class="lf-media-outer" style="width: 400px"><span class="lf-media-inner" style="padding-bottom: 74.750%"><img alt="“That's the way I like it!” says Grampa&#10;Simpson" src="https://d2pzklc15kok91.cloudfront.net/images/simpson-s06e18@1x.27ade3072859d5.jpg" srcset="https://d2pzklc15kok91.cloudfront.net/images/simpson-s06e18@1x.27ade3072859d5.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>The metric system is the tool of the devil! My car gets forty rods to the hogshead, and that's the way I like it! ― <em>Grampa Simpson</em>, A Star Is Burns</figcaption></figure>
<h2 id="storing-unquoted-history">Storing unquoted history</h2>
<p>As is, Zsh records the <em>quoted</em> expression in history. You must unquote it
before submitting it again. Otherwise, the <abbr title="Zsh Line Editor">ZLE</abbr> widget quotes it a second time.
<a href="https://www.zsh.org/mla/users/2026/msg00021.html" title="Re: A ZLE widget for calculator">Bart Schaefer</a> provided a solution to store the
original version:</p>
<div class="language-bash codehilite"><pre><span/>_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
</pre></div>


<p>The <code>zshaddhistory</code> hook returns 1 if we are evaluating an expression, telling
<em>Zsh</em> not to record the command. The <code>preexec</code> hook then adds the original,
unquoted command with <code>print -s</code>.</p>
<hr/>
<p>The complete code is available in my <a href="https://github.com/vincentbernat/zshrc/blob/9af588820bed37b3b64b2f06777a77d32f4654c4/rc/alias.zsh#L451-L480">zshrc</a>. A common alternative is the
<a href="https://manpages.debian.org/zshmisc.1.html#noglob" title="zshmisc(1) manual page"><code>noglob</code></a> precommand modifier. If you stick with <code>to</code> instead of <code>-&gt;</code>
for unit conversion, it covers 90% of use cases. For a related Zsh line editor
trick, see how I use <a href="/en/blog/2025-zsh-autoexpand-aliases" title="Auto-expanding aliases in Zsh">auto-expanding aliases</a> to fix common typos.</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn-marseille">
<p>This is the fastest a packet can travel back and forth between
Paris and Marseille over optical fiber. <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 is less understanding with units. For example, it parses
“Mbps” as megabarn per picosecond: ☢️</p>
<div class="language-bash-session codehilite"><pre><span/><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>
</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">Automatic Prometheus metrics discovery with Docker labels</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2026-prometheus-metrics-discovery-docker-labels" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2026-prometheus-metrics-discovery-docker-labels#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-03-05T15:40:24Z</updated>
    <id>http://www.luffy.cx/en/blog/2026-prometheus-metrics-discovery-docker-labels.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p><a href="/en/blog/2025-akvorado-2.0" title="Akvorado release 2.0">Akvorado</a>, a network flow collector, relies on <a href="https://traefik.io/traefik" title="Traefik: modern HTTP reverse proxy and load-balancer">Traefik</a>, a reverse HTTP
proxy, to expose HTTP endpoints for its <a href="https://docs.docker.com/compose/" title="Docker Compose documentation">Docker Compose</a> services. <a href="https://docs.docker.com/engine/manage-resources/labels/" title="Docker object labels">Docker
labels</a> attached to each service define the routing rules. Traefik picks them
up automatically when a container starts. Instead of maintaining a static
configuration file to collect <a href="https://prometheus.io/docs/concepts/data_model/" title="Prometheus Data Model">Prometheus metrics</a>, we apply the same approach
with <a href="https://grafana.com/docs/alloy/latest/" title="Grafana Alloy documentation">Grafana Alloy</a>.</p>
<div class="toc">
<ul>
<li><a href="#traefik-docker">Traefik &amp; Docker</a></li>
<li><a href="#metrics-discovery-with-alloy">Metrics discovery with Alloy</a><ul>
<li><a href="#discovering-docker-containers">Discovering Docker containers</a></li>
<li><a href="#relabeling-targets">Relabeling targets</a></li>
<li><a href="#scraping-and-forwarding">Scraping and forwarding</a></li>
</ul>
</li>
<li><a href="#built-in-exporters">Built-in exporters</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">listens for events on the Docker socket</a>. Each service advertises its
configuration through labels. For example, here is the Loki service in Akvorado:</p>
<div class="language-yaml codehilite"><pre><span/><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>
</pre></div>


<p>Once the container is healthy, Traefik creates a router forwarding requests
matching <code>/loki</code> to its first exposed port. Colocating Traefik configuration
with the service definition is attractive. How do we achieve the same for
Prometheus metrics?</p>
<h1 id="metrics-discovery-with-alloy">Metrics discovery with Alloy</h1>
<p><a href="https://grafana.com/docs/alloy/latest/" title="Grafana Alloy documentation">Grafana Alloy</a>, a metrics collector that scrapes Prometheus endpoints,
includes a <a href="https://grafana.com/docs/alloy/latest/reference/components/discovery/discovery.docker/" title="Alloy: discovery.docker"><code>discovery.docker</code></a> component. Just like Traefik,
it connects to the Docker socket.<sup id="fnref-socket"><a class="footnote-ref" href="#fn-socket">1</a></sup> With a few relabeling rules, we teach
it to use Docker labels to locate and scrape metrics.</p>
<p>We define three labels on each service:</p>
<ul>
<li><code>metrics.enable</code> set to <code>true</code> enables metrics collection,</li>
<li><code>metrics.port</code> specifies the port exposing the Prometheus endpoint, and</li>
<li><code>metrics.path</code> specifies the path to the metrics endpoint.</li>
</ul>
<p>If a service exposes more than one port, <code>metrics.port</code> is mandatory. Otherwise,
it defaults to the only exposed port. The default value for <code>metrics.path</code> is
<code>/metrics</code>. The Loki service from earlier becomes:</p>
<div class="language-yaml codehilite"><pre><span/><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></pre></div>


<p>Alloy’s configuration is split into four parts:</p>
<ol>
<li><strong>discover</strong> containers through the Docker socket,</li>
<li><strong>filter and relabel</strong> targets using Docker labels,</li>
<li><strong>scrape</strong> the matching endpoints, and</li>
<li><strong>forward</strong> the metrics to Prometheus.</li>
</ol>
<h2 id="discovering-docker-containers">Discovering Docker containers</h2>
<p>The first building block discovers running containers:</p>
<div class="language-terraform codehilite"><pre><span/><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>
</pre></div>


<p>This connects to the Docker socket and lists containers every 30
seconds.<sup id="fnref-events"><a class="footnote-ref" href="#fn-events">2</a></sup> The <code>filter</code> block restricts discovery to containers belonging
to the <code>akvorado</code> project, avoiding interference with unrelated containers on
the same host. For each discovered container, Alloy produces a target with
labels such as <code>__meta_docker_container_label_metrics_port</code> for the
<code>metrics.port</code> Docker label.</p>
<h2 id="relabeling-targets">Relabeling targets</h2>
<p>The relabeling step filters and transforms raw targets from Docker discovery
into scrape targets. The first stage keeps only targets with <code>metrics.enable</code>
set to <code>true</code>:</p>
<div class="language-terraform codehilite"><pre><span/><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>
</pre></div>


<p>The second stage overrides the discovered port when the service defines
<code>metrics.port</code>:</p>
<div class="language-terraform codehilite"><pre><span/><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>
</pre></div>


<p>Next, we handle containers in <code>host</code> network mode. When
<code>__meta_docker_network_name</code> equals <code>host</code>, Alloy rewrites the address to
<code>host.docker.internal</code> instead of <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/><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>
</pre></div>


<p>The next stage derives the job name from the service name, stripping any
numbered suffix. The instance label is the address without the port:</p>
<div class="language-terraform codehilite"><pre><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_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>
</pre></div>


<p>If a container defines <code>metrics.path</code>, Alloy uses it. Otherwise, it defaults to
<code>/metrics</code>:</p>
<div class="language-terraform codehilite"><pre><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_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>
</pre></div>


<h2 id="scraping-and-forwarding">Scraping and forwarding</h2>
<p>With the targets properly relabeled, scraping and forwarding are
straightforward:</p>
<div class="language-terraform codehilite"><pre><span/><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>
</pre></div>


<p><code>prometheus.scrape</code> periodically fetches metrics from the discovered targets.
<code>prometheus.remote_write</code> sends them to Prometheus.</p>
<h1 id="built-in-exporters">Built-in exporters</h1>
<p>Some services do not expose a Prometheus endpoint. Redis and Kafka are common
examples. Alloy ships built-in <a href="https://grafana.com/docs/alloy/latest/reference/components/prometheus/" title="Alloy: Prometheus components">Prometheus exporters</a> that
query these services and expose metrics on their behalf.</p>
<div class="language-terraform codehilite"><pre><span/><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>
</pre></div>


<p>The same pattern applies to Kafka:</p>
<div class="language-terraform codehilite"><pre><span/><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>
</pre></div>


<p>Each exporter is a separate component with its own relabeling and scrape
configuration. We set the <code>job</code> label explicitly since no Docker metadata can
provide it.</p>
<hr/>
<p>With this setup, adding metrics to a new service with a Prometheus endpoint
requires only a few labels in <code>docker-compose.yml</code>, just like adding a Traefik
route. Alloy picks it up automatically. You can apply the same pattern with
another discovery method, like <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>, or <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>Both Traefik and Alloy require access to the Docker socket, which
grants root-level access to the host. A <a href="https://github.com/Tecnativa/docker-socket-proxy" title="Docker Socket Proxy: security-enhanced proxy for Docker socket">Docker socket proxy</a> mitigates
this by exposing only the read-only API endpoints needed for discovery. <a class="footnote-backref" href="#fnref-socket" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn-events">
<p>Unlike Traefik, which watches for events, Grafana Alloy polls the
container list at regular intervals—a behavior inherited from 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>The Alloy service needs <code>extra_hosts:
["host.docker.internal:host-gateway"]</code> in its definition. <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>