Я долго писал логи как художественную прозу: «Получен запрос от пользователя, начинаем обработку, всё ок». Потом в инциденте нужно было ответить на простой вопрос — сколько запросов от одного клиента упало за последние десять минут, — и оказалось, что ответа в логах нет. Есть текст, из которого его надо выковыривать регулярками.
Что менять
Формат. 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()).