Логи, по которым можно искать
Разница между «мы пишем логи» и «мы находим причину за минуту» — это структура, уровни и request id.
Лог — это данные, а не текст
Строка Ошибка при обработке заказа! бесполезна ровно в тот момент, когда нужна: по ней нельзя ни сгруппировать, ни отфильтровать, ни найти соседние события того же запроса.
Структурированный лог — это событие с полями:
{"ts":"2026-07-24T12:03:11Z","level":"error","msg":"payment failed",
"requestId":"a1f3","userId":42,"orderId":911,"err":"card_declined","durationMs":840}
Тот же объём информации, но теперь «все ошибки этого пользователя за час» — один запрос, а не чтение глазами.
Request id — нить через всю систему
Запрос проходит nginx, приложение, воркер и базу. Без общего идентификатора их логи — четыре несвязанные кучи. Правило: id генерируется на входе (или берётся из заголовка X-Request-Id) и пишется в каждую строку, включая фоновые задачи, которые запрос породил. Найти один инцидент = отфильтровать один id.
Уровни, которые что-то значат
- error — требуется вмешательство или это потерянная операция;
- warn — само восстановилось, но чаще обычного (ретраи, таймауты);
- info — события бизнес-уровня: старт, деплой, заказ создан;
- debug — подробности для разработки, в проде выключен.
Если error'ов сотни в час и «это нормально» — уровни мертвы: настоящая ошибка утонет.
Что не писать
Пароли, токены, полные номера карт, персональные данные — логи хранятся долго, копируются в сторонние системы и читаются людьми, которым эти данные не нужны. Утечка через логи — классика инцидентов.
Хранилище
Логи всех контейнеров должны стекаться в одно место (в этом проекте — Loki, читается из Grafana). Один важный нюанс индексирования: метки должны быть с малой кардинальностью (сервис, уровень), а не userId — иначе индекс раздувается на каждое значение. Высококардинальное — в тело лога, поиск по нему всё равно работает.