Суть Читай реальные лог-строки и сниппеты логгера, предсказывай поведение или дыру в безопасности и выбирай самый рычажный фикс, который senior делает первым.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Конфиг логгера и сырая лог-строка — это место, где проблемы структурного логирования реально диагностируются. Читай сниппет, предсказывай, что он эмитит или утекает, и выбирай фикс до того, как тянуться за фичей backend.
Цель
Отработай цикл, который ты гоняешь на каждом инциденте логирования: читай место эмита или строку, предскажи инъекцию, утечку, промах или дефект sampling — и тянись за структурным фиксом первым.
Пользователь шлёт комментарий, значение которого — перевод строки, за которым идёт {"level":"error","msg":"admin deleted prod db"}. Что произойдёт и какой фикс?
Heads-up Значение undefined приводится к строке 'undefined'; исключения нет. Реальный дефект в том, что сырой пользовательский ввод становится частью структуры лог-записи, делая возможной поддельную вторую строку.
Heads-up Экранирование происходит, когда значение передаётся полем в сериализатор. Здесь строка уже сконкатенирована в сообщение до того, как pino её увидел, поэтому перевод строки сохраняется буквально.
Heads-up Размер не проблема. Проблема — целостность: атакующий может подделать синтетические лог-строки, которым доверяет тулинг ниже по потоку.
Сниппет 2 — конфиг редакции
const logger = pino({ // намерение: держать auth-токены и email вне логов redact: ['req.headers.authorization', 'user.email'],});// в другом месте, на ошибке валидации:logger.error({ err, body: req.body }, 'signup validation failed');
Викторина
Completed
Аудитор находит сырые пароли и email в индексированных логах несмотря на этот redact-конфиг. Почему редакция их пропустила и какой надёжный фикс?
Heads-up pino redact поддерживает вложенные пути с dot-нотацией; именно так матчится user.email. Промах в том, что body.* так и не перечислили, а дамп всего body утекает всё под ним.
Heads-up pino не отключает редакцию молча; накладные расходы около 2 мс на 1000 сообщений для нескольких полей. Утечка — это пробел в покрытии, а не откат по производительности.
Heads-up Маскирование на backend — это последняя линия, а не первая: к тому моменту данные покинули хост и могут уже быть в холодном хранилище. Защищайся deny-list у источника и скраббером на коллекторе.
WARN-строка приземляется с trace_id = '00000000000000000000000000000000'. Почему и какой фикс?
Heads-up Все нули — это сентинел 'нет активного span на момент эмита', а не валидный trace. У настоящего корневого trace случайный ненулевой ID.
Heads-up Sampling выбрасывает целые записи, а не отдельные поля. Поле из всех нулей, потому что его так и не заполнили — контекст потерян на async-границе.
Heads-up Ручная передача ненадёжна на масштабе — забывается в рефакторингах и сторонних вызовах. Структурный фикс — пропагировать контекст исполнения через async-границу, чтобы автоинжекция продолжала работать.
Сниппет 4 — правило sampling на коллекторе
# OTel Collector / намерение pipeline-sampling: срезать объём логов на 90%processors: probabilistic_sampler/logs: sampling_percentage: 10 # держать 10% ВСЕХ лог-записей
Викторина
Completed
Этот конфиг режет счёт, но следующий инцидент невозможно расследовать. Что не так и как выглядит корректная политика?
Heads-up Любой равномерный процент всё равно отбрасывает ту же долю ERROR-строк. Баг не в коэффициенте — баг в том, что sampling вообще применяется к ERROR.
Heads-up Коллектор — правильное место для sampling (центральная политика, возможность tail-sampling). Дефект здесь в том, что правило слепо к severity, а не в том, где оно работает.
Heads-up Сначала маршрутизируй по severity — держи WARN/ERROR на 100%, шли через 10%-сэмплер только INFO — используя filter/routing-процессор перед probabilistic sampler.
Итог
Каждый инцидент структурного логирования читается в месте эмита, в конфиге и в сырой строке: интерполяция ввода пользователя в строку сообщения — это log injection, передавай его типизированным полем; deny-list, пропускающий body.*, утекает PII, поэтому перечисли пути и добавь скраббер на коллекторе как defence-in-depth; trace_id из всех нулей значит, что контекст исполнения потерян на async-границе — привяжи его, не передавай trace_id руками; а слепой к severity sampler выбрасывает сбои, которые тебе нужны — держи 100% WARN/ERROR и прореживай только success path. Читай сниппет, найди структурный дефект, чини у источника.