Логи без персональных данных
Что писать в лог, что маскировать, сколько хранить и как идентификатор запроса помогает разбираться в проблемах, не зная, кто был пользователь.
Логи обычно растут сами. Кто-то добавил console.log(req.body), чтобы поймать баг, баг починили, строчка осталась. Через полгода в логах лежат пароли из формы входа, телефоны из анкет и токены из заголовков, а доступ к логам есть у всех разработчиков, у сервиса мониторинга и у подрядчика, которому дали посмотреть одну ошибку.
Логи — такая же база персональных данных, как основная, только хуже защищённая и без политики удаления. Проще не класть туда лишнего, чем потом выяснять, кому всё это ушло.
Что писать
Хорошая строка лога отвечает на вопросы «что произошло, где и чем кончилось», а не «кто этот человек»:
- идентификатор запроса;
- маршрут в виде шаблона:
/orders/:id, а не/orders/8412?token=...; - метод, код ответа и время выполнения;
- внутренний идентификатор пользователя — непрозрачный, из базы, а не почта;
- для ошибок — тип, сообщение и стек;
- ключевые события продукта: заказ создан, оплата прошла, подписка отменена.
Этого хватает, чтобы разобраться почти в любой проблеме. Если нужно понять, кто этот пользователь, это делается в основной базе, где есть права доступа и журнал.
Что не писать никогда
- пароли, коды подтверждения и одноразовые ссылки;
- токены, ключи API, заголовки
AuthorizationиCookie; - номера карт и платёжные реквизиты;
- тела запросов и ответов целиком;
- содержимое обращений к ИИ, если туда попадает текст пользователя.
Почты и телефоны, если без них никак, маскируй: a***@example.com, последние две цифры номера. IP-адрес тоже персональные данные во многих юрисдикциях; если он нужен для борьбы со злоупотреблениями, храни его коротко или в обрезанном виде.
Разрешённые поля, а не запрещённые
Список того, что нельзя логировать, всегда отстаёт: завтра появится поле passport_number, и о нём никто не вспомнит. Надёжнее обратный подход — логировать объекты только через явный набор разрешённых полей. Маскирование на уровне логгера остаётся страховкой для того, что всё-таки проскочило:
const logger = pino({
redact: {
paths: ["req.headers.authorization", "req.headers.cookie", "*.password", "*.token"],
censor: "[redacted]",
},
});
Отдельно проверь трекер ошибок. Многие из них по умолчанию собирают тела запросов, куки и локальные переменные — у них есть настройки очистки, и их стоит включить в первый же день.
Секреты в адресах
Ссылка для сброса пароля с токеном в параметре запроса попадает в логи прокси, балансировщика и CDN, причём в те, которые ты не контролируешь. Если токен в адресе неизбежен, он должен быть одноразовым и короткоживущим, а логирование адресов — без параметров запроса.
Идентификатор запроса
Каждый входящий запрос получает уникальный идентификатор на самом первом шаге: на прокси или в первом обработчике. Дальше он передаётся во все внутренние вызовы, попадает в фоновые задачи, которые запрос поставил, и пишется в каждую строку лога.
Это даёт две вещи. Во-первых, по одному идентификатору видна вся история запроса через все сервисы. Во-вторых, его можно вернуть в заголовке ответа и показать на странице ошибки. Человек пишет в поддержку «у меня ошибка, код такой-то», и ты находишь её за секунды, не спрашивая его почту и не ища по времени.
Сколько хранить
Подробные логи нужны для разбора недавних проблем, и обычно хватает пары недель. Агрегированные метрики — сколько запросов, сколько ошибок — можно хранить сколько угодно, в них нет людей. Журнал аудита — кто что поменял в данных — это отдельная история со своими правилами и более долгим сроком, и смешивать его с отладочными логами не стоит.
Короткий срок хранения решает ещё одну задачу: когда пользователь просит удалить его данные, логи очищаются сами, без отдельной операции.
Кто читает
Доступ к логам — такой же доступ к данным, как к базе. Его стоит выдавать по необходимости и пересматривать, а внешним подрядчикам давать конкретную выборку, а не весь поток.
SUBXCAT может пройтись по тому, что сейчас пишется в логи твоего проекта, и настроить маскирование и идентификаторы запросов.