На 31 июля 2026 года строгий аудит проходит 91 из 358 созданных материалов. Остальные 267 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
На 31 июля 2026 года строгий аудит проходит 94 из 358 созданных материалов. Остальные 264 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
Module export не задаёт <code>date</code> или <code>author</code>. При будущей
интеграции registry может наложить только редакционные поля на базовые записи
архива. <code>articles.json</code>, registry, стандарт, очередь, package config
и Git данным пакетом не менялись. Команда <code>--print-revisions</code>
печатает только JSON, а <code>--verify-fixture</code> запускает отдельную
детерминированную проверку в памяти.
## Проход 1. Факты и техника — пройдено
| Утверждение | Первичный или официальный источник | Проверенная граница |
| --- | --- | --- |
| Имя метрики описывает одну величину и единицу; для counter используется суффикс <code>_total</code>, duration измеряется в seconds | [Prometheus: Metric and label naming](https://prometheus.io/docs/practices/naming/) | <code>store_http_requests_total</code> отделён от <code>store_http_request_duration_seconds</code>; учебные имена не смешивают запросы, ошибки и время |
| Для online-serving системы полезно считать завершённые запросы, ошибки и latency; errors нужны рядом с числом попыток | [Prometheus: Instrumentation](https://prometheus.io/docs/practices/instrumentation/) | Counter увеличивается после завершения operation; <code>operation</code> и <code>outcome</code> ограничены allowlist |
| Counter накапливает события и при рестарте может сброситься; server-side query над окном не равен ручной разности двух точек | [Prometheus: Instrumentation](https://prometheus.io/docs/practices/instrumentation/), [Prometheus: Querying basics](https://prometheus.io/docs/prometheus/latest/querying/basics/) | PromQL с <code>increase(...[10m])</code> приведён как будущий вопрос к серверу; fixture проверяет только монотонную учебную серию и не моделирует reset или scrape |
| Уникальная комбинация имени и labels создаёт отдельную time series; неограниченные значения раздувают storage | [Prometheus: Metric and label naming](https://prometheus.io/docs/practices/naming/), [Prometheus: Instrumentation](https://prometheus.io/docs/practices/instrumentation/) | Учебная арифметика 3 operations × 2 outcomes и пример с 100 user ID не выданы за RAM-, disk- или workload-замер |
| Counter, gauge, histogram и summary описывают разные формы величины | [Prometheus: Metric types](https://prometheus.io/docs/concepts/metric_types/) | Counter не назван current state; fixture с sum/count не выдаёт p95 или распределение за production telemetry |
Все четыре официальные страницы использованы для сверки смысла names, labels,
types и запросов. Они не представлены как доказательство развернутого в августе
2020 года production-контура. В тексте нет claim о реальном Prometheus server,
exporter, scrape interval, dashboard, alert delivery, нагрузке или
производственном пороге.
### Техническая граница учебной фикстуры
<code>runMetricsFixture()</code> создаёт четыре synthetic event, допускает
ровно <code>catalog</code>, <code>checkout</code>, <code>profile</code> и
<code>success/error</code>, затем формирует локальный exposition-текст. Вторая
часть fixture сравнивает две серии counter:
- <code>0 → 1 → 2</code> за учебное окно даёт delta <code>2</code> и
пересекает упражнение <code>>= 2</code>;
- <code>0 → 1 → 1</code> даёт delta <code>1</code> и его не пересекает.
Эта проверка не подменяет PromQL, target scrape, counter reset, network,
storage, client library или alerting. Её задача уже: зафиксировать, что
изменение labels или числа в учебном контракте не пройдёт незаметно.
Вердикт прохода: **пройден**. Утверждения о Prometheus отделены от учебной
модели, а учебная модель — от запуска на реальной инфраструктуре.
## Проход 2. Редактура, глубина и голос М3 — пройдено
| Ревизия | Симптом и цена в первых двух абзацах | Главный вопрос | Объём основного текста |
| --- | --- | --- | --- |
| Практика | Общий график requests не показывает operation и outcome; цена — поиск не по той границе и разрастание бесполезных линий | Как выбрать один completed-request signal, bounded labels и учебный threshold | **9 182** знаков body |
| Механизм | Уникальный label размножает series и делает запрос неясным; цена — хранение и потеря агрегированного смысла | Где заканчивается полезная dimension и почему request context живёт в журнале | **10 230** знаков body |
| Полевой разбор | Одна линия requests не объясняет деградацию; цена — шумный порог и правка timeout без факта | Как прочитать controlled counter-series, окно и threshold, не выдав их за production-диагноз | **9 480** знаков body |
- Во всех материалах начало устроено по схеме «симптом → цена → ограниченная
учебная граница», затем следует «причина → проверка → действие».
- У каждой revision больше пяти смысловых разделов, есть figure с
самостоятельным <code>alt</code>/<code>figcaption</code>, таблица с
<code>caption</code>/<code>thead</code>, code/query/fixture example,
нумерованный маршрут и четыре официальные ссылки.
- Статьи не используют общую риторику про важность наблюдаемости. Они
фиксируют completed request, counter, seconds, operation/outcome, time
series, окно, порог, log и следующий сценарий.
- Голос соответствует М3 / августу 2020 года. Автор связывает application-code
с наблюдаемым измерением, но не приписывает себе SLO, error budget,
observability platform, реальные production-значения или опыт большого
инцидента.
- Все цифры в графике, таблице cardinality и пороге явно названы учебными.
Нагрузка, допустимая ошибка и стоимость alert не придуманы вместо данных
проекта.
Второй редакторский проход проверил, что count, duration и latency не
смешиваются. Counter не назван current state, а <code>increase()</code> не
выдана за ручную разность snapshots. Pseudocode над client library отмечен как
контракт labels, а не как запущенный exporter.
Вердикт прохода: **пройден**. Тексты укладываются в 5 000–15 000 знаков,
остаются прагматичными и не перескакивают к зрелой платформенной терминологии.
## Проход 3. Визуал, fixture и выпусковой preflight — пройдено в пределах пакета
- <code>metrics-signal-contract-2020.svg</code> ведёт от операционного
вопроса к counter, двум ограниченным labels, учебному окну и действию после
| Import-safe export и draft gate | PASS: **9 182 / 10 230 / 9 480** знаков body; три slug, tables, figures, code, routes, sources и assets найдены |
| In-memory fixture | PASS: bounded labels сохранены, counter error checkout найден, threshold пересекается на 2 и не пересекается на 1 |
| <code>xmllint --noout</code> | PASS, все три SVG — корректный XML |
| Sharp mobile preflight | PASS: три PNG шириной 375 px просмотрены; нет clipping, наложения или horizontal overflow внутри схем |
| Scope/self-review | PASS: в revision нет <code>date</code>/<code>author</code>; созданы только пять файлов П30; чужие незакоммиченные пакеты не редактировались и не индексировались |
## Независимая интеграционная приёмка
Основной редактор 31 июля 2026 года подключил три revision к
<code>web/data/editorial-revisions.mjs</code>, не меняя базовый
<code>articles.json</code>, даты или автора архивных записей. После подключения
в registry стало 85 revision. Отдельно выполнены:
| Проверка после интеграции | Реальный результат |
| --- | --- |
| Строгий audit трёх slug | PASS: 9 182 / 10 230 / 9 480 знаков; у каждой статьи один figure, одна table и два code example |
| Production build | PASS: Next.js собрал 374 статические страницы |
| Независимый mobile visual review | PASS: основной редактор повторно просмотрел все три SVG, отрендеренные Sharp в 375 px; clipping, overlap и overflow не обнаружены |
Ни этот отчёт, ни интеграция не утверждают, что был запущен настоящий
Prometheus, exporter, scrape, browser или assistive technology. Они фиксируют
границы автономного учебного пакета и результат статических проверок.
Выпусковой вердикт: **ACCEPT**. Commit и push выполняются отдельной
публикационной операцией; Git остаётся источником её фактической записи.
<titleid="title">Учебный разбор порога по counter</title>
<descid="desc">Вертикальная схема показывает: общий график не является диагнозом. Для одной series checkout error берут три синтетические точки counter ноль, один и два, проверяют прирост за десять минут против учебного порога две и затем открывают один связанный журнал.</desc>
<textx="360"y="1339"text-anchor="middle"fill="#ffffff"font-family="Arial, sans-serif"font-size="20"font-weight="700">Дальше: один связанный журнал или стендовый сценарий</text>
<titleid="title">Граница полезных labels в метрике</title>
<descid="desc">Вертикальная схема показывает, что событие HTTP проходит allowlist operation и outcome в агрегированную time series. Request ID, email и полный URL уходят в журнал для поиска одного события и не становятся labels.</desc>
<titleid="title">Контракт учебного сигнала для HTTP-операции</title>
<descid="desc">Вертикальная схема: один операционный вопрос про ошибки checkout ведёт к counter с ограниченными labels operation и outcome, затем к учебному окну десять минут, порогу две ошибки и следующей диагностической проверке.</desc>
note:'официальные рекомендации для online-serving систем: считать завершённые запросы, ошибки и latency; не раздувать labels и держать счётчик ошибок рядом с числом попыток',
excerpt:'График количества запросов не объясняет деградацию сам по себе. Выбираем одну операцию, bounded labels, учебный запрос и порог, который можно проверить на контролируемой серии.',
readingMinutes:14,
},
[
paragraph('Симптом знакомый: на графике есть общее число HTTP-запросов, а ответить на вопрос «в какой операции появились ошибки?» всё равно нельзя. Цена такой метрики проявляется во время разбора. Инженер сравнивает несвязанные пики, добавляет ещё одну линию или начинает искать проблему по логам без точки входа. Если в название метрики положить всё подряд, следующий шаг становится ещё дороже: график растёт, а причина по-прежнему не отделена от фонового шума.'),
paragraph('В августе 2020 года я бы не начинал с набора дашбордов и не называл это готовой observability-платформой. Достаточно выбрать один повторяемый вопрос для HTTP-операции: «появились ли в учебном окне ошибки завершения checkout?» Ниже только контролируемая фикстура с выдуманными значениями. Она показывает форму сигнала, labels, запрос и порог; ни Prometheus server, ни реальные нагрузки, ни production-alert в этом материале не запускались.'),
heading('Сначала формулируем вопрос, потом имя метрики'),
paragraph('Общее число запросов отвечает лишь на вопрос «сколько завершений увидел обработчик». Оно не различает каталог, checkout и профиль, не отделяет успешный исход от ошибки и не говорит, что делать дальше. Поэтому вопрос надо писать до кода. Для этой заметки он ограничен одной границей: обработчик уже закончил HTTP-операцию и знает её стабильное имя и исход. Вход в счётчик ставится после завершения, чтобы число попыток, ошибок и длительностей относилось к одному и тому же моменту.'),
paragraph('Такое ограничение полезнее, чем попытка измерить всё на первом шаге. Путь до приложения, ответ базы, очередь, клиентский браузер и ручная работа оператора остаются отдельными границами. Если их смешать в одну величину, название получится широким, а действие по ней — неясным. Сигнал ниже не доказывает доступность для пользователя и не заменяет трассу или журнал. Он лишь даёт проверяемую развилку: в выбранной операции были успехи, ошибки или не было завершений вовсе.'),
dataTable(
'Малый контракт учебного сигнала: каждая строка отвечает на один вопрос',
['Величина','Тип и единица','Разрешённые labels','Операционный вопрос','Не доказывает'],
[
['<code>store_http_requests_total</code>','counter, завершённые запросы','<code>operation</code>: 3 имени; <code>outcome</code>: success/error','В какой операции и с каким исходом завершилась работа?','почему произошла ошибка и что видел браузер'],
['<code>store_http_request_duration_seconds</code>','учёт длительностей, seconds','<code>operation</code>: 3 имени','Есть ли данные о длительности выбранной операции?','конкретную причину медленного запроса'],
['Учебный порог <code>>= 2</code>','условие над counter за 10 минут','только checkout/error','Пересекла ли контролируемая серия границу упражнения?','production-порог, SLO или допустимую нагрузку'],
],
),
paragraph('В названии есть доменная часть <code>store</code>, сущность <code>http_requests</code> и суффикс <code>_total</code> для накапливаемого счётчика. Это не косметика: из имени видно, что значение не является миллисекундами или текущим числом подключений. Для длительности используем seconds, а не смешиваем миллисекунды в одном месте и секунды в другом. Официальные рекомендации Prometheus предлагают одну величину и одну единицу на имя; это делает запросы и ревью понятнее даже без готового dashboard.'),
heading('Выбираем тип по форме состояния'),
paragraph('Counter растёт на каждое завершение и может обнулиться при перезапуске процесса. Он подходит для числа запросов и ошибок, но сам по себе редко отвечает на вопрос «что происходило за последние пять минут». Для окна используют функцию над изменением counter, например <code>increase()</code> или <code>rate()</code>; в этой статье нужен именно count ошибок в контролируемом окне, поэтому выбираю <code>increase()</code>. Gauge меняется в обе стороны и здесь не подходит: «ошибки прямо сейчас» не являются состоянием, которое приложение должно произвольно выставлять в ноль.'),
paragraph('Для длительности нужен не один усреднённый number на всю систему, а набор наблюдений. Клиентская библиотека может экспортировать histogram или summary; выбор зависит от задачи и версии библиотеки. В первом упражнении я не строю p95, не сравниваю SLO и не объявляю latency-контракт. Достаточно сохранить seconds и стабильное имя операции, чтобы следующая проверка могла сравнить один и тот же вход. Если операции не ограничены словарём, сначала надо договориться о словаре, а не добавлять route или произвольный URL в label.'),
heading('Контролируемая фикстура вместо обещания реального графика'),
paragraph('Ниже находится исполняемый фрагмент из этого revision-модуля. Он принимает четыре учебных события, разрешает только три операции и два исхода, затем печатает текстовую exposition с counter и суммой/количеством длительностей. В нём нет сетевого вызова, метрики не отправляются в Pushgateway и нет зависимости от конкретной client library. Именно поэтому пример можно проверить как контракт имён и labels, не выдавая его за снятый с production endpoint результат.'),
codeBlock(controlledExpositionCode),
paragraph('В примере специально нет <code>user_id</code>, email, полного URL, request ID, текста исключения или IP-адреса. Эти значения помогают найти один случай в журнале, но почти никогда не являются ограниченной размерностью метрики. У <code>operation</code> есть короткий allowlist, у <code>outcome</code> — два допустимых значения. Если операция неизвестна, фикстура завершается ошибкой. Это лучше, чем незаметно породить новую series из имени нового endpoint-а или строки, пришедшей из запроса.'),
'Вертикальная схема выбора метрики: сначала один вопрос про завершённую HTTP-операцию, затем counter с operation и outcome, отдельно duration в seconds, ограниченный учебный запрос и действие после его результата',
'Схема отделяет сигнал от диагноза: counter показывает ветку для расследования, но не объясняет источник ошибки без следующей проверки.',
),
heading('Порог — это граница решения, а не украшение графика'),
paragraph('Порог имеет смысл только вместе с действием. Для контролируемой серии вопрос звучит так: «если за десять минут fixture получила две или больше ошибок checkout, надо ли открыть один журнал или повторить изолированный сценарий?» Число <code>2</code> здесь выбрано для упражнения: одна ошибка проверяет, что счётчик и label существуют, две — что условие меняет ветку. Оно не выводится из пользовательского трафика, не описывает допустимый процент ошибок и не должно копироваться в alert rule.'),
paragraph('PromQL-выражение ниже суммирует изменение одного counter в окне. В реальном Prometheus counter reset и фактические точки scrape обрабатываются механизмом самого движка; in-memory fixture ниже проверяет только прозрачную арифметику двух snapshots. Это важное различие. Нельзя сказать «локальный delta равен результату production PromQL» и пропустить проверку на выбранной версии сервера. Но можно сначала договориться, какой именно label и какое пересечение должно поменять следующее действие.'),
codeBlock(thresholdQueryCode),
heading('Короткий маршрут первой метрики'),
orderedList([
'Записать один вопрос в форме «операция, исход, окно, следующее действие». Не начинать с имени dashboard или общей фразы «нужны метрики».',
'Выбрать момент завершения операции и определить, что считается success и error. Если исход ещё не известен, не увеличивать окончательный counter раньше времени.',
'Составить allowlist labels. Для этого примера оставить только <code>operation</code> и <code>outcome</code>; отдельно записать, где живут request ID и текст ошибки.',
'Назвать metric одной величиной и одной единицей: counter получает <code>_total</code>, длительность хранится в seconds. Проверить название по документации Prometheus до добавления в код.',
'Прогнать контролируемые события и убедиться, что неизвестная operation отвергается, а строка exposition не содержит user-specific labels.',
'Сформулировать учебный запрос и порог, затем записать действие по обе стороны границы. Перед реальным alert отдельно проверить scrape interval, reset, нагрузку и владельца реакции.',
]),
heading('Граница первой проверки'),
paragraph('После этого шага у проекта не появляется полный мониторинг. Нет данных о клиентах, сетевых hop-ах, базе, очереди, релизе или фактической нагрузке. Нет также SLO, error budget и обещания, что две ошибки одинаково важны для любого продукта. Это нормально для первой метрики: она должна сделать один вопрос проверяемым, а не создать видимость знания о всей системе. Если counter пересекает учебную границу, следующий артефакт — один связанный запрос или журнал, а не бесконечное добавление labels.'),
paragraph('Числа, длительности, операции и окно в тексте учебные. Реальный порог выбирают после того, как есть согласованный смысл ошибки, период наблюдения, известная стоимость ложного срабатывания и возможность проверить контекст. В августе 2020 года автор только начинает связывать приложение с наблюдаемым сигналом: он умеет поставить маленький договор над кодом, но не приписывает себе опыт эксплуатации общей платформы или реальные production-значения.'),
excerpt:'Label помогает разложить один сигнал, пока его значения ограничены. Разбираем, почему request ID и полный URL не становятся диагностикой, как посчитать учебную cardinality и где поставить контракт в коде.',
readingMinutes:14,
},
[
paragraph('Симптом появляется не в коде обработчика, а после добавления «ещё одного полезного label». Метрика по-прежнему показывает запросы, но одинаковый график превращается в множество почти уникальных series, а простой запрос по операции начинает возвращать лишние строки. Цена — не только память и диск сервера метрик. В разборе становится неясно, какое измерение является частью вопроса, а какое случайно сохранило одну пользовательскую историю вместо агрегированного сигнала.'),
paragraph('Для августа 2020 года мне достаточно разобрать один механизм: каждая уникальная комбинация name и labels образует отдельную time series. Ниже нет фактического размера Prometheus, нагрузки или claim о production инциденте. Есть учебный расчёт, короткий allowlist и fixture, который отвергает неограниченные значения. Цель не в том, чтобы запретить labels, а в том, чтобы различать размерность вопроса и идентификатор конкретного события.'),
heading('Series — результат выбора, а не побочный эффект строки'),
paragraph('Представим один counter <code>store_http_requests_total</code>. Без labels у него один ряд для target. С <code>operation="catalog"</code> и <code>outcome="success"</code> появляется ряд для этой пары. Если добавить <code>request_id</code>, новый запрос почти наверняка принесёт новое значение. Такая строка может выглядеть информативно, но это уже не ответ на вопрос «как меняется число ошибок checkout», а попытка поместить журнал события в storage метрик. Для поиска единичного случая есть correlation ID в логе; для агрегирования — короткий набор измерений.'),
paragraph('Проверка здесь простая: можно ли заранее перечислить значения label и останется ли осмысленным <code>sum by (...)</code>, если часть dimensions убрать? Для <code>operation</code> ответ обычно да: проект заранее знает небольшой набор логических обработчиков. Для <code>outcome</code> тоже да, если договор ограничен <code>success</code> и <code>error</code>. Для email, UUID, сырого path с ID товара, stack trace и текста ошибки ответ нет: новые значения приходят извне или растут вместе с числом запросов.'),
dataTable(
'Граница labels в учебном HTTP-сигнале',
['Кандидат','Значения известны заранее?','Какой вопрос поддерживает','Решение в этом контракте'],
[
['<code>operation</code>','да: catalog, checkout, profile','В какой логической операции выросло число завершений или ошибок?','оставить; проверять allowlist'],
['<code>outcome</code>','да: success/error','Есть ли различие между удачными и ошибочными завершениями?','оставить; не хранить текст ошибки'],
['<code>status_code</code>','почти ограничен, но для первого вопроса избыточен','Нужны ли отдельные HTTP-классы?','добавить только после нового вопроса; пока outcome достаточно'],
['<code>request_id</code>, user ID, email','нет: новое значение почти на каждую операцию','Найти один конкретный запрос','не добавлять; оставить в журнале'],
['Полный URL <code>/orders/12345</code>','нет: ID и query string растут','Понять маршрут','нормализовать в operation или route template до метрики'],
],
),
paragraph('Эта таблица не означает, что <code>status_code</code> всегда плох. Он может быть полезным, когда действительно нужен вопрос о 404 и 500. Но нельзя добавлять его по привычке, если следующий шаг всё равно одинаков для всех error. Контракт должен быть небольшим: одна добавленная dimension меняет не только текст exposition, но и число рядов, агрегации, правила и стоимость хранения. Если ей не соответствует отдельное действие, она пока не проходит ревью.'),
heading('Кардинальность можно оценить до первого scrape'),
paragraph('У учебного counter есть три операции и два исхода. Для одного target верхняя граница — шесть комбинаций, если все они встретятся. Если такой же application exporter запускается на двух target, Prometheus добавляет target labels на стороне scrape, и в простом мысленном расчёте получится до двенадцати наблюдаемых рядов. Это не замер сервера и не предел всех связанных metric families: histogram создаёт дополнительные bucket, sum и count series. Расчёт нужен для другого — увидеть мультипликацию до того, как в неё попадёт неограниченный input.'),
paragraph('Добавим в этот же контракт user ID со ста учебными значениями. Уже для counter получаем не шесть, а до шестисот комбинаций на target; с двумя target — до тысячи двухсот. Цифры специально синтетические. Они не описывают настоящих пользователей, RAM или пропускную способность Prometheus, но показывают форму ошибки: значение, которое растёт вместе с пользователями, умножает каждый уже выбранный label. Поэтому полезнее спросить «можно ли получить это из лога по request ID?» до того, как переносить поле в метрику.'),
heading('Проверяем boundary в коде, а не глазами на dashboard'),
paragraph('Ниже псевдокод обёртки над client library. Его можно реализовать на выбранном клиенте, но в этой статье не создаётся HTTP endpoint и не вызывается внешняя библиотека. Важна граница перед инструментированием: operation и outcome проходят allowlist, а произвольные поля не имеют места в label object. Такое правило полезнее комментария «не использовать high cardinality», потому что новый endpoint или ошибка перестают незаметно менять форму metric family.'),
codeBlock(labelBoundaryCode),
paragraph('Слово «псевдокод» здесь важно. Имена методов <code>inc</code> и <code>observe</code> часто похожи в client libraries, но конкретный API, регистрация и exposition зависят от выбранной версии. Нельзя копировать фрагмент и считать, что он создал metrics endpoint. Однако контракт входа проверяем без инфраструктуры: функция <code>renderFixtureExposition</code> в этом module принимает только тот же набор bounded values. Unknown operation завершает fixture ошибкой, а выходной текст не может получить <code>request_id</code> или <code>user_id</code>.'),
'Вертикальная схема границы labels: событие запроса проходит через allowlist operation и outcome, попадает в агрегированную time series; request ID, email и полный URL направлены в журнал и не становятся labels',
'Один signal хранит ограниченные dimensions. Идентификатор отдельного события остаётся ключом поиска в журнале, а не генератором новой series.',
),
heading('Запрос должен агрегировать тот же договор'),
paragraph('Если metric family содержит <code>operation</code> и <code>outcome</code>, запрос может явно сохранить оба измерения. В примере ниже <code>sum by</code> группирует изменение counter за учебные десять минут. Это полезно именно потому, что labels заранее ограничены: результат имеет шесть или меньше понятных строк, а не одну строку на пользователя. Если выражение приходится постоянно фильтровать по уникальным идентификаторам, проблема не в синтаксисе PromQL, а в том, что журнал и метрика получили одну и ту же работу.'),
codeBlock(labelQueryCode),
paragraph('У counter есть ещё одна граница: процесс может перезапуститься и значение станет меньше. Prometheus предназначен для работы с такими series, но вручную вычитать две точки и называть результатом <code>increase()</code> нельзя. Контролируемая fixture ниже намеренно выбрасывает ошибку на убывающем наборе, потому что она тестирует лишь договор порога без модели reset. После подключения реального сервера нужно проверить scrape interval, фактическую экспозицию и поведение выражения на используемой версии Prometheus.'),
heading('Когда новый label всё-таки оправдан'),
paragraph('Новый label появляется не потому, что поле уже есть в request object. Сначала появляется новый вопрос и другое действие. Например, команда действительно готова разбирать 4xx отдельно от 5xx; тогда можно договориться о bounded <code>status_class</code> и добавить его после теста, что все значения нормализуются в известные классы. Или один exporter измеряет две заранее перечисленные внешние базы; тогда <code>dependency</code> может быть допустимой размерностью. Но строка SQL, hostname от пользователя и путь с UUID остаются данными для лога или отдельного хранилища.'),
paragraph('Перед добавлением полезно сделать маленький расчёт: сколько значений есть сейчас, какое верхнее значение допускает код, с чем оно перемножится и какой запрос станет возможным. Если на любой строке ответ «не знаю, значения приходят из входа», действие откладывают. Нельзя компенсировать неопределённость надеждой, что график потом подскажет. График уже создан из series, и потерянная граница будет дорого стоить следующему расследованию.'),
heading('Маршрут ревью labels'),
orderedList([
'Записать один operational question и назвать действие после его ответа. Вопрос «собрать всё на будущее» для labels не подходит.',
'Выписать каждый candidate label, источник его значения и максимальное число значений. Отдельно отметить поля из URL, пользователя, исключения и request context.',
'Оставить только измерения, которые можно заранее перечислить и по которым действительно будет агрегация. Для первого контракта выбрать <code>operation</code> и <code>outcome</code>.',
'Посчитать учебную верхнюю границу combinations с уже существующими labels и target. Для histogram отдельно учесть, что buckets создают дополнительные series.',
'Поставить allowlist или нормализацию на границе instrumentation и прогнать controlled fixture с допустимым и недопустимым событием.',
'Сформировать PromQL-запрос с явным <code>sum by</code>. Перед выпуском на реальный сервер отдельно проверить scrape, reset, storage и ответственность за действие.',
]),
heading('Что этот механизм не решает'),
paragraph('Bounded labels не заменяют нормальный журнал, trace context или доступ к исходному событию. Они также не дают ответ на вопрос, почему запрос завершился ошибкой: для этого после срабатывания нужны связанный request ID и контекст кода. В статье не оценивается память Prometheus, не запускается exporter и не сравниваются latency на живой системе. Упомянутые числа — простая арифметика учебной модели, а не production telemetry.'),
paragraph('Это и есть уровень М3 для 2020 года: автор начинает видеть, что наблюдаемый сигнал зависит от границы данных, а не от цвета dashboard. Он умеет сказать «эта dimension принадлежит журналу, а эта — ограниченному metric contract», но не заявляет опыт управления общей платформой наблюдаемости, SLO или error budget. Следующий проверяемый шаг после такого текста — один изолированный endpoint и один запрос, а не массовое тиражирование labels по приложению.'),
excerpt:'Одна линия «requests» не даёт причины для действия. Разбираем синтетическую серию counter, выбор окна и порога, чтобы отличить факт пересечения от выдуманного production-диагноза.',
readingMinutes:14,
},
[
paragraph('Симптом в разборе обычно звучит слишком широко: «на графике есть запросы, но непонятно, что ухудшилось». Цена поспешного ответа — шумный порог, который игнорируют, или изменение timeout без доказательства, что проблема вообще в этом endpoint. Одна точка counter не показывает, сколько ошибок появилось за окно; общий request count не показывает операцию; отдельный stack trace не показывает, повторяется ли случай. Пока эти три вещи смешаны, график украшает разбор, но не направляет действие.'),
paragraph('Ниже — не отчёт о реальном инциденте. Это контролируемая серия для одной метрики <code>store_http_requests_total{operation="checkout",outcome="error"}</code> и один учебный вопрос: «пересекло ли число новых ошибок checkout за десять минут границу двух?» Значения, timestamps, threshold и графическая форма выдуманы для проверки порядка рассуждений. Нет настоящего Prometheus server, scrape, нагрузки, alerting, SLO или production-значений.'),
heading('Начинаем не с графика, а с различимой ветки'),
paragraph('Первое решение — выбрать сигнал. В нашем случае это counter завершённых ошибок, потому что операция имеет чёткий конец, а интересует число событий, накопленных за окно. Причина выбора не в том, что counter «самый популярный». Он позволяет сравнить два момента и узнать, появлялись ли новые error outcomes. Если задача была бы про число активных соединений в конкретную секунду, подошёл бы gauge. Если задача была бы про распределение длительностей, нужна отдельная metric family с seconds и выбранным типом распределения. Один тип не должен притворяться ответом на все вопросы.'),
paragraph('Второе решение — оставить labels достаточными, но не уникальными. <code>operation="checkout"</code> говорит, о какой логической работе идёт речь. <code>outcome="error"</code> отделяет ошибки от успешных завершений. Они не содержат пользователя, URL с ID или request ID. Поэтому при пересечении порога можно открыть журнал по времени и операции, а не искать одну уникальную series. Если нужен конкретный запрос, его корреляционный идентификатор берут из лога; попытка добавить его в counter разрушила бы агрегирование ещё до расследования.'),
dataTable(
'Учебная диагностика: сигнал ведёт к следующей проверке, но не заменяет её',
['Наблюдение','Что оно действительно говорит','Чего из него нельзя вывести','Следующее ограниченное действие'],
[
['<code>increase(...error...[10m]) = 0</code>','в выбранной серии нет новых error increments в окне','что все пользователи получили успех и что scrape не пропущен','сверить, соответствует ли operation исходной жалобе; не объявлять систему здоровой'],
['<code>increase(...error...[10m]) = 1</code>','в учебной серии появилась одна новая ошибка','масштаб, причину и необходимость paging','проверить один связанный лог или повторить изолированный сценарий'],
['<code>increase(...error...[10m]) = 2</code>','учебный порог пересечён','production severity, SLO или допустимую долю ошибок','открыть ветку диагностики checkout и зафиксировать контекст'],
['Общий <code>requests_total</code> растёт','обработчик завершал какие-то запросы','какая operation или outcome изменилась','добавить bounded group-by, не менять timeout по одной общей линии'],
],
),
paragraph('Таблица намеренно отделяет факт от решения. Ноль в одном counter не доказывает отсутствие проблемы: metric может не покрывать нужный путь, scrape может отсутствовать, а браузер может отвалиться до приложения. Две ошибки не доказывают, что нужно будить человека ночью: это всего лишь порог учебной серии. Чтобы порог стал реальным правилом, проект должен отдельно назвать окно, владельца, стоимость ложного срабатывания и связь с фактическим пользователем. В этом материале мы останавливаемся раньше и проверяем форму логики.'),
heading('Сырые snapshots полезнее легенды о «пике»'),
paragraph('Для counter важен прирост, а не последняя цифра. В синтетической записи ниже значение равно нулю, затем единице и двум. За десять минут разница между первой и последней точкой равна двум. Такая арифметика подходит для controlled fixture, где мы заранее запретили reset и знаем все точки. Она не заменяет реализацию <code>increase()</code> в Prometheus: настоящий движок работает с range vector, точками scrape и правилами обработки counter. Поэтому в документе рядом существуют две вещи — PromQL-формулировка вопроса и более простая in-memory проверка того же порога.'),
codeBlock(diagnosisSamplesCode),
paragraph('Ошибка здесь часто начинается с фразы «на графике был пик». Пик без имени series, окна и сравниваемой величины не даёт следующего действия. Правильная запись короче: «для <code>operation=checkout</code> и <code>outcome=error</code> в учебных snapshots прирост за 10 минут равен двум; в упражнении это открывает проверку одного лога». В ней не содержится догадки о базе, сетевом hop-е или пользователе. Эти причины проверяются после того, как signal сузил вход в разбор.'),
'Вертикальная схема разборa метрики: общий график не отвечает на вопрос, поэтому выбирается counter ошибок checkout, проверяются две крайние точки учебного окна, сравнивается порог два и затем открывается один связанный журнал или изолированный сценарий',
'Порог не называет причину. Он только переводит разбор от общей линии к одной ограниченной операции и следующей проверке.',
),
heading('Запрос и fixture проверяют разные части договора'),
paragraph('PromQL ниже выражает желаемую форму запроса к серверу метрик: выбрать operation и outcome, взять изменение counter за десять минут и сравнить с границей. В реальном проекте его нужно выполнить на той же версии Prometheus, с известным scrape interval и реальной конфигурацией target. Только тогда можно читать ответ и обсуждать график. В автономном пакете запрос не выполняется: вместо этого <code>runMetricsFixture()</code> возвращает два прозрачных набора snapshots и проверяет, что простая разность даёт true на двух ошибках и false на одной.'),
codeBlock(thresholdQueryCode),
paragraph('Эта разница между expression и fixture не является недостатком. Она защищает от ложного отчёта «PromQL проверен», когда запущен был только Node. Fixture доказывает ограниченный контракт: labels bounded, counter не убывает в модели, две ошибки пересекают учебный threshold, одна — нет. Она не доказывает scrape, storage, alert delivery, restart process или поведение любого exporter. Такой маленький тест полезен для ревью, потому что будущая правка не сможет тихо превратить порог в <code>>= 1</code> или добавить поле пользователя в labels.'),
heading('Почему окно и порог выбирают вместе'),
paragraph('Окно в десять минут и число два не существуют по отдельности. В маленькой controlled fixture десять минут дают три понятных snapshots, а две ошибки создают ветку, отличную от одной. Если выбрать минуту, но scrape происходит реже, результат может быть пустым или зависеть от случайной точки. Если выбрать сутки, краткий отказ растворится в общей сумме. Эти рассуждения не дают готовое число для production: они требуют сверить частоту scrape, тип операции, возможный burst и того, кто умеет реагировать на сигнал.'),
paragraph('Также нельзя заменить counter ошибок общей долей без определения знаменателя. Соотношение error/attempt полезно только тогда, когда оба числа считаются в одном месте и за одно окно. Если success заканчивается на proxy, а error пишется внутри приложения, отношение будет смесью разных границ. Поэтому первая версия текста не рисует процент и не объявляет «норму ошибок». Она оставляет более узкий, проверяемый вопрос о числе завершённых error в одной operation. Следующий metric contract может добавить attempts и проверить их общую точку увеличения.'),
heading('Маршрут учебного разбора'),
orderedList([
'Записать исходный симптом без диагноза: какой пользовательский путь подозревается и какая цена ошибки, если она повторится.',
'Выбрать одну metric family и момент увеличения. Для этого упражнения counter увеличивается только после завершения checkout с известным outcome.',
'Проверить labels: operation и outcome должны быть bounded; request ID и полный URL остаются в журнале, а не в series.',
'Собрать три синтетические snapshots, явно пометить их учебными и посчитать только ту величину, которую fixture умеет моделировать.',
'Сформулировать PromQL-вопрос с тем же окном и labels, но не объявлять его выполненным до запуска на реальном сервере метрик.',
'После пересечения учебного threshold открыть один связанный журнал или изолированный сценарий. Изменять timeout, retry или код только после нового наблюдаемого факта.',
]),
heading('Какие ошибки этот порядок останавливает'),
paragraph('Такой разбор останавливает четыре распространённые подмены. Он не позволяет принять общую линию запросов за доказательство ошибки checkout. Он не позволяет считать последнюю цифру counter скоростью изменения. Он не даёт уникальному request ID стать label только потому, что его удобно видеть на графике. И он не превращает учебное число два в обещание о production alert. Всё это звучит скромно, но именно эти подмены делают первую метрику бессмысленной: сигнал начинает жить отдельно от вопроса и действия.'),
paragraph('В реальном продолжении после такого упражнения понадобится отдельный стенд: endpoint с bounded operation, Prometheus scrape, одно известное изменение в тестовой нагрузке и сохранённый результат запроса. Потом можно обсуждать более широкую систему. Здесь автор августа 2020 года осваивает только связку «операционный вопрос → signal → labels → проверка → действие». Он не выдумывает реальную деградацию, не приписывает себе SLO/error budget и не подменяет контрольную серию производственным измерением.'),
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.