--- prefix: SLOG extends: arch/time.md --- # Логирование Как и когда писать логи. Это правила оформления кода (How), а не спецификация поведения: наблюдаемые требования к логам, входящие в контракт функциональности, живут в спеках. Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 2 — тогда и только тогда, когда написаны заглавными. Лог читают инструментами, а не глазами: повседневно — `jq` (`jq 'select(.download_id=="a1b2")' app.jsonl`), тяжёлое (агрегации, JOIN) — DuckDB поверх JSONL прямо из файла. Отсюда почти все правила ниже: запись существует для запроса к ней. ```json {"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-логгер) | `_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. Запись о сущности несёт её идентификатор **ДОЛЖЕН.** Поле `_id` в каждой записи, относящейся к сущности. **ПОЧЕМУ.** Принадлежность записи восстанавливается только в момент записи; постфактум её не вывести — остаётся воспроизводить инцидент заново. Это же условие, при котором работает SLOG-18: отказ от `trace_id` оплачен тем, что идентификатор стоит везде, а не в удобных местах. Все записи одной операции собираются одним фильтром: `jq 'select(.download_id=="01jz…")' app.jsonl`. Если идентификатор глобально уникален across сущностей, штатно работает и простой `grep` по голому значению — он находит все упоминания независимо от имени поля. ### SLOG-20. Долгая операция ведётся scoped-логгером через `context.Context` **СЛЕДУЕТ.** Логгер с дописанным ключом протаскивается сквозь асинхронные стадии: ```go 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-8 к отказам: пользователь уже увидел причину на экране — владельцу разбирать нечего; целостность первичных данных отделяет «надо посмотреть» от «надо чинить сейчас». Привязка к месту дала бы разный уровень для одного и того же отказа в зависимости от того, какой транспорт его вызвал, — и невалидный ввод из формы копился бы в `ERROR` наравне с упавшей базой. Нарушение инварианта в собственном коде — паника, недостижимая ветка — в таблицу не входит: это не доменный отказ, и логирует его recover-граница вместе со стеком (конвенция `errors`). Искать его класс здесь не нужно. Мимо таблицы идёт и доменная ошибка, которой нет в маппинге: класса у неё нет, потому что её просто забыли завести. Она логируется `ERROR` с признаком непокрытой (`GERR-25`). ### 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).