<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="cs"><id>https://blog.diba.dev/cs</id><title>blog.diba.dev</title><updated>2026-06-20T14:49:46.976909+00:00</updated><link href="https://blog.diba.dev/cs" rel="alternate"/><link href="https://blog.diba.dev/cs/feed.xml" rel="self"/><generator uri="https://lkiesow.github.io/python-feedgen" version="1.0.0">python-feedgen</generator><subtitle>Technický blog o Pythonu, Flasku a dalších věcech.</subtitle><entry><id>https://blog.diba.dev/cs/posts/hello-world</id><title>Hello World</title><updated>2026-02-21T00:00:00+01:00</updated><content type="html">&lt;p&gt;Vítejte na blogu! Toto je první článek, který slouží jako ukázka formátování.&lt;/p&gt;
&lt;h2 id="proc-flask"&gt;Proč Flask?&lt;/h2&gt;
&lt;p&gt;Flask je minimalistický webový framework pro Python. Na rozdíl od Djanga vám nediktuje strukturu projektu — postavíte si přesně to, co potřebujete.&lt;/p&gt;
&lt;h2 id="ukazka-kodu"&gt;Ukázka kódu&lt;/h2&gt;
&lt;p&gt;Jednoduchá Flask aplikace:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;flask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Hello, World!&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A tady je příklad práce se soubory v Pythonu:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pathlib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;posts_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;posts&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;md_file&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;posts_dir&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*.md&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Nalezen článek: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;md_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="seznamy"&gt;Seznamy&lt;/h2&gt;
&lt;p&gt;Věci, které tento blog umí:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Markdown články s YAML frontmatter&lt;/li&gt;
&lt;li&gt;Syntax highlighting přes Pygments&lt;/li&gt;
&lt;li&gt;Lazy cache — HTML se generuje při prvním přístupu&lt;/li&gt;
&lt;li&gt;RSS/Atom feed&lt;/li&gt;
&lt;li&gt;Tagy a filtrování podle nich&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="kod-inline"&gt;Kód inline&lt;/h2&gt;
&lt;p&gt;Konfiguraci najdete v souboru &lt;code&gt;app.py&lt;/code&gt;. Články se ukládají do složky &lt;code&gt;posts/&lt;/code&gt; jako &lt;code&gt;.md&lt;/code&gt; soubory.&lt;/p&gt;</content><link href="https://blog.diba.dev/cs/posts/hello-world"/><summary>&lt;p&gt;První článek na blogu. Ukázka syntaxe, kódu a formátování.&lt;/p&gt;</summary><published>2026-02-21T00:00:00+01:00</published></entry><entry><id>https://blog.diba.dev/cs/posts/sentry-suppress-logs-capture-exception</id><title>Jak logovat informativně a nerozhazovat Sentry grouping</title><updated>2026-02-22T00:00:00+01:00</updated><content type="html">&lt;p&gt;Context manager, který potlačí duplicitní Sentry log-eventy a pošle jednu výjimku s plným kontextem — informativní logy v aplikaci, čistý grouping v Sentry.&lt;/p&gt;
&lt;h2 id="problem"&gt;Problém&lt;/h2&gt;
&lt;p&gt;Sentry Python SDK s &lt;code&gt;LoggingIntegration&lt;/code&gt; zachytává každý &lt;code&gt;logger.error()&lt;/code&gt; / &lt;code&gt;logger.warning()&lt;/code&gt; jako samostatný Sentry event. To přináší dva problémy najednou.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Duplikáty.&lt;/strong&gt; Když chybu ošetříte a zalogujete, dostanete v Sentry dva eventy: log-event + exception event (pokud voláte i &lt;code&gt;capture_exception&lt;/code&gt;). Jeden caught error vygeneruje 2–3 Sentry eventy, každý s jiným groupingem, a kvóta se plní zbytečně rychle.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rozbitý grouping.&lt;/strong&gt; Sentry u log-eventů vidí jen text zprávy. Nemá výjimku, nemá stacktrace — nemá se čeho chytit. Zprávy &lt;code&gt;Download failed: url=AAA&lt;/code&gt; a &lt;code&gt;Download failed: url=BBB&lt;/code&gt; jsou pro Sentry dva různé problémy, i když jde o tutéž chybu se stejným stacktrace. Naivní řešení je nedávat specifické informace do log message — ale to je přesně to, co v logu chceme. Informativní zpráva &lt;code&gt;Download failed: url=https://example.com/api/v2/products&lt;/code&gt; je v logu neocenitelná, ale pro Sentry grouping je jed.&lt;/p&gt;
&lt;h2 id="co-context-manager-dela"&gt;Co context manager dělá&lt;/h2&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_suppress_logs_capture_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Download failed: url=&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Uvnitř bloku:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;logger.warning()&lt;/code&gt;/&lt;code&gt;logger.error()&lt;/code&gt; jde jen do lokálního logu — Sentry log-event se zahodí (přes &lt;code&gt;before_send&lt;/code&gt; hook)&lt;/li&gt;
&lt;li&gt;Můžete logovat kolik chcete, žádný z logů nevytvoří Sentry event&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Po ukončení bloku:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sentry dostane přesně 1 event: &lt;code&gt;capture_exception(exc)&lt;/code&gt; s úrovní &lt;code&gt;warning&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Event obsahuje plný stacktrace, exception chain, tagy a context&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="jak-to-funguje-uvnitr"&gt;Jak to funguje uvnitř&lt;/h2&gt;
&lt;p&gt;Tři spolupracující komponenty:&lt;/p&gt;
&lt;h3 id="1-context-variable"&gt;1. Context variable&lt;/h3&gt;
&lt;p&gt;Signalizuje, že jsme uvnitř suppress bloku:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;_suppress_ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contextvars&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contextvars&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;sentry_suppress&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="2-log-record-factory-hook"&gt;2. Log record factory hook&lt;/h3&gt;
&lt;p&gt;Označí log záznamy z potlačeného bloku:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;original_factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;levelno&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WARNING&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;_suppress_ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sentry_skip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="3-before_send-hook"&gt;3. before_send hook&lt;/h3&gt;
&lt;p&gt;Zahodí označené log-eventy:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;before_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;log_record&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;sentry_skip&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="context-manager-samotny"&gt;Context manager samotný&lt;/h3&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;sentry_suppress_logs_capture_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ne"&gt;BaseException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_suppress_ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_suppress_ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_scope&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_level&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;warning&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_extra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;exc_type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_extra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;exc_message&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;warn.context&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

            &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capture_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="typicke-pouziti"&gt;Typické použití&lt;/h2&gt;
&lt;p&gt;Caught error, který nechceme propagovat, ale chceme ho vidět v Sentry:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;downloader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_suppress_logs_capture_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Download failed: url=&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DownloadPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# fallback&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Ošetření chyby v error handleru — nechceme, aby sekundární selhání přebilo primární:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;refetch_err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_suppress_logs_capture_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refetch_err&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Failed to re-fetch task &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;refetch_err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Race condition — objekt smazán jiným procesem:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;StaleDataError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_suppress_logs_capture_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;Product &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; deleted before update could commit, skipping.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="bez-vs-s-context-managerem"&gt;Bez vs. s context managerem&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bez&lt;/th&gt;
&lt;th&gt;S&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;logger.error()&lt;/code&gt; → 1 Sentry log-event (bez stacktrace)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;logger.warning()&lt;/code&gt; → jen lokální log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;capture_exception()&lt;/code&gt; → 1 Sentry exception event&lt;/td&gt;
&lt;td&gt;&lt;code&gt;capture_exception()&lt;/code&gt; → 1 Sentry exception event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2 eventy v Sentry, duplikátní šum&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 event v Sentry, plný kontext&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="proc-contextvars-a-ne-thread-local"&gt;Proč contextvars a ne thread-local&lt;/h2&gt;
&lt;p&gt;Aplikace je async (&lt;code&gt;asyncio&lt;/code&gt;). Thread-local by nefungoval — coroutiny sdílejí vlákno. &lt;code&gt;contextvars.ContextVar&lt;/code&gt; je asyncio-aware a správně izoluje kontext per-task.&lt;/p&gt;</content><link href="https://blog.diba.dev/cs/posts/sentry-suppress-logs-capture-exception"/><summary>&lt;p&gt;Context manager, který potlačí duplicitní Sentry log-eventy a pošle jednu výjimku s plným kontextem — informativní logy v aplikaci, čistý grouping v Sentry.&lt;/p&gt;</summary><published>2026-02-22T00:00:00+01:00</published></entry><entry><id>https://blog.diba.dev/cs/posts/sentry-exception-group-title</id><title>Více vypovídající titulky u Sentry eventů</title><updated>2026-02-22T00:00:00+01:00</updated><content type="html">&lt;p&gt;Jak proměnit titulek Sentry issue z &lt;code&gt;ExceptionGroup: Download failed&lt;/code&gt; na &lt;code&gt;DownloadError -&amp;gt; [HTTPDownloadError -&amp;gt; RuntimeError, HTTPStatusError]&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="problem"&gt;Problém&lt;/h2&gt;
&lt;p&gt;Sentry zobrazuje issue tak, že titulkem je &lt;code&gt;exception_type&lt;/code&gt; a podtitulkem &lt;code&gt;exception_value&lt;/code&gt;. U &lt;code&gt;ExceptionGroup&lt;/code&gt; to vede k identickým titulkům pro zcela odlišné root causes — titulek je vždy jen &lt;code&gt;ExceptionGroup&lt;/code&gt; a podtitulek generický message:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ExceptionGroup: Download failed
ExceptionGroup: Download failed
ExceptionGroup: Download failed
ExceptionGroup: Download failed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Čtyři různé Sentry issues, každý s jiným problémem (DNS error, HTTP 403, HTTP 404, nevalidní URL), ale na první pohled nerozlišitelné.
Stejný problém nastává u výjimek balených do generického obalu. To je běžný pattern — obal nese metainformaci (retry count, endpoint URL, batch ID), ale Sentry zobrazí jen typ obalu:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;DownloadError: Download failed
DownloadError: Download failed
DownloadError: Download failed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Příčiny jsou přitom pokaždé jiné — &lt;code&gt;ConnectionTimeout&lt;/code&gt;, &lt;code&gt;SSLCertVerificationError&lt;/code&gt;, &lt;code&gt;HTTPStatusError(403)&lt;/code&gt; — ale v seznamu issues vidíme jen generický &lt;code&gt;DownloadError&lt;/code&gt;. Musíme rozkliknout každý event, abychom zjistili, co se skutečně stalo.&lt;/p&gt;
&lt;p&gt;Nebo třeba:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;TaskError: Task processing failed
TaskError: Task processing failed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Jeden je &lt;code&gt;JSONDecodeError&lt;/code&gt;, druhý &lt;code&gt;PermissionError&lt;/code&gt; — ale titulek je identický.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Jádro problému:&lt;/strong&gt; Jak &lt;code&gt;ExceptionGroup&lt;/code&gt;, tak výjimky s chain (&lt;code&gt;__cause__&lt;/code&gt;/&lt;code&gt;__context__&lt;/code&gt;) nesou skutečnou informaci ve vnořených výjimkách, ne v top-level message. Sentry tuhle strukturu neumí zobrazit v titulku.&lt;/p&gt;
&lt;h2 id="reseni-sdk-tag-server-side-fingerprint-rule"&gt;Řešení: SDK tag + server-side fingerprint rule&lt;/h2&gt;
&lt;p&gt;Kombinace dvou mechanismů:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SDK&lt;/strong&gt; (Python, &lt;code&gt;before_send&lt;/code&gt; hook) — má přístup k živému exception objektu, umí rekurzivně projít &lt;code&gt;ExceptionGroup.exceptions&lt;/code&gt; a &lt;code&gt;__cause__&lt;/code&gt;/&lt;code&gt;__context__&lt;/code&gt; chainy. Výsledek zapíše jako tag.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sentry server&lt;/strong&gt; (fingerprint rule) — umí přepsat titulek, ale potřebuje hotová data. Tag z SDK je v eventu k dispozici.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="proc-to-funguje"&gt;Proč to funguje&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;before_send&lt;/code&gt; se volá před odesláním eventu na server — tag je součástí payloadu&lt;/li&gt;
&lt;li&gt;Server-side fingerprint rules se aplikují po přijetí eventu — vidí tagy z SDK&lt;/li&gt;
&lt;li&gt;&lt;code&gt;title=&lt;/code&gt; v fingerprint rule je jediný dokumentovaný způsob, jak přepsat titulek issue&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="proc-tag-a-ne-jiny-mechanismus"&gt;Proč tag a ne jiný mechanismus&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Tag je standardní součást Event payloadu — žádné hackování &lt;code&gt;TypedDict&lt;/code&gt;u&lt;/li&gt;
&lt;li&gt;Je viditelný v Sentry UI (filtrování, vyhledávání)&lt;/li&gt;
&lt;li&gt;Fingerprint rule &lt;code&gt;tags.*&lt;/code&gt; matcher je dokumentovaný a podporovaný&lt;/li&gt;
&lt;li&gt;Čistá separace: backend dodá data, server je zobrazí&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="implementace"&gt;Implementace&lt;/h2&gt;
&lt;h3 id="mixin-pro-vyjimky"&gt;Mixin pro výjimky&lt;/h3&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;SentryTitleMixin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Mixin for exceptions with informative Sentry titles.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="nd"&gt;@staticmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_format_sentry_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ne"&gt;BaseException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BaseExceptionGroup&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;, &amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;SentryTitleMixin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_format_sentry_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]&amp;quot;&lt;/span&gt;
        &lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__cause__&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__context__&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SentryTitleMixin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_format_sentry_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;sentry_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Return a human-readable title describing the exception chain.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ne"&gt;BaseException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_format_sentry_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;_format_sentry_title()&lt;/code&gt; je &lt;code&gt;@staticmethod&lt;/code&gt;, protože rekurzivně prochází vnořené výjimky, které samy &lt;code&gt;SentryTitleMixin&lt;/code&gt; nemají — potřebuje přijmout libovolný &lt;code&gt;BaseException&lt;/code&gt;, ne jen &lt;code&gt;self&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Rekurzivní &lt;code&gt;_format_sentry_title()&lt;/code&gt; sestaví hierarchický popis:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ExceptionGroup&lt;/strong&gt; → &lt;code&gt;Name -&amp;gt; [Sub1, Sub2]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chained exception&lt;/strong&gt; (&lt;code&gt;__cause__&lt;/code&gt;/&lt;code&gt;__context__&lt;/code&gt;) → &lt;code&gt;Name -&amp;gt; CauseName&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kombinace&lt;/strong&gt; → &lt;code&gt;DownloadError -&amp;gt; [HTTPDownloadError -&amp;gt; RuntimeError, HTTPStatusError]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="vyjimka-s-mixinem"&gt;Výjimka s mixinem&lt;/h3&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;DownloadError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ExceptionGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SentryTitleMixin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Download failed in all attempts.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Žádný &lt;code&gt;__new__&lt;/code&gt;, žádný &lt;code&gt;derive()&lt;/code&gt; — mixin je opt-in a přidává se jen na výjimky, kde to dává smysl. Zasahuje pouze do Sentry zobrazení, žádné změny v chování výjimek.&lt;/p&gt;
&lt;h3 id="before_send-hook"&gt;&lt;code&gt;before_send&lt;/code&gt; hook&lt;/h3&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;before_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ... existing filters ...&lt;/span&gt;

    &lt;span class="c1"&gt;# SentryTitleMixin — set tag for server-side fingerprint rules.&lt;/span&gt;
    &lt;span class="c1"&gt;#   Requires a fingerprint rule in Sentry (Project Settings → Issue Grouping):&lt;/span&gt;
    &lt;span class="c1"&gt;#   tags.sentry_title:* -&amp;gt; {{ default }} title=&amp;quot;{{ tags.sentry_title }}&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;exc_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;exc_info&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exc_info&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;exc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SentryTitleMixin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tags&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sentry_title&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sentry_title&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="fingerprint-rule-v-sentry-ui"&gt;Fingerprint rule v Sentry UI&lt;/h3&gt;
&lt;p&gt;Project Settings → Issue Grouping → Fingerprint Rules:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;tags.sentry_title:* -&amp;gt; {{ default }} title=&amp;quot;{{ tags.sentry_title }}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;{{ default }}&lt;/code&gt; zachovává výchozí grouping — pravidlo mění pouze zobrazený titulek.&lt;/p&gt;
&lt;h2 id="vysledek"&gt;Výsledek&lt;/h2&gt;
&lt;p&gt;Místo:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ExceptionGroup: Download failed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Sentry zobrazí:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;DownloadError -&amp;gt; [HTTPDownloadError -&amp;gt; RuntimeError, HTTPStatusError]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="obecnejsi-pozorovani"&gt;Obecnější pozorování&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Sentry SDK a server mají &lt;strong&gt;asymetrické schopnosti&lt;/strong&gt;: SDK vidí runtime objekty, server vidí jen serializovaný payload. Ale titulek umí přepsat jen server (přes fingerprint rules).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExceptionGroup&lt;/code&gt; (Python 3.11+) je v Sentry stále občan druhé kategorie — titulek zobrazuje jen top-level message, ne strukturu sub-výjimek.&lt;/li&gt;
&lt;li&gt;Fingerprint rules jsou primárně navržené pro grouping, ale &lt;code&gt;title=&lt;/code&gt; atribut z nich dělá jediný způsob, jak ovlivnit zobrazení issue v seznamu.&lt;/li&gt;
&lt;li&gt;Pattern &lt;strong&gt;"SDK připraví data jako tag, server je spotřebuje v pravidle"&lt;/strong&gt; je obecně použitelný i pro jiné scénáře, kde potřebujete runtime informaci v server-side konfiguraci.&lt;/li&gt;
&lt;/ul&gt;</content><link href="https://blog.diba.dev/cs/posts/sentry-exception-group-title"/><summary>&lt;p&gt;Jak proměnit titulek Sentry issue z &lt;code&gt;ExceptionGroup: Download failed&lt;/code&gt; na &lt;code&gt;DownloadError -&amp;gt; [HTTPDownloadError -&amp;gt; RuntimeError, HTTPStatusError]&lt;/code&gt;.&lt;/p&gt;</summary><published>2026-02-22T00:00:00+01:00</published></entry><entry><id>https://blog.diba.dev/cs/posts/unittests</id><title>Běžné projekty netrpí nedostatkem testů, ale dominancí unittestů</title><updated>2026-04-01T00:00:00+01:00</updated><content type="html">&lt;h1 id="bezne-projekty-netrpi-nedostatkem-testu-ale-dominanci-unittestu"&gt;Běžné projekty netrpí nedostatkem testů, ale dominancí unittestů&lt;/h1&gt;
&lt;p&gt;V mnoha projektech nejsou testy problém proto, že by chyběly. Problém je, že jich je hodně právě tam, kde chrání málo.&lt;/p&gt;
&lt;p&gt;Build padá, ale jeho pád je hlučný a málo užitečný. Build projde, ale nikdo mu úplně nevěří. Refaktor je stres, přestože „přece máme testy“. A když se člověk podívá dovnitř, zjistí, že většina té údajné ochrany jsou izolované unittesty, často dopsané až na hotový kód, často silně mockované, často motivované coverage.&lt;/p&gt;
&lt;p&gt;Tohle není nedostatek testů. Tohle je špatně složená testovací sada.&lt;/p&gt;
&lt;h2 id="dogma-ktere-zni-nevinne"&gt;Dogma, které zní nevinně&lt;/h2&gt;
&lt;p&gt;Spousta programátorů zná skoro jen unittesty. Když se řekne „dopiš testy“, téměř vždy se tím myslí „dopiš unittesty“. Když se řekne „kvalita“, často se tím myslí „coverage“. Když se řekne „správný test“, často se tím myslí „izolovaný test jedné třídy s mocky okolo“.&lt;/p&gt;
&lt;p&gt;Jenže tohle není technická pravda. To je kulturní zvyk.&lt;/p&gt;
&lt;p&gt;Unittesty jsou dominantní ne proto, že by vždy nejlépe chránily projekt. Jsou dominantní proto, že se dobře učí, dobře měří a dobře mechanicky vyrábějí. A právě proto jich bývá v projektech příliš mnoho vůči zbytku testovací sady.&lt;/p&gt;
&lt;p&gt;Největší problém běžných projektů není absolutní počet testů. Je to jejich poměr.&lt;/p&gt;
&lt;p&gt;Šest set unittestů a tři integrační testy není silná testovací sada. Je to známka toho, že tým testuje hlavně to, co se nejsnáze izoluje, ne to, co se nejsnáze rozbije.&lt;/p&gt;
&lt;h2 id="ano-unittesty-maji-sve-misto"&gt;Ano, unittesty mají své místo&lt;/h2&gt;
&lt;p&gt;Unit test je dobrý nástroj pro lokální pravidlo. Pro hraniční podmínku. Pro malou transformační logiku. Pro deterministický výpočet. Tam funguje dobře.&lt;/p&gt;
&lt;p&gt;Jenže takového kódu bývá v běžném projektu překvapivě málo.&lt;/p&gt;
&lt;p&gt;Typický backend netvoří hlavně elegantní izolovaná pravidla. Tvoří ho orchestrace, persistence, serializace, DTO, framework, glue code, side effecty a rozhraní mezi částmi systému. A právě tam vznikají chyby, které při změně bolí.&lt;/p&gt;
&lt;p&gt;Unit testy tedy nejsou zbytečné. Jen bývají přeceněné. Jsou dobré na úzký typ problému. Spousta projektů se ale tváří, jako by ten úzký problém byl celý systém.&lt;/p&gt;
&lt;h2 id="mentalni-experiment-jaky-projekt-bych-chtel-prevzit"&gt;Mentální experiment: jaký projekt bych chtěl převzít?&lt;/h2&gt;
&lt;p&gt;Představ si, že jako nově příchozí člen týmu přebíráš cizí projekt. Máš ho měnit, opravovat a refaktorovat.&lt;/p&gt;
&lt;p&gt;Co chceš najít?&lt;/p&gt;
&lt;p&gt;Nechceš najít stovky testů, které mockují vlastní vrstvy aplikace a dokazují, že controller zavolal service a service zavolala repository.&lt;/p&gt;
&lt;p&gt;Chceš najít projekt, kde červený build něco znamená.&lt;/p&gt;
&lt;p&gt;Chceš vidět testy, které hlídají, že:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;endpoint, service a repository si opravdu rozumí,&lt;/li&gt;
&lt;li&gt;důležitý flow projde od vstupu k výsledku,&lt;/li&gt;
&lt;li&gt;hranice mezi moduly drží kontrakt,&lt;/li&gt;
&lt;li&gt;historické bugy se nevracejí,&lt;/li&gt;
&lt;li&gt;nebezpečný refaktor rozbije testy tehdy, když rozbije systém, ne když jen přeskupí vnitřní volání.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A teď druhá část experimentu: jak takové projekty většinou vypadají doopravdy?&lt;/p&gt;
&lt;p&gt;Přesně opačně.&lt;/p&gt;
&lt;p&gt;Najdeš hodně unittestů. Často izolovaných. Často mock-heavy. Často dopsaných až po dokončení kódu. Málo integračních testů. Téměř žádné contract testy. Pár E2E scénářů, pokud vůbec. A build, kterému tým věří méně, než by odpovídalo množství testů.&lt;/p&gt;
&lt;p&gt;To není detail. To je jádro problému.&lt;/p&gt;
&lt;h2 id="v-projektech-se-vetsinou-nerozbije-izolovana-funkce"&gt;V projektech se většinou nerozbije izolovaná funkce&lt;/h2&gt;
&lt;p&gt;Nejnebezpečnější chyba při změně často nevypadá takto:&lt;/p&gt;
&lt;p&gt;„Napsal jsem špatný if.“&lt;/p&gt;
&lt;p&gt;Mnohem častěji vypadá takto:&lt;/p&gt;
&lt;p&gt;„Změnil jsem něco lokálně správně, ale okolí čekalo něco jiného.“&lt;/p&gt;
&lt;p&gt;Změní se shape dat. Jiný název pole. Jiný význam pole. &lt;code&gt;None&lt;/code&gt; místo výjimky. &lt;code&gt;Decimal&lt;/code&gt; místo integeru v centech. Jiný payload eventu. Jiný invariant. Jiná serializace. Jiná interpretace stavu.&lt;/p&gt;
&lt;p&gt;Lokálně je kód korektní. Systémově je rozbitý.&lt;/p&gt;
&lt;p&gt;To je důležitý rozdíl. Jedna chyba vzniká při psaní nové logiky. Druhá vzniká při změně systému, když jinak korektní kód naruší tiché předpoklady okolí. A právě ta druhá bývá v praxi nebezpečnější.&lt;/p&gt;
&lt;p&gt;Proto nestačí říct „máme unittesty“. Záleží, co vlastně chrání.&lt;/p&gt;
&lt;h2 id="mnoho-unittestu-ma-nejvetsi-hodnotu-v-den-sveho-vzniku"&gt;Mnoho unittestů má největší hodnotu v den svého vzniku&lt;/h2&gt;
&lt;p&gt;Tohle je nepříjemné, ale důležité přiznání.&lt;/p&gt;
&lt;p&gt;Mnoho unit testů je opravdu užitečných ve chvíli, kdy vznikají spolu s kódem. Autor si jimi ověřuje, že právě napsané lokální pravidlo funguje. V ten moment dávají smysl. Pomáhají při tvorbě.&lt;/p&gt;
&lt;p&gt;Jenže jejich dlouhodobá hodnota bývá přeceňovaná.&lt;/p&gt;
&lt;p&gt;Jakmile je kód hotový a projekt dál žije hlavně změnami na rozhraních, mnoho takových testů už nepřináší reálnou ochranu proti budoucím regresím. Zůstávají v projektu spíš jako pomníček tehdejší implementace. Archiv lokální snahy. Důkaz, že jsme kdysi chtěli být pečliví.&lt;/p&gt;
&lt;p&gt;Projekt pak nenese jejich přínos. Nese jejich údržbu.&lt;/p&gt;
&lt;p&gt;To neznamená, že všechny unit testy jsou po týdnu zbytečné. Znamená to, že jejich dlouhodobý přínos bývá systematicky nadhodnocený a jejich údržbový náklad podceňovaný.&lt;/p&gt;
&lt;h2 id="co-myslim-mock-heavy-testem"&gt;Co myslím mock-heavy testem&lt;/h2&gt;
&lt;p&gt;Mock-heavy test je test, který nahradí většinu vnitřních spolupracovníků aplikace mocky a pak hlavně ověřuje, kdo koho zavolal, s jakými argumenty a v jakém pořadí.&lt;/p&gt;
&lt;p&gt;Controller mockuje service. Service mockuje repository. Repository mockuje klienta. Test nakonec skončí sérií &lt;code&gt;assert_called_once_with(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Takový test často netestuje pozorovatelné chování systému. Testuje choreografii jeho vnitřních volání.&lt;/p&gt;
&lt;p&gt;A právě proto bývá křehký při refaktoru a slabý při chytání skutečných regresí.&lt;/p&gt;
&lt;h2 id="mock-heavy-testy-maji-casto-zapornou-hodnotu"&gt;Mock-heavy testy mají často zápornou hodnotu&lt;/h2&gt;
&lt;p&gt;Tohle je potřeba říct naplno.&lt;/p&gt;
&lt;p&gt;Mock-heavy testy často změny neulevňují, ale prodražují.&lt;/p&gt;
&lt;p&gt;Nejen že často nechytnou důležitou regresi. Ony navíc při změně samy vytvářejí práci. Neopravuješ chybu v systému. Opravuješ testy, které byly napsané na starou strukturu volání.&lt;/p&gt;
&lt;p&gt;Takový test často neříká:
„Systém přestal fungovat.“&lt;/p&gt;
&lt;p&gt;Říká spíš:
„Implementace už nevypadá tak, jak jsem si ji kdysi představoval.“&lt;/p&gt;
&lt;p&gt;To je jejich záporná hodnota pro projekt. Platíš jejich psaní. Platíš jejich údržbu. A přitom ti často neposkytují odpovídající ochranu tam, kde se systém opravdu láme.&lt;/p&gt;
&lt;p&gt;Největší slabina mock-heavy testů není jen to, že občas něco nechytnou. Je to to, že při změně samy vytvářejí práci.&lt;/p&gt;
&lt;h2 id="nemockujte-vnitrek-aplikace-mockujte-jeji-okoli"&gt;Nemockujte vnitřek aplikace. Mockujte její okolí.&lt;/h2&gt;
&lt;p&gt;Pokud má test opravdu chránit projekt při změně, neměl by co nejvíc odřezávat vnitřek aplikace. Měl by naopak nechat spolupracovat co nejvíc skutečných částí systému a mockovat až to, co leží za jeho hranou.&lt;/p&gt;
&lt;p&gt;Uvnitř aplikace chceme co nejvíc reality, ne co nejvíc izolace.&lt;/p&gt;
&lt;p&gt;Právě uvnitř aplikace totiž nejčastěji vznikají regresní chyby v návaznosti částí. Když tyto návaznosti nahradíme mocky, netestujeme systém. Testujeme představu o systému.&lt;/p&gt;
&lt;p&gt;Dobrými kandidáty na mock nebo náhradu jsou hlavně:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;externí HTTP API,&lt;/li&gt;
&lt;li&gt;email a SMS brány,&lt;/li&gt;
&lt;li&gt;čas, UUID, random,&lt;/li&gt;
&lt;li&gt;message broker mimo proces,&lt;/li&gt;
&lt;li&gt;filesystem nebo cloud storage,&lt;/li&gt;
&lt;li&gt;platby a jiné nevratné side effecty.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To jsou místa, kde izolace opravdu pomáhá. Ne proto, že „správný unittest musí vše mockovat“, ale proto, že tyto závislosti jsou mimo naši aplikaci, jsou drahé, pomalé, nedeterministické nebo nebezpečné.&lt;/p&gt;
&lt;h2 id="kdyz-uz-izolovat-casto-je-lepsi-fake-nez-mock"&gt;Když už izolovat, často je lepší fake než mock&lt;/h2&gt;
&lt;p&gt;Mock jen tvrdí, že se něco zavolalo. Fake se snaží chovat jako jednoduchá, ale skutečná implementace.&lt;/p&gt;
&lt;p&gt;Mock repository ti řekne, že někdo zavolal &lt;code&gt;save(order)&lt;/code&gt;. Fake repository ti umožní opravdu objednávku uložit do in-memory kolekce a pak ověřit výsledek.&lt;/p&gt;
&lt;p&gt;To je důležitý rozdíl.&lt;/p&gt;
&lt;p&gt;Mock hlídá volání. Fake hlídá chování.&lt;/p&gt;
&lt;p&gt;Právě proto bývá fake do budoucna cennější. Méně lže. Méně fixuje vnitřní call-flow. Častěji přežije refaktor. Když už tedy něco izolovat, je často lepší použít jednoduchou náhradní realitu než prázdnou atrapu.&lt;/p&gt;
&lt;h2 id="prestanme-resit-co-je-spravny-unittest"&gt;Přestaňme řešit, co je „správný unittest“&lt;/h2&gt;
&lt;p&gt;V mnoha týmech se zbytečně řeší, jestli test spadá do správné škatulky. Jestli je to ještě unit test, nebo už integrační test. Jestli je dost izolovaný. Jestli sahá na moc vrstev.&lt;/p&gt;
&lt;p&gt;Tohle není podstatná otázka.&lt;/p&gt;
&lt;p&gt;Podstatná otázka zní jinak: zachytí ten test důležitou regresi za rozumnou cenu?&lt;/p&gt;
&lt;p&gt;Test, který projde přes controller, service, mapper a repository, může být pro projekt výrazně cennější než deset čistých unittestů na jednotlivé třídy. I když tím porušuje učebnicová pravidla.&lt;/p&gt;
&lt;p&gt;Projekt nechrání ten nejčistší test. Projekt chrání ten test, který zachytí skutečný způsob poruchy.&lt;/p&gt;
&lt;h2 id="coverage-je-metrika-aktivity-ne-ochrany"&gt;Coverage je metrika aktivity, ne ochrany&lt;/h2&gt;
&lt;p&gt;Coverage se dobře ukazuje v dashboardu. Dobře se porovnává. Dobře se tlačí v code review. A právě proto je nebezpečná.&lt;/p&gt;
&lt;p&gt;Coverage neříká, že je projekt chráněný. Říká jen, že se kód nějak vykonal.&lt;/p&gt;
&lt;p&gt;Coverage neříká:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;že test hlídá správný kontrakt,&lt;/li&gt;
&lt;li&gt;že test dává důvěryhodný fail,&lt;/li&gt;
&lt;li&gt;že test chrání důležitou hranici systému,&lt;/li&gt;
&lt;li&gt;že build má skutečnou vypovídací hodnotu.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Proto můžeš mít projekt s vysokou coverage a nízkou důvěrou v testy. To není paradox. To je běžná realita.&lt;/p&gt;
&lt;p&gt;Když tým optimalizuje na coverage, začne snadno vyrábět testy, které se dobře počítají, ale málo chrání.&lt;/p&gt;
&lt;h2 id="lepsi-model-testy-podle-typu-poruchy"&gt;Lepší model: testy podle typu poruchy&lt;/h2&gt;
&lt;p&gt;Nemáme se ptát:
„Kde dopíšeme unittesty?“&lt;/p&gt;
&lt;p&gt;Máme se ptát:
„Jaký typ poruchy tato změna nejspíš zavede?“&lt;/p&gt;
&lt;p&gt;Když je riziko v lokálním pravidle, napiš unit test.&lt;/p&gt;
&lt;p&gt;Když je riziko v návaznosti vrstev, napiš integrační test.&lt;/p&gt;
&lt;p&gt;Když je riziko na hranici modulů nebo služeb, napiš contract test.&lt;/p&gt;
&lt;p&gt;Když je riziko v kritickém flow, napiš E2E nebo aspoň smoke test.&lt;/p&gt;
&lt;p&gt;Když opravuješ historický bug, přidej regresní test na místě, kde chyba skutečně vznikla.&lt;/p&gt;
&lt;p&gt;Test se nemá vybírat podle tradice. Má se vybírat podle místa, kde se to opravdu rozbije.&lt;/p&gt;
&lt;h2 id="co-bych-chtel-po-tymu-a-po-llm-agentech"&gt;Co bych chtěl po týmu a po LLM agentech&lt;/h2&gt;
&lt;p&gt;Nechci, aby automaticky dopisovali unittesty na hotový kód.&lt;/p&gt;
&lt;p&gt;Chci, aby se nejdřív ptali:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;kde je skutečné regresní riziko,&lt;/li&gt;
&lt;li&gt;co tady mock skryje,&lt;/li&gt;
&lt;li&gt;co má být v testu reálné,&lt;/li&gt;
&lt;li&gt;která nejnižší vrstva ten problém opravdu odhalí,&lt;/li&gt;
&lt;li&gt;a jestli ten test chrání systém, nebo jen aktuální implementaci.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nechci testy, které dokazují, že controller zavolal service.&lt;/p&gt;
&lt;p&gt;Chci testy, kterým budu věřit, když přepíšu půlku flow objednávky.&lt;/p&gt;
&lt;p&gt;To je úplně jiná motivace. A právě tu dnes v mnoha projektech postrádáme.&lt;/p&gt;
&lt;h2 id="zaver"&gt;Závěr&lt;/h2&gt;
&lt;p&gt;Unittesty nejsou špatné. Jejich dominance ano.&lt;/p&gt;
&lt;p&gt;Běžné projekty netrpí nedostatkem testů. Trpí tím, že mají testy ve špatném poměru. Příliš mnoho unittestů. Příliš mnoho mocků uvnitř systému. Příliš málo testů na hranách a návaznostech, kde se systém skutečně láme.&lt;/p&gt;
&lt;p&gt;Dokud budeme na tento typ poruchy odpovídat hlavně unittesty, budeme mít víc testů, víc coverage a méně skutečné jistoty.&lt;/p&gt;
&lt;p&gt;Silná testovací sada nevzniká tím, že má hodně testů. Vzniká tím, že má testy na správných místech.&lt;/p&gt;</content><link href="https://blog.diba.dev/cs/posts/unittests"/><summary>&lt;p&gt;Problém mnoha projektů nejsou chybějící testy, ale špatný poměr testovací sady — příliš unittestů, příliš mocků a málo testů na hranách, kde se systém skutečně láme.&lt;/p&gt;</summary><published>2026-04-01T00:00:00+01:00</published></entry><entry><id>https://blog.diba.dev/cs/posts/unittests-tdd</id><title>Když zelené testy nahradí dobrý návrh</title><updated>2026-04-01T00:00:00+01:00</updated><content type="html">&lt;h1 id="kdyz-zelene-testy-nahradi-dobry-navrh"&gt;Když zelené testy nahradí dobrý návrh&lt;/h1&gt;
&lt;p&gt;U TDD se často říká, že vede k lepšímu návrhu. Nejdřív test, pak implementace, nakonec zelený build. Zní to disciplinovaně. Čistě. Profesionálně.&lt;/p&gt;
&lt;p&gt;Jenže právě v tom je i jeho nechtěný efekt: TDD velmi snadno mění definici hotovo.&lt;/p&gt;
&lt;p&gt;Místo „kód je zjevně správný, čitelný a dobře navržený“ začne hotovo znamenat „testy jsou zelené“.&lt;/p&gt;
&lt;p&gt;A to není totéž.&lt;/p&gt;
&lt;h2 id="tdd-nevyrabi-jen-testy-vyrabi-i-urcity-zpusob-mysleni"&gt;TDD nevyrábí jen testy. Vyrábí i určitý způsob myšlení&lt;/h2&gt;
&lt;p&gt;Ten postup známe:&lt;/p&gt;
&lt;p&gt;Napíšu testy.
Snažím se, aby byly zelené.
Když jsou zelené, mám hotovo.
Když něco nesedí, upravím implementaci tak, aby to prošlo.&lt;/p&gt;
&lt;p&gt;V normální podobě to zní nevinně. V přehnané podobě už je ale dobře vidět, kam ten tlak míří: ne k co nejzřejměji správnému kódu, ale ke kódu, který je dostatečně podepřený testy, aby se tvářil funkčně.&lt;/p&gt;
&lt;p&gt;To není obvinění, že TDD vždy vede ke špatnému kódu. Je to upozornění, že &lt;strong&gt;optimalizační tlak&lt;/strong&gt; jde jinam, než se často tvrdí.&lt;/p&gt;
&lt;p&gt;TDD systematicky zvýhodňuje jednu otázku:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Co ještě musím upravit, aby testy prošly?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Jenže kvalitní návrh často začíná jinou otázkou:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Jak napsat kód tak, aby z něj bylo co nejvíc vidět, že je správný?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="dva-mentalni-poly"&gt;Dva mentální póly&lt;/h2&gt;
&lt;p&gt;Je užitečné si představit dva extrémy.&lt;/p&gt;
&lt;p&gt;První pól je tento:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hlavně ať jsou testy zelené. Kód doladím tak, aby vyhověl testovací sadě.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Druhý pól je tento:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Musím napsat tak dobrý kód, aby byl sám o sobě co nejčitelnější, nejjednodušší a co nejzřejměji správný.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;V čisté podobě se v praxi skoro nevyskytuje ani jeden. Ale právě proto jsou užitečné. Vymezují prostor, ve kterém se opravdu pohybujeme.&lt;/p&gt;
&lt;p&gt;A TDD nás často táhne směrem k prvnímu pólu.&lt;/p&gt;
&lt;p&gt;Ne proto, že by to bylo jeho deklarovaným cílem. Ale proto, že dává velmi silný signál hotovosti: build je zelený, tedy je hotovo.&lt;/p&gt;
&lt;h2 id="zelene-testy-nejsou-dukaz-dobreho-navrhu"&gt;Zelené testy nejsou důkaz dobrého návrhu&lt;/h2&gt;
&lt;p&gt;Tohle je podle mě klíčový bod.&lt;/p&gt;
&lt;p&gt;Zelené testy dokazují, že implementace uspokojila určitou testovací sadu. Nedokazují, že je návrh dobrý. Nedokazují, že je kód srozumitelný. Nedokazují, že je jednoduchý. Nedokazují, že budou budoucí změny levné.&lt;/p&gt;
&lt;p&gt;A přesto se v praxi tyto věci velmi často směšují.&lt;/p&gt;
&lt;p&gt;Testy prošly, takže je to správně.
Testy prošly, takže je to dobře navržené.
Testy prošly, takže můžeme přestat přemýšlet.&lt;/p&gt;
&lt;p&gt;Jenže právě tady vzniká falešný pocit hotovosti.&lt;/p&gt;
&lt;p&gt;Kód může být lokálně otestovaný a zároveň:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;strukturálně nepřehledný,&lt;/li&gt;
&lt;li&gt;plný pomocných vrstev jen kvůli testovatelnosti,&lt;/li&gt;
&lt;li&gt;rozlámaný na rozhraní, která doménově nedávají smysl,&lt;/li&gt;
&lt;li&gt;závislý na call-flow, které bude při příštím refaktoru překážet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Zelené testy a dobrý návrh spolu mohou souviset. Ale jedno není důkazem druhého.&lt;/p&gt;
&lt;h2 id="tdd-zvyhodnuje-to-co-se-dobre-testuje-jako-prvni"&gt;TDD zvýhodňuje to, co se dobře testuje jako první&lt;/h2&gt;
&lt;p&gt;A to je další problém.&lt;/p&gt;
&lt;p&gt;Nejlépe se test-first stylem píše malá, izolovaná logika. Funkce. Transformace. Výpočet. Validace. Tam TDD dává smysl a je fér mu to přiznat.&lt;/p&gt;
&lt;p&gt;Jenže běžný projekt se často nerozbíjí hlavně tam.&lt;/p&gt;
&lt;p&gt;Rozbíjí se:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;na rozhraních mezi vrstvami,&lt;/li&gt;
&lt;li&gt;ve změně shape dat,&lt;/li&gt;
&lt;li&gt;v implicitních očekáváních okolí,&lt;/li&gt;
&lt;li&gt;v kontraktech mezi částmi systému,&lt;/li&gt;
&lt;li&gt;v návaznostech, které lokální unit test nevidí.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A právě to se TDD test-first stylem chytá mnohem hůř.&lt;/p&gt;
&lt;p&gt;TDD tedy často nevyrábí testy, které nejlépe chrání projekt. Vyrábí testy, které se nejlépe píšou před implementací.&lt;/p&gt;
&lt;p&gt;To je zásadní rozdíl.&lt;/p&gt;
&lt;h2 id="a-potom-uz-mame-unittesty-hotove"&gt;A potom už máme unittesty hotové?&lt;/h2&gt;
&lt;p&gt;Tohle se často uvádí jako výhoda TDD.&lt;/p&gt;
&lt;p&gt;„Když píšeme testy předem, máme je pak automaticky hotové. Nemusíme je dopisovat.“&lt;/p&gt;
&lt;p&gt;Jenže to je výhoda jen tehdy, pokud právě tyto testy mají dlouhodobou hodnotu.&lt;/p&gt;
&lt;p&gt;A tady se vrací problém z předchozího článku: v běžných projektech bývá dlouhodobá hodnota mnoha unittestů systematicky přeceňovaná.&lt;/p&gt;
&lt;p&gt;Jinými slovy: TDD nám skutečně pomáhá rychle vyrobit unittesty. Jenže pokud je právě dominance unittestů součást problému, pak to není automaticky výhoda. Je to spíš efektivnější výroba toho, čeho už máme příliš mnoho ve špatném poměru vůči zbytku testovací sady.&lt;/p&gt;
&lt;p&gt;Získáváme tedy benefit, který v tom okamžiku už benefitem být nemusí.&lt;/p&gt;
&lt;p&gt;Ano, máme testy hotové.
Ale možná máme hotové hlavně ty testy, které projekt dlouhodobě chrání méně, než si myslíme.&lt;/p&gt;
&lt;h2 id="tdd-muze-deformovat-i-architekturu"&gt;TDD může deformovat i architekturu&lt;/h2&gt;
&lt;p&gt;Tohle bývá vidět méně, ale je to důležité.&lt;/p&gt;
&lt;p&gt;Když je hlavní tlak na to, aby šel kód pohodlně testovat po malých izolovaných jednotkách, začne se architektura přizpůsobovat této potřebě.&lt;/p&gt;
&lt;p&gt;Vznikají rozhraní, která nejsou doménově přirozená, ale dobře se mockují.
Vznikají vrstvy, jejichž hlavní hodnotou je, že jdou v testu oddělit.
Vznikají malé třídy a malé metody ne proto, že by to zlepšovalo model systému, ale proto, že se na ně dobře píše test-first.&lt;/p&gt;
&lt;p&gt;Výsledkem pak může být architektura optimalizovaná na testovatelnost, ne na srozumitelnost nebo robustnost.&lt;/p&gt;
&lt;p&gt;To je vysoká cena.&lt;/p&gt;
&lt;p&gt;Protože systém nežijeme jen při psaní testů. Žijeme s ním při údržbě, čtení, refaktoru a změnách.&lt;/p&gt;
&lt;h2 id="tdd-je-silne-jen-v-uzsim-prostoru-nez-se-tvrdi"&gt;TDD je silné. Jen v užším prostoru, než se tvrdí.&lt;/h2&gt;
&lt;p&gt;Aby to bylo fér: TDD není nesmysl.&lt;/p&gt;
&lt;p&gt;Je silné tam, kde člověk právě tvoří malou logiku a potřebuje rychlou zpětnou vazbu. Pro parser, výpočet, převod dat, hraniční podmínky nebo malé pravidlo to může být výborný nástroj.&lt;/p&gt;
&lt;p&gt;Tam opravdu pomáhá.&lt;/p&gt;
&lt;p&gt;Jenže z toho ještě neplyne, že je to dobrý obecný model pro návrh běžného projektu. Už vůbec z toho neplyne, že TDD automaticky produkuje lepší architekturu nebo lepší dlouhodobou ochranu systému.&lt;/p&gt;
&lt;p&gt;TDD dobře pomáhá s lokální správností.
To ale není totéž jako dobrý návrh.
A už vůbec to není totéž jako ochrana proti regresím v systémových návaznostech.&lt;/p&gt;
&lt;h2 id="co-je-praktictejsi-pristup"&gt;Co je praktičtější přístup&lt;/h2&gt;
&lt;p&gt;Nepotřebujeme další ideologii. Potřebujeme lepší otázky.&lt;/p&gt;
&lt;p&gt;Ne „jak to udělat test-first“.
Spíš „co se tady může rozbít a jak to nejlépe odhalíme“.&lt;/p&gt;
&lt;p&gt;Ne „už jsou testy zelené“.
Spíš „je ten kód sám o sobě přesvědčivý, čitelný a zjevně správný“.&lt;/p&gt;
&lt;p&gt;Ne „máme už unittesty hotové“.
Spíš „chrání ty testy opravdu to, co bude při příští změně bolet“.&lt;/p&gt;
&lt;p&gt;To znamená dvě věci najednou:&lt;/p&gt;
&lt;p&gt;První: netlačit všechno do TDD rytmu jen proto, že to vypadá disciplinovaně.
Druhá: neplést si průchod testy s kvalitou návrhu.&lt;/p&gt;
&lt;p&gt;Testy jsou nástroj. Ne důkaz elegance. Ne důkaz správné architektury. Ne důkaz, že už nemusíme přemýšlet.&lt;/p&gt;
&lt;h2 id="zaver"&gt;Závěr&lt;/h2&gt;
&lt;p&gt;TDD není problematické proto, že vede k testům. Je problematické tam, kde se průchod testy začne plést s důkazem dobrého návrhu.&lt;/p&gt;
&lt;p&gt;Jeho nechtěný efekt je jednoduchý: mění definici hotovo. Místo zjevně správného a dobře navrženého kódu začíná stačit kód, který uspokojí testovací sadu.&lt;/p&gt;
&lt;p&gt;A to je mnohem nižší laťka, než se tváří.&lt;/p&gt;
&lt;p&gt;Pokud má mít software dlouhý život, nestačí, aby byl otestovaný. Musí být také čitelný, přesvědčivý a konstrukčně poctivý. Ve chvíli, kdy zelené testy začnou tyto vlastnosti nahrazovat, přestává být TDD disciplínou návrhu a stává se disciplínou uspokojování testů.&lt;/p&gt;</content><link href="https://blog.diba.dev/cs/posts/unittests-tdd"/><summary>&lt;p&gt;TDD často mění definici hotovo — místo zjevně správného a dobře navrženého kódu začíná stačit kód, který uspokojí testovací sadu.&lt;/p&gt;</summary><published>2026-04-01T00:00:00+01:00</published></entry><entry><id>https://blog.diba.dev/cs/posts/unittests-comment</id><title>Komentář k draftu: Běžné projekty netrpí nedostatkem testů, ale dominancí unittestů</title><updated>2026-06-12T00:00:00+01:00</updated><content type="html">&lt;h1 id="komentar-k-draftu-o-dominanci-unittestu"&gt;Komentář k draftu o dominanci unittestů&lt;/h1&gt;
&lt;p&gt;Měl jsem příležitost konfrontovat draft s čerstvou zkušeností: vícedenní refaktor
extrakční pipeline, při kterém se pod ~28 stagemi vyměnil celý storage engine
(klíčované stores → append-only záznamy s meta adresováním), změnil se wire
kontrakt API a přepsala se vnitřní choreografie tří stagí. Testovací sada
~650 testů, z toho silná vrstva e2e testů s nahranými HTTP kazetami (VCR),
které porovnávají odchozí LLM requesty byte po bytu.&lt;/p&gt;
&lt;p&gt;Hlavní zpráva: &lt;strong&gt;žádná teze článku se v praxi nevyvrátila.&lt;/strong&gt; Skoro každá má
z toho refaktoru konkrétní inkarnaci. Zároveň praxe dala čtyři jemnější
rozlišení, která v textu chybí — a aspoň dvě z nich by myslím článek
znatelně posílila.&lt;/p&gt;
&lt;h2 id="co-refaktor-potvrdil"&gt;Co refaktor potvrdil&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;„Změnil jsem něco lokálně správně, ale okolí čekalo něco jiného."&lt;/strong&gt;
Učebnicový exemplář: wire vstupy se překlopily z dictu na list záznamů a kód
&lt;code&gt;has_image = "image" in cascade_inputs.strings&lt;/code&gt; zůstal. Na listu je to validní
Python, lokálně korektní — a tiše vždy False. Pipeline jen vynechala jednu
stage, nic nespadlo. Žádná jednotka se nechovala špatně, systém ano. Chytil to
přesně ten test, který článek doporučuje: e2e přes celý flow s nahranou
hranicí — kazeta odmítla request, ve kterém chyběly čtyři řádky promptu.
Je to mimochodem přesně položka „změní se shape dat" z tvého výčtu — kdyby
článek chtěl jeden konkrétní příběh místo abstraktního výčtu, tenhle typ bugu
je ideální kandidát.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;„Nemockujte vnitřek, mockujte okolí" + „fake &amp;gt; mock".&lt;/strong&gt; Sada měla typované
fakes jen na hranicích (LLM služba, vyhledávač) a reálné vnitřní wiring.
Výsledek za celý refaktor: na boundary fakes jsem nesáhl ani jednou a testy
s reálným vnitřkem přežily výměnu celého úložiště. Rozbilo se v podstatě jen
to, co znalo vnitřní strukturu.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Záporná hodnota testů znalých vnitřku.&lt;/strong&gt; ~113 míst ve 14 souborech sahalo
přímo na tvar úložiště (&lt;code&gt;ctx.vars.string_outputs[(stage, key)] = ...&lt;/code&gt;).
Při refaktoru se migrovala skriptem třikrát a nechytila přitom jedinou
reálnou regresi. Tvoje formulace „implementace už nevypadá tak, jak jsem si
ji představoval" je doslova to, co ty failury říkaly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Poměr sady.&lt;/strong&gt; Odhadem 90 % jistoty nesla hrstka kazetových e2e testů při
zlomku údržbových nákladů; bez nich by refaktor téhle velikosti byl hazard.
Projekt s opačným poměrem, než článek kritizuje, je zároveň důkaz, že
doporučení funguje.&lt;/p&gt;
&lt;h2 id="ctyri-mista-k-doostreni"&gt;Čtyři místa k doostření&lt;/h2&gt;
&lt;h3 id="1-coupling-se-schovava-i-ve-fixtures-nejen-v-assertech"&gt;1. Coupling se schovává i ve fixtures, nejen v assertech&lt;/h3&gt;
&lt;p&gt;Článek míří na mocky a &lt;code&gt;assert_called_once_with(...)&lt;/code&gt;. Naše největší
migrační zátěž ale měla &lt;strong&gt;behaviorální asserty a seed přes vnitřnosti&lt;/strong&gt;:
testy ověřovaly výsledek poctivě, jenže výchozí stav zakládaly přímými zápisy
do úložiště, obcházením veřejného povrchu. Při změně úložiště se rozbily
úplně stejně jako choreografické testy. „Seed-heavy" fixtures jsou tatáž
nemoc jiným kanálem — test může mít čisté asserty a přesto být přibetonovaný
k implementaci. Doporučil bych tomu věnovat odstavec vedle mock-heavy sekce:
&lt;em&gt;stav zakládej přes veřejné API (vstupní payload, veřejné operace), ne přes
strukturu skladu.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="2-rozlis-rozbiti-na-vnitrni-zmene-a-rozbiti-na-zmene-kontraktu"&gt;2. Rozliš „rozbití na vnitřní změně" a „rozbití na změně kontraktu"&lt;/h3&gt;
&lt;p&gt;Při refaktoru spadlo dohromady ~350 testů. Z článku by čtenář mohl vyvodit
„špatná sada". Jenže velká část byly &lt;strong&gt;contract testy padající na záměrném
breaking change&lt;/strong&gt; — testovaly wire tvar, který jsme měnili. To není
křehkost; to je test dělající přesně svou práci. Užitečný indikátor z praxe:
tyhle failury šly migrovat mechanicky skriptem, protože konzistentně
testovaly tvar, který se konzistentně změnil. Křehké testy se poznají podle
toho, že padají, &lt;em&gt;aniž se změnil kontrakt&lt;/em&gt;. Věta „rozbije testy tehdy, když
rozbije systém" to implikuje, ale explicitní rozlišení by čtenáře ochránilo
před závěrem „testy, co se při refaktoru rozbily = špatné testy".&lt;/p&gt;
&lt;h3 id="3-seda-zona-choreografie-ktera-je-produkt"&gt;3. Šedá zóna: choreografie, která JE produkt&lt;/h3&gt;
&lt;p&gt;Měli jsme testy assertující tvar access-event logu (která operace zapíše
Read, která Write, která nic). Podle taxonomie článku čistá choreografie →
záporná hodnota. Jenže ten log je pozorovatelný kontrakt — konzumuje ho
debugovací UI. Takže to jsou legitimní contract testy… a přesto se při
každém vnitřním pivotu přepisovaly. Závěr z praxe: i legitimní
choreografický kontrakt je drahý závazek a má se vědomě držet malý. Binární
dělení „chování dobré / choreografie špatná" tenhle případ nezachytí —
stačila by věta, že choreografie povýšená na produkt se testuje jako
kontrakt, ale platí se jako choreografie.&lt;/p&gt;
&lt;h3 id="4-nika-unittestu-jde-rict-pozitivneji-jednotky-s-vlastnim-kontraktem"&gt;4. Nika unittestů jde říct pozitivněji: jednotky s vlastním kontraktem&lt;/h3&gt;
&lt;p&gt;Sekce „hodnota v den vzniku" platí pro unittesty &lt;em&gt;implementace&lt;/em&gt;. Při
refaktoru ale vznikl klasický unittestový soubor na nový storage engine
(match sémantika, last-write-wins, diagnostika chyb — deterministická logika
bez závislostí): byl nepostradatelný při stavbě a pak přežil šest navazujících
fází beze změny jediného řádku. Rozdíl není v izolovanosti, ale v tom, že ta
jednotka &lt;strong&gt;má vlastní dokumentovatelný kontrakt&lt;/strong&gt;. To je prakticky použitelné
kritérium, které by sekci „Ano, unittesty mají své místo" dalo ostřejší
hranu: &lt;em&gt;pokud bys sémantiku jednotky dokázal sepsat jako dokumentaci pro
cizího čtenáře, unittest na ni stárne dobře; pokud testuješ „co zrovna dělá
můj kód", stárne jako pomníček.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="drobnost-na-zaver"&gt;Drobnost na závěr&lt;/h2&gt;
&lt;p&gt;Mentální experiment „jaký projekt bych chtěl převzít" je nejsilnější část
textu — a dal by se uzavřít zpětným odkazem z praxe: projekt s tím správným
poměrem se pozná podle toho, že velký refaktor je &lt;em&gt;nudný&lt;/em&gt;. Failury jsou buď
očekávané (kontrakt se měnil) nebo informativní (systém se rozbil), a ta
jedna záludná regrese, kterou by nikdo nenapsal do unit testu, se chytí na
hraně. Přesně tak to proběhlo.&lt;/p&gt;</content><link href="https://blog.diba.dev/cs/posts/unittests-comment"/><summary>&lt;p&gt;Recenzní komentář k nevydanému článku — konfrontace tezí s čerstvým velkým refaktorem a čtyři místa, kde by šel text doostřit.&lt;/p&gt;</summary><published>2026-06-12T00:00:00+01:00</published></entry><entry><id>https://blog.diba.dev/cs/posts/thread-dump-sigusr1</id><title>Thread dump zaseknutého procesu přes signál SIGUSR1</title><updated>2026-06-20T00:00:00+01:00</updated><content type="html">&lt;p&gt;Když se produkční proces zasekne, nechcete ho restartovat ani připojovat debugger. Stačí mu poslat SIGUSR1 a nechat ho vysypat stacky všech vláken i asyncio tasků do souboru.&lt;/p&gt;
&lt;h2 id="problem"&gt;Problém&lt;/h2&gt;
&lt;p&gt;Worker v kontejneru přestane brát práci. CPU na nule, nic se neděje, žádná chyba v logu. Deadlock? Zaseknuté &lt;code&gt;await&lt;/code&gt;? Čeká se na lock, který nikdo nepustí?&lt;/p&gt;
&lt;p&gt;Restartem problém zameteete pod koberec a stav zmizí. Připojit &lt;code&gt;gdb&lt;/code&gt; nebo &lt;code&gt;py-spy&lt;/code&gt; do produkčního kontejneru často nejde — chybí nástroje, práva, nebo prostě nechcete sahat na běžící proces zvenčí. A standardní logy vám neřeknou, kde přesně proces stojí.&lt;/p&gt;
&lt;p&gt;Chcete jednu věc: &lt;strong&gt;přimět běžící proces, aby sám řekl, co právě dělá každé jeho vlákno&lt;/strong&gt; — bez restartu, bez externího nástroje.&lt;/p&gt;
&lt;h2 id="reseni-signal-handler-na-sigusr1"&gt;Řešení: signal handler na SIGUSR1&lt;/h2&gt;
&lt;p&gt;Unixové procesy umí reagovat na signály. &lt;code&gt;SIGUSR1&lt;/code&gt; (a &lt;code&gt;SIGUSR2&lt;/code&gt;) jsou k tomu přímo určené — systém je nijak nevyužívá, jsou „k volnému použití". Zaregistrujeme si na něj handler, který vysype thread dump:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;signal&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;init_stack_dumps&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGUSR1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dump_stacks_sig_handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Tohle se zavolá jednou při startu aplikace. Od té chvíle stačí procesu poslat signál a on vyplivne dump.&lt;/p&gt;
&lt;h2 id="proc-handler-spousti-nove-vlakno"&gt;Proč handler spouští nové vlákno&lt;/h2&gt;
&lt;p&gt;Signal handler v Pythonu má přísná omezení — běží mezi bajtkódy hlavního vlákna a neměl by dělat nic složitého. Proto handler sám jen &lt;strong&gt;odpálí daemon vlákno&lt;/strong&gt; a hned se vrátí:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;threading&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;dump_stacks_sig_handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dump_stacks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daemon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Vlastní práci pak dělá &lt;code&gt;dump_stacks()&lt;/code&gt; v klidu mimo handler. &lt;code&gt;daemon=True&lt;/code&gt; zajistí, že dumpovací vlákno nebude bránit ukončení procesu.&lt;/p&gt;
&lt;h2 id="sber-stacku"&gt;Sběr stacků&lt;/h2&gt;
&lt;p&gt;Jádro je &lt;code&gt;sys._current_frames()&lt;/code&gt; — vrátí aktuální frame každého běžícího vlákna. To projdeme a u každého vypíšeme formátovaný stack:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;os&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;threading&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;dump_stacks&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getpid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/tmp/python_thread_dump_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.log&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;w&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Thread dump for PID &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Parent PID &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getppid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Process breadcrumbs: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;, &amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;process_breadcrumbs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_current_frames&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;thread_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_thread&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
            &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Thread &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;thread_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;):&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writelines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;format_stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Dump jde do &lt;code&gt;/tmp/python_thread_dump_&amp;lt;pid&amp;gt;.log&lt;/code&gt; — soubor pojmenovaný podle PID, takže při více procesech (gunicorn, multiprocessing) se dumpy nepřepíšou navzájem.&lt;/p&gt;
&lt;h2 id="asyncio-tasky-ne-jen-vlakna"&gt;Asyncio tasky, ne jen vlákna&lt;/h2&gt;
&lt;p&gt;V async aplikaci &lt;code&gt;sys._current_frames()&lt;/code&gt; nestačí — uvidíte jen event loop vlákno, ale ne jednotlivé pozastavené coroutiny. Ty visí jako &lt;code&gt;Task&lt;/code&gt; objekty na loopu a mají vlastní stack:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_running_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;--- asyncio tasks ---&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all_tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Task: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_coro&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Coro: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_coro&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_stack&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_stack&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writelines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;format_stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;No running asyncio loop detected.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;asyncio.all_tasks()&lt;/code&gt; vyjmenuje všechny živé tasky a &lt;code&gt;task.get_stack()&lt;/code&gt; ukáže, na kterém &lt;code&gt;await&lt;/code&gt; přesně každý stojí. Tohle je přesně to, co u zaseknuté async aplikace potřebujete vidět — který task čeká na čem. &lt;code&gt;RuntimeError&lt;/code&gt; ošetřuje případ, kdy proces žádný loop neběží (čistě synchronní worker).&lt;/p&gt;
&lt;h2 id="vlastni-kod-zvyrazneny-hvezdickou"&gt;Vlastní kód zvýrazněný hvězdičkou&lt;/h2&gt;
&lt;p&gt;Stack zaseknutého procesu je dlouhý a většina řádků je šum z knihoven a stdlib. Trik: řádky, které vedou do vlastního kódu aplikace, dostanou prefix &lt;code&gt;*&lt;/code&gt;:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;format_stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;traceback&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format_stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;formatted_stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;  File &amp;quot;&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;app_root&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;formatted_stack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;* &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;formatted_stack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;formatted_stack&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;app_root&lt;/code&gt; je kořen aplikace (odvozený z &lt;code&gt;__file__&lt;/code&gt;). V dumpu pak stačí grepnout &lt;code&gt;^\*&lt;/code&gt; a vidíte jen své vlastní rámce — místo, kde se to ve vašem kódu zaseklo, bez prokousávání se stdlib.&lt;/p&gt;
&lt;p&gt;K tomu se na začátku dumpu vypíšou &lt;strong&gt;breadcrumbs&lt;/strong&gt; — krátká stopa posledních událostí, kterou si aplikace průběžně udržuje. Často hned napoví, co proces dělal těsně před zaseknutím.&lt;/p&gt;
&lt;h2 id="pouziti"&gt;Použití&lt;/h2&gt;
&lt;p&gt;Návod je ideální nechat rovnou v komentáři u zdrojáku. Přes Docker Compose to vypadá takhle:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# uklidit staré dumpy&lt;/span&gt;
docker&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;service-process&lt;span class="w"&gt; &lt;/span&gt;sh&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;rm /tmp/python_thread_dump_*&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# poslat signál všem python procesům v kontejneru&lt;/span&gt;
docker&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;service-process&lt;span class="w"&gt; &lt;/span&gt;pkill&lt;span class="w"&gt; &lt;/span&gt;-SIGUSR1&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;python

&lt;span class="c1"&gt;# vytáhnout jen podstatné řádky — hlavičky a vlastní kód&lt;/span&gt;
docker&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;service-process&lt;span class="w"&gt; &lt;/span&gt;sh&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;grep -e &amp;#39;^Thread&amp;#39; -e &amp;#39;^\*&amp;#39; -e &amp;#39;^Process breadcrumbs&amp;#39; /tmp/python_thread_dump_*&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;pkill -SIGUSR1 -f python&lt;/code&gt; zasáhne všechny matchnuté procesy najednou — každý si zapíše vlastní soubor podle PID, takže u multiprocess setupu dostanete kompletní obrázek jedním příkazem.&lt;/p&gt;
&lt;h2 id="drobnost-vnorene-uvozovky-v-f-stringu"&gt;Drobnost: vnořené uvozovky v f-stringu&lt;/h2&gt;
&lt;p&gt;Řádek s breadcrumbs používá uvozovky uvnitř f-stringu i kolem něj:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Process breadcrumbs: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;, &amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;process_breadcrumbs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;", ".join(...)&lt;/code&gt; se stejnými uvozovkami jako celý f-string by před Pythonem 3.12 byla syntaktická chyba. Od &lt;a href="https://peps.python.org/pep-0701/"&gt;PEP 701&lt;/a&gt; (Python 3.12) to projde — f-stringy už používají plnohodnotný tokenizer a vnořování uvozovek je povolené.&lt;/p&gt;
&lt;h2 id="obecnejsi-pozorovani"&gt;Obecnější pozorování&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SIGUSR1&lt;/code&gt;/&lt;code&gt;SIGUSR2&lt;/code&gt; jsou zdarma k dispozici&lt;/strong&gt; pro vlastní ovládání běžícího procesu. Thread dump je nejtypičtější použití, ale stejně tak jde signálem přepnout log level nebo vyžádat metriky.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signal handler musí být minimální&lt;/strong&gt; — odpálit vlákno a vrátit se. Veškerou logiku dělejte mimo handler.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;sys._current_frames()&lt;/code&gt; + &lt;code&gt;asyncio.all_tasks()&lt;/code&gt; se doplňují.&lt;/strong&gt; Jen vlákna vám u async aplikace ukážou prázdný event loop; teprve tasky řeknou, kde coroutiny stojí.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Soubor podle PID&lt;/strong&gt; je nutnost u multiprocess služeb — jinak si procesy dumpy přepíšou.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefix vlastního kódu&lt;/strong&gt; mění nepoužitelně dlouhý dump v něco, co přečtete jedním grepem. Drobnost s velkým dopadem.&lt;/li&gt;
&lt;/ul&gt;</content><link href="https://blog.diba.dev/cs/posts/thread-dump-sigusr1"/><summary>&lt;p&gt;Když se produkční proces zasekne, nechcete ho restartovat ani připojovat debugger. Stačí mu poslat SIGUSR1 a nechat ho vysypat stacky všech vláken i asyncio tasků do souboru.&lt;/p&gt;</summary><published>2026-06-20T00:00:00+01:00</published></entry><entry><id>https://blog.diba.dev/cs/posts/log-markery-delayed-logmarker</id><title>Korelační log markery napříč async kontextem (a DelayedLogmarker)</title><updated>2026-06-20T00:00:00+01:00</updated><content type="html">&lt;p&gt;Jak nad standardním &lt;code&gt;logging&lt;/code&gt; postavit korelační ID, která se sama propíšou do všech vnořených logů přes &lt;code&gt;ContextVar&lt;/code&gt;, hashují citlivé hodnoty — a jak bufferovat logy, dokud ještě neznáte ID tasku.&lt;/p&gt;
&lt;h2 id="problem"&gt;Problém&lt;/h2&gt;
&lt;p&gt;V asynchronní aplikaci běží desítky tasků naráz a jejich logy se v jednom streamu prolínají. Bez korelačního ID nepoznáte, které řádky patří k sobě:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;level=INFO message=Task started.
level=INFO message=Downloading source...
level=INFO message=Task started.
level=ERROR message=Download failed.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Které „started" patří k tomu „failed"? Klasické řešení je protáhnout &lt;code&gt;task_id&lt;/code&gt; parametrem skrz každou funkci, která loguje. To je otrava a stejně to nevydrží — jakmile log vznikne o tři vrstvy hlouběji v knihovně, kontext je pryč.&lt;/p&gt;
&lt;p&gt;K tomu dva přidružené požadavky:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Citlivá ID nechci v logu plaintextem&lt;/strong&gt; (userId, e-mail) — ale chci je dohledat. Potřebuju stabilní pseudonym, ne čitelnou hodnotu.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Korelační ID někdy ještě neznám&lt;/strong&gt;, když chci začít logovat. Na startu tasku zaloguju „task started", ale samotné ID tasku se přiřadí až o pár řádků níž.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Všechno tohle jde vyřešit bez externí knihovny (structlog, loguru) — stačí &lt;code&gt;contextvars&lt;/code&gt; a vlastní &lt;code&gt;logging.Filter&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="markery-ktere-se-nesou-samy"&gt;Markery, které se nesou samy&lt;/h2&gt;
&lt;p&gt;Jádro je &lt;code&gt;ContextVar&lt;/code&gt; se slovníkem markerů a context manager, který do něj přidává:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;contextvars&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ContextVar&lt;/span&gt;

&lt;span class="n"&gt;markers_var&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;markers&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;

&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;logmarker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;old_markers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;markers_var&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;new_markers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashed_markers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;markers_var&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;old_markers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;new_markers&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;markers_var&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;ContextVar&lt;/code&gt; je asyncio-aware — hodnota se izoluje per-task a automaticky se nese skrz &lt;code&gt;await&lt;/code&gt;. Vnořené &lt;code&gt;logmarker()&lt;/code&gt; bloky se slévají, takže markery z vnějšího kontextu zůstávají dostupné uvnitř:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;logmarker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;extract&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Task started.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# markers=[process=... task=...]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;logmarker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Downloading...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# markers=[process=... task=... source=...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="hashovani-citlivych-hodnot"&gt;Hashování citlivých hodnot&lt;/h2&gt;
&lt;p&gt;Hodnoty markerů se neukládají syrově. Projdou přes SHA256 → base32 → prvních 8 znaků:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;short_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stable_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;utf-8&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;b32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b32encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ascii&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;=&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;b32&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;hashed_markers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;short_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;_&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Hash je &lt;strong&gt;stabilní&lt;/strong&gt; — stejný &lt;code&gt;userId&lt;/code&gt; dá vždycky stejný 8znakový kód, takže napříč logy (i napříč službami) půjde grepnout, ale samotnou hodnotu z něj nikdo nezíská.&lt;/p&gt;
&lt;p&gt;Konvence: klíče začínající podtržítkem se &lt;strong&gt;nehashují&lt;/strong&gt;. Slouží pro meta/vizuální pole, která nejsou citlivá (&lt;code&gt;_cls_name&lt;/code&gt;, &lt;code&gt;_visual&lt;/code&gt;) — tam chcete čitelnou hodnotu.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;stable_string&lt;/code&gt; ještě řeší serializaci čehokoliv: nejdřív zkusí &lt;code&gt;json.dumps(sort_keys=True)&lt;/code&gt;, a když to neprojde, spadne na &lt;code&gt;pickle&lt;/code&gt; + base85. Tím se hashování nikdy nerozbije na neserializovatelném vstupu.&lt;/p&gt;
&lt;h2 id="filtr-ktery-markery-vstrikne-do-kazdeho-logu"&gt;Filtr, který markery vstříkne do každého logu&lt;/h2&gt;
&lt;p&gt;Markery se do log záznamu dostanou přes vlastní &lt;code&gt;logging.Filter&lt;/code&gt;, který sedí na handleru. Filtr ze slovníku poskládá &lt;code&gt;marker_str&lt;/code&gt; a přilepí ho na &lt;code&gt;record&lt;/code&gt;:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;MarkerFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;markers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;markers_var&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;marker_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;_&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;markers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;markers&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Formatter pak &lt;code&gt;marker_str&lt;/code&gt; jen vypíše:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;HumanReadableFormatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Formatter&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;markers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;marker_str&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;timestamp=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;formatTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;quot;&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;logger=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; level=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;levelname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;quot;&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;markers=[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;markers&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] message=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Důležité je, že se &lt;code&gt;MarkerFilter&lt;/code&gt; registruje na root handleru — funguje tedy i pro logy z knihoven třetích stran, aniž by o markerech cokoliv věděly.&lt;/p&gt;
&lt;h2 id="delayedlogmarker-bufferovani-nez-znate-id"&gt;DelayedLogmarker — bufferování, než znáte ID&lt;/h2&gt;
&lt;p&gt;Teď to zajímavé. Co když chcete logovat &lt;strong&gt;dřív, než znáte marker&lt;/strong&gt;? Typicky na startu tasku: chcete zalogovat průběh inicializace, ale ID tasku se ustaví až někde v jejím průběhu.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DelayedLogmarker&lt;/code&gt; to řeší tak, že logy během bloku nezahazuje ani nevypisuje — &lt;strong&gt;pozdrží je&lt;/strong&gt;, a vypíše je zpětně, jakmile markery doplníte. Funguje to přes druhý &lt;code&gt;ContextVar&lt;/code&gt;, který drží buffer:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;buffer_var&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;buffer_var&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A &lt;code&gt;MarkerFilter&lt;/code&gt; má kvůli tomu druhou půlku, kterou jsem výš vynechal:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;markers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;markers_var&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;marker_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# jako výše&lt;/span&gt;

        &lt;span class="n"&gt;buffer_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buffer_var&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;buffer_list&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;                  &lt;span class="c1"&gt;# běžný režim — log projde&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;buffer_list&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;                 &lt;span class="c1"&gt;# buffer režim — log se pozdrží&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Klíčový trik: v buffer režimu filtr vrátí &lt;code&gt;False&lt;/code&gt; (log se hned nevypíše) a místo toho si uloží &lt;strong&gt;closure&lt;/strong&gt; &lt;code&gt;record.handle&lt;/code&gt; do bufferu. Záznam je zachycený celý — až ho později pustíte, projde znovu celou cestou formatterem, takže dostane i markery, které v té chvíli ještě neexistovaly.&lt;/p&gt;
&lt;p&gt;Použití:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;DelayedLogmarker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;extract&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;delayed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Initializing task...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# pozdrženo&lt;/span&gt;
    &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;delayed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_markers_and_flush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ↑ teď se &amp;quot;Initializing task...&amp;quot; vypíše — už s task=&amp;lt;hash&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Task ready.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;# vypíše se rovnou&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Když &lt;code&gt;set_markers_and_flush&lt;/code&gt; nezavoláte, dotáhne ho &lt;code&gt;__exit__&lt;/code&gt; automaticky — buffer se vždycky vyprázdní a žádný log se neztratí. Opakované volání naopak hlídá a vyhodí &lt;code&gt;RuntimeError&lt;/code&gt;, aby nedošlo k dvojímu flushnutí.&lt;/p&gt;
&lt;h2 id="napojeni-na-sentry-zadarmo"&gt;Napojení na Sentry zadarmo&lt;/h2&gt;
&lt;p&gt;Protože markery žijí v &lt;code&gt;ContextVar&lt;/code&gt;, jdou přečíst odkudkoliv — třeba v Sentry &lt;code&gt;before_send&lt;/code&gt; hooku, který je promítne jako tagy:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_sentry_logmarkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;markers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;logmarker_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;markers_var&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tags&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;V Sentry pak filtrujete chyby podle &lt;code&gt;logmarker_task&lt;/code&gt; stejným hashem, jaký vidíte v logu. Korelace mezi logem a chybou je tím hotová bez jediného řádku navíc v aplikačním kódu.&lt;/p&gt;
&lt;h2 id="obecnejsi-pozorovani"&gt;Obecnější pozorování&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ContextVar&lt;/code&gt; je správný nástroj pro per-task kontext v asyncio.&lt;/strong&gt; Thread-local by selhal — coroutiny sdílejí vlákno. &lt;code&gt;ContextVar&lt;/code&gt; se izoluje per-task a nese se skrz &lt;code&gt;await&lt;/code&gt; automaticky.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Registrace filtru na root handleru&lt;/strong&gt; dává markery i logům z knihoven, které o systému nevědí. Žádné protahování parametrů.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bufferování přes uloženou closure &lt;code&gt;record.handle&lt;/code&gt;&lt;/strong&gt; je elegantní: nemusíte řešit re-formátování, stačí pozdržet volání. Záznam si pamatuje všechno sám.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stabilní hash jako pseudonym&lt;/strong&gt; je levný kompromis mezi „nelogovat citlivé hodnoty vůbec" a „logovat je plaintextem". Dohledatelnost zůstává, hodnota mizí.&lt;/li&gt;
&lt;li&gt;Cena za eleganci je &lt;strong&gt;dvojí význam jednoho filtru&lt;/strong&gt; — &lt;code&gt;MarkerFilter&lt;/code&gt; zároveň vstřikuje markery i řídí bufferování. Bez znalosti &lt;code&gt;DelayedLogmarker&lt;/code&gt; ten &lt;code&gt;return False&lt;/code&gt; v &lt;code&gt;filter()&lt;/code&gt; vypadá jako bug.&lt;/li&gt;
&lt;/ul&gt;</content><link href="https://blog.diba.dev/cs/posts/log-markery-delayed-logmarker"/><summary>&lt;p&gt;Jak nad standardním &lt;code&gt;logging&lt;/code&gt; postavit korelační ID, která se sama propíšou do všech vnořených logů přes &lt;code&gt;ContextVar&lt;/code&gt;, hashují citlivé hodnoty — a jak bufferovat logy, dokud ještě neznáte ID tasku.&lt;/p&gt;</summary><published>2026-06-20T00:00:00+01:00</published></entry></feed>