- пятая, необязательная часть правила: код парой «плохо → хорошо» после обоснования; метка добавлена в словарь (EXAMPLES) и в строку о версии языка во всех тринадцати файлах - сказано, чем примеры не являются: требований в блоке нет, дословным сниппетом он не служит, при расхождении с нормой правят пример - READING.md обновлён по META-30, в машинные проверки добавлен порядок блоков, в читательские — что примеры норму не расширяют
40 KiB
topic, prefix, extends
| topic | prefix | extends |
|---|---|---|
| logging | SLOG | arch/time.md |
Логирование
Как и когда писать логи. Это правила оформления кода (How), а не спецификация поведения: наблюдаемые требования к логам, входящие в контракт функциональности, живут в спеках.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
Лог читают инструментами, а не глазами: повседневно — jq
(jq 'select(.download_id=="a1b2")' app.jsonl), тяжёлое (агрегации, JOIN) —
DuckDB поверх JSONL прямо из файла. Отсюда почти все правила ниже: запись
существует для запроса к ней.
{"time":"2026-06-28T11:23:45.123Z","level":"INFO","msg":"download accepted","download_id":"01jz2k7f8q9r3s4t5v6w7x8y9z","media_type":"movie"}
Формат записи
SLOG-1. Структурированный JSON, один формат для dev и prod
ДОЛЖЕН. Хендлер — slog.JSONHandler, одинаково в разработке и в
проде.
ПОЧЕМУ. Довод не в том, что текстовый вывод «расходит поля»: смена
хендлера структуру атрибутов не меняет. Довод в читателе — с текстовым
dev-выводом перестаёшь ежедневно гонять собственные jq-пайплайны, и
поломки словаря (опечатка в имени поля, потерянный атрибут, склеенное
значение) обнаруживаются только в проде, где заметить их заранее уже
некому.
SLOG-2. Данные — в типизированных полях, а не в тексте сообщения
ДОЛЖЕН. Каждая величина — отдельный ключ со значением своего типа.
ПОЧЕМУ. Фильтрация и агрегация работают по ключам; величина, вклеенная в текст, достаётся только регуляркой, а регулярка ломается при первой же правке формулировки. Тип важен отдельно от ключа: число внутри строки не сравнивается и не суммируется, то есть попадает в лог, но не в отчёт.
SLOG-3. Время записи — UTC
ДОЛЖЕН. time приводится к UTC через ReplaceAttr по slog.TimeKey
(см. конвенцию time).
ПОЧЕМУ. По умолчанию UTC не получится: встроенные хендлеры пишут время
в зоне самого time.Time, то есть в локальной зоне процесса. Записи одного
процесса до и после смены TZ (или записи рядом с данными из БД) перестают
складываться в одну хронологию, причём сдвиг на целые часы глазом не виден
— в отличие от явно неверной даты, он выглядит как правдоподобный порядок
событий.
Точность JSONHandler — миллисекунды фиксированной ширины; это другая
точность, чем в БД, и по TIME-2 так и должно быть: ширина фиксируется
на носитель.
Сообщение
SLOG-4. msg — константа в нижнем регистре
ДОЛЖЕН. Текст сообщения не собирается из переменных:
log.Info("download accepted", "download_id", id).
ПОЧЕМУ. msg — то, по чему записи группируют и считают. Интерполяция
превращает одну категорию в множество уникальных строк, и вопрос «сколько
раз это случилось» перестаёт решаться группировкой. Нижний регистр — чтобы
одна категория не двоилась на варианты, различающиеся только заглавной
буквой.
SLOG-5. msg не несёт префикса подсистемы
НЕ ДОЛЖЕН. recognition done, а не recognize: done; подсистема —
отдельное поле.
ПОЧЕМУ. Префикс кладёт в текст ровно то, по чему потом фильтруют, и фильтр по подсистеме становится сопоставлением с началом строки вместо сравнения значения поля. Заодно это второй способ записать одно и то же: категория дробится на варианты с префиксом и без, а совпадать они обязаны посимвольно.
SLOG-6. Смена состояния сущности — единая категория
ДОЛЖЕН. state transition с полями from/to/code; какое именно
состояние и по какой причине — данные, а не текст.
ПОЧЕМУ. С отдельной категорией на каждый переход жизненный цикл
сущности собирается перечислением всех известных msg — и переход,
добавленный в код позже, в это перечисление не попадёт: выборка тихо
останется неполной. Единая категория даёт весь цикл одним фильтром и не
требует обновлять запрос вслед за кодом.
SLOG-7. Физический эффект — отдельная запись, а не вместо перехода
НЕ ДОЛЖЕН. Запись о действии, сопровождающем переход, не подменяет запись самого перехода.
ПОЧЕМУ. Иначе из выборки по SLOG-6 выпадают именно те переходы, у которых был заметный эффект, — то есть самые интересные. Вторая запись стоит одной строки в логе; восстановление пропущенного перехода не стоит ничего, потому что невозможно.
Уровни
SLOG-8. Уровень выбирается по адресату
ДОЛЖЕН. Уровень отвечает на вопрос «кому сообщение», а не «насколько громко сломалось».
| № | Уровень | Кому и когда |
|---|---|---|
| SLOG-8.1 | DEBUG |
разработчику при отладке; в проде выключен |
| SLOG-8.2 | INFO |
владельцу, аудит постфактум |
| SLOG-8.3 | WARN |
владельцу, «может стать проблемой» |
| SLOG-8.4 | ERROR |
владельцу, в разбор |
ПОЧЕМУ. Адресат — единственный признак, по которому разные авторы в разных местах кода выберут уровень одинаково. «Насколько серьёзно» каждый оценивает по-своему, шкала расползается — и вместе с ней теряет смысл базовый порог в проде (SLOG-40), потому что он отсекает уже не то, что задумано.
SLOG-9. Уровень не зависит от подсистемы
НЕ ДОЛЖЕН. Происхождение записи на выбор уровня не влияет: ERROR
везде одинаково серьёзен.
ПОЧЕМУ. Фильтр по уровню собирает записи из всех подсистем сразу. Если
в шумной подсистеме ERROR «дешевле», читателю приходится помнить
происхождение каждой записи, чтобы понять, надо ли реагировать, — то есть
уровень перестаёт быть фильтром и становится подсказкой, требующей знания
кода.
SLOG-10. WARN — только когда «может стать проблемой»
ДОЛЖЕН. Если «может» не про эту запись, уровень — INFO.
ПОЧЕМУ. WARN разбирают вручную и целиком. Как только в нём заводится
«ничего страшного», его перестают читать — и вместе с шумом теряется то
единственное, ради чего уровень существует: предупреждение, на которое ещё
есть время отреагировать.
SLOG-11. Событийное — INFO, рутинно-частое — DEBUG
ДОЛЖЕН. Уровень зависит от того, стоит ли за операцией событие.
| № | Операция | Уровень |
|---|---|---|
| SLOG-11.1 | по реальному действию или изменению | INFO |
| SLOG-11.2 | повторяющаяся служебная, по таймеру или поллингу, сама по себе события не несущая (healthcheck, опрос статуса, авто-рефреш UI) | DEBUG |
ПОЧЕМУ. INFO — аудит постфактум (SLOG-8.2), и его пригодность
определяется долей записей, за которыми что-то стоит. Периодическая
операция даёт ровный поток при нулевой информации, в котором настоящие
события тонут количественно: их не отфильтровать, потому что фильтровать
приходится по содержанию, а не по уровню.
SLOG-12. Фатальный сбой на старте — ERROR и ненулевой код возврата
ДОЛЖЕН. Фатальный сбой на старте пишется как ERROR и завершает процесс
ненулевым кодом.
ПОЧЕМУ. slog не разделяет CRITICAL и FATAL, поэтому недостающую степень
выражает не уровень записи, а сам факт завершения. Супервизор (docker,
journald, systemd) отличает падение от штатной остановки по коду возврата, а
не по уровню последней записи. Процесс, который написал ERROR и продолжил
жить с неработающей конфигурацией, выглядит здоровым и будет получать трафик;
изобретать же уровень выше ERROR не нужно — сам факт завершения
информативнее.
Поля: единый словарь
SLOG-13. Одно поле — одно имя по всему коду
ДОЛЖЕН. Не mediaType/media/media_type вперемешку.
ПОЧЕМУ. Имя поля — и есть интерфейс запроса к логам. Второе имя для той же величины делает любую выборку по ней молча неполной: фильтр отработает, часть записей в него не попадёт, и заметить это можно, только заранее зная, что они должны были быть.
SLOG-14. Форма имени зависит от вида поля
ДОЛЖЕН. Две формы, третьей нет.
| № | Вид поля | Форма имени |
|---|---|---|
| SLOG-14.1 | бизнес-поле | плоский snake_case: download_id, media_type |
| SLOG-14.2 | системный домен | точечная иерархия (адаптация OpenTelemetry): http.*, ext.* |
ПОЧЕМУ. Точка отделяет поля, приходящие от инфраструктуры и одинаковые в любом проекте, от доменных, которые в каждом свои: по общему префиксу запрос «все внешние вызовы» пишется без перечисления имён. Заимствование словаря OpenTelemetry снимает необходимость изобретать имена тому, что уже названо, и спорить о них на каждом ревью.
SLOG-15. Запись плоская
НЕ ДОЛЖЕН. Вложенных объектов в записи нет; точка в имени — часть имени, а не уровень вложенности.
ПОЧЕМУ. Плоский ключ адресуется одинаково в jq, в DuckDB и в любой
записи независимо от её категории. Вложенность требует знать глубину
заранее, а она у разных категорий разная — и один запрос перестаёт покрывать
весь лог, распадаясь на запрос под каждую форму записи.
SLOG-16. Набор полей определяется ситуацией
ДОЛЖЕН. Записи каждой ситуации несут её набор целиком.
| № | Когда добавляем | Поля |
|---|---|---|
| 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 не отделяет «легла зависимость» от «у нас
баг», запись о сущности без идентификатора не корреллируется (SLOG-19). Полный
набор делает записи однородными — один запрос работает по всем вызовам, а
не по тем, где автор вспомнил про поле.
SLOG-17. service.* и host.* не заводим
НЕ СЛЕДУЕТ. Поле, значение которого одинаково во всех записях, не
заводится — для одного бинаря на одном хосте это service.* и host.*.
ПОЧЕМУ. Такое поле не несёт информации, но стоит места в каждой строке
и внимания при чтении. Критерий один на все поля словаря — им же решается,
нужен ли transport (SLOG-16.1): пока транспорт один, поле постоянно. Условие
названо явно, поэтому правило отпадёт вместе со своей причиной: с
появлением нескольких инстансов различающее поле (service.version)
добавляется одной строкой при старте.
Корреляция
SLOG-18. Ключ корреляции — идентификатор сущности, а не trace_id
НЕ СЛЕДУЕТ. Отдельный случайный trace_id не заводится, если у
сущностей есть стабильные уникальные идентификаторы. (Как их выбирают —
конвенция db-identifiers, если взята.)
ПОЧЕМУ. Идентификатор сущности уже существует, стабилен между
процессами и во времени — по нему собираются записи не одного прохода, а
всей истории сущности, включая вчерашнюю. trace_id даёт то же самое
только внутри одной операции, то есть дублирует ключ и добавляет второй
способ спросить об одном. Условие применимости названо: там, где сущности
со стабильным идентификатором нет, связывать записи больше нечем.
SLOG-19. Запись о сущности несёт её идентификатор
ДОЛЖЕН. Поле <entity>_id в каждой записи, относящейся к сущности.
ПОЧЕМУ. Принадлежность записи восстанавливается только в момент
записи; постфактум её не вывести — остаётся воспроизводить инцидент заново.
Это же условие, при котором работает SLOG-18: отказ от trace_id оплачен тем,
что идентификатор стоит везде, а не в удобных местах.
Все записи одной операции собираются одним фильтром:
jq 'select(.download_id=="01jz…")' app.jsonl. Если идентификатор
глобально уникален across сущностей, штатно работает и простой grep по
голому значению — он находит все упоминания независимо от имени поля.
SLOG-20. Долгая операция ведётся scoped-логгером через context.Context
СЛЕДУЕТ. Логгер с дописанным ключом протаскивается сквозь асинхронные стадии:
log := log.With("download_id", id)
ctx = logctx.With(ctx, log) // достаём логгер из ctx в каждой стадии
ПОЧЕМУ. Ручное дописывание ключа пропускают не в основном сценарии, а в редких ветках — обработке ошибок и ранних выходах, где корреляция нужнее всего. Логгер из контекста дописывает ключ сам, и запись без идентификатора становится невозможной, а не маловероятной.
Ошибки
SLOG-21. Ошибка логируется атрибутом error
ДОЛЖЕН. log.Error("layout failed", "error", err, "download_id", id).
ПОЧЕМУ. Ошибка, вклеенная в текст сообщения, дробит категорию (SLOG-4) и уносит текст туда, где по нему нельзя отфильтровать. Ключ единый — так же, как по умолчанию в zap/zerolog: выборка «все записи с ошибкой» не должна зависеть от того, кто писал конкретный вызов, и ради этого единообразия краткостью жертвуют.
SLOG-22. Промежуточный слой либо логирует, либо возвращает
НЕ ДОЛЖЕН. Слой, возвращающий ошибку выше, её не логирует — только
оборачивает (%w).
ПОЧЕМУ. Иначе один сбой даёт столько записей, сколько слоёв он прошёл,
и количество ERROR перестаёт соответствовать количеству отказов — а
считают именно его. Контекст при этом не теряется: он накапливается в
цепочке обёрток и попадает в единственную запись на границе (SLOG-23).
SLOG-23. Ошибка логируется один раз — на границе доменного слоя
ДОЛЖЕН. Логирует единый чокпоинт, определяющий исход операции.
ПОЧЕМУ. У ошибки нужен ровно один логирующий, иначе неизбежны дубли; и этим местом выбрана доменная граница, а не транспорт, потому что там известен исход операции целиком и, значит, класс отказа (SLOG-25) — транспорт знает лишь то, что ему вернули ошибку. Побочный эффект того же выбора: транспорты остаются тонкими.
SLOG-24. Транспорт не логирует ошибку повторно
НЕ ДОЛЖЕН. Транспорт переводит возвращённую ошибку в свой ответ (статус, сообщение пользователю) и на этом останавливается.
ПОЧЕМУ. Запись уже сделана на границе (SLOG-23); вторая отличается от неё только формулировкой и читается как второй сбой. Когда транспортов над одним доменом несколько, дублирование ещё и множится, а расследование начинается с вопроса, один это инцидент или два.
SLOG-25. Уровень доменного отказа — по классу отказа
ДОЛЖЕН. Уровень выбирает единственный логирующий (SLOG-23), и выбирает по
классу, а не по месту в коде. Классификация покрывает доменные отказы —
те, что операция вернула значением error.
| № | Класс отказа | Кому | Уровень |
|---|---|---|---|
| SLOG-25.1 | штатный конфликт состояния или некорректный ввод | пользователю, он уже получил ответ | DEBUG |
| SLOG-25.2 | расхождение производного или учётного состояния, первичные данные целы | владельцу, «может стать проблемой» | WARN |
| SLOG-25.3 | сбой БД, ФС, недоступность зависимости | владельцу, в разбор | ERROR |
| SLOG-25.4 | класса нет: отказ в классификацию не заведён | владельцу, как пропуск в классификации | ERROR с отметкой о непокрытом классе |
ПОЧЕМУ. Это применение SLOG-8 к отказам: пользователь уже увидел причину на
экране — владельцу разбирать нечего; целостность первичных данных отделяет
«надо посмотреть» от «надо чинить сейчас». Привязка к месту дала бы разный
уровень для одного и того же отказа в зависимости от того, какой транспорт
его вызвал, — и невалидный ввод из формы копился бы в ERROR наравне с
упавшей базой.
Нарушение инварианта в собственном коде — паника, недостижимая ветка — в
таблицу не входит: это не доменный отказ, и логирует его recover-граница
вместе со стеком (конвенция errors). Искать его класс здесь не нужно.
Строка SLOG-25.4 говорит не о классе отказа, а о пропуске в самой
классификации: ошибку забыли завести в маппинге. ERROR здесь — громкость,
по которой пропуск находят фильтром, а не оценка тяжести отказа; саму отметку
о непокрытом классе ставит трансляция ошибки (GERR-25 в конвенции
errors).
SLOG-26. Тот же отказ в асинхронной стадии — уровнем выше
ДОЛЖЕН. Когда пользователь не ждёт результата, отказ адресован
владельцу как деградация автоматики: коллизия в ручном действии — DEBUG,
она же в авто-обработке — WARN.
ПОЧЕМУ. В ручном действии человек видит причину на экране и сам решает, что делать дальше; запись нужна только для отладки. В автоматике не увидел никто, задача осталась недоведённой, и лог — единственное место, где это вообще проявится.
SLOG-27. Повторяющийся сбой фонового цикла — WARN
ДОЛЖЕН. Тот же класс сбоя внутри синхронной операции — ERROR:
уровень задаёт наличие штатного повтора, а не текст ошибки.
ПОЧЕМУ. Одиночный промах тика транзиентен — следующий тик повторит, и
вмешательство не требуется; ERROR на каждый такой промах обесценивает
уровень, на который смотрят в первую очередь. Синхронная операция повтора
не имеет: она провалилась целиком, результат никто не восстановит, и это
ровно тот случай, ради которого ERROR держат чистым.
Внешние сервисы
SLOG-28. Каждый вызов внешнего сервиса логируется
ДОЛЖЕН. Все вызовы, включая успешные; поля — по SLOG-16.4.
ПОЧЕМУ. Это единственный способ отличить «у нас баг» от «зависимость
легла»: на своей стороне видно лишь то, что операция не удалась.
Выборочное логирование ломает и второе применение — доля неуспехов и
распределение duration_ms считаются, только если знаменатель полный.
SLOG-29. Уровень ext-записи — по исходу вызова
ДОЛЖЕН. Исход считается по одному вызову с его ретраями.
| № | Исход | Уровень |
|---|---|---|
| SLOG-29.1 | успешный событийный вызов | INFO |
| SLOG-29.2 | успешный рутинно-частый вызов (поллинг, авто-рефреш) | DEBUG |
| SLOG-29.3 | попытка не удалась, делается retry | WARN |
| SLOG-29.4 | ретраи исчерпаны, сервис недоступен | ERROR |
ПОЧЕМУ. Неудачная попытка, за которой следует повтор, — ещё не отказ:
операция может завершиться успешно, и ERROR на каждую попытку сделал бы
уровень непригодным для главного вопроса «зависимость доступна?».
Исчерпание ретраев и есть момент, когда транспорт сдался и дальше
разбираться владельцу. Различение SLOG-29.1 и SLOG-29.2 — то же самое разделение
событийного и рутинного, что в SLOG-11: поллинг внешнего сервиса зашумляет
аудит так же, как любой другой.
Два цикла повтора — не путать
Слово «ретрай» означает два разных механизма, и уровень считается по
каждому отдельно: повтор вызова внутри одной операции (ретраи HTTP-клиента)
задаёт уровень ext-записи, повтор тика внешним циклом (поллинг, сверка) —
уровень доменной записи об исходе тика.
КОГДА зависимость недоступна И ретраи вызова исчерпаны
ТОГДА ext-запись `ERROR` (SLOG-29.4)
И тик фонового цикла, упавший по той же причине,
даёт доменную запись `WARN` (SLOG-27)
Из этого следует, что у лежащей зависимости ext-запись пишет ERROR
каждый тик. Это и есть механизм эскалации: доменный слой не паникует, а
телеметрия зависимости честно показывает, что она недоступна. Если поток
ERROR от поллинга мешает — это лечится понижением частоты тика или
подавлением повторов в самом клиенте, а не переклассификацией уровня.
SLOG-30. Ответ 4xx — успех на транспортном уровне
ДОЛЖЕН. Завершённый HTTP-ответ с 4xx логируется как успешный вызов
(ext.status_code записан); решение «это ошибка» принимает доменный
вызывающий.
ПОЧЕМУ. Транспорт своё дело сделал: запрос доставлен, ответ получен и
разобран. Классифицировать 4xx как сбой транспорта значит смешать «сервис
недоступен» с «сервис ответил нам нет» — это разные инциденты с разной
реакцией, и различает их как раз ext-уровень. Что 404 значит для
операции, знает только вызывающий: для одной это отказ, для другой —
штатный ответ.
HTTP и healthcheck
SLOG-31. Входящий запрос — INFO независимо от кода ответа
ДОЛЖЕН. Поля по SLOG-16.1; 4xx остаётся INFO-записью доступа.
ПОЧЕМУ. Это аудит обращений, а не отладка: запись отвечает на «кто и когда приходил», и ценность у неё одинаковая при любом коде ответа. Уровень, зависящий от кода, делает аудит неполным именно на тех запросах, которые чаще всего разбирают. Доменную оценку исхода даёт отдельная запись (SLOG-25) — она и адресована по-другому.
SLOG-32. Для корреляции запроса допустим request_id
ДОПУСКАЕТСЯ. Это отдельный слой от корреляции по сущности.
ПОЧЕМУ. Явное разрешение снимает вопрос, не запрещает ли request_id
правило SLOG-18. Не запрещает: SLOG-18 отказывается от случайного ключа там, где
уже есть стабильный идентификатор сущности, а у HTTP-запроса собственной
сущности нет — связать его записи между собой больше нечем.
SLOG-33. Healthcheck, liveness, readiness — DEBUG
ДОЛЖЕН. Периодические проверки живости пишутся на отладочном уровне.
ПОЧЕМУ. Частный случай SLOG-11.2, названный отдельно, потому что нарушают
его чаще всего: проверку дёргают по таймеру, и на INFO она вытесняет из
аудита всё остальное — в проде с базовым INFO (SLOG-40) лог превратился бы в
опрос самого себя. На DEBUG она не пишется вовсе и при этом остаётся
доступной при отладке.
Безопасность: что не логируем
SLOG-34. Секреты не логируются
НЕ ДОЛЖЕН. Ни в полях, ни в сообщениях: пароли и cookie сессий,
API-ключи и токены, Authorization-заголовки, аутентификационные параметры
в ссылках.
ПОЧЕМУ. Лог уезжает целиком в чужое хранилище, читается шире, чем код, и переживает ротацию самого секрета. Попавший в него секрет скомпрометирован с момента записи, а не с момента, когда это заметили, и вычистить его задним числом из уже собранных копий нельзя.
SLOG-35. Недоверенные и большие тела — только на DEBUG, после вычистки и обрезки
ДОЛЖЕН. Тела запросов и ответов внешних API, сырой вывод LLM —
DEBUG, с вычисткой секретов и обрезкой по длине.
ПОЧЕМУ. Содержимое пришло снаружи: размер не ограничен, состав
неизвестен, а секрет в нём возможен по недосмотру той стороны. DEBUG
выключен в проде (SLOG-40), поэтому цена ошибки ограничена отладочной сессией;
обрезка не даёт одной записи вытеснить весь остальной лог за период.
SLOG-36. При сомнении логируется факт, а не значение
СЛЕДУЕТ. "has_api_key", true вместо самого значения.
ПОЧЕМУ. Для отладки почти всегда достаточно ответа «значение было или не было» — потеря полезности близка к нулю, а риск снимается целиком. Правило нужно потому, что решение принимается в момент написания строки, когда чувствительность значения ещё неочевидна, а перечитывать этот выбор никто не придёт.
SLOG-37. *url.Error санитизируется на границе клиента
ДОЛЖЕН. Ошибка разворачивается в первопричину до лога и до
обёртки — раньше трансляции в доменную (конвенция errors).
ПОЧЕМУ. *url.Error встраивает полный URL запроса, а секрет живёт
прямо в нём: токен в пути, api_key в query. Go редактирует только пароль
из userinfo, остального не трогает, поэтому ошибка уносит секрет и в
обёртку, и в лог целиком. Порядок — часть нормы: санитизация после
трансляции уже опоздала, секрет к этому моменту скопирован в текст обёртки.
Цена — теряется Op и сам факт «это был HTTP-транспорт» (errors.Is на
причину сохраняется); альтернатива с редактированием URL сохранила бы
структуру, но сложнее.
SLOG-38. Секрет не кладётся в URL, если у API есть заголовок
НЕ ДОЛЖЕН. Аутентификация параметром ссылки — только когда другого способа нет.
ПОЧЕМУ. Секрет в URL попадает не только в ошибку транспорта (SLOG-37), но и в любую запись, куда URL попал целиком, — то есть обязывает помнить про санитизацию в каждой такой точке, и одна забытая сводит остальные на нет. Заголовок снимает задачу в источнике: чего нет в URL, того нет и в ошибке.
Куда пишем
SLOG-39. Логи идут в stdout одним потоком
ДОЛЖЕН. Сбор и ротацию делает окружение (docker, journald); по файлам не маршрутизируем.
ПОЧЕМУ. Приложение, которое само решает, что куда писать, дублирует работу супервизора и расходится с ней при первой же смене окружения: срок хранения, сжатие и ротация оказываются настроены в двух местах и по-разному. Один поток вдобавок сохраняет порядок записей — маршрутизация по файлам теряет его ровно там, где важен ход событий.
SLOG-40. Базовый уровень — INFO в проде и DEBUG в dev
ДОЛЖЕН. DEBUG в проде включается конфигом.
ПОЧЕМУ. Уровень — единственный регулятор объёма, доступный без
пересборки; если DEBUG в проде включается только правкой кода, его не
включают, и разбор инцидента идёт вслепую. INFO выбран базовым потому,
что на нём аудит полон (SLOG-8.2), а рутинно-частое уже отсечено (SLOG-11.2).
Связано
- конвенция
time— точность и зона меток времени фиксируются на носитель; как ставится UTC вReplaceAttr(SLOG-3). - конвенция
errors— трансляция ошибки в доменную, порядок относительно санитизации (SLOG-37). - конвенция
db-identifiers— откуда берутся стабильные идентификаторы, на которых держится корреляция (SLOG-18).