ПОЧЕМУ стало ключевым словом, язык поднят до версии 2
- метка обоснования пишется заглавными и вошла в словарь набора: скелет правила теперь целиком из ключевых слов, а не смесь `**ДОЛЖЕН.**` и `**Почему.**`; в переводе на другой язык метка меняется как остальные слова (ПОЧЕМУ / WHY), 235 вхождений заменены - метки правила выделены из шкалы в отдельный перечень: ПОЧЕМУ и МЕХАНИЗИРОВАНО обязательности не задают, а размечают части, и стандартом не даются ни в одном языке — раньше МЕХАНИЗИРОВАНО висело строкой в таблице модальности - версия языка поднята до 2, потому что изменение формы меняет чтение уже написанного текста; строка о версии в двенадцати конвенциях перечисляет теперь и метки, а служебные слова сценария в неё по-прежнему не входят
This commit is contained in:
@@ -9,9 +9,9 @@ extends: arch/time.md
|
||||
спецификация поведения: наблюдаемые требования к логам, входящие в контракт
|
||||
функциональности, живут в спеках.
|
||||
|
||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
||||
только тогда, когда написаны заглавными.
|
||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 2 —
|
||||
тогда и только тогда, когда написаны заглавными.
|
||||
|
||||
Лог читают инструментами, а не глазами: повседневно — `jq`
|
||||
(`jq 'select(.download_id=="a1b2")' app.jsonl`), тяжёлое (агрегации, JOIN) —
|
||||
@@ -29,7 +29,7 @@ DuckDB поверх JSONL прямо из файла. Отсюда почти в
|
||||
**ДОЛЖЕН.** Хендлер — `slog.JSONHandler`, одинаково в разработке и в
|
||||
проде.
|
||||
|
||||
**Почему.** Довод не в том, что текстовый вывод «расходит поля»: смена
|
||||
**ПОЧЕМУ.** Довод не в том, что текстовый вывод «расходит поля»: смена
|
||||
хендлера структуру атрибутов не меняет. Довод в читателе — с текстовым
|
||||
dev-выводом перестаёшь ежедневно гонять собственные `jq`-пайплайны, и
|
||||
поломки словаря (опечатка в имени поля, потерянный атрибут, склеенное
|
||||
@@ -40,7 +40,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
|
||||
**ДОЛЖЕН.** Каждая величина — отдельный ключ со значением своего типа.
|
||||
|
||||
**Почему.** Фильтрация и агрегация работают по ключам; величина, вклеенная
|
||||
**ПОЧЕМУ.** Фильтрация и агрегация работают по ключам; величина, вклеенная
|
||||
в текст, достаётся только регуляркой, а регулярка ломается при первой же
|
||||
правке формулировки. Тип важен отдельно от ключа: число внутри строки не
|
||||
сравнивается и не суммируется, то есть попадает в лог, но не в отчёт.
|
||||
@@ -50,7 +50,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
**ДОЛЖЕН.** `time` приводится к UTC через `ReplaceAttr` по `slog.TimeKey`
|
||||
(см. конвенцию `time`).
|
||||
|
||||
**Почему.** По умолчанию UTC не получится: встроенные хендлеры пишут время
|
||||
**ПОЧЕМУ.** По умолчанию UTC не получится: встроенные хендлеры пишут время
|
||||
в зоне самого `time.Time`, то есть в локальной зоне процесса. Записи одного
|
||||
процесса до и после смены TZ (или записи рядом с данными из БД) перестают
|
||||
складываться в одну хронологию, причём сдвиг на целые часы глазом не виден
|
||||
@@ -68,7 +68,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
**ДОЛЖЕН.** Текст сообщения не собирается из переменных:
|
||||
`log.Info("download accepted", "download_id", id)`.
|
||||
|
||||
**Почему.** `msg` — то, по чему записи группируют и считают. Интерполяция
|
||||
**ПОЧЕМУ.** `msg` — то, по чему записи группируют и считают. Интерполяция
|
||||
превращает одну категорию в множество уникальных строк, и вопрос «сколько
|
||||
раз это случилось» перестаёт решаться группировкой. Нижний регистр — чтобы
|
||||
одна категория не двоилась на варианты, различающиеся только заглавной
|
||||
@@ -79,7 +79,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
**НЕ ДОЛЖЕН.** `recognition done`, а не `recognize: done`; подсистема —
|
||||
отдельное поле.
|
||||
|
||||
**Почему.** Префикс кладёт в текст ровно то, по чему потом фильтруют, и
|
||||
**ПОЧЕМУ.** Префикс кладёт в текст ровно то, по чему потом фильтруют, и
|
||||
фильтр по подсистеме становится сопоставлением с началом строки вместо
|
||||
сравнения значения поля. Заодно это второй способ записать одно и то же:
|
||||
категория дробится на варианты с префиксом и без, а совпадать они обязаны
|
||||
@@ -90,7 +90,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
**ДОЛЖЕН.** `state transition` с полями `from`/`to`/`code`; какое именно
|
||||
состояние и по какой причине — данные, а не текст.
|
||||
|
||||
**Почему.** С отдельной категорией на каждый переход жизненный цикл
|
||||
**ПОЧЕМУ.** С отдельной категорией на каждый переход жизненный цикл
|
||||
сущности собирается перечислением всех известных `msg` — и переход,
|
||||
добавленный в код позже, в это перечисление не попадёт: выборка тихо
|
||||
останется неполной. Единая категория даёт весь цикл одним фильтром и не
|
||||
@@ -101,7 +101,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
**НЕ ДОЛЖЕН.** Запись о действии, сопровождающем переход, не подменяет
|
||||
запись самого перехода.
|
||||
|
||||
**Почему.** Иначе из выборки по SLOG-6 выпадают именно те переходы, у которых
|
||||
**ПОЧЕМУ.** Иначе из выборки по SLOG-6 выпадают именно те переходы, у которых
|
||||
был заметный эффект, — то есть самые интересные. Вторая запись стоит одной
|
||||
строки в логе; восстановление пропущенного перехода не стоит ничего, потому
|
||||
что невозможно.
|
||||
@@ -120,7 +120,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
| SLOG-8.3 | `WARN` | владельцу, «может стать проблемой» |
|
||||
| SLOG-8.4 | `ERROR` | владельцу, в разбор |
|
||||
|
||||
**Почему.** Адресат — единственный признак, по которому разные авторы в
|
||||
**ПОЧЕМУ.** Адресат — единственный признак, по которому разные авторы в
|
||||
разных местах кода выберут уровень одинаково. «Насколько серьёзно» каждый
|
||||
оценивает по-своему, шкала расползается — и вместе с ней теряет смысл
|
||||
базовый порог в проде (SLOG-40), потому что он отсекает уже не то, что
|
||||
@@ -131,7 +131,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
**НЕ ДОЛЖЕН.** Происхождение записи на выбор уровня не влияет: `ERROR`
|
||||
везде одинаково серьёзен.
|
||||
|
||||
**Почему.** Фильтр по уровню собирает записи из всех подсистем сразу. Если
|
||||
**ПОЧЕМУ.** Фильтр по уровню собирает записи из всех подсистем сразу. Если
|
||||
в шумной подсистеме `ERROR` «дешевле», читателю приходится помнить
|
||||
происхождение каждой записи, чтобы понять, надо ли реагировать, — то есть
|
||||
уровень перестаёт быть фильтром и становится подсказкой, требующей знания
|
||||
@@ -141,7 +141,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
|
||||
**ДОЛЖЕН.** Если «может» не про эту запись, уровень — `INFO`.
|
||||
|
||||
**Почему.** `WARN` разбирают вручную и целиком. Как только в нём заводится
|
||||
**ПОЧЕМУ.** `WARN` разбирают вручную и целиком. Как только в нём заводится
|
||||
«ничего страшного», его перестают читать — и вместе с шумом теряется то
|
||||
единственное, ради чего уровень существует: предупреждение, на которое ещё
|
||||
есть время отреагировать.
|
||||
@@ -155,7 +155,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
| SLOG-11.1 | по реальному действию или изменению | `INFO` |
|
||||
| SLOG-11.2 | повторяющаяся служебная, по таймеру или поллингу, сама по себе события не несущая (healthcheck, опрос статуса, авто-рефреш UI) | `DEBUG` |
|
||||
|
||||
**Почему.** `INFO` — аудит постфактум (SLOG-8.2), и его пригодность
|
||||
**ПОЧЕМУ.** `INFO` — аудит постфактум (SLOG-8.2), и его пригодность
|
||||
определяется долей записей, за которыми что-то стоит. Периодическая
|
||||
операция даёт ровный поток при нулевой информации, в котором настоящие
|
||||
события тонут количественно: их не отфильтровать, потому что фильтровать
|
||||
@@ -166,7 +166,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
**ДОЛЖЕН.** Фатальный сбой на старте пишется как `ERROR` и завершает процесс
|
||||
ненулевым кодом.
|
||||
|
||||
**Почему.** `slog` не разделяет CRITICAL и FATAL, поэтому недостающую степень
|
||||
**ПОЧЕМУ.** `slog` не разделяет CRITICAL и FATAL, поэтому недостающую степень
|
||||
выражает не уровень записи, а сам факт завершения. Супервизор (docker,
|
||||
journald, systemd) отличает падение от штатной остановки по коду возврата, а
|
||||
не по уровню последней записи. Процесс, который написал `ERROR` и продолжил
|
||||
@@ -180,7 +180,7 @@ journald, systemd) отличает падение от штатной оста
|
||||
|
||||
**ДОЛЖЕН.** Не `mediaType`/`media`/`media_type` вперемешку.
|
||||
|
||||
**Почему.** Имя поля — и есть интерфейс запроса к логам. Второе имя для той
|
||||
**ПОЧЕМУ.** Имя поля — и есть интерфейс запроса к логам. Второе имя для той
|
||||
же величины делает любую выборку по ней молча неполной: фильтр отработает,
|
||||
часть записей в него не попадёт, и заметить это можно, только заранее зная,
|
||||
что они должны были быть.
|
||||
@@ -194,7 +194,7 @@ journald, systemd) отличает падение от штатной оста
|
||||
| SLOG-14.1 | бизнес-поле | плоский `snake_case`: `download_id`, `media_type` |
|
||||
| SLOG-14.2 | системный домен | точечная иерархия (адаптация OpenTelemetry): `http.*`, `ext.*` |
|
||||
|
||||
**Почему.** Точка отделяет поля, приходящие от инфраструктуры и одинаковые
|
||||
**ПОЧЕМУ.** Точка отделяет поля, приходящие от инфраструктуры и одинаковые
|
||||
в любом проекте, от доменных, которые в каждом свои: по общему префиксу
|
||||
запрос «все внешние вызовы» пишется без перечисления имён. Заимствование
|
||||
словаря OpenTelemetry снимает необходимость изобретать имена тому, что уже
|
||||
@@ -205,7 +205,7 @@ journald, systemd) отличает падение от штатной оста
|
||||
**НЕ ДОЛЖЕН.** Вложенных объектов в записи нет; точка в имени — часть
|
||||
имени, а не уровень вложенности.
|
||||
|
||||
**Почему.** Плоский ключ адресуется одинаково в `jq`, в DuckDB и в любой
|
||||
**ПОЧЕМУ.** Плоский ключ адресуется одинаково в `jq`, в DuckDB и в любой
|
||||
записи независимо от её категории. Вложенность требует знать глубину
|
||||
заранее, а она у разных категорий разная — и один запрос перестаёт покрывать
|
||||
весь лог, распадаясь на запрос под каждую форму записи.
|
||||
@@ -221,7 +221,7 @@ journald, systemd) отличает падение от штатной оста
|
||||
| SLOG-16.3 | запись об ошибке | `error` |
|
||||
| SLOG-16.4 | вызов внешнего сервиса | `ext.service`, `ext.operation` (логическая операция, не URL), `ext.status_code`, `duration_ms`, `retry` |
|
||||
|
||||
**Почему.** Набор задан не «на всякий случай»: без него запись не отвечает
|
||||
**ПОЧЕМУ.** Набор задан не «на всякий случай»: без него запись не отвечает
|
||||
на свой вопрос. HTTP-запись без `duration_ms` не показывает деградацию,
|
||||
`ext`-запись без `ext.service` не отделяет «легла зависимость» от «у нас
|
||||
баг», запись о сущности без идентификатора не корреллируется (SLOG-19). Полный
|
||||
@@ -233,7 +233,7 @@ journald, systemd) отличает падение от штатной оста
|
||||
**НЕ СЛЕДУЕТ.** Поле, значение которого одинаково во всех записях, не
|
||||
заводится — для одного бинаря на одном хосте это `service.*` и `host.*`.
|
||||
|
||||
**Почему.** Такое поле не несёт информации, но стоит места в каждой строке
|
||||
**ПОЧЕМУ.** Такое поле не несёт информации, но стоит места в каждой строке
|
||||
и внимания при чтении. Критерий один на все поля словаря — им же решается,
|
||||
нужен ли `transport` (SLOG-16.1): пока транспорт один, поле постоянно. Условие
|
||||
названо явно, поэтому правило отпадёт вместе со своей причиной: с
|
||||
@@ -248,7 +248,7 @@ journald, systemd) отличает падение от штатной оста
|
||||
сущностей есть стабильные уникальные идентификаторы. (Как их выбирают —
|
||||
конвенция `db-identifiers`, если взята.)
|
||||
|
||||
**Почему.** Идентификатор сущности уже существует, стабилен между
|
||||
**ПОЧЕМУ.** Идентификатор сущности уже существует, стабилен между
|
||||
процессами и во времени — по нему собираются записи не одного прохода, а
|
||||
всей истории сущности, включая вчерашнюю. `trace_id` даёт то же самое
|
||||
только внутри одной операции, то есть дублирует ключ и добавляет второй
|
||||
@@ -259,7 +259,7 @@ journald, systemd) отличает падение от штатной оста
|
||||
|
||||
**ДОЛЖЕН.** Поле `<entity>_id` в каждой записи, относящейся к сущности.
|
||||
|
||||
**Почему.** Принадлежность записи восстанавливается только в момент
|
||||
**ПОЧЕМУ.** Принадлежность записи восстанавливается только в момент
|
||||
записи; постфактум её не вывести — остаётся воспроизводить инцидент заново.
|
||||
Это же условие, при котором работает SLOG-18: отказ от `trace_id` оплачен тем,
|
||||
что идентификатор стоит везде, а не в удобных местах.
|
||||
@@ -279,7 +279,7 @@ log := log.With("download_id", id)
|
||||
ctx = logctx.With(ctx, log) // достаём логгер из ctx в каждой стадии
|
||||
```
|
||||
|
||||
**Почему.** Ручное дописывание ключа пропускают не в основном сценарии, а в
|
||||
**ПОЧЕМУ.** Ручное дописывание ключа пропускают не в основном сценарии, а в
|
||||
редких ветках — обработке ошибок и ранних выходах, где корреляция нужнее
|
||||
всего. Логгер из контекста дописывает ключ сам, и запись без
|
||||
идентификатора становится невозможной, а не маловероятной.
|
||||
@@ -290,7 +290,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
|
||||
**ДОЛЖЕН.** `log.Error("layout failed", "error", err, "download_id", id)`.
|
||||
|
||||
**Почему.** Ошибка, вклеенная в текст сообщения, дробит категорию (SLOG-4) и
|
||||
**ПОЧЕМУ.** Ошибка, вклеенная в текст сообщения, дробит категорию (SLOG-4) и
|
||||
уносит текст туда, где по нему нельзя отфильтровать. Ключ единый — так же,
|
||||
как по умолчанию в zap/zerolog: выборка «все записи с ошибкой» не должна
|
||||
зависеть от того, кто писал конкретный вызов, и ради этого единообразия
|
||||
@@ -301,7 +301,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
**НЕ ДОЛЖЕН.** Слой, возвращающий ошибку выше, её не логирует — только
|
||||
оборачивает (`%w`).
|
||||
|
||||
**Почему.** Иначе один сбой даёт столько записей, сколько слоёв он прошёл,
|
||||
**ПОЧЕМУ.** Иначе один сбой даёт столько записей, сколько слоёв он прошёл,
|
||||
и количество `ERROR` перестаёт соответствовать количеству отказов — а
|
||||
считают именно его. Контекст при этом не теряется: он накапливается в
|
||||
цепочке обёрток и попадает в единственную запись на границе (SLOG-23).
|
||||
@@ -310,7 +310,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
|
||||
**ДОЛЖЕН.** Логирует единый чокпоинт, определяющий исход операции.
|
||||
|
||||
**Почему.** У ошибки нужен ровно один логирующий, иначе неизбежны дубли; и
|
||||
**ПОЧЕМУ.** У ошибки нужен ровно один логирующий, иначе неизбежны дубли; и
|
||||
этим местом выбрана доменная граница, а не транспорт, потому что там
|
||||
известен исход операции целиком и, значит, класс отказа (SLOG-25) — транспорт
|
||||
знает лишь то, что ему вернули ошибку. Побочный эффект того же выбора:
|
||||
@@ -321,7 +321,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
**НЕ ДОЛЖЕН.** Транспорт переводит возвращённую ошибку в свой ответ
|
||||
(статус, сообщение пользователю) и на этом останавливается.
|
||||
|
||||
**Почему.** Запись уже сделана на границе (SLOG-23); вторая отличается от неё
|
||||
**ПОЧЕМУ.** Запись уже сделана на границе (SLOG-23); вторая отличается от неё
|
||||
только формулировкой и читается как второй сбой. Когда транспортов над
|
||||
одним доменом несколько, дублирование ещё и множится, а расследование
|
||||
начинается с вопроса, один это инцидент или два.
|
||||
@@ -338,7 +338,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
| SLOG-25.2 | расхождение производного или учётного состояния, первичные данные целы | владельцу, «может стать проблемой» | `WARN` |
|
||||
| SLOG-25.3 | сбой БД, ФС, недоступность зависимости | владельцу, в разбор | `ERROR` |
|
||||
|
||||
**Почему.** Это применение SLOG-8 к отказам: пользователь уже увидел причину на
|
||||
**ПОЧЕМУ.** Это применение SLOG-8 к отказам: пользователь уже увидел причину на
|
||||
экране — владельцу разбирать нечего; целостность первичных данных отделяет
|
||||
«надо посмотреть» от «надо чинить сейчас». Привязка к месту дала бы разный
|
||||
уровень для одного и того же отказа в зависимости от того, какой транспорт
|
||||
@@ -359,7 +359,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
владельцу как деградация автоматики: коллизия в ручном действии — `DEBUG`,
|
||||
она же в авто-обработке — `WARN`.
|
||||
|
||||
**Почему.** В ручном действии человек видит причину на экране и сам решает,
|
||||
**ПОЧЕМУ.** В ручном действии человек видит причину на экране и сам решает,
|
||||
что делать дальше; запись нужна только для отладки. В автоматике не увидел
|
||||
никто, задача осталась недоведённой, и лог — единственное место, где это
|
||||
вообще проявится.
|
||||
@@ -369,7 +369,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
**ДОЛЖЕН.** Тот же класс сбоя внутри синхронной операции — `ERROR`:
|
||||
уровень задаёт наличие штатного повтора, а не текст ошибки.
|
||||
|
||||
**Почему.** Одиночный промах тика транзиентен — следующий тик повторит, и
|
||||
**ПОЧЕМУ.** Одиночный промах тика транзиентен — следующий тик повторит, и
|
||||
вмешательство не требуется; `ERROR` на каждый такой промах обесценивает
|
||||
уровень, на который смотрят в первую очередь. Синхронная операция повтора
|
||||
не имеет: она провалилась целиком, результат никто не восстановит, и это
|
||||
@@ -381,7 +381,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
|
||||
**ДОЛЖЕН.** Все вызовы, включая успешные; поля — по SLOG-16.4.
|
||||
|
||||
**Почему.** Это единственный способ отличить «у нас баг» от «зависимость
|
||||
**ПОЧЕМУ.** Это единственный способ отличить «у нас баг» от «зависимость
|
||||
легла»: на своей стороне видно лишь то, что операция не удалась.
|
||||
Выборочное логирование ломает и второе применение — доля неуспехов и
|
||||
распределение `duration_ms` считаются, только если знаменатель полный.
|
||||
@@ -397,7 +397,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
| SLOG-29.3 | попытка не удалась, делается retry | `WARN` |
|
||||
| SLOG-29.4 | ретраи исчерпаны, сервис недоступен | `ERROR` |
|
||||
|
||||
**Почему.** Неудачная попытка, за которой следует повтор, — ещё не отказ:
|
||||
**ПОЧЕМУ.** Неудачная попытка, за которой следует повтор, — ещё не отказ:
|
||||
операция может завершиться успешно, и `ERROR` на каждую попытку сделал бы
|
||||
уровень непригодным для главного вопроса «зависимость доступна?».
|
||||
Исчерпание ретраев и есть момент, когда транспорт сдался и дальше
|
||||
@@ -431,7 +431,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
(`ext.status_code` записан); решение «это ошибка» принимает доменный
|
||||
вызывающий.
|
||||
|
||||
**Почему.** Транспорт своё дело сделал: запрос доставлен, ответ получен и
|
||||
**ПОЧЕМУ.** Транспорт своё дело сделал: запрос доставлен, ответ получен и
|
||||
разобран. Классифицировать 4xx как сбой транспорта значит смешать «сервис
|
||||
недоступен» с «сервис ответил нам нет» — это разные инциденты с разной
|
||||
реакцией, и различает их как раз `ext`-уровень. Что 404 значит для
|
||||
@@ -444,7 +444,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
|
||||
**ДОЛЖЕН.** Поля по SLOG-16.1; 4xx остаётся `INFO`-записью доступа.
|
||||
|
||||
**Почему.** Это аудит обращений, а не отладка: запись отвечает на «кто и
|
||||
**ПОЧЕМУ.** Это аудит обращений, а не отладка: запись отвечает на «кто и
|
||||
когда приходил», и ценность у неё одинаковая при любом коде ответа.
|
||||
Уровень, зависящий от кода, делает аудит неполным именно на тех запросах,
|
||||
которые чаще всего разбирают. Доменную оценку исхода даёт отдельная запись
|
||||
@@ -454,7 +454,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
|
||||
**ДОПУСКАЕТСЯ.** Это отдельный слой от корреляции по сущности.
|
||||
|
||||
**Почему.** Явное разрешение снимает вопрос, не запрещает ли `request_id`
|
||||
**ПОЧЕМУ.** Явное разрешение снимает вопрос, не запрещает ли `request_id`
|
||||
правило SLOG-18. Не запрещает: SLOG-18 отказывается от случайного ключа там, где
|
||||
уже есть стабильный идентификатор сущности, а у HTTP-запроса собственной
|
||||
сущности нет — связать его записи между собой больше нечем.
|
||||
@@ -463,7 +463,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
|
||||
**ДОЛЖЕН.** Периодические проверки живости пишутся на отладочном уровне.
|
||||
|
||||
**Почему.** Частный случай SLOG-11.2, названный отдельно, потому что нарушают
|
||||
**ПОЧЕМУ.** Частный случай SLOG-11.2, названный отдельно, потому что нарушают
|
||||
его чаще всего: проверку дёргают по таймеру, и на `INFO` она вытесняет из
|
||||
аудита всё остальное — в проде с базовым `INFO` (SLOG-40) лог превратился бы в
|
||||
опрос самого себя. На `DEBUG` она не пишется вовсе и при этом остаётся
|
||||
@@ -477,7 +477,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
API-ключи и токены, `Authorization`-заголовки, аутентификационные параметры
|
||||
в ссылках.
|
||||
|
||||
**Почему.** Лог уезжает целиком в чужое хранилище, читается шире, чем код,
|
||||
**ПОЧЕМУ.** Лог уезжает целиком в чужое хранилище, читается шире, чем код,
|
||||
и переживает ротацию самого секрета. Попавший в него секрет скомпрометирован
|
||||
с момента записи, а не с момента, когда это заметили, и вычистить его задним
|
||||
числом из уже собранных копий нельзя.
|
||||
@@ -487,7 +487,7 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
||||
**ДОЛЖЕН.** Тела запросов и ответов внешних API, сырой вывод LLM —
|
||||
`DEBUG`, с вычисткой секретов и обрезкой по длине.
|
||||
|
||||
**Почему.** Содержимое пришло снаружи: размер не ограничен, состав
|
||||
**ПОЧЕМУ.** Содержимое пришло снаружи: размер не ограничен, состав
|
||||
неизвестен, а секрет в нём возможен по недосмотру той стороны. `DEBUG`
|
||||
выключен в проде (SLOG-40), поэтому цена ошибки ограничена отладочной сессией;
|
||||
обрезка не даёт одной записи вытеснить весь остальной лог за период.
|
||||
@@ -496,7 +496,7 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
||||
|
||||
**СЛЕДУЕТ.** `"has_api_key", true` вместо самого значения.
|
||||
|
||||
**Почему.** Для отладки почти всегда достаточно ответа «значение было или
|
||||
**ПОЧЕМУ.** Для отладки почти всегда достаточно ответа «значение было или
|
||||
не было» — потеря полезности близка к нулю, а риск снимается целиком.
|
||||
Правило нужно потому, что решение принимается в момент написания строки,
|
||||
когда чувствительность значения ещё неочевидна, а перечитывать этот выбор
|
||||
@@ -507,7 +507,7 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
||||
**ДОЛЖЕН.** Ошибка разворачивается в первопричину **до** лога и до
|
||||
обёртки — раньше трансляции в доменную (конвенция `errors`).
|
||||
|
||||
**Почему.** `*url.Error` встраивает полный URL запроса, а секрет живёт
|
||||
**ПОЧЕМУ.** `*url.Error` встраивает полный URL запроса, а секрет живёт
|
||||
прямо в нём: токен в пути, `api_key` в query. Go редактирует только пароль
|
||||
из userinfo, остального не трогает, поэтому ошибка уносит секрет и в
|
||||
обёртку, и в лог целиком. Порядок — часть нормы: санитизация после
|
||||
@@ -521,7 +521,7 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
||||
**НЕ ДОЛЖЕН.** Аутентификация параметром ссылки — только когда другого
|
||||
способа нет.
|
||||
|
||||
**Почему.** Секрет в URL попадает не только в ошибку транспорта (SLOG-37), но и
|
||||
**ПОЧЕМУ.** Секрет в URL попадает не только в ошибку транспорта (SLOG-37), но и
|
||||
в любую запись, куда URL попал целиком, — то есть обязывает помнить про
|
||||
санитизацию в каждой такой точке, и одна забытая сводит остальные на нет.
|
||||
Заголовок снимает задачу в источнике: чего нет в URL, того нет и в ошибке.
|
||||
@@ -533,7 +533,7 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
||||
**ДОЛЖЕН.** Сбор и ротацию делает окружение (docker, journald); по файлам
|
||||
не маршрутизируем.
|
||||
|
||||
**Почему.** Приложение, которое само решает, что куда писать, дублирует
|
||||
**ПОЧЕМУ.** Приложение, которое само решает, что куда писать, дублирует
|
||||
работу супервизора и расходится с ней при первой же смене окружения: срок
|
||||
хранения, сжатие и ротация оказываются настроены в двух местах и по-разному.
|
||||
Один поток вдобавок сохраняет порядок записей — маршрутизация по файлам
|
||||
@@ -543,7 +543,7 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
||||
|
||||
**ДОЛЖЕН.** `DEBUG` в проде включается конфигом.
|
||||
|
||||
**Почему.** Уровень — единственный регулятор объёма, доступный без
|
||||
**ПОЧЕМУ.** Уровень — единственный регулятор объёма, доступный без
|
||||
пересборки; если `DEBUG` в проде включается только правкой кода, его не
|
||||
включают, и разбор инцидента идёт вслепую. `INFO` выбран базовым потому,
|
||||
что на нём аудит полон (SLOG-8.2), а рутинно-частое уже отсечено (SLOG-11.2).
|
||||
|
||||
Reference in New Issue
Block a user