<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Incident-Detection on DevOps Way - Практические гайды</title>
    <link>https://devopsway.ru/tags/incident-detection/</link>
    <description>Recent content in Incident-Detection on DevOps Way - Практические гайды</description>
    <image>
      <title>DevOps Way - Практические гайды</title>
      <url>https://devopsway.ru/images/devopsway-og.png</url>
      <link>https://devopsway.ru/images/devopsway-og.png</link>
    </image>
    <generator>Hugo -- 0.163.3</generator>
    <language>ru</language>
    <lastBuildDate>Sat, 20 Jun 2026 11:08:30 -0400</lastBuildDate>
    <atom:link href="https://devopsway.ru/tags/incident-detection/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Ошибка, которой нет в логах: как не изобрести велосипед с LLM-мотором на дежурстве</title>
      <link>https://devopsway.ru/posts/incident-detector-error-registration/</link>
      <pubDate>Fri, 19 Jun 2026 12:00:00 +0300</pubDate>
      <guid>https://devopsway.ru/posts/incident-detector-error-registration/</guid>
      <description>Разбор инцидента, где система не записала причину отказа, и как из этого вырастает детектор инцидентов: регистрация ошибок с контекстом, детерминированное ядро, dead-man switch и LLM сбоку.</description>
      <content:encoded><![CDATA[<h2 id="сказ-о-12-мегапикселях-и-потерянном-времени">Сказ о 12 мегапикселях и потерянном времени</h2>
<p>Пользователь Татьяна пытается скормить нашему AI-сервису фото со своего айфона. Картинка обычная: весит около 5 МБ, разрешение – 12 мегапикселей. Татьяна жмет кнопку &ldquo;Сгенерировать&rdquo; и видит лаконичную заглушку: &ldquo;Что-то пошло не так&rdquo;. Вторая попытка с другим стилем дает тот же результат.</p>
<p>Естественно, у девелоперов на локалках и тестовых стендах всё летает. Баг воспроизводится только у Татьяны и только на её конкретном файле. Мы открываем логи в надежде сразу всё починить, но там нас встречает классическая заглушка:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">resp</span> <span class="o">=</span> <span class="n">provider</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span><span class="n">image_bytes</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">resp</span><span class="o">.</span><span class="n">raise_for_status</span><span class="p">()</span>   <span class="c1"># 400, Bad Request, лови стектрейс, развлекайся</span>
</span></span></code></pre></div><p>Метод <code>raise_for_status()</code> честно упал, завалив лог стандартным трейсом. Но само тело ответа от внешнего провайдера, где человеческим языком была написана причина, никто не догадался сохранить. Мы знаем, <em>что</em> упало, но понятия не имеем, <em>почему</em>.</p>
<p>В итоге пришлось заниматься ручным саппортом: выпрашивать у Татьяны исходный файл через мессенджеры и ковырять его руками. Оказалось, картинка – 4032×3024, честные 12.2 Мп, упакованные в контейнер MPO. И наш замечательный провайдер режет входящие файлы не по весу в байтах (тут мы укладывались), а по габаритам. Лимит эндпоинта – ровно 12 Мп, а у Татьяны чуть больше.</p>
<p>Два часа высокооплачиваемого инженерного времени ушли на то, что автоматика должна была выплюнуть в одну строку:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">resp</span> <span class="o">=</span> <span class="n">provider</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span><span class="n">image_bytes</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="ow">not</span> <span class="n">resp</span><span class="o">.</span><span class="n">ok</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">log</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="s2">&#34;provider rejected image&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">status</span><span class="o">=</span><span class="n">resp</span><span class="o">.</span><span class="n">status_code</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">body</span><span class="o">=</span><span class="n">resp</span><span class="o">.</span><span class="n">text</span><span class="p">[:</span><span class="mi">2000</span><span class="p">],</span>          <span class="c1"># Сэкономило бы два часа жизни</span>
</span></span><span class="line"><span class="cl">              <span class="n">px</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">w</span><span class="si">}</span><span class="s2">x</span><span class="si">{</span><span class="n">h</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">mp</span><span class="o">=</span><span class="nb">round</span><span class="p">(</span><span class="n">w</span><span class="o">*</span><span class="n">h</span><span class="o">/</span><span class="mf">1e6</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">              <span class="nb">bytes</span><span class="o">=</span><span class="nb">len</span><span class="p">(</span><span class="n">image_bytes</span><span class="p">),</span> <span class="n">user_id</span><span class="o">=</span><span class="n">user_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">resp</span><span class="o">.</span><span class="n">raise_for_status</span><span class="p">()</span>
</span></span></code></pre></div><p>Вменяемое логирование ошибок – это не просто умение обмазать код конструкциями <code>try/except</code> или вывалить сырой трейс в Kibana. Это сбор контекста. Если вы не записали, что пришло на вход, что ответил бэкенд и под каким <code>user_id</code> это произошло, ваш лог – просто цифровой мусор.</p>
<p>Без этого контекста построить адекватный мониторинг инцидентов невозможно. Системе на вход нужны конкретные факты: &ldquo;упало, потому что превышен лимит в 12 Мп у юзера 458&rdquo;. И раз уж мы заговорили об автоматизации этого процесса, давайте разберем модный тренд – попытку запихнуть LLM в детекторы инцидентов.</p>
<hr>
<h2 id="почему-llm-не-стоит-ставить-в-цепочку-оповещения">Почему LLM не стоит ставить в цепочку оповещения</h2>
<p>Откуда вообще в статье про регистрацию ошибок взялись нейросети? А вот откуда. Разбирая кейс Татьяны, мы пошли смотреть, как разбор логов автоматизируют другие, и нашли на Хабре показательный пример: команда взяла локальную LLM и прикрутила её прямо к мониторингу – модель сама читает логи и решает, что критично, а что нет. Звучит модно, по факту – попытка скрестить ужа с ежом: детерминированный алертинг и недетерминированную генерацию в одном флаконе. Раз уж такие схемы поехали в продакшен и в корпоративные блоги, разберём по косточкам, почему в цепочке оповещения это не работает.</p>
<p>Сам рецепт обычно выглядит так: грепаем <code>journalctl -f</code>, скармливаем отфильтрованные строки локальной языковой модели, а она пускай вешает теги <code>critical / warning</code> и шлет алерты в телеграм. На бумаге красиво, на практике в продакшене подводит ровно там, где нужна надёжность.</p>
<p>Во-первых, если ваш первичный фильтр уже отобрал строки по ключевым словам вроде <code>oom</code>, <code>panic</code>, <code>machine check</code> или <code>i/o error</code>, то задача классификации уже решена. Эти строки критичны сами по себе. Зачем здесь LLM? Чтобы переклеить ярлык с задержкой в пару секунд и сожрать ресурсы GPU?</p>
<p>Во-вторых, недетерминизм. В мониторинге предсказуемость – это всё. Квантованная модель на одну и ту же строчку лога в зависимости от фазы луны и температуры на Марсе может выдать разные вердикты. Даже JSON она начинает отдавать стабильно только под жесткими grammar-констрейнтами, а не потому, что вы вежливо попросили об этом в промпте. Худшее, что может сделать мониторинг – это молча проглотить <code>critical</code>, присвоив ему статус <code>ignore</code>. Ложная уверенность, что &ldquo;всё под контролем&rdquo;, гораздо опаснее полного отсутствия алертов.</p>
<p>Ну и классика – деградация под нагрузкой. Когда падает коммутатор или начинается OOM-шторм, логи сыплются тысячами строк в секунду. Очередь к вашей LLM мгновенно забивается, GPU захлебывается, и критический алерт прилетает тогда, когда сервер уже остывает в серверной. А если сам детектор крутится на той же инфраструктуре, он просто тихо умрет от нехватки памяти, и вы даже не узнаете об этом.</p>
<p>Языковые модели полезны, но ставить их барьером между упавшим сервисом и дежурным инженером не стоит. Модель должна стоять сбоку от цепочки оповещения.</p>
<hr>
<h2 id="трехуровневая-архитектура-здравого-смысла">Трехуровневая архитектура здравого смысла</h2>
<h3 id="tier-0-детерминированное-ядро-без-магии">Tier 0: Детерминированное ядро (без магии)</h3>
<p>Это единственный уровень, который находится в цепочке оповещения. Здесь всё подчинено правилу: &ldquo;пришло событие – ушел алерт&rdquo;. Никакого AI.</p>
<ul>
<li><strong>Железо смотрим по метрикам, а не по логам.</strong> Пытаться предсказать смерть диска по строчкам в <code>journald</code> – глупость. Для этого есть <code>smartctl_exporter</code> (смотрим на reallocated и pending sectors). Для памяти – <code>node_exporter</code> с метрикой <code>node_edac_correctable_errors_total</code> или <code>rasdaemon</code>.</li>
<li><strong>Логи – только для прикладного софта.</strong> Но вместо нейросети используем жесткий список сигнатур и регулярных выражений, зафиксированный в Git.</li>
<li><strong>Дедупликация шаблонов.</strong> Вместо хэширования сырой строки (где из-за разницы в PID или таймстампах хэши всегда будут уникальными) используем <code>drain3</code>. Он группирует логи по <code>template_id</code>. Похожие ошибки схлопываются в один инцидент.</li>
<li><strong>Приемник алертов.</strong> Всё это уходит вебхуком в нормальную alert-платформу (например, Pusk): она дедуплицирует всплески от ретраев, даёт ACK одной кнопкой с авто-silence в Alertmanager и пуш на телефон, а не сыплет в рабочий чатик.</li>
</ul>
<h3 id="tier-1-контроль-за-контролером-dead-man-switch">Tier 1: Контроль за контролером (Dead-man switch)</h3>
<p>Детектор каждые N секунд подаёт сигнал &ldquo;я жив&rdquo; (heartbeat). Но само отсутствие сигнала ловит не приёмник алертов, а мониторинг – <code>absent()</code> в Prometheus или штатный Watchdog в Alertmanager; сработавшее &ldquo;детектор молчит&rdquo; падает в ту же платформу обычным алертом. Мониторинг не имеет права умирать молча.</p>
<h3 id="tier-2-пакетный-llm-анализ-офлайн">Tier 2: Пакетный LLM-анализ (офлайн)</h3>
<p>Вот сюда мы и отправляем нейросеть. Она работает не в реальном времени, а, скажем, раз в сутки или раз в час, разгребая &ldquo;хвост&rdquo; того, что не подошло ни под одно жесткое правило из Tier 0.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">[Лог-строка] ──► [Tier 0: Регулярки] ──(Есть совпадение)──► [Алерт мгновенно]
</span></span><span class="line"><span class="cl">                       │
</span></span><span class="line"><span class="cl">                 (Нет совпадения)
</span></span><span class="line"><span class="cl">                       │
</span></span><span class="line"><span class="cl">                       ▼
</span></span><span class="line"><span class="cl">         [Tier 2: Накопление в хвост] ──► [Раз в сутки: LLM] ──► [Предложение нового правила человеку]
</span></span></code></pre></div><ol>
<li>Скрипт собирает уникальные шаблоны за день, которые <code>drain3</code> пометил как новые и неизвестные.</li>
<li>Эти шаблоны скармливаются LLM с включенным структурированным выводом (JSON-schema).</li>
<li>Модель генерирует не алерт инженеру в 3 часа ночи, а pull request или тикет: <em>&ldquo;Вот этот шаблон похож на отвал базы данных. Предлагаю добавить регулярку X, severity – critical&rdquo;</em>.</li>
<li>Человек аппрувит правило, и оно уезжает в детерминированный Tier 0.</li>
</ol>
<p>Система сама дообучает свое жесткое ядро. Со временем количество неизвестных строк стремится к нулю, а надежность – к сотне процентов.</p>
<hr>
<h2 id="как-это-выглядит-в-коде">Как это выглядит в коде</h2>
<p>Простейший скелет для цепочки оповещения (Tier 0), который не упадет под нагрузкой и не выдумает галлюцинаций:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">re</span><span class="o">,</span> <span class="nn">subprocess</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">drain3</span> <span class="kn">import</span> <span class="n">TemplateMiner</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sink</span> <span class="kn">import</span> <span class="n">alert</span><span class="p">,</span> <span class="n">heartbeat</span>   <span class="c1"># alert() -&gt; вебхук в Pusk; heartbeat() -&gt; pushgateway/watchdog</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Явный, понятный и проверяемый список правил</span>
</span></span><span class="line"><span class="cl"><span class="n">RULES</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\b(kernel panic|machine check|mce:)&#34;</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">I</span><span class="p">),</span> <span class="s2">&#34;critical&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\bout of memory|oom-kill&#34;</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">I</span><span class="p">),</span>            <span class="s2">&#34;critical&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\bI/O error|EXT4-fs error|medium error&#34;</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">I</span><span class="p">),</span> <span class="s2">&#34;critical&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\bedac|ecc.*(corrected|error)&#34;</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">I</span><span class="p">),</span>       <span class="s2">&#34;warning&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">miner</span> <span class="o">=</span> <span class="n">TemplateMiner</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Читаем journalctl в реальном времени</span>
</span></span><span class="line"><span class="cl"><span class="n">proc</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">Popen</span><span class="p">([</span><span class="s2">&#34;journalctl&#34;</span><span class="p">,</span> <span class="s2">&#34;-f&#34;</span><span class="p">,</span> <span class="s2">&#34;-n&#34;</span><span class="p">,</span> <span class="s2">&#34;0&#34;</span><span class="p">,</span> <span class="s2">&#34;-o&#34;</span><span class="p">,</span> <span class="s2">&#34;cat&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">                        <span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">proc</span><span class="o">.</span><span class="n">stdout</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Схлопываем похожие строки по шаблону через drain3</span>
</span></span><span class="line"><span class="cl">    <span class="n">tmpl</span> <span class="o">=</span> <span class="n">miner</span><span class="o">.</span><span class="n">add_log_message</span><span class="p">(</span><span class="n">line</span><span class="p">)[</span><span class="s2">&#34;cluster_id&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Ищем совпадения по нашей базе сигнатур</span>
</span></span><span class="line"><span class="cl">    <span class="n">sev</span> <span class="o">=</span> <span class="nb">next</span><span class="p">((</span><span class="n">s</span> <span class="k">for</span> <span class="n">rx</span><span class="p">,</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">RULES</span> <span class="k">if</span> <span class="n">rx</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="n">line</span><span class="p">)),</span> <span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">sev</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Уходит понятный алерт с дедупликацией по ID шаблона</span>
</span></span><span class="line"><span class="cl">        <span class="n">alert</span><span class="p">(</span><span class="n">sev</span><span class="p">,</span> <span class="n">line</span><span class="p">,</span> <span class="n">dedup_key</span><span class="o">=</span><span class="n">tmpl</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Всё незнакомое бережно складываем для последующего разбора моделью</span>
</span></span><span class="line"><span class="cl">        <span class="n">unknown_sink</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">tmpl</span><span class="p">,</span> <span class="n">line</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Сигнализируем, что цикл жив и работает</span>
</span></span><span class="line"><span class="cl">    <span class="n">heartbeat</span><span class="p">()</span>
</span></span></code></pre></div><hr>
<h2 id="возвращаемся-к-татьяне-помните-её">Возвращаемся к Татьяне (помните её?)</h2>
<p>Мы оставили Татьяну с её 12.2 мегапикселями и двумя часами ручного саппорта. Логичный вопрос: а как это выглядит, когда сделано по-человечески? На самом деле проблема лечится в два захода – сначала нужно нормально увидеть ошибку, а затем перекрыть ей кислород на входе.</p>
<h3 id="заход-первый-регистрация-и-контекст">Заход первый: регистрация и контекст</h3>
<p>Тот самый <code>log.error</code> с метаданными в продакшене обычно живет не в текстовом файлике на сервере, а в трекере ошибок. Самый ходовой инструмент здесь – Sentry. Но тут всплывает ровно та мысль, с которой мы начинали: Sentry сам по себе не знает про скрытые мотивы вашего внешнего провайдера. Он перехватит исключение и построит стектрейс, но тело ответа (<code>resp.text</code>) вы обязаны положить туда руками.</p>
<p>Выглядит это примерно так:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sentry_sdk</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">resp</span> <span class="o">=</span> <span class="n">provider</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span><span class="n">image_bytes</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">resp</span><span class="o">.</span><span class="n">raise_for_status</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">exc</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">sentry_sdk</span><span class="o">.</span><span class="n">new_scope</span><span class="p">()</span> <span class="k">as</span> <span class="n">scope</span><span class="p">:</span>          <span class="c1"># В SDK 2.x push_scope депрекейтнут</span>
</span></span><span class="line"><span class="cl">        <span class="n">scope</span><span class="o">.</span><span class="n">set_user</span><span class="p">({</span><span class="s2">&#34;id&#34;</span><span class="p">:</span> <span class="n">user_id</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">        <span class="n">scope</span><span class="o">.</span><span class="n">set_tag</span><span class="p">(</span><span class="s2">&#34;provider&#34;</span><span class="p">,</span> <span class="s2">&#34;image-edit&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">scope</span><span class="o">.</span><span class="n">set_tag</span><span class="p">(</span><span class="s2">&#34;image.mp&#34;</span><span class="p">,</span> <span class="nb">round</span><span class="p">(</span><span class="n">w</span> <span class="o">*</span> <span class="n">h</span> <span class="o">/</span> <span class="mf">1e6</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="n">scope</span><span class="o">.</span><span class="n">set_context</span><span class="p">(</span><span class="s2">&#34;provider_response&#34;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;status&#34;</span><span class="p">:</span> <span class="n">resp</span><span class="o">.</span><span class="n">status_code</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;body&#34;</span><span class="p">:</span>   <span class="n">resp</span><span class="o">.</span><span class="n">text</span><span class="p">[:</span><span class="mi">2000</span><span class="p">],</span>             <span class="c1"># Вот ради этой строчки всё и затевалось</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;px&#34;</span><span class="p">:</span>     <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">w</span><span class="si">}</span><span class="s2">x</span><span class="si">{</span><span class="n">h</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;bytes&#34;</span><span class="p">:</span>  <span class="nb">len</span><span class="p">(</span><span class="n">image_bytes</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">})</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Все отказы провайдера схлопываются в один issue, а не плодят 47 разных карточек</span>
</span></span><span class="line"><span class="cl">        <span class="n">scope</span><span class="o">.</span><span class="n">fingerprint</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;provider-reject&#34;</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">resp</span><span class="o">.</span><span class="n">status_code</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">        <span class="n">sentry_sdk</span><span class="o">.</span><span class="n">capture_exception</span><span class="p">(</span><span class="n">exc</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">raise</span>
</span></span></code></pre></div><p>Теперь вместо абстрактного сообщения &ldquo;у кого-то что-то сломалось&rdquo; в панели появляется одна вменяемая карточка. Внутри – точный ответ провайдера, габариты картинки, айдишник пользователя и счетчик, показывающий, сколько еще человек наткнулись на эти грабли. Это и есть правильный Tier 0 для прикладного уровня: поймали эксепшен, прикрепили контекст, сгруппировали и отправили дежурным. И никакой магии.</p>
<p>Если тащить полноценный Sentry с его тяжелым обвесом из PostgreSQL, Redis и ClickHouse нет желания или ресурсов, есть лайтовый вариант – <strong>GlitchTip</strong>. Это минималистичный open-source трекер, который полностью совместим с протоколом Sentry. Он использует тот же <code>sentry_sdk</code> на клиенте, но сам по себе представляет собой один легковесный контейнер и базу данных. Отличное решение, которое ставится за пять минут рядом с тем же Pusk и закрывает вопрос сбора контекста без лишней головной боли.</p>
<h3 id="заход-второй-исправление">Заход второй: исправление</h3>
<p>Сбор контекста помог нам мгновенно понять причину, но пользователю от этого не легче – картинка-то не сгенерировалась. Раз провайдер жестко режет файлы по мегапикселям, глупо надеяться, что клиенты сами начнут предварительно сжимать свои фотографии. Проблему нужно решать на нашей стороне, прямо на границе взаимодействия с внешним API:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">io</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">PIL</span> <span class="kn">import</span> <span class="n">Image</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">MAX_MP</span> <span class="o">=</span> <span class="mi">12_000_000</span>   <span class="c1"># Лимит эндпоинта провайдера в пикселях</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">normalize_for_edit</span><span class="p">(</span><span class="n">image_bytes</span><span class="p">:</span> <span class="nb">bytes</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bytes</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Контейнер MPO с айфона Pillow прочитает без проблем, взяв первый кадр</span>
</span></span><span class="line"><span class="cl">    <span class="n">img</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="n">io</span><span class="o">.</span><span class="n">BytesIO</span><span class="p">(</span><span class="n">image_bytes</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">img</span><span class="o">.</span><span class="n">width</span> <span class="o">*</span> <span class="n">img</span><span class="o">.</span><span class="n">height</span> <span class="o">&lt;=</span> <span class="n">MAX_MP</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">image_bytes</span>                      <span class="c1"># Картинка в лимите – не тратим ресурсы</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">scale</span> <span class="o">=</span> <span class="p">(</span><span class="n">MAX_MP</span> <span class="o">/</span> <span class="p">(</span><span class="n">img</span><span class="o">.</span><span class="n">width</span> <span class="o">*</span> <span class="n">img</span><span class="o">.</span><span class="n">height</span><span class="p">))</span> <span class="o">**</span> <span class="mf">0.5</span>
</span></span><span class="line"><span class="cl">    <span class="n">new_size</span> <span class="o">=</span> <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">img</span><span class="o">.</span><span class="n">width</span> <span class="o">*</span> <span class="n">scale</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">img</span><span class="o">.</span><span class="n">height</span> <span class="o">*</span> <span class="n">scale</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Ресайзим с сохранением пропорций</span>
</span></span><span class="line"><span class="cl">    <span class="n">img</span> <span class="o">=</span> <span class="n">img</span><span class="o">.</span><span class="n">convert</span><span class="p">(</span><span class="s2">&#34;RGB&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">resize</span><span class="p">(</span><span class="n">new_size</span><span class="p">,</span> <span class="n">Image</span><span class="o">.</span><span class="n">Resampling</span><span class="o">.</span><span class="n">LANCZOS</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">out</span> <span class="o">=</span> <span class="n">io</span><span class="o">.</span><span class="n">BytesIO</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">img</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">out</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s2">&#34;JPEG&#34;</span><span class="p">,</span> <span class="n">quality</span><span class="o">=</span><span class="mi">92</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">out</span><span class="o">.</span><span class="n">getvalue</span><span class="p">()</span>
</span></span></code></pre></div><p>Добавляем вызов <code>normalize_for_edit()</code> перед отправкой провайдеру – и кейс Татьяны закрывается раз и навсегда. Нам больше не нужно дебажить этот баг в мессенджерах. При этом трекер ошибок остается стоять на посту, готовый поймать следующую неожиданную проблему, которую мы не предусмотрели. Получается точно такая же петля обратной связи, как и в случае с инфраструктурными логами: произошел инцидент → мы собрали контекст → внесли одно детерминированное правило в код.</p>
<hr>
<h2 id="сухой-остаток">Сухой остаток</h2>
<ol>
<li><strong>Контекст решает всё.</strong> Прежде чем вызывать <code>raise</code>, запишите в лог параметры запроса и ответ сервера. Иначе вы проведёте незабываемые часы, выпрашивая у клиентов исходники файлов.</li>
<li><strong>Сначала зарегистрируй, потом чини.</strong> Поймал ошибку – положи её в трекер с контекстом (Sentry или его лайтовый собрат GlitchTip), и только потом лечи источник. Кейс Татьяны закрывается нормализацией картинки на входе, но увидеть его без записанного тела ответа было невозможно.</li>
<li><strong>Детерминизм в приоритете.</strong> В цепочке оповещения не должно быть нейросетей. Только жесткие правила, регулярные выражения и метрики.</li>
<li><strong>LLM – это ассистент, а не дежурный админ.</strong> Оставьте модели роль офлайн-аналитика. Пусть она разгребает логи постфактум и помогает вам писать новые регулярки для ядра.</li>
<li><strong>Доставка – половина успеха.</strong> Мало поймать ошибку, её нужно донести до правильной смены. Для этого мы у себя используем <a href="https://github.com/getpusk/pusk">Pusk</a> (наша self-hosted платформа для алертов): принимает вебхук из мониторинга, дедуплицирует всплески от ретраев, даёт ACK одной кнопкой и пуш на телефон – чтобы инцидент не потонул в общем чате.</li>
</ol>
]]></content:encoded>
    </item>
  </channel>
</rss>
