Я долго писал логи как художественную прозу: «Получен запрос от пользователя, начинаем обработку, всё ок». Потом в инциденте нужно было ответить на простой вопрос — сколько запросов от одного клиента упало за последние десять минут, — и оказалось, что ответа в логах нет. Есть текст, из которого его надо выковыривать регулярками.

Что менять

Формат. JSON с фиксированными полями, по строке на событие:

{"ts":"2026-08-20T11:04:21.314Z","lvl":"error","msg":"payment failed","req_id":"a91f","user_id":4021,"amount":1290,"err":"timeout","dur_ms":30114}

Идентификатор запроса. Одно поле req_id, которое генерируется на входе и протаскивается через все слои — включая вызовы в другие сервисы. Без него поиск по логам в микросервисах превращается в гадание.

Отсутствие «успешных» строк. Логируем вход и выход одного запроса (с кодом и длительностью) и все ошибки. Всё промежуточное — не нужно; если нужен трейс, это задача трассировщика, а не логов.

Уровни, которые действительно работают

  • error — сломалось, и человек это увидел. Требует реакции.
  • warn — сломалось, но обработали (ретрай удался, кэш промахнулся при деградации).
  • info — значимые изменения состояния: старт, конфиг, деплой. Не «каждый запрос».

Если info пишется на каждый запрос, диск кончится раньше, чем вы найдёте ошибку. У нас на 30 ГБ это случилось однажды за выходные.

Ротация и цена хранения

logrotate с daily, rotate 14, compress. Хранить больше двух недель в горячем виде почти никогда не нужно: инциденты разбираются за час, а не за месяц. Если нужен длинный срок — выгружайте агрегаты, а не сырые строки.

И маленькое правило, которое спасло пару раз: в логах не должно быть персональных данных и токенов. Маскирование ставится на уровне логгера, а не на уровне «мы аккуратно пишем». Потому что однажды ночью кто-то обязательно напишет log.info(request.toString()).