<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Monolog on Guillaume Delré</title><link>https://guillaumedelre.github.io/fr/tags/monolog/</link><description>Recent content in Monolog on Guillaume Delré</description><generator>Hugo</generator><language>fr-FR</language><lastBuildDate>Fri, 15 May 2026 10:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/fr/tags/monolog/index.xml" rel="self" type="application/rss+xml"/><item><title>Aucun témoin</title><link>https://guillaumedelre.github.io/fr/2026/05/15/aucun-t%C3%A9moin/</link><pubDate>Fri, 15 May 2026 10:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/05/15/aucun-t%C3%A9moin/</guid><description>Part 3 of 8 in &amp;quot;Symfony vers le Cloud : Douze Facteurs, Treize Services&amp;quot;: Un service s&amp;#39;est crashé en production sans laisser de logs. Pourquoi fingers_crossed et les déploiements cloud ne font pas bon ménage.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>Le service s&rsquo;était crashé. On avait l&rsquo;alerte. On avait le timestamp à la seconde. On avait <a href="https://grafana.com/oss/loki/" target="_blank" rel="noopener noreferrer">Loki</a> ouvert avec une requête prête.</p>
<p>Ce qu&rsquo;on n&rsquo;avait pas, c&rsquo;était les logs des cinq minutes précédant le crash.</p>
<p>Promtail tournait. Il était healthy. Il collectait les logs de tous les autres services sans problème. Mais pour celui-ci, dans la fenêtre qui comptait, il n&rsquo;y avait rien. Le service s&rsquo;était crashé sans laisser de trace.</p>
<h2 id="le-setup-qui-semblait-correct">Le setup qui semblait correct</h2>
<p>La stack de logging était raisonnable. Chaque service écrivait du JSON structuré vers stdout avec le formatter logstash de Monolog :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">stdout</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">stream</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;php://stdout&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">level</span>: <span style="color:#e6db74">&#34;%env(MONOLOG_LEVEL__DEFAULT)%&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">formatter</span>: <span style="color:#e6db74">&#39;monolog.formatter.logstash&#39;</span>
</span></span></code></pre></div><p><a href="https://grafana.com/docs/loki/latest/" target="_blank" rel="noopener noreferrer">Promtail</a> collectait la sortie des containers via la socket Docker, parsait le JSON, extrayait des labels, poussait vers Loki :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">scrape_configs</span>:
</span></span><span style="display:flex;"><span>    -
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">job_name</span>: <span style="color:#ae81ff">docker</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">docker_sd_configs</span>:
</span></span><span style="display:flex;"><span>            -
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">host</span>: <span style="color:#ae81ff">unix:///var/run/docker.sock</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">refresh_interval</span>: <span style="color:#ae81ff">5s</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">pipeline_stages</span>:
</span></span><span style="display:flex;"><span>            -
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">drop</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">older_than</span>: <span style="color:#ae81ff">168h</span>
</span></span><span style="display:flex;"><span>            -
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">json</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">expressions</span>:
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">level</span>: <span style="color:#ae81ff">level</span>
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">msg</span>: <span style="color:#ae81ff">message</span>
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">service</span>: <span style="color:#ae81ff">service</span>
</span></span><span style="display:flex;"><span>            -
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">level</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">relabel_configs</span>:
</span></span><span style="display:flex;"><span>            -
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">source_labels</span>: [ <span style="color:#e6db74">&#39;__meta_docker_container_log_stream&#39;</span> ]
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">target_label</span>: <span style="color:#ae81ff">stream</span>
</span></span></code></pre></div><p>Deux stages font plus de travail que les autres. Le stage <code>json</code> extrait <code>level</code> et <code>service</code> de chaque ligne de log ; le stage <code>labels</code> qui suit immédiatement les promeut en labels d&rsquo;index Loki, ce qui fait de <code>{service=&quot;content&quot;, level=&quot;error&quot;}</code> une lookup directe plutôt qu&rsquo;un scan plein texte sur les lignes stockées. Le relabeling <code>stream</code> conserve si une ligne venait de stdout ou stderr — une distinction requêtable dès que Monolog envoie les erreurs vers stderr et le reste vers stdout. Le stage <code>drop older_than: 168h</code> est une soupape de sécurité : si Promtail redémarre après une longue interruption et rejoue des lignes bufferisées, tout ce qui est plus vieux de sept jours est écarté avant d&rsquo;atteindre Loki.</p>
<p>En théorie : les logs vont vers stdout, Promtail lit stdout, les logs apparaissent dans Loki. La <a href="https://12factor.net/logs" target="_blank" rel="noopener noreferrer">méthodologie twelve-factor</a> décrit exactement ce modèle pour le Facteur XI — traiter les logs comme des flux d&rsquo;événements, écrire vers stdout, laisser l&rsquo;environnement gérer la collecte et le routage.</p>
<p>L&rsquo;application avait stdout. Promtail lisait stdout. Qu&rsquo;est-ce qui pouvait mal tourner.</p>
<h2 id="ce-que-fingers_crossed-emporte-avec-lui">Ce que fingers_crossed emporte avec lui</h2>
<p>En production, le bloc <code>when@prod</code> remplaçait le simple handler <code>stream</code> par quelque chose de plus sophistiqué :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">when@prod</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">monolog</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">handlers</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">main</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">type</span>: <span style="color:#ae81ff">fingers_crossed</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">action_level</span>: <span style="color:#ae81ff">error</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">handler</span>: <span style="color:#ae81ff">main_group</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">excluded_http_codes</span>: [<span style="color:#ae81ff">404</span>]
</span></span></code></pre></div><p>La ligne <code>excluded_http_codes: [404]</code> est elle-même révélatrice : sans elle, chaque 404 d&rsquo;un scanner ou d&rsquo;un crawler déclenche un flush complet du buffer, déversant des mégaoctets de logs debug pour des URLs malformées. Quelqu&rsquo;un avait déjà appris ça à ses dépens.</p>
<p><code>fingers_crossed</code> est un pattern Monolog bien connu. L&rsquo;idée est élégante : ne pas noyer les logs de production dans le bruit debug, mais si quelque chose tourne mal, retrouver rétrospectivement ce qui s&rsquo;est passé avant l&rsquo;erreur. Le handler bufferise chaque entrée de log en mémoire. Au moment où il voit une <code>error</code>, il flush le buffer entier vers le handler imbriqué — en donnant le contexte complet qui a précédé la défaillance.</p>
<p>Le problème, c&rsquo;est ce qui se passe quand la défaillance n&rsquo;est pas une erreur loguée. C&rsquo;est un OOM kill. Un SIGKILL de l&rsquo;orchestrateur. Un segfault. Un process qui arrête de répondre et est tué de force.</p>
<p>Dans ces cas, <code>fingers_crossed</code> n&rsquo;atteint jamais son <code>action_level</code>. Le buffer existe, plein des cinq dernières minutes d&rsquo;activité, et il disparaît avec le process. Les logs étaient là. Ils étaient en mémoire. Ils sont morts avant d&rsquo;atteindre stdout.</p>
<p>Le Facteur IX du twelve-factor parle de disposabilité : les processus doivent démarrer vite et s&rsquo;arrêter proprement. Sur un arrêt normal (SIGTERM), un processus bien élevé finit son travail en cours et quitte. Mais les crashes ne sont pas des arrêts propres, et les buffers mémoire ne sont pas résistants aux crashes. Le service était disposable au sens où on pouvait le redémarrer ; il ne l&rsquo;était pas au sens où sa sortie était transparente.</p>
<h2 id="les-fichiers-que-personne-ne-lisait">Les fichiers que personne ne lisait</h2>
<p>Il y avait un deuxième problème, plus silencieux mais tout aussi persistant.</p>
<p>Chaque service avait un handler <code>main_group</code> qui routait les logs vers deux destinations en parallèle :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">main_group</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">group</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">members</span>: [<span style="color:#ae81ff">main_file, stdout]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">main_file</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">stream</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;%kernel.logs_dir%/%kernel.environment%.log&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">formatter</span>: <span style="color:#e6db74">&#34;monolog.formatter.logstash&#34;</span>
</span></span></code></pre></div><p><code>var/log/prod.log</code> était écrit sur chaque service, dans chaque environnement, y compris en production. Le même contenu qui allait vers stdout allait aussi vers un fichier à l&rsquo;intérieur du container. Le fichier grossissait sans rotation. Le fichier n&rsquo;était pas accessible à Promtail (qui lisait depuis la socket Docker, pas depuis le filesystem du container). Le fichier consommait de l&rsquo;espace disque. Personne ne le lisait.</p>
<p>Le channel audit était pire :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">audit_file</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">stream</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;%kernel.logs_dir%/audit.log&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">formatter</span>: <span style="color:#e6db74">&#39;monolog.formatter.line&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">audit</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">group</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">members</span>: [<span style="color:#ae81ff">audit_file, stderr]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">channels</span>: [<span style="color:#e6db74">&#39;audit&#39;</span>]
</span></span></code></pre></div><p>Les logs d&rsquo;audit allaient vers <code>stderr</code> (visible par Promtail) et vers <code>audit.log</code> (invisible à Promtail). Le format dans le fichier était une ligne brute, pas le JSON structuré qu&rsquo;attendait Promtail. En pratique, la piste d&rsquo;audit existait à deux endroits : l&rsquo;une requêtable, l&rsquo;autre enfouie dans un répertoire de container qui ne survivait que le temps du container.</p>
<h2 id="ce-que-le-facteur-xi-demande-vraiment">Ce que le Facteur XI demande vraiment</h2>
<p>Le onzième facteur est direct là-dessus : une application ne doit pas se soucier du routage ou du stockage de son flux de sortie. Elle écrit vers stdout. Tout le reste est le job de l&rsquo;environnement.</p>
<p>Ça veut dire pas de handlers de fichiers en production. Pas en backup. Pas pour les pistes d&rsquo;audit. Pas &ldquo;au cas où&rdquo;. Du moment qu&rsquo;une application se met à gérer des fichiers, elle prend en charge la rotation, la rétention, l&rsquo;espace disque, et l&rsquo;accessibilité — rien de tout ça n&rsquo;appartient à l&rsquo;intérieur d&rsquo;un container.</p>
<p>La correction pour les handlers de fichiers est directe. Dans <code>when@prod</code>, supprimer chaque handler <code>*_file</code> et chaque group qui en inclut un. Le channel audit reçoit le même traitement : stderr uniquement, JSON structuré, pas de fichier :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">when@prod</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">monolog</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">handlers</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">stdout</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">type</span>: <span style="color:#ae81ff">stream</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;php://stdout&#34;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e"># défaut &#34;warning&#34; — configurable par déploiement via variable d&#39;env pour du debug ciblé</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">level</span>: <span style="color:#e6db74">&#34;%env(default:default_log_level:MONOLOG_LEVEL__DEFAULT)%&#34;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">formatter</span>: <span style="color:#e6db74">&#39;monolog.formatter.logstash&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">stderr</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">type</span>: <span style="color:#ae81ff">stream</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;php://stderr&#34;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">level</span>: <span style="color:#ae81ff">error</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">formatter</span>: <span style="color:#e6db74">&#39;monolog.formatter.logstash&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">main</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">type</span>: <span style="color:#ae81ff">group</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">members</span>: [<span style="color:#ae81ff">stdout]</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">channels</span>: [<span style="color:#e6db74">&#39;!event&#39;</span>, <span style="color:#e6db74">&#39;!http_client&#39;</span>, <span style="color:#e6db74">&#39;!doctrine&#39;</span>, <span style="color:#e6db74">&#39;!deprecation&#39;</span>, <span style="color:#e6db74">&#39;!audit&#39;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">audit</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">type</span>: <span style="color:#ae81ff">stream</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;php://stderr&#34;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">level</span>: <span style="color:#ae81ff">debug</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">formatter</span>: <span style="color:#e6db74">&#39;monolog.formatter.logstash&#39;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">channels</span>: [<span style="color:#e6db74">&#39;audit&#39;</span>]
</span></span></code></pre></div><p>stdout pour le channel principal. stderr pour les erreurs et l&rsquo;audit. Rien d&rsquo;autre. Promtail récupère les deux via la socket Docker. Le container n&rsquo;écrit rien sur disque. Et les logs d&rsquo;audit sont maintenant du JSON structuré, requêtable dans Loki avec tout le reste.</p>
<h2 id="la-question-plus-dure-sur-fingers_crossed">La question plus dure sur fingers_crossed</h2>
<p>Les handlers de fichiers, c&rsquo;était simple. <code>fingers_crossed</code> est plus nuancé.</p>
<p>Le pattern résout un vrai problème : dans un service de production actif, tout logger en debug crée du bruit et des coûts. <code>fingers_crossed</code> permet de capturer le contexte sans le payer sauf si quelque chose tourne vraiment mal. C&rsquo;est un compromis raisonnable quand le mode de défaillance contre lequel on protège est une erreur applicative (une exception, une 500, une requête lente).</p>
<p>Ce n&rsquo;est pas un compromis raisonnable quand le mode de défaillance est un crash de process. Et dans un environnement Kubernetes, les crashes de process arrivent : évictions OOM, échecs de liveness probe, pression sur les nodes. Exactement les cas où on a le plus besoin des logs.</p>
<p>Une approche : garder <code>fingers_crossed</code> mais réduire la taille du buffer. Par défaut il garde tout depuis le dernier reset. Mettre <code>buffer_size: 50</code> plafonne l&rsquo;usage mémoire, ce qui limite aussi ce qui se perd lors d&rsquo;un crash. On n&rsquo;aura pas le contexte complet, mais on aura les cinquante dernières entrées. Cette voie réduit le périmètre de perte sans supprimer la cause : l&rsquo;opacité dépend toujours d&rsquo;un seuil d&rsquo;erreur qui peut ne jamais se déclencher.</p>
<p>Une autre approche : accepter que les logs debug soient coûteux et monter le niveau par défaut en production. Alors on n&rsquo;a plus besoin de <code>fingers_crossed</code> du tout — si info et au-dessus vont directement vers stdout, rien n&rsquo;est jamais bufferisé.</p>
<p>L&rsquo;approche retenue : supprimer <code>fingers_crossed</code>, monter le niveau par défaut à <code>warning</code>, garder un override debug disponible via variable d&rsquo;env pour les investigations ciblées. Les logs qui comptent apparaissent immédiatement. Ceux qui ne comptent pas ne sont jamais écrits. Rien n&rsquo;est bufferisé.</p>
<h2 id="les-crashes-ne-flushent-pas">Les crashes ne flushent pas</h2>
<p>Le Facteur XI et le Facteur IX se rejoignent au même point : un process qui meurt en plein milieu d&rsquo;une requête. <a href="/2026/05/16/the-cache-that-was-lying-to-us/">un autre article de cette série</a>
 décrivait l&rsquo;illusion d&rsquo;un service qui fonctionnait parfaitement sur un pod mais se comportait silencieusement mal sur deux. C&rsquo;est la même illusion, un niveau au-dessus : un service qui semblait logger correctement, jusqu&rsquo;au moment où il en avait le plus besoin.</p>
<p>La règle pour Monolog en production est sans appel : si ça n&rsquo;atteint pas stdout ou stderr avant que le process quitte, ça n&rsquo;existe pas. Un handler de fichier à l&rsquo;intérieur d&rsquo;un container est invisible pour le collecteur de logs et meurt avec le pod. Un buffer <code>fingers_crossed</code> est invisible pour le collecteur de logs et meurt avec le process.</p>
<p>La production tend à créer les conditions où on a le plus besoin des logs — pression OOM, défaillances en cascade, mauvais déploiements — et c&rsquo;est exactement les conditions où ces deux patterns échouent simultanément. Écrire vers stdout, adopter un niveau par défaut qui ne nécessite pas de bufferisation, et rendre l&rsquo;override disponible pour quand on en a vraiment besoin. Les logs seront là. Ils n&rsquo;attendront pas un seuil d&rsquo;erreur qui ne se déclenche jamais.</p>
]]></content:encoded></item></channel></rss>