Содержание урока · 10 разделов
Что скрываем всегда: доступ и ПДн#
Категория доступа — самая чувствительная, потому что утечка здесь равна передаче ключей. Пароль не логируется никогда, даже маскированным: маска выдаёт длину и крайние символы, а для перебора это подсказка, а не защита. Токен доступа — маскируется: две первые и две последние литеры дают возможность сопоставить записи между собой, не раскрывая значения. Заголовок Authorization пишется только схемой — Bearer без самого значения; если хотите показать, что аутентификация была, поле auth со значением bearer стоит дешевле и безопаснее. Кука сессии заменяется session_id — идентификатором без подписи, по которому сессию не восстановить. Строка запроса из урока про HTTP-логи — отдельная ловушка: r.URL.RawQuery регулярно тащит токены сброса пароля и приглашений, поэтому по умолчанию логируется только путь; у этого правила длинная история, и оно разобрано в уроке про HTTP-логи. Категория ПДн — фамилия, имя, телефон, почта, адрес, паспорт: то, что 152-ФЗ называет персональными данными. Если такие значения осели в логах, архив становится хранилищем ПДн со всеми требованиями закона: основание обработки, уровни защищённости, сроки, реакция на запросы субъектов. Требование «удалите всё обо мне» в неизменяемом лог-архиве технически невыполнимо — дешевле, чтобы там этого никогда не было.
Маска значения до вызова Str
GOpackage main
import (
"os"
"github.com/rs/zerolog"
)
func mask(s string) string {
r := []rune(s)
if len(r) <= 4 {
return "***"
}
return string(r[:2]) + "***" + string(r[len(r)-2:])
}
func main() {
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
logger.Info().
Str("user_id", "u-1024").
Str("contact", mask("anna@example.com")).
Str("token", mask("ya29.aBcDeFgHiJkLmNoP")).
Msg("login ok")
}
// {"level":"info","time":"...","user_id":"u-1024",
// "contact":"an***om","token":"ya***oP","message":"login ok"}Маска режет строку по рунам, а не по байтам: кириллическая фамилия, разрезанная по байтам, дала бы битые символы в JSON. Значения до четырёх рун закрываются целиком — из двух литер нечего сохранять для корреляции. Маска применяется до вызова Str: в буфер события уже уходит готовая строка, и после записи её не изменить.
Маскировать до Str: почему после — поздно#
Механика поля объясняет, почему хук не спасёт. Вызов Str дописывает в буфер события готовые JSON-байты немедленно — ключ, кавычки, значение; никакого промежуточного представления, которое можно было бы отредактировать, не существует. Хук получает событие уже после ваших полей: его Run вызывается в момент Msg, когда буфер собран, а сам буфер — неприватное поле структуры, снаружи пакета zerolog его не прочитать. Доступные хуку операции — дописать собственные ключи в конец и отменить запись целиком; переписать или удалить чужое поле он не может технически. Поэтому маскирование живёт на стороне вызывающего кода, там, где про значение всё известно: функция mask рядом с местом, где строка появилась, а лучше — хелпер уровня пакета, единственная точка, через которую контакт или токен вообще попадает в лог. Такой хелпер заодно решает проблему забывчивости: если в кодовой базе нет ни одного прямого Str с контактным полем, маскировать нечего — точка входа одна, и она уже маскирует. Ключи при этом держите в константах, как договаривались в уроке про поля события: строковый литерал, продублированный в десяти местах, — это десять шансов опустить маску.
Если рассчитываете, что хук перепишет уже добавленное поле token, — не дождётесь: к моменту Run байты лежат в буфере, а сам буфер снаружи нечитаем. Хук умеет ровно две вещи — дописать свои ключи и вызвать Discard, отменив запись целиком. Если секрет уже уехал в Str, единственное, что сделает хук, — честно удалит событие вместе со всеми полями и секретом впридачу.
Хук-фильтр: интерфейс Hook и Discard#
Хук — интерфейс из одного метода: Run(e *Event, level Level, message string). Логгер принимает хуки методом Hook, который, как и With, возвращает копию — value-семантика, копию можно свободно раздавать горутинам. Выполняются хуки в момент финализации, при Msg, Msgf или Send, и раньше, чем message дописывается в буфер: хук видит текст сообщения параметром, а не полем. Отсюда честные сценарии. Вентиль: если в тексте сообщения встретилось слово, которого в логах быть не должно, событие отбрасывается целиком — e.Discard() помечает событие отключённым, и write пропускает его без записи; это ровно то, что написано в док-комментарии: Discard отключает событие, чтобы Msg его не напечатал. Дозапись: хук добавляет общие поля — имя окружения, версию сборки, — они ложатся после ваших полей и до message. Маркер: событие, прошедшее гейт, хук может пометить флагом, чтобы на стороне сбора отличать отфильтрованное. Для точечной настройки есть LevelHook и адаптер HookFunc из обычной функции — гейт вешается только на Debug, не трогая остальные уровни. Несколько хуков на одном логгере выполняются в порядке добавления, и каждый вызов Hook возвращает новую копию: собирайте набор хуков один раз на старте, вместе с контекстом и уровнем, а не в горячем пути.
Гейт на недопустимые сообщения
GOpackage main
import (
"os"
"strings"
"github.com/rs/zerolog"
)
type SecretGate struct{}
func (SecretGate) Run(e *zerolog.Event, level zerolog.Level, msg string) {
if strings.Contains(strings.ToLower(msg), "password") {
e.Discard()
}
}
func main() {
logger := zerolog.New(os.Stdout).Hook(SecretGate{}).With().Timestamp().Logger()
logger.Info().Str("op", "login").Msg("user signed in")
logger.Info().Str("op", "reset").Msg("password change requested")
}
// {"level":"info","time":"...","op":"login","message":"user signed in"}
// вторая запись не появится: гейт отбросил событие целикомГейт проверяет текст сообщения в нижнем регистре и отбрасывает событие вызовом Discard: уровень становится Disabled, и write пропускает запись. Первая строка проходит — слова password в сообщении нет. Хук добавлен методом Hook до With, но порядок здесь не важен: и Hook, и With возвращают копию логгера, обе можно продолжать цепочкой.
Гигиена полей: чем заменять секреты#
Замена работает лучше запрета: вместо списка «нельзя» — список «пишем вместо этого». Пользователь — user_id: обезличенный идентификатор сшивает записи так же надёжно, как почта, и не является ПДн. Контакт — маской, и только там, где дежурному правда нужно опознать человека: разбор обращения в поддержку. Токен — маской ради корреляции записей одного клиента. Сессия — session_id без подписи. Карта — последними четырьмя цифрами: отраслевой стандарт, и для разбора инцидента этого достаточно. Ревью-чеклист короткий: посчитайте в диффе вызовы Str, чьё значение приходит из заголовков, форм или тел запросов; каждый такой вызов — кандидат в маскирование или в идентификатор. Следующий рубеж — тест: программа, которая пишет события в буфер и сканирует вывод на слова-маркеры вроде password, authorization, bearer; механика таких проверок — тема урока про тестирование логов. Последняя линия — правила маскирования на стороне сборщика: пайплайн умеет вырезать и заменять значения уже после записи. Это лечение симптома: строка покинула процесс, копии разъехались по узлам, и гарантий, что вычистили всё, нет. Сборщик смягчает аварию, но не отменяет дисциплину в коде.
Что скрываем и чем заменяем#
| Значение | В лог | Почему |
|---|---|---|
| Пароль | не пишется вовсе | маска выдаёт длину и крайние символы |
| Токен доступа | первые 2 + *** + последние 2 | края сохраняют корреляцию записей |
| Authorization | только схема: bearer | само значение — ключ доступа |
| Кука сессии | session_id | идентификатор без подписи не оживит сессию |
| Почта, телефон | маска или user_id | ПДн: архив логов не должен их содержать |
| Номер карты | последние 4 цифры | отраслевой стандарт, для разбора достаточно |
| ФИО | user_id | имя не нужно, чтобы понять, что случилось |
Если секреты пишутся на Debug «временно, под уровнем», то в ночь инцидента порог опустят до debug — и всё уедет в архив вместе с токенами: уровень прячет запись из потока, но не делает её безопасной. Если значение нельзя показать на Info, его нельзя показывать вообще: маскируйте или не пишите — уровня, на котором можно всё, не существует.
Про 152-ФЗ с практической стороны. Закон требует основания для обработки персональных данных и защиты в зависимости от угроз; лог-архив, в котором осели фамилии и телефоны, формально становится местом обработки, о котором вы, скорее всего, ничего не декларировали. Обезличивание решает вопрос радикально: идентификатор user_id не позволяет восстановить человека без второй таблицы, а вторая таблица под замком и есть ваша база ПДн, о которой вы всё знаете. Отсюда вторая половина гигиены — доступ и срок жизни. Кто читает логи: доступ к архиву даётся тем же ролям, что и к базе, а не всем инженерам по умолчанию. Сколько живут записи: ротация из урока про lumberjack ограничивает возраст архива — месяц логов вместо бесконечного накопления сужает окно утечки. Ретеншн и маскирование работают в паре: маска не даёт секрету попасть в архив, ретеншн не даёт архиву жить вечно. Комбинация «в логах нет ПДн, доступ ограничен, хранение ограничено» закрывает типовые вопросы проверки почти без правок в приложении: всё это настройки writer-а и инфраструктуры.
Правила гигиены одним списком#
- пароль не логируется никогда — даже маской: маска выдаёт структуру значения
- токен и контакт — маской до вызова Str: две первые и две последние литеры
- Authorization — только схемой; RawQuery — только осознанно, по умолчанию путь
- человек в логе — это user_id, а не фамилия и не почта
- ключи чувствительных полей — в константах пакета, запись — через один хелпер
- тест сканирует вывод лога на слова-маркеры: password, authorization, bearer
- правило на сборщике — последняя линия, а не замена дисциплине в коде
- хук умеет дописать поле или Discard — читать и править поля он не может
Внедрение — вопрос команды, а не одного энтузиаста. Хелпер и константы живут в пакете логирования, и правило звучит просто: чувствительное значение попадает в лог только через него; прямой Str с токеном в ревью — блокер пул-реквеста. Тест на слова-маркеры запускается в CI рядом с юнит-тестами: если кто-то протащил сырой заголовок, сборка краснеет до того, как код доедет до продакшена. Дежурному стоит знать и процедуру на случай находки: секрет в логе — это инцидент утечки, и лечится он ротацией значения, а не правкой записи. Токен отзывается и выдаётся новый, пароль меняется, сессия инвалидируется; стереть строку из уже собранного архива нельзя, и пытаться «поправить файл» бессмысленно — копии уже разъехались по узлам. Для онбординга полезен живой пример: новичок запускает демо с настоящим токеном в поле, видит его в ленте в чистом виде, затем повторяет с маской — контраст запоминается лучше памятки. Гигиена держится не запретами, а дешёвым правильным путём: когда маскировать так же просто, как не маскировать, маскируют все.
В песочнице библиотека не запускается: программа урока разбирается построчно, а маскирование видно по комментариям с выводом. Локально всё повторяется терминалом: go run main.go на примере с гейтом — и запись с password в сообщении исчезнет из ленты, самый наглядный способ увидеть Discard в деле.
Проверка: значения полей
Что сделает zerolog при вызове Str("token", "abc123") с настоящим токеном?
Проверка: возможности хука
Что хук может сделать с событием, у которого поля уже добавлены?
Что вернёт mask?
Прочитайте код и выберите, что он выведет:
mask("ab")
mask("ABCDEFGH")Гейт на пароль
Восстановите хук-гейт и его подключение к логгеру.
Программа с маской
Расставьте строки в правильном порядке: импорты, функция маски и запись с замаскированным значением.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: секреты и ПДн
Что дальше по курсу#
Следующий урок — структура JSON: имена level, time и message, вложенность через Dict и Array и почему системе сбора нужна плоская структура. Затем — тесты логов: буфер вместо writer-а и сканирование вывода на слова-маркеры из этого урока. Про то, как выглядит доступ и хранение на настоящем сервере, — логи на проде: роли читателей, ретенция и то, что из этого урока превращается в настройки инфраструктуры.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Zerolog не маскирует ничего: значения полей уходят в JSON как есть.
- Пароль не пишется никогда; токен и контакт — маской до вызова Str.
- Маска: две первые и две последние литеры, короче — целиком закрытая.
- Хук — интерфейс Run(e, level, message): дописать ключи или Discard, не больше.
- Хук не читает и не переписывает добавленные поля: буфер неприватный.
- Человек в логе — user_id, а не фамилия: архив логов не хранилище ПДн.
- Тест на слова-маркеры и ретеншн: дисциплина в коде плюс ограниченный срок хранения.
Домашнее задание
Возьмите middleware из урока про HTTP-логи и проведите аудит его полей: найдите места, где в лог могут уехать RawQuery, заголовки или тело запроса. Напишите функцию mask по правилу урока — руны, порог в четыре литеры — и повесьте её на все строковые поля, чьи значения приходят от клиента. Добавьте хук-гейт, отбрасывающий события с password в сообщении, и проверьте его двумя записями: чистой и с запрещённым словом. Затем соберите тест, который пишет события в bytes.Buffer через SyncWriter и сканирует вывод на маркеры password, authorization и bearer — это будет ваш регресс на утечки. Напоследок посчитайте, сколько дней живут файлы логов в вашей конфигурации ротации, и решите, устраивает ли вас это окно.
Личные заметкиПусто
Вопросы и ответы по уроку
Есть ли в zerolog встроенное маскирование секретов?
Нет. Библиотека печатает значения полей как есть и не имеет API для фильтрации или замены содержимого. Маскирование строится на стороне приложения: функция, которая укорачивает строку до первых и последних литер, вызывается до Str. Хук тоже не поможет переписать поле — он умеет лишь дописывать ключи и отменять событие через Discard.
Что может хук zerolog?
Интерфейс из одного метода Run(e *Event, level Level, message string), который вызывается при финализации события, до дописывания message. Хук видит текст сообщения параметром и может дописать свои поля в конец или вызвать Discard, отменив запись целиком. Читать и править уже добавленные поля он не может: буфер события — неприватное поле структуры.
Что делать с персональными данными в логах по 152-ФЗ?
Не писать их вовсе: человек в логе — это обезличенный user_id вместо фамилии, почты и телефона. Если ПДн всё же осели в архиве, он формально становится местом обработки персональных данных со всеми требованиями закона, а запрос субъекта «удалите обо мне» в неизменяемом логе технически невыполним. Маскирование до записи и ограничение срока хранения снижают риск, но идентификаторы вместо атрибутов — самое дешёвое решение.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Валидация в Go: go-playground/validator
Курс validator для Go: теги required/min/max, email, dive, вложенные структуры, свои валидаторы, Gin и Echo, русские сообщения, тесты. 25 уроков.
- Валидировать структуры тегами binding и validate
- Понимать ValidationErrors и писать понятные сообщения
- Работать с вложенностью, срезами и картами через dive
- Писать свои функции-валидаторы и StructLevel-правила