заведён реестр префиксов, правила канона перенумерованы
- идентификатор правила теперь `<ПРЕФИКС>-<номер>` вместо `R<номер>`: префикс уникален по всему канону, поэтому ссылка больше не требует пути к файлу и не зависит от того, на какой оси файл лежит - префикс выбирается под файл, а не выводится по формуле, и хранится в conventions/prefixes.toml вместе с выбывшими; номера сохранены один в один вместе с дырами
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
---
|
||||
prefix: SLOG
|
||||
extends: arch/time.md
|
||||
---
|
||||
|
||||
@@ -19,7 +20,7 @@ DuckDB поверх JSONL прямо из файла. Отсюда почти в
|
||||
|
||||
## Формат записи
|
||||
|
||||
### R1. Структурированный JSON, один формат для dev и prod
|
||||
### SLOG-1. Структурированный JSON, один формат для dev и prod
|
||||
|
||||
**ДОЛЖЕН.** Хендлер — `slog.JSONHandler`, одинаково в разработке и в
|
||||
проде.
|
||||
@@ -31,7 +32,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
значение) обнаруживаются только в проде, где заметить их заранее уже
|
||||
некому.
|
||||
|
||||
### R2. Данные — в типизированных полях, а не в тексте сообщения
|
||||
### SLOG-2. Данные — в типизированных полях, а не в тексте сообщения
|
||||
|
||||
**ДОЛЖЕН.** Каждая величина — отдельный ключ со значением своего типа.
|
||||
|
||||
@@ -40,7 +41,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
правке формулировки. Тип важен отдельно от ключа: число внутри строки не
|
||||
сравнивается и не суммируется, то есть попадает в лог, но не в отчёт.
|
||||
|
||||
### R3. Время записи — UTC
|
||||
### SLOG-3. Время записи — UTC
|
||||
|
||||
**ДОЛЖЕН.** `time` приводится к UTC через `ReplaceAttr` по `slog.TimeKey`
|
||||
(см. `lang/go/time.md`).
|
||||
@@ -58,7 +59,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
|
||||
## Сообщение
|
||||
|
||||
### R4. `msg` — константа в нижнем регистре
|
||||
### SLOG-4. `msg` — константа в нижнем регистре
|
||||
|
||||
**ДОЛЖЕН.** Текст сообщения не собирается из переменных:
|
||||
`log.Info("download accepted", "download_id", id)`.
|
||||
@@ -69,7 +70,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
одна категория не двоилась на варианты, различающиеся только заглавной
|
||||
буквой.
|
||||
|
||||
### R5. `msg` не несёт префикса подсистемы
|
||||
### SLOG-5. `msg` не несёт префикса подсистемы
|
||||
|
||||
**НЕ ДОЛЖЕН.** `recognition done`, а не `recognize: done`; подсистема —
|
||||
отдельное поле.
|
||||
@@ -80,7 +81,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
категория дробится на варианты с префиксом и без, а совпадать они обязаны
|
||||
посимвольно.
|
||||
|
||||
### R6. Смена состояния сущности — единая категория
|
||||
### SLOG-6. Смена состояния сущности — единая категория
|
||||
|
||||
**ДОЛЖЕН.** `state transition` с полями `from`/`to`/`code`; какое именно
|
||||
состояние и по какой причине — данные, а не текст.
|
||||
@@ -91,37 +92,37 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
останется неполной. Единая категория даёт весь цикл одним фильтром и не
|
||||
требует обновлять запрос вслед за кодом.
|
||||
|
||||
### R7. Физический эффект — отдельная запись, а не вместо перехода
|
||||
### SLOG-7. Физический эффект — отдельная запись, а не вместо перехода
|
||||
|
||||
**НЕ ДОЛЖЕН.** Запись о действии, сопровождающем переход, не подменяет
|
||||
запись самого перехода.
|
||||
|
||||
**Почему.** Иначе из выборки по R6 выпадают именно те переходы, у которых
|
||||
**Почему.** Иначе из выборки по SLOG-6 выпадают именно те переходы, у которых
|
||||
был заметный эффект, — то есть самые интересные. Вторая запись стоит одной
|
||||
строки в логе; восстановление пропущенного перехода не стоит ничего, потому
|
||||
что невозможно.
|
||||
|
||||
## Уровни
|
||||
|
||||
### R8. Уровень выбирается по адресату
|
||||
### SLOG-8. Уровень выбирается по адресату
|
||||
|
||||
**ДОЛЖЕН.** Уровень отвечает на вопрос «кому сообщение», а не «насколько
|
||||
громко сломалось».
|
||||
|
||||
| № | Уровень | Кому и когда |
|
||||
|---|---|---|
|
||||
| R8.1 | `DEBUG` | разработчику при отладке; в проде выключен |
|
||||
| R8.2 | `INFO` | владельцу, аудит постфактум |
|
||||
| R8.3 | `WARN` | владельцу, «может стать проблемой» |
|
||||
| R8.4 | `ERROR` | владельцу, в разбор |
|
||||
| SLOG-8.1 | `DEBUG` | разработчику при отладке; в проде выключен |
|
||||
| SLOG-8.2 | `INFO` | владельцу, аудит постфактум |
|
||||
| SLOG-8.3 | `WARN` | владельцу, «может стать проблемой» |
|
||||
| SLOG-8.4 | `ERROR` | владельцу, в разбор |
|
||||
|
||||
**Почему.** Адресат — единственный признак, по которому разные авторы в
|
||||
разных местах кода выберут уровень одинаково. «Насколько серьёзно» каждый
|
||||
оценивает по-своему, шкала расползается — и вместе с ней теряет смысл
|
||||
базовый порог в проде (R40), потому что он отсекает уже не то, что
|
||||
базовый порог в проде (SLOG-40), потому что он отсекает уже не то, что
|
||||
задумано.
|
||||
|
||||
### R9. Уровень не зависит от подсистемы
|
||||
### SLOG-9. Уровень не зависит от подсистемы
|
||||
|
||||
**НЕ ДОЛЖЕН.** Происхождение записи на выбор уровня не влияет: `ERROR`
|
||||
везде одинаково серьёзен.
|
||||
@@ -132,7 +133,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
уровень перестаёт быть фильтром и становится подсказкой, требующей знания
|
||||
кода.
|
||||
|
||||
### R10. `WARN` — только когда «может стать проблемой»
|
||||
### SLOG-10. `WARN` — только когда «может стать проблемой»
|
||||
|
||||
**ДОЛЖЕН.** Если «может» не про эту запись, уровень — `INFO`.
|
||||
|
||||
@@ -141,22 +142,22 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
единственное, ради чего уровень существует: предупреждение, на которое ещё
|
||||
есть время отреагировать.
|
||||
|
||||
### R11. Событийное — `INFO`, рутинно-частое — `DEBUG`
|
||||
### SLOG-11. Событийное — `INFO`, рутинно-частое — `DEBUG`
|
||||
|
||||
**ДОЛЖЕН.** Уровень зависит от того, стоит ли за операцией событие.
|
||||
|
||||
| № | Операция | Уровень |
|
||||
|---|---|---|
|
||||
| R11.1 | по реальному действию или изменению | `INFO` |
|
||||
| R11.2 | повторяющаяся служебная, по таймеру или поллингу, сама по себе события не несущая (healthcheck, опрос статуса, авто-рефреш UI) | `DEBUG` |
|
||||
| SLOG-11.1 | по реальному действию или изменению | `INFO` |
|
||||
| SLOG-11.2 | повторяющаяся служебная, по таймеру или поллингу, сама по себе события не несущая (healthcheck, опрос статуса, авто-рефреш UI) | `DEBUG` |
|
||||
|
||||
**Почему.** `INFO` — аудит постфактум (R8.2), и его пригодность
|
||||
**Почему.** `INFO` — аудит постфактум (SLOG-8.2), и его пригодность
|
||||
определяется долей записей, за которыми что-то стоит. Периодическая
|
||||
операция даёт ровный поток при нулевой информации, в котором настоящие
|
||||
события тонут количественно: их не отфильтровать, потому что фильтровать
|
||||
приходится по содержанию, а не по уровню.
|
||||
|
||||
### R12. Фатальный сбой на старте — `ERROR` и ненулевой код возврата
|
||||
### SLOG-12. Фатальный сбой на старте — `ERROR` и ненулевой код возврата
|
||||
|
||||
**ДОЛЖЕН.** `slog` не разделяет CRITICAL/FATAL, поэтому недостающую
|
||||
степень даёт завершение процесса.
|
||||
@@ -169,7 +170,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
|
||||
## Поля: единый словарь
|
||||
|
||||
### R13. Одно поле — одно имя по всему коду
|
||||
### SLOG-13. Одно поле — одно имя по всему коду
|
||||
|
||||
**ДОЛЖЕН.** Не `mediaType`/`media`/`media_type` вперемешку.
|
||||
|
||||
@@ -178,14 +179,14 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
часть записей в него не попадёт, и заметить это можно, только заранее зная,
|
||||
что они должны были быть.
|
||||
|
||||
### R14. Форма имени зависит от вида поля
|
||||
### SLOG-14. Форма имени зависит от вида поля
|
||||
|
||||
**ДОЛЖЕН.** Две формы, третьей нет.
|
||||
|
||||
| № | Вид поля | Форма имени |
|
||||
|---|---|---|
|
||||
| R14.1 | бизнес-поле | плоский `snake_case`: `download_id`, `media_type` |
|
||||
| R14.2 | системный домен | точечная иерархия (адаптация OpenTelemetry): `http.*`, `ext.*` |
|
||||
| SLOG-14.1 | бизнес-поле | плоский `snake_case`: `download_id`, `media_type` |
|
||||
| SLOG-14.2 | системный домен | точечная иерархия (адаптация OpenTelemetry): `http.*`, `ext.*` |
|
||||
|
||||
**Почему.** Точка отделяет поля, приходящие от инфраструктуры и одинаковые
|
||||
в любом проекте, от доменных, которые в каждом свои: по общему префиксу
|
||||
@@ -193,7 +194,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
словаря OpenTelemetry снимает необходимость изобретать имена тому, что уже
|
||||
названо, и спорить о них на каждом ревью.
|
||||
|
||||
### R15. Запись плоская
|
||||
### SLOG-15. Запись плоская
|
||||
|
||||
**НЕ ДОЛЖЕН.** Вложенных объектов в записи нет; точка в имени — часть
|
||||
имени, а не уровень вложенности.
|
||||
@@ -203,32 +204,32 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
заранее, а она у разных категорий разная — и один запрос перестаёт покрывать
|
||||
весь лог, распадаясь на запрос под каждую форму записи.
|
||||
|
||||
### R16. Набор полей определяется ситуацией
|
||||
### SLOG-16. Набор полей определяется ситуацией
|
||||
|
||||
**ДОЛЖЕН.** Записи каждой ситуации несут её набор целиком.
|
||||
|
||||
| № | Когда добавляем | Поля |
|
||||
|---|---|---|
|
||||
| R16.1 | входящий HTTP-запрос (middleware) | `http.method`, `http.route`, `http.status_code`, `duration_ms`, `transport` — пока его значение различается между записями (R17) |
|
||||
| R16.2 | работа с сущностью (scoped-логгер) | `<entity>_id` и доменные атрибуты |
|
||||
| R16.3 | запись об ошибке | `error` |
|
||||
| R16.4 | вызов внешнего сервиса | `ext.service`, `ext.operation` (логическая операция, не URL), `ext.status_code`, `duration_ms`, `retry` |
|
||||
| SLOG-16.1 | входящий HTTP-запрос (middleware) | `http.method`, `http.route`, `http.status_code`, `duration_ms`, `transport` — пока его значение различается между записями (SLOG-17) |
|
||||
| SLOG-16.2 | работа с сущностью (scoped-логгер) | `<entity>_id` и доменные атрибуты |
|
||||
| SLOG-16.3 | запись об ошибке | `error` |
|
||||
| SLOG-16.4 | вызов внешнего сервиса | `ext.service`, `ext.operation` (логическая операция, не URL), `ext.status_code`, `duration_ms`, `retry` |
|
||||
|
||||
**Почему.** Набор задан не «на всякий случай»: без него запись не отвечает
|
||||
на свой вопрос. HTTP-запись без `duration_ms` не показывает деградацию,
|
||||
`ext`-запись без `ext.service` не отделяет «легла зависимость» от «у нас
|
||||
баг», запись о сущности без идентификатора не корреллируется (R19). Полный
|
||||
баг», запись о сущности без идентификатора не корреллируется (SLOG-19). Полный
|
||||
набор делает записи однородными — один запрос работает по всем вызовам, а
|
||||
не по тем, где автор вспомнил про поле.
|
||||
|
||||
### R17. `service.*` и `host.*` не заводим
|
||||
### SLOG-17. `service.*` и `host.*` не заводим
|
||||
|
||||
**НЕ СЛЕДУЕТ.** Поле, значение которого одинаково во всех записях, не
|
||||
заводится — для одного бинаря на одном хосте это `service.*` и `host.*`.
|
||||
|
||||
**Почему.** Такое поле не несёт информации, но стоит места в каждой строке
|
||||
и внимания при чтении. Критерий один на все поля словаря — им же решается,
|
||||
нужен ли `transport` (R16.1): пока транспорт один, поле постоянно. Условие
|
||||
нужен ли `transport` (SLOG-16.1): пока транспорт один, поле постоянно. Условие
|
||||
названо явно, поэтому правило отпадёт вместе со своей причиной: с
|
||||
появлением нескольких инстансов различающее поле (`service.version`)
|
||||
добавляется одной строкой при старте.
|
||||
@@ -238,7 +239,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
|
||||
## Корреляция
|
||||
|
||||
### R18. Ключ корреляции — идентификатор сущности, а не `trace_id`
|
||||
### SLOG-18. Ключ корреляции — идентификатор сущности, а не `trace_id`
|
||||
|
||||
**НЕ СЛЕДУЕТ.** Отдельный случайный `trace_id` не заводится, если у
|
||||
сущностей есть стабильные уникальные идентификаторы. (Как их выбирают —
|
||||
@@ -251,13 +252,13 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
способ спросить об одном. Условие применимости названо: там, где сущности
|
||||
со стабильным идентификатором нет, связывать записи больше нечем.
|
||||
|
||||
### R19. Запись о сущности несёт её идентификатор
|
||||
### SLOG-19. Запись о сущности несёт её идентификатор
|
||||
|
||||
**ДОЛЖЕН.** Поле `<entity>_id` в каждой записи, относящейся к сущности.
|
||||
|
||||
**Почему.** Принадлежность записи восстанавливается только в момент
|
||||
записи; постфактум её не вывести — остаётся воспроизводить инцидент заново.
|
||||
Это же условие, при котором работает R18: отказ от `trace_id` оплачен тем,
|
||||
Это же условие, при котором работает SLOG-18: отказ от `trace_id` оплачен тем,
|
||||
что идентификатор стоит везде, а не в удобных местах.
|
||||
|
||||
Все записи одной операции собираются одним фильтром:
|
||||
@@ -265,7 +266,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
||||
глобально уникален across сущностей, штатно работает и простой `grep` по
|
||||
голому значению — он находит все упоминания независимо от имени поля.
|
||||
|
||||
### R20. Долгая операция ведётся scoped-логгером через `context.Context`
|
||||
### SLOG-20. Долгая операция ведётся scoped-логгером через `context.Context`
|
||||
|
||||
**СЛЕДУЕТ.** Логгер с дописанным ключом протаскивается сквозь асинхронные
|
||||
стадии:
|
||||
@@ -282,17 +283,17 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
|
||||
## Ошибки
|
||||
|
||||
### R21. Ошибка логируется атрибутом `error`
|
||||
### SLOG-21. Ошибка логируется атрибутом `error`
|
||||
|
||||
**ДОЛЖЕН.** `log.Error("layout failed", "error", err, "download_id", id)`.
|
||||
|
||||
**Почему.** Ошибка, вклеенная в текст сообщения, дробит категорию (R4) и
|
||||
**Почему.** Ошибка, вклеенная в текст сообщения, дробит категорию (SLOG-4) и
|
||||
уносит текст туда, где по нему нельзя отфильтровать. Ключ единый — так же,
|
||||
как по умолчанию в zap/zerolog: выборка «все записи с ошибкой» не должна
|
||||
зависеть от того, кто писал конкретный вызов, и ради этого единообразия
|
||||
краткостью жертвуют.
|
||||
|
||||
### R22. Промежуточный слой либо логирует, либо возвращает
|
||||
### SLOG-22. Промежуточный слой либо логирует, либо возвращает
|
||||
|
||||
**НЕ ДОЛЖЕН.** Слой, возвращающий ошибку выше, её не логирует — только
|
||||
оборачивает (`%w`).
|
||||
@@ -300,44 +301,44 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
**Почему.** Иначе один сбой даёт столько записей, сколько слоёв он прошёл,
|
||||
и количество `ERROR` перестаёт соответствовать количеству отказов — а
|
||||
считают именно его. Контекст при этом не теряется: он накапливается в
|
||||
цепочке обёрток и попадает в единственную запись на границе (R23).
|
||||
цепочке обёрток и попадает в единственную запись на границе (SLOG-23).
|
||||
|
||||
### R23. Ошибка логируется один раз — на границе доменного слоя
|
||||
### SLOG-23. Ошибка логируется один раз — на границе доменного слоя
|
||||
|
||||
**ДОЛЖЕН.** Логирует единый чокпоинт, определяющий исход операции.
|
||||
|
||||
**Почему.** У ошибки нужен ровно один логирующий, иначе неизбежны дубли; и
|
||||
этим местом выбрана доменная граница, а не транспорт, потому что там
|
||||
известен исход операции целиком и, значит, класс отказа (R25) — транспорт
|
||||
известен исход операции целиком и, значит, класс отказа (SLOG-25) — транспорт
|
||||
знает лишь то, что ему вернули ошибку. Побочный эффект того же выбора:
|
||||
транспорты остаются тонкими.
|
||||
|
||||
<!-- local:границы -->
|
||||
<!-- /local -->
|
||||
|
||||
### R24. Транспорт не логирует ошибку повторно
|
||||
### SLOG-24. Транспорт не логирует ошибку повторно
|
||||
|
||||
**НЕ ДОЛЖЕН.** Транспорт переводит возвращённую ошибку в свой ответ
|
||||
(статус, сообщение пользователю) и на этом останавливается.
|
||||
|
||||
**Почему.** Запись уже сделана на границе (R23); вторая отличается от неё
|
||||
**Почему.** Запись уже сделана на границе (SLOG-23); вторая отличается от неё
|
||||
только формулировкой и читается как второй сбой. Когда транспортов над
|
||||
одним доменом несколько, дублирование ещё и множится, а расследование
|
||||
начинается с вопроса, один это инцидент или два.
|
||||
|
||||
### R25. Уровень доменного отказа — по классу отказа
|
||||
### SLOG-25. Уровень доменного отказа — по классу отказа
|
||||
|
||||
**ДОЛЖЕН.** Уровень выбирает единственный логирующий (R23), и выбирает по
|
||||
**ДОЛЖЕН.** Уровень выбирает единственный логирующий (SLOG-23), и выбирает по
|
||||
классу, а не по месту в коде. Классификация покрывает **доменные** отказы —
|
||||
те, что операция вернула значением `error`.
|
||||
|
||||
| № | Класс отказа | Кому | Уровень |
|
||||
|---|---|---|---|
|
||||
| R25.1 | штатный конфликт состояния или некорректный ввод | пользователю, он уже получил ответ | `DEBUG` |
|
||||
| R25.2 | расхождение производного или учётного состояния, первичные данные целы | владельцу, «может стать проблемой» | `WARN` |
|
||||
| R25.3 | сбой БД, ФС, недоступность зависимости | владельцу, в разбор | `ERROR` |
|
||||
| SLOG-25.1 | штатный конфликт состояния или некорректный ввод | пользователю, он уже получил ответ | `DEBUG` |
|
||||
| SLOG-25.2 | расхождение производного или учётного состояния, первичные данные целы | владельцу, «может стать проблемой» | `WARN` |
|
||||
| SLOG-25.3 | сбой БД, ФС, недоступность зависимости | владельцу, в разбор | `ERROR` |
|
||||
|
||||
**Почему.** Это применение R8 к отказам: пользователь уже увидел причину на
|
||||
**Почему.** Это применение SLOG-8 к отказам: пользователь уже увидел причину на
|
||||
экране — владельцу разбирать нечего; целостность первичных данных отделяет
|
||||
«надо посмотреть» от «надо чинить сейчас». Привязка к месту дала бы разный
|
||||
уровень для одного и того же отказа в зависимости от того, какой транспорт
|
||||
@@ -350,9 +351,9 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
|
||||
Мимо таблицы идёт и доменная ошибка, которой нет в маппинге: класса у неё
|
||||
нет, потому что её просто забыли завести. Она логируется `ERROR` с
|
||||
признаком непокрытой (`lang/go/errors.md` R25).
|
||||
признаком непокрытой (`GERR-25`).
|
||||
|
||||
### R26. Тот же отказ в асинхронной стадии — уровнем выше
|
||||
### SLOG-26. Тот же отказ в асинхронной стадии — уровнем выше
|
||||
|
||||
**ДОЛЖЕН.** Когда пользователь не ждёт результата, отказ адресован
|
||||
владельцу как деградация автоматики: коллизия в ручном действии — `DEBUG`,
|
||||
@@ -363,7 +364,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
никто, задача осталась недоведённой, и лог — единственное место, где это
|
||||
вообще проявится.
|
||||
|
||||
### R27. Повторяющийся сбой фонового цикла — `WARN`
|
||||
### SLOG-27. Повторяющийся сбой фонового цикла — `WARN`
|
||||
|
||||
**ДОЛЖЕН.** Тот же класс сбоя внутри синхронной операции — `ERROR`:
|
||||
уровень задаёт наличие штатного повтора, а не текст ошибки.
|
||||
@@ -376,32 +377,32 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
|
||||
## Внешние сервисы
|
||||
|
||||
### R28. Каждый вызов внешнего сервиса логируется
|
||||
### SLOG-28. Каждый вызов внешнего сервиса логируется
|
||||
|
||||
**ДОЛЖЕН.** Все вызовы, включая успешные; поля — по R16.4.
|
||||
**ДОЛЖЕН.** Все вызовы, включая успешные; поля — по SLOG-16.4.
|
||||
|
||||
**Почему.** Это единственный способ отличить «у нас баг» от «зависимость
|
||||
легла»: на своей стороне видно лишь то, что операция не удалась.
|
||||
Выборочное логирование ломает и второе применение — доля неуспехов и
|
||||
распределение `duration_ms` считаются, только если знаменатель полный.
|
||||
|
||||
### R29. Уровень `ext`-записи — по исходу вызова
|
||||
### SLOG-29. Уровень `ext`-записи — по исходу вызова
|
||||
|
||||
**ДОЛЖЕН.** Исход считается по одному вызову с его ретраями.
|
||||
|
||||
| № | Исход | Уровень |
|
||||
|---|---|---|
|
||||
| R29.1 | успешный событийный вызов | `INFO` |
|
||||
| R29.2 | успешный рутинно-частый вызов (поллинг, авто-рефреш) | `DEBUG` |
|
||||
| R29.3 | попытка не удалась, делается retry | `WARN` |
|
||||
| R29.4 | ретраи исчерпаны, сервис недоступен | `ERROR` |
|
||||
| SLOG-29.1 | успешный событийный вызов | `INFO` |
|
||||
| SLOG-29.2 | успешный рутинно-частый вызов (поллинг, авто-рефреш) | `DEBUG` |
|
||||
| SLOG-29.3 | попытка не удалась, делается retry | `WARN` |
|
||||
| SLOG-29.4 | ретраи исчерпаны, сервис недоступен | `ERROR` |
|
||||
|
||||
**Почему.** Неудачная попытка, за которой следует повтор, — ещё не отказ:
|
||||
операция может завершиться успешно, и `ERROR` на каждую попытку сделал бы
|
||||
уровень непригодным для главного вопроса «зависимость доступна?».
|
||||
Исчерпание ретраев и есть момент, когда транспорт сдался и дальше
|
||||
разбираться владельцу. Различение R29.1 и R29.2 — то же самое разделение
|
||||
событийного и рутинного, что в R11: поллинг внешнего сервиса зашумляет
|
||||
разбираться владельцу. Различение SLOG-29.1 и SLOG-29.2 — то же самое разделение
|
||||
событийного и рутинного, что в SLOG-11: поллинг внешнего сервиса зашумляет
|
||||
аудит так же, как любой другой.
|
||||
|
||||
## Два цикла повтора — не путать
|
||||
@@ -412,8 +413,10 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
уровень доменной записи об исходе тика.
|
||||
|
||||
```
|
||||
WHEN зависимость недоступна и ретраи вызова исчерпаны → ext-запись `ERROR` (R29.4)
|
||||
AND тик фонового цикла упал по той же причине → доменная запись `WARN` (R27)
|
||||
WHEN зависимость недоступна и ретраи вызова исчерпаны
|
||||
→ ext-запись `ERROR` (SLOG-29.4)
|
||||
AND тик фонового цикла упал по той же причине
|
||||
→ доменная запись `WARN` (SLOG-27)
|
||||
```
|
||||
|
||||
Из этого следует, что у лежащей зависимости `ext`-запись пишет `ERROR`
|
||||
@@ -422,7 +425,7 @@ AND тик фонового цикла упал по той же причине
|
||||
`ERROR` от поллинга мешает — это лечится понижением частоты тика или
|
||||
подавлением повторов в самом клиенте, а не переклассификацией уровня.
|
||||
|
||||
### R30. Ответ 4xx — успех на транспортном уровне
|
||||
### SLOG-30. Ответ 4xx — успех на транспортном уровне
|
||||
|
||||
**ДОЛЖЕН.** Завершённый HTTP-ответ с 4xx логируется как успешный вызов
|
||||
(`ext.status_code` записан); решение «это ошибка» принимает доменный
|
||||
@@ -437,38 +440,38 @@ AND тик фонового цикла упал по той же причине
|
||||
|
||||
## HTTP и healthcheck
|
||||
|
||||
### R31. Входящий запрос — `INFO` независимо от кода ответа
|
||||
### SLOG-31. Входящий запрос — `INFO` независимо от кода ответа
|
||||
|
||||
**ДОЛЖЕН.** Поля по R16.1; 4xx остаётся `INFO`-записью доступа.
|
||||
**ДОЛЖЕН.** Поля по SLOG-16.1; 4xx остаётся `INFO`-записью доступа.
|
||||
|
||||
**Почему.** Это аудит обращений, а не отладка: запись отвечает на «кто и
|
||||
когда приходил», и ценность у неё одинаковая при любом коде ответа.
|
||||
Уровень, зависящий от кода, делает аудит неполным именно на тех запросах,
|
||||
которые чаще всего разбирают. Доменную оценку исхода даёт отдельная запись
|
||||
(R25) — она и адресована по-другому.
|
||||
(SLOG-25) — она и адресована по-другому.
|
||||
|
||||
### R32. Для корреляции запроса допустим `request_id`
|
||||
### SLOG-32. Для корреляции запроса допустим `request_id`
|
||||
|
||||
**ДОПУСКАЕТСЯ.** Это отдельный слой от корреляции по сущности.
|
||||
|
||||
**Почему.** Явное разрешение снимает вопрос, не запрещает ли `request_id`
|
||||
правило R18. Не запрещает: R18 отказывается от случайного ключа там, где
|
||||
правило SLOG-18. Не запрещает: SLOG-18 отказывается от случайного ключа там, где
|
||||
уже есть стабильный идентификатор сущности, а у HTTP-запроса собственной
|
||||
сущности нет — связать его записи между собой больше нечем.
|
||||
|
||||
### R33. Healthcheck, liveness, readiness — `DEBUG`
|
||||
### SLOG-33. Healthcheck, liveness, readiness — `DEBUG`
|
||||
|
||||
**ДОЛЖЕН.** Периодические проверки живости пишутся на отладочном уровне.
|
||||
|
||||
**Почему.** Частный случай R11.2, названный отдельно, потому что нарушают
|
||||
**Почему.** Частный случай SLOG-11.2, названный отдельно, потому что нарушают
|
||||
его чаще всего: проверку дёргают по таймеру, и на `INFO` она вытесняет из
|
||||
аудита всё остальное — в проде с базовым `INFO` (R40) лог превратился бы в
|
||||
аудита всё остальное — в проде с базовым `INFO` (SLOG-40) лог превратился бы в
|
||||
опрос самого себя. На `DEBUG` она не пишется вовсе и при этом остаётся
|
||||
доступной при отладке.
|
||||
|
||||
## Безопасность: что не логируем
|
||||
|
||||
### R34. Секреты не логируются
|
||||
### SLOG-34. Секреты не логируются
|
||||
|
||||
**НЕ ДОЛЖЕН.** Ни в полях, ни в сообщениях: пароли и cookie сессий,
|
||||
API-ключи и токены, `Authorization`-заголовки, аутентификационные параметры
|
||||
@@ -479,17 +482,17 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
||||
с момента записи, а не с момента, когда это заметили, и вычистить его задним
|
||||
числом из уже собранных копий нельзя.
|
||||
|
||||
### R35. Недоверенные и большие тела — только на `DEBUG`, после вычистки и обрезки
|
||||
### SLOG-35. Недоверенные и большие тела — только на `DEBUG`, после вычистки и обрезки
|
||||
|
||||
**ДОЛЖЕН.** Тела запросов и ответов внешних API, сырой вывод LLM —
|
||||
`DEBUG`, с вычисткой секретов и обрезкой по длине.
|
||||
|
||||
**Почему.** Содержимое пришло снаружи: размер не ограничен, состав
|
||||
неизвестен, а секрет в нём возможен по недосмотру той стороны. `DEBUG`
|
||||
выключен в проде (R40), поэтому цена ошибки ограничена отладочной сессией;
|
||||
выключен в проде (SLOG-40), поэтому цена ошибки ограничена отладочной сессией;
|
||||
обрезка не даёт одной записи вытеснить весь остальной лог за период.
|
||||
|
||||
### R36. При сомнении логируется факт, а не значение
|
||||
### SLOG-36. При сомнении логируется факт, а не значение
|
||||
|
||||
**СЛЕДУЕТ.** `"has_api_key", true` вместо самого значения.
|
||||
|
||||
@@ -499,7 +502,7 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
||||
когда чувствительность значения ещё неочевидна, а перечитывать этот выбор
|
||||
никто не придёт.
|
||||
|
||||
### R37. `*url.Error` санитизируется на границе клиента
|
||||
### SLOG-37. `*url.Error` санитизируется на границе клиента
|
||||
|
||||
**ДОЛЖЕН.** Ошибка разворачивается в первопричину **до** лога и до
|
||||
обёртки — раньше трансляции в доменную (`lang/go/errors.md`).
|
||||
@@ -513,12 +516,12 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
||||
причину сохраняется); альтернатива с редактированием URL сохранила бы
|
||||
структуру, но сложнее.
|
||||
|
||||
### R38. Секрет не кладётся в URL, если у API есть заголовок
|
||||
### SLOG-38. Секрет не кладётся в URL, если у API есть заголовок
|
||||
|
||||
**НЕ ДОЛЖЕН.** Аутентификация параметром ссылки — только когда другого
|
||||
способа нет.
|
||||
|
||||
**Почему.** Секрет в URL попадает не только в ошибку транспорта (R37), но и
|
||||
**Почему.** Секрет в URL попадает не только в ошибку транспорта (SLOG-37), но и
|
||||
в любую запись, куда URL попал целиком, — то есть обязывает помнить про
|
||||
санитизацию в каждой такой точке, и одна забытая сводит остальные на нет.
|
||||
Заголовок снимает задачу в источнике: чего нет в URL, того нет и в ошибке.
|
||||
@@ -528,7 +531,7 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
||||
|
||||
## Куда пишем
|
||||
|
||||
### R39. Логи идут в `stdout` одним потоком
|
||||
### SLOG-39. Логи идут в `stdout` одним потоком
|
||||
|
||||
**ДОЛЖЕН.** Сбор и ротацию делает окружение (docker, journald); по файлам
|
||||
не маршрутизируем.
|
||||
@@ -539,23 +542,23 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
||||
Один поток вдобавок сохраняет порядок записей — маршрутизация по файлам
|
||||
теряет его ровно там, где важен ход событий.
|
||||
|
||||
### R40. Базовый уровень — `INFO` в проде и `DEBUG` в dev
|
||||
### SLOG-40. Базовый уровень — `INFO` в проде и `DEBUG` в dev
|
||||
|
||||
**ДОЛЖЕН.** `DEBUG` в проде включается конфигом.
|
||||
|
||||
**Почему.** Уровень — единственный регулятор объёма, доступный без
|
||||
пересборки; если `DEBUG` в проде включается только правкой кода, его не
|
||||
включают, и разбор инцидента идёт вслепую. `INFO` выбран базовым потому,
|
||||
что на нём аудит полон (R8.2), а рутинно-частое уже отсечено (R11.2).
|
||||
что на нём аудит полон (SLOG-8.2), а рутинно-частое уже отсечено (SLOG-11.2).
|
||||
|
||||
## Связано
|
||||
|
||||
- `arch/time.md` — точность и зона меток времени фиксируются на носитель.
|
||||
- `lang/go/time.md` — как ставится UTC в `ReplaceAttr` (R3).
|
||||
- `lang/go/time.md` — как ставится UTC в `ReplaceAttr` (SLOG-3).
|
||||
- `lang/go/errors.md` — трансляция ошибки в доменную, порядок относительно
|
||||
санитизации (R37).
|
||||
санитизации (SLOG-37).
|
||||
- `arch/db-identifiers.md` — откуда берутся стабильные идентификаторы,
|
||||
на которых держится корреляция (R18).
|
||||
на которых держится корреляция (SLOG-18).
|
||||
|
||||
<!-- local:механизировано -->
|
||||
<!-- /local -->
|
||||
|
||||
Reference in New Issue
Block a user