Содержание урока · 13 разделов
- Централизованные логи: конвейер от stdout до хранилища
- Стеки сбора одной строкой
- Индексация JSON-полей: что даёт zap на входе
- Поиск на практике: от инцидента до записей
- Хранение логов: retention-политики и стоимость
- Чувствительные данные: 152-ФЗ, GDPR и минимизация
- Алерты по логам: бёрсты, латентность, дедупликация
- Правила алерта, которые переживают первую же ночь
- Поля под поиск: что закладывать заранее
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Логи вашего сервиса пишутся на пять виртуалок по сорок гигабайт в сутки, и первый же инцидент превращает их чтение в квест: дежурный ходит по машинам с grep, на каждой по пять минут, а хронология запроса размазана по всем пяти. Проблема не в zap и не в формате — записи идеальные, JSON с полями, как в уроке про энкодеры. Проблема в том, что каждая запись умирает на своём диске: поиск, срок жизни и цена никуда не выносятся. Эксплуатация начинается ровно здесь — централизованный сбор, поиск по полям, retention, стоимость хранения, чувствительные данные и алерты, на которые дежурный не боится подписываться. Всё это надстройка над тем, что вы уже умеете: структурные записи идут по конвейеру сами, если вы дали им правильные поля и уровни. Остальная часть курса дала вам записи; этот урок — про мир после stdout, где записи живут дольше, чем процесс, который их создал.
Централизованные логи: конвейер от stdout до хранилища#
Каноничный конвейер устроен по принципу разделения труда. Приложение пишет JSON в stdout или файл — OutputPaths из урока про выводы, — и на этом его ответственность кончается. Дальше работает агент на хосте: Vector или Fluent Bit читают поток, разбирают JSON и пересылают батчами в хранилище. Разделение не случайное: приложение не платит за сетевые ретраи и не держит очередь в памяти, backpressure живёт в агенте, который для этого и сделан. Vector — конвейер на Rust с конфигом из трёх ролей: source забирает хвост файла, transform выравнивает поля, sink отправляет в назначение. Fluent Bit — лёгкий агент с теми же ролями, стандарт де-факто на узлах Kubernetes. Хранилище выбирают под запросы. Loki индексирует только метки и жмёт тело чанками — дёшево и сердито. Elasticsearch из стека ELK строит инвертированный индекс по всему тексту — дорого, зато полнотекстовый поиск. ClickHouse — колоночная СУБД, где каждое JSON-поле становится колонкой, а поиск — обычным SQL. Конвейер целиком потрогать можно и на ноутбуке: локальный Go, docker compose с агентом и Loki — честный способ увидеть поиск до прода.
Стеки сбора одной строкой#
| Инструмент | Что это | Чем берёт |
|---|---|---|
| Vector | агент-конвейер: source → transform → sink | ремапы полей, батчи, один TOML-конфиг |
| Fluent Bit | лёгкий агент узла и Kubernetes | tail-плагины, парсеры, низкий оверхед |
| Loki | хранилище логов Grafana | индекс только по меткам, тело сжато чанками |
| ELK | Elasticsearch + Logstash + Kibana | инвертированный индекс, полнотекстовый поиск |
| ClickHouse | колоночная СУБД | поля как колонки, SQL, дешёвый гигабайт |
Индексация JSON-полей: что даёт zap на входе#
Конвейер выгоден ровно настолько, насколько структурны ваши записи. JSON-энкодер выравнивает каждое поле в ключ верхнего уровня, и агент отдаёт хранилищу готовые метки: service, level, request_id, status. Поиск «ошибки сервиса tasks за полчаса с таким request_id» становится запросом по индексу, а не сканом текста: цепочка, как в уроке про request id, собирается одним кликом. Отсюда правило для полей: то, по чему вы собираетесь искать, должно быть колонкой, меткой или термином индекса, а не куском сообщения. Тонкое место — кардинальность. Метка с миллионом разных значений раздувает индекс и замедляет запросы: user_id в качестве метки Loki — классическая ошибка, после которой индекс оказывается толще самих логов. Я больше не кладу идентификаторы сущностей в метки после одного месяца с раздутым индексом: идентификатор живёт в теле записи или в колонке ClickHouse, где высокая кардинальность обходится дешевле. Метки — для малого набора: сервис, окружение, уровень.
Поиск на практике: от инцидента до записей#
Как это выглядит руками дежурного. Пришла страница «бёрст Error в tasks». Первый запрос — по меткам: service=tasks, level=error, последние пятнадцать минут; ответ — сотни записей, и их сужают полями: конкретный хендлер, конкретный код ответа. Второй запрос — по request_id из первой подозрительной записи: он вытаскивает всю цепочку запроса — access-лог, slow sql из моста GORM, запись об ошибке с полем error. Третий — по сообщению или его фрагменту, когда симптома ещё не знаешь и ищешь похожие строки за прошлые сутки. Именно на третьем шаге становится видно, почему формат записи стоит держать стабильным: сообщение — это тоже поисковый ключ, и переформулировка «не удалось обновить» вместо «update failed» ломает сохранённые запросы дежурных. Поля ищутся, сообщение ищется, время фильтрует — этих трёх средств хватает на девять из десяти разборов без какого-либо парсинга.
Проверка: поле в метки
Какое из полей не стоит делать меткой Loki и почему?
Хранение логов: retention-политики и стоимость#
Retention — политика срока жизни: что удаляем, что сжимаем, что отдаём в архив. Рабочая схема — три слоя: горячие данные за последние семь дней на быстром диске с полным индексом, тёплый слой до тридцати дней со сжатием, архив до года на дешёвых томах, куда ходят только по запросу безопасности. Сроки — не эстетика: 152-ФЗ и внутренние регламенты фиксируют, сколько живут записи с персональными данными, и удерживать их дольше — риск, а не запас. Стоимость считается на салфетке: суточный поток, сжатие, цена гигабайта, дни. JSON жмётся в пять-десять раз — цифры условные, свои замерьте. Второй рычаг дешевле любого диска — писать меньше: сэмплинг из урока про прореживание, дисциплина уровней и отказ от логирования тел запросов режут поток на входе, и каждый несохранённый гигабайт не нужно ни хранить, ни архивировать. Для файлов на самом хосте та же математика в меньшем масштабе: ротация lumberjack с MaxAge и есть локальный retention, пока централизация не выросла. В Loki и ClickHouse срок жизни настраивается правилами TTL: хранилище само удаляет или перекладывает партиции по возрасту — политика живёт в нём, а не в cron-скрипте.
Что выведет калькуляция стоимости?
Прочитайте код и выберите, что он выведет:
daily := 50 // ГБ сырого JSON за сутки
ratio := 10 // сжатие, раз
days := 30 // тёплый retention, дней
monthly := daily / ratio * days
fmt.Println(monthly, "ГБ")Чувствительные данные: 152-ФЗ, GDPR и минимизация#
В логи просачивается то, что закон запрещает хранить без нужды: пароли, токены, номера карт, паспорта, история болезней. 152-ФЗ в России и GDPR в Европе требуют минимизации данных: собирать ровно столько, сколько нужно для цели, и держать не дольше срока этой цели. Минимизация — не про юридический страх, а про цену ошибки: утёкший лог с токенами — инцидент безопасности, утёкший лог с request_id — просто лог. Порядок обороны идёт от дешёвого к дорогому. Первая линия — не логировать вовсе: конвенция команды и ревью, где поле password просто не имеет права появиться. Вторая — маскировать при записи: в месте формирования поля значение заменяется звёздочками или хвостом из четырёх символов. Третья — последняя линия: обёртка WriteSyncer, прогоняющая готовую строку через маскирование на выходе. Обёртка спасает и от чужих ошибок в старом коде, но регэксп на каждой записи не бесплатен: меряйте цену, прежде чем вешать её на нагруженный сервис.
Обёртка WriteSyncer с маскированием
GOtype MaskingWriter struct {
next zapcore.WriteSyncer
}
func (m MaskingWriter) Write(p []byte) (int, error) {
return m.next.Write([]byte(maskSecrets(string(p))))
}
func (m MaskingWriter) Sync() error {
return m.next.Sync()
}
var secretRe = regexp.MustCompile(
`("password"|"token"):"([^"]+)"`,
)
func maskSecrets(s string) string {
return secretRe.ReplaceAllString(s, `$1:"***"`)
}Обёртка реализует контракт zapcore.WriteSyncer — Write и Sync — и встаёт между ядром и файлом: zapcore.NewCore(encoder, MaskingWriter{next: file}, atom). Регэксп ищет ключи password и token вместе со значениями и подменяет значение звёздочками: работает и на полях zap, и на тексте, просочившемся из чужих библиотек. Это последняя линия, а не замена прикладному маскированию: копия строки и проход регэкспа на каждой записи стоят такта, и на нагруженном сервисе их видно в бенчмарке. Обёртку проверяют тестом со специально испорченной записью: пароль в поле — на выходе звёздочки.
GET /reset?token=... попадает в access-логи приложения, прокси и балансера до всякой вашей обёртки — этими потоками вы не управляете. Правило: секреты ходят в заголовках и теле запроса, а в лог пишут путь без query.
Алерты по логам: бёрсты, латентность, дедупликация#
Хранилище без алертов — архив, куда ходят по праздникам. Алерты по логам строятся на трёх приёмах — и на четвёртом, без которого первые три бессмысленны ночью. Первый — Error-бёрст: правило считает записи уровня error за окно и стреляет только после порога с фазой ожидания; одиночная ошибка — не инцидент, серия — инцидент, порог подбирается по истории. Второй — латентность: zap.Duration пишет числовое поле, и по нему считается доля запросов медленнее порога; алерт ставят на долю — «больше одного процента запросов дольше 300 мс», а не на каждую медленную запись. Третий — дедупликация: пятьсот одинаковых ошибок одного цикла — один инцидент, и алерт выстреливает один раз; отпечаток собирают из сервиса, сообщения и ключевых полей. Четвёртый — runbook: в аннотации алерта живёт ссылка на инструкцию — что проверить, как откатить, кому звонить. Алерт без runbook — загадка в три часа ночи, и дежурный, однажды решивший её в одиночку, перестанет верить всем вашим правилам. Отсюда привычка ревизии: раз в квартал команда проходит по правилам и удаляет те, что не сработали ни разу.
Правила алерта, которые переживают первую же ночь#
- Бёрст, а не строчка: порог по окну с фазой ожидания, подобранный по истории инцидентов.
- Дедупликация по отпечатку: сервис, сообщение, ключевые поля — серия становится одним алертом.
- Runbook-ссылка в каждой аннотации: что проверить, как откатить, кому звонить.
- Канал по серьёзности: Error-бёрсты — в пейджер, Warn — в чат, Info — только в дашборд.
- Латентность — доля запросов за порогом, не каждая медленная запись по отдельности.
- У каждого правила есть владелец и дата ревизии: мёртвые правила глушат живые.
Проверка: дедупликация
Один цикл ретрая выдал 500 одинаковых записей Error за минуту. Сколько алертов увидит дежурный при настроенной дедупликации?
Поля под поиск: что закладывать заранее#
Разметка полей под поиск проектируется до первого инцидента, потому что задним числом её не добавить: старые записи останутся без новых колонок. Минимальный набор для сервиса: service с именем приложения, env с окружением, level, request_id и поле компоненты — http, repo, scheduler. К нему добавляются доменные идентификаторы: task_id, order_id, user_id в теле записи — что угодно, по чему поддержка найдёт историю конкретного клиента. Чего закладывать не стоит — полей на всякий случай. Каждое поле — это байты на каждой записи и ещё одна колонка в хранилище; поле, которое никто не ищет, дороже своего отсутствия. Рабочий приём — ревью полей вместе с алертами: если поле фигурирует в правиле или в рантбуке, оно обязано быть; если нет — вопрос, зачем оно вообще. Так набор остаётся коротким годами, а поиск остаётся быстрым, потому что индекс не разросся от щедрости.
Соберите маскирующую обёртку
Восстановите обёртку WriteSyncer и её место в ядре.
Соберите прод-приёмник с маскированием
Расставьте строки сборки в порядке выполнения.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: эксплуатация
Что дальше по курсу#
Сбор, поиск, retention, маскирование, алерты — эксплуатационный контур собран, и осталась одна вещь: сложить весь курс в один сервис. Итоговый проект — урок 25 — делает это на observable-tasks: HTTP-сервис, где конфиг из окружения встречается с цепочкой посредников, GORM с сэмплером, а тесты observer с чеклистом запуска. Карта всех уроков — на странице курса zap.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Централизованные логи: агент на хосте — Vector или Fluent Bit — доставляет записи в хранилище: Loki, ELK или ClickHouse.
- JSON-поля zap становятся метками и колонками: поиск по индексу вместо скана текста.
- Кардинальность решает, что метка, а что тело: user_id в метки не кладут.
- Retention — три слоя и цена на салфетке: поток, сжатие, срок; 152-ФЗ фиксирует срок хранения персональных данных.
- Оборона от утечек: не логировать, маскировать в приложении, обёртка WriteSyncer как последняя линия.
- Алерты: бёрсты с окном, доля медленных запросов, дедупликация и runbook в каждой аннотации.
Домашнее задание
Поднимите docker compose с Vector и Loki, напишите сервис, который пишет JSON в файл, и настройте конвейер до поиска по request_id. Добавьте MaskingWriter с двумя ключами и тестом на испорченную запись, затем правило бёрста Error с runbook-ссылкой в аннотации.
Личные заметкиПусто
Вопросы и ответы по уроку
Loki или ClickHouse: что выбрать под логи zap?
Смотрите на запросы: поиск по меткам и сообщению — Loki с дешёвым хранением; SQL-аналитика по полям и долгие сроки — ClickHouse. Записи из zap приходят в оба одинаково, JSON-потоком, поэтому выбор не меняет прикладной код.
Можно ли маскировать прямо в энкодере zap?
Готового маскирования в zap нет. Честные места — поле перед записью (Skip или звёздочки) и обёртка WriteSyncer на выходе; энкодер для этого не предназначен, и подмена его под эту задачу — хрупкое решение.
С какого порога начинать алерт на Error-бёрст?
С грубого: двадцать ошибок за пять минут с фазой for две минуты — стартовая точка, дальше правьте по истории инцидентов. Одна ошибка не должна будить дежурного: алерт стреляет по бёрсту, а не по записи.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Colly: парсинг и веб-скрапинг на Go
Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.
- Писать скраперы на Colly от первого Visit до обхода целого сайта
- Вынимать данные через CSS-селекторы и складывать в структуры Go
- Настраивать темп: задержки, параллелизм, лимиты на домен
- Работать с капризными сайтами: заголовки, cookies, прокси