Files
progcode/editorial/agent-rewrites/270.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

7 lines
20 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 270,
"slug": "editorial-2020-07-practice-structured-logs",
"title": "Структурированные логи: как связать один запрос и быстро найти ошибку",
"excerpt": "Контракт JSON-события, единый request_id и проверка отрицательного пути помогают найти запрос без поиска по случайному тексту. Разбираем форму записи, границы корреляции и redaction.",
"contentHtml": "<p>В журнале появляются три строки: «ошибка оплаты», «запрос завершён» и стек исключения. Все записи стоят рядом, но неизвестно, относятся ли они к одному запросу. Инженер ищет по минуте, маршруту и фрагменту текста. Он легко связывает чужие события. Цена ошибки — неверная правка, повторный инцидент и потерянное время во время сбоя.</p>\n<p>Причина обычно не в отсутствии логов. Приложение пишет слишком мало устойчивых признаков. Сообщение меняется от версии к версии, порядок строк зависит от параллельной работы, а полный объект запроса содержит лишние и чувствительные данные. Одна строка не даёт надёжного ключа поиска.</p>\n<p>Рабочий минимум — контракт одного JSON-события и один <code>request_id</code> на входное HTTP-действие. Обязательные поля имеют постоянные имена. Локальный модуль добавляет только свой контекст. Такой журнал отвечает на узкий вопрос: какие известные события принадлежат этому запросу? Он не заменяет распределённую трассировку и не доказывает причинность.</p>\n<h2>Из чего состоит событие</h2>\n<p>Структурированный лог — это объект, а не строка, которую потом приходится разбирать регулярным выражением. Человек читает короткое поле <code>message</code>. Поиск и агрегация используют <code>event</code>, <code>service</code>, уровень и идентификатор запроса. JSON сам по себе ничего не гарантирует. Поля становятся полезными только тогда, когда команда закрепила их смысл и форму.</p>\n<table><caption>Минимальный контракт учебного события</caption><thead><tr><th scope=\"col\">Поле</th><th scope=\"col\">Кто задаёт</th><th scope=\"col\">Форма</th><th scope=\"col\">Вопрос</th></tr></thead><tbody><tr><td><code>timestamp</code></td><td>логгер</td><td>UTC ISO-8601</td><td>Когда произошло событие?</td></tr><tr><td><code>level</code></td><td>операция</td><td><code>debug</code>, <code>info</code>, <code>warn</code>, <code>error</code></td><td>Насколько срочно его смотреть?</td></tr><tr><td><code>service</code></td><td>конфигурация</td><td>устойчивое имя</td><td>Где оно произошло?</td></tr><tr><td><code>environment</code></td><td>конфигурация</td><td>например, <code>training</code></td><td>Не смешаны ли контуры?</td></tr><tr><td><code>event</code></td><td>владелец операции</td><td><code>noun.verb</code></td><td>Что произошло?</td></tr><tr><td><code>request_id</code></td><td>HTTP-граница</td><td>одно проверенное значение</td><td>Какие записи относятся к запросу?</td></tr><tr><td><code>message</code></td><td>владелец операции</td><td>короткий текст</td><td>Что увидит читатель?</td></tr></tbody></table>\n<p>Общие поля живут в корне объекта. Контекст операции — во вложенном блоке <code>http</code>, <code>job</code> или <code>adapter</code>. Не передавайте в логгер целиком <code>req</code>, ответ базы или объект пользователя. Форма такого объекта меняется без предупреждения. В ней могут оказаться заголовки, тело запроса и секреты. Выберите несколько полей, которые закрывают конкретный вопрос.</p>\n<figure><img src=\"/assets/editorial/2020/structured-log-event-2020.svg\" alt=\"Схема учебного JSON-события: обязательные поля отделены от локального блока HTTP\" loading=\"lazy\" /><figcaption>Общий контракт остаётся коротким, а локальный HTTP-контекст находится в отдельном блоке. Учебная иллюстрация показывает форму записи, а не результат production-наблюдения.</figcaption></figure>\n<h2>Собираем запись до вывода</h2>\n<p>Функция формирования события должна принимать только известный контекст. Она проверяет обязательные ключи до сериализации. Это не универсальная библиотека и не схема всей системы. В учебном примере время и идентификатор заданы явно, чтобы результат можно было повторить. В рабочем приложении логгер обычно добавляет время сам.</p>\n<pre><code>const required = [\n 'timestamp', 'level', 'service', 'environment',\n 'event', 'request_id', 'message'\n];\n\nfunction buildEvent(base, local) {\n const event = { ...base, ...local };\n const missing = required.filter((name) =&gt; event[name] === undefined);\n if (missing.length) {\n throw new Error('missing log fields: ' + missing.join(', '));\n }\n return event;\n}\n\nconst entry = buildEvent(\n {\n timestamp: '2020-07-14T09:30:11.042Z',\n level: 'info',\n service: 'demo-catalog-api',\n environment: 'training',\n event: 'http.request.completed',\n request_id: 'req-demo-20200714-01',\n message: 'Synthetic request completed'\n },\n { http: { method: 'POST', route: '/training/orders/:orderId', status_code: 202 } }\n);\n\nprocess.stdout.write(JSON.stringify(entry) + '\\n');</code></pre>\n<p><code>route</code> здесь — шаблон, а не полный URL с номером заказа. Так сто заказов не создают сто новых значений маршрута. Имя <code>event</code> тоже выбирают из небольшого словаря: <code>http.request.received</code>, <code>order.validation.failed</code>, <code>http.request.completed</code>. Не включайте в имя номер ошибки, текст исключения или идентификатор пользователя. Поле перестанет быть пригодным для группировки.</p>\n<p>Проверка формы и проверка цепочки — разные проверки. Первая смотрит один объект: есть ли ключи, допустим ли уровень, замаскированы ли чувствительные значения. Вторая смотрит несколько событий: совпадает ли <code>request_id</code>, есть ли ожидаемые начало и завершение, не пропал ли контекст на границе сервиса. Если первая проверка падает, исправляют builder. Если вторая — место передачи контекста.</p>\n<h2>Один синтетический запрос</h2>\n<p>Ниже приведены учебные записи. Они не взяты из production, не описывают реального пользователя и не доказывают работу конкретной системы. Их задача — показать минимальный запрос, который можно выбрать по одному ключу.</p>\n<pre><code>{\"timestamp\":\"2020-07-14T09:30:11.001Z\",\"level\":\"info\",\"service\":\"demo-gateway\",\"environment\":\"training\",\"event\":\"http.request.received\",\"request_id\":\"req-demo-20200714-01\",\"message\":\"Synthetic request accepted\"}\n{\"timestamp\":\"2020-07-14T09:30:11.021Z\",\"level\":\"info\",\"service\":\"demo-catalog-api\",\"environment\":\"training\",\"event\":\"order.validation.completed\",\"request_id\":\"req-demo-20200714-01\",\"message\":\"Synthetic order passed validation\"}\n{\"timestamp\":\"2020-07-14T09:30:11.042Z\",\"level\":\"info\",\"service\":\"demo-gateway\",\"environment\":\"training\",\"event\":\"http.request.completed\",\"request_id\":\"req-demo-20200714-01\",\"message\":\"Synthetic request completed\"}\n\n# Учебный поиск в JSONL, не production-команда:\njq -c 'select(.request_id == \"req-demo-20200714-01\")' training.jsonl</code></pre>\n<p>Ожидаемый результат содержит три записи и один идентификатор. Если запись API отсутствует, проверяют передачу контекста и наличие события в этом модуле. Если API пишет другой идентификатор, ищут вторую генерацию на исходящем вызове. Если завершение повторяется, проверяют повторный вызов обработчика. Эти выводы ограничены учебным набором. По ним нельзя утверждать, что все реальные сервисы системы пишут логи.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Короткий маршрут диагностики</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Ошибка есть, но запрос не найти</td><td>Нет общего <code>request_id</code></td><td>Сравнить обязательные поля у соседних событий</td><td>Назначить владельца идентификатора на HTTP-входе</td></tr><tr><td>Один модуль виден, второй нет</td><td>Контекст не передан через HTTP-клиент</td><td>Проверить заголовок и событие на обеих сторонах</td><td>Передавать контекст явно в сигнатуре адаптера</td></tr><tr><td>JSON валиден, поиск ломается</td><td><code>requestId</code> вместо <code>request_id</code> или свободное имя события</td><td>Проверить ключи и словарь событий</td><td>Исправить builder и добавить проверку схемы</td></tr><tr><td>Лог содержит токен или cookie</td><td>Сериализовали сырой объект запроса</td><td>Проверить запись до stdout и fixture redaction</td><td>Исключить поле или заменить значение до JSON</td></tr><tr><td>События рядом по времени, но связь не доказана</td><td>Время ошибочно приняли за корреляцию</td><td>Найти общий идентификатор в каждой записи</td><td>Не делать вывод без ключа; для причинности нужна трассировка</td></tr></tbody></table>\n<h2>Не теряем request_id на границах</h2>\n<p>Идентификатор создаёт или принимает первая HTTP-граница. Она проверяет длину и допустимые символы. Внутренний формат учебного примера — <code>req-demo-YYYYMMDD-NN</code>. В рабочем коде формат может быть другим, но владельцем остаётся один слой. Клиентский заголовок нельзя без проверки копировать в журнал: он может быть слишком длинным, содержать управляющие символы или использоваться для загрязнения поиска.</p>\n<p>После входа создают дочерний логгер с общими полями и передают его в следующий модуль. Не храните текущий идентификатор в глобальной переменной. Два параллельных запроса перезапишут значение. Не генерируйте новый ключ в адаптере. Иначе каждая запись будет выглядеть аккуратно, но цепочка распадётся.</p>\n<p>Обработчик ошибки должен использовать тот же контекст. Частая ошибка — создать новый логгер внутри <code>catch</code> и записать стек без <code>request_id</code>. Для диагностики это самое дорогое место: именно важное событие выпадает из поиска. Сериализуйте нормализованный код ошибки и короткое сообщение. Полный объект исключения может содержать запрос, заголовки и данные внешней системы.</p>\n<p>Фоновая задача требует отдельного решения. Она может продолжать исходное действие, а может быть новым действием. Механически копировать <code>request_id</code> нельзя: это создаёт ложную непрерывную трассу. Если связь нужна, храните её как явную ссылку, а для попытки используйте отдельный <code>operation_id</code>. В этой статье фоновая очередь не моделируется.</p>\n<h2>Что нельзя смешивать</h2>\n<p><code>request_id</code> полезен для поиска одной истории. Он высококардинален и плохо подходит для графика. <code>event</code> и <code>service</code> имеют ограниченный словарь и подходят для подсчёта повторяющихся случаев. Не превращайте уникальный идентификатор в имя метрики или тег каждого агрегата.</p>\n<p>Корреляция не равна причинности. Одинаковый ключ показывает принадлежность одному входному действию, но не показывает точный порядок параллельных операций. Часы машин могут расходиться. Отложенная запись может появиться позже. Если нужен критический путь, дочерние операции или межсервисные задержки, нужна отдельная модель трассировки. Сортировка строк по времени эту модель не создаёт.</p>\n<p>Redaction выполняют до сериализации. Маска в интерфейсе просмотра недостаточна: секрет уже мог попасть в файл, транспорт или резервную копию. Минимальный список для отдельной проверки — <code>authorization</code>, <code>cookie</code>, <code>password</code>, <code>token</code> и <code>secret</code>. Реальный список зависит от приложения и должен жить рядом с кодом формирования записи.</p>\n<h2>Порядок действий</h2>\n<ol><li>Выберите один HTTP-маршрут и назовите владельца <code>request_id</code>.</li><li>Зафиксируйте обязательные поля и словарь из нескольких имён событий.</li><li>Добавьте валидатор идентификатора и явное поведение для отсутствующего или неверного входного значения.</li><li>Создайте дочерний логгер на входе и передайте контекст через исходящий клиент.</li><li>Соберите три синтетические записи от двух модулей с одним ключом.</li><li>Проверьте форму каждого объекта и поиск всей цепочки в JSONL.</li><li>Добавьте redaction до сериализации и отдельную отрицательную проверку для секретного поля.</li><li>Только после успешной проверки перенесите контракт на соседний маршрут.</li></ol>\n<h2>Ограничения и критерий готовности</h2>\n<p>Минимальный контракт не показывает работу сервиса, полноту журнала, доставку записи, задержку коллектора или реальный пользовательский эффект. Учебный fixture не читает production-логи, не отправляет сеть и не устанавливает причину инцидента. Синтетические значения нельзя выдавать за измеренные результаты.</p>\n<p>Практика готова, когда разрешённый учебный запрос возвращает все ожидаемые события по одному <code>request_id</code>; каждый объект проходит проверку обязательных полей; отрицательный тест обнаруживает потерю или замену ключа; redaction не выпускает заданные чувствительные значения; команда может назвать границу, на которой нужно искать пропажу контекста. Если хотя бы один пункт не выполнен, контракт ещё не даёт проверяемой диагностики.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.rfc-editor.org/rfc/rfc5424.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 5424: The Syslog Protocol</a> — описывает структурированные данные syslog.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc8259.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format</a> — фиксирует формат JSON.</li><li><a href=\"https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP Logging Cheat Sheet</a> — перечисляет практики безопасного журналирования и данные, которые нельзя бездумно записывать.</li></ul>"}