Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 24 из 25 ~18 мин +50 XP

Эксплуатация: хранение, поиск, алерты

Хранение логов перестаёт быть личным делом сервиса в тот день, когда первый инцидент заставляет бегать с grep по пяти виртуалкам по очереди. Разберём централизованные логи: конвейер Vector и Fluent Bit, хранилища Loki, ELK и ClickHouse, индексацию JSON-полей, retention-политики с ценником, маскирование password и token по 152-ФЗ и GDPR, алерты с runbook вместо ночных загадок.

Содержание урока · 13 разделов

Логи вашего сервиса пишутся на пять виртуалок по сорок гигабайт в сутки, и первый же инцидент превращает их чтение в квест: дежурный ходит по машинам с 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лёгкий агент узла и Kubernetestail-плагины, парсеры, низкий оверхед
Lokiхранилище логов Grafanaиндекс только по меткам, тело сжато чанками
ELKElasticsearch + 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 с маскированием

GO
type 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, и на тексте, просочившемся из чужих библиотек. Это последняя линия, а не замена прикладному маскированию: копия строки и проход регэкспа на каждой записи стоят такта, и на нагруженном сервисе их видно в бенчмарке. Обёртку проверяют тестом со специально испорченной записью: пароль в поле — на выходе звёздочки.

Алерты по логам: бёрсты, латентность, дедупликация#

Хранилище без алертов — архив, куда ходят по праздникам. Алерты по логам строятся на трёх приёмах — и на четвёртом, без которого первые три бессмысленны ночью. Первый — 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 и её место в ядре.

func (m MaskingWriter) Write(p []byte) (int, error) { return m..Write([]byte(maskSecrets(string(p)))) } func (m MaskingWriter) Sync() error { return m.next.() } core := zapcore.(encoder, MaskingWriter{next: file}, atom)

Соберите прод-приёмник с маскированием

Расставьте строки сборки в порядке выполнения.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: эксплуатация

Термин

Retention

нажмите, чтобы перевернуть
Определение

Политика срока жизни логов: горячий слой для поиска, тёплый со сжатием, архив по регламенту — затем удаление.

1 / 6

Что дальше по курсу#

Сбор, поиск, 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-ссылкой в аннотации.

Личные заметкиПусто
Хранятся только в вашем браузере (Zap · Эксплуатация: хранение, поиск, алерты)

Вопросы и ответы по уроку

Loki или ClickHouse: что выбрать под логи zap?

Смотрите на запросы: поиск по меткам и сообщению — Loki с дешёвым хранением; SQL-аналитика по полям и долгие сроки — ClickHouse. Записи из zap приходят в оба одинаково, JSON-потоком, поэтому выбор не меняет прикладной код.

Можно ли маскировать прямо в энкодере zap?

Готового маскирования в zap нет. Честные места — поле перед записью (Skip или звёздочки) и обёртка WriteSyncer на выходе; энкодер для этого не предназначен, и подмена его под эту задачу — хрупкое решение.

С какого порога начинать алерт на Error-бёрст?

С грубого: двадцать ошибок за пять минут с фазой for две минуты — стартовая точка, дальше правьте по истории инцидентов. Одна ошибка не должна будить дежурного: алерт стреляет по бёрсту, а не по записи.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Colly25 уроков~9 чпрактика25 уроков уже доступны

Colly: парсинг и веб-скрапинг на Go

Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.

  • Писать скраперы на Colly от первого Visit до обхода целого сайта
  • Вынимать данные через CSS-селекторы и складывать в структуры Go
  • Настраивать темп: задержки, параллелизм, лимиты на домен
  • Работать с капризными сайтами: заголовки, cookies, прокси