- беклог и план переехали в docs/tasks (38 задач, 11 целей), слаги переименованы с транслита на английские, 85 ссылок поправлены - conventions.md разобран в docs/conventions/, local-research.md — в docs/research/, review-journal.md — в docs/review.md с разделом настройки конвейера; заведены security.md, adr/ и .pm.json - шаг docs.py check добавлен в task gate; поведение в architecture.md помечено девятью маркерами долга, database.md получил настройки с числовым значением
3.3 KiB
Логи
Структурированный JSON (log/slog) в stdout, один формат для dev и prod.
Сбор и ротацию делает окружение.
-
msg— короткая константа в нижнем регистре, категория события (delivery accepted,parse failed). Данные — атрибутами, не в тексте. Подсистему выносим в полеcapability(ingest/parse/query), не в префикс сообщения. -
Уровень — это адресат, а не громкость поломки:
Уровень Кому Примеры DEBUGразработчику при отладке /healthz, тела запросов, шаги разбораINFOвладельцу, аудит постфактум принята доставка, разбор завершён, старт WARNвладельцу, «может стать проблемой» точка не разобрана, незнакомая форма метрики ERRORвладельцу, в разбор не записался архив, сбой БД -
Невалидный ввод от отправителя —
DEBUG, а неERROR: это норма, разбирать нечего.WARN≠ «ничего страшного»,WARN= «может стать проблемой». -
Событийное →
INFO, рутинно-частое (healthcheck, поллинг) →DEBUG. -
Либо лог, либо возврат, не оба. Промежуточные слои только оборачивают и возвращают. Ошибка логируется один раз, на границе доменного слоя, которая определяет исход операции (
ingest) — не в транспорте. Транспорт переводит ошибку в ответ и не логирует повторно. -
Ошибка — атрибутом:
log.Error("parse failed", "error", err, "delivery_id", id). -
Время в логах — UTC, RFC 3339 с долями секунды.
-
Корреляция — по
delivery_id(ULID), отдельныйtrace_idне заводим. -
Секреты в логи не попадают: токены приёма и чтения,
Authorization. При сомнении логируем факт наличия, не значение. -
Данные о здоровье — чувствительные. Тела запросов пишем только на
DEBUGи с обрезкой по длине. -
Текст ошибки разбора не содержит значений из входа — только род токена (словарём JSON, не именем типа языка) и смещение. Инвариант выше обходится одним
fmt.Errorf("%v", tok): тело в 8 МиБ дало текст ошибки в 8 МиБ, и он уехал атрибутомerrorна уровеньWARN. Предел держит само сообщение, а не обрезка на стороне логирующего: обрезка живёт в другом месте и о новой ошибке разбора не узнает.