Содержание урока · 10 разделов
- Sampling в zerolog: интерфейс Sampler
- BasicSampler и RandomSampler: детерминированно или случайно
- BurstSampler: всплеск пропустить, хвост отрезать
- Sampling или Disabled: выбор по ситуации
- Правила sampling одним списком
- Встроенные семплеры zerolog
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Цикл, который пишет Debug-строку на каждую итерацию по миллиону элементов, отключать жалко, а оставлять страшно: диск умирает за часы, а система сбора тонет в ленте, где девяносто девять строк из ста не читал никто. Ручное решение — счётчик и условие — расползается по коду и забывается в рефакторингах. В zerolog эта задача вынесена в отдельный слой: семплер. Он встаёт между порогом уровня и writer-ом и решает для каждого события, доехать ли до записи. Sampling логов — не про экономию наносекунд, а про экономию объёма: прореженный поток остаётся репрезентативным, всплеск целиком попадает в ленту, а хвост режется по правилу, которое видно в конфигурации, а не в коде. В этом уроке — все встроенные семплеры: детерминированный, случайный, всплесковый и поуровневый, плюс правила, при которых прореживание не превращается в слепую зону.
Sampling в zerolog: интерфейс Sampler#
Семплер — это интерфейс Sampler с одним методом Sample(lvl Level) bool: вернуть true — событие пишется, false — отбрасывается. Вешается он на копию логгера методом Sample: sampled := base.Sample(&zerolog.BasicSampler{N: 10}) — и дальше все события этой копии проходят через семплер. Интерфейс экспортирован не зря: свой семплер — это один метод Sample(lvl Level) bool, и правило вроде «прореживать всё, кроме Warn и Error», пишется четырьмя строками, если встроенных не хватило. Важно понимать, где он стоит: метод should сначала проверяет порог локального уровня и глобальный SetGlobalLevel из урока про уровни, и только потом вызывает семплер — то есть прореживаются только те события, которые уже прошли порог. Уровень при этом передаётся в Sample первым аргументом, и поуровневый семплер из середины урока именно им пользуется. Счётчики детерминированных семплеров — BasicSampler и BurstSampler — крутятся на атомарных операциях, поэтому одна копия логгера с семплером безопасна для сотен горутин: счётчик общий, а не у каждого воркера. RandomSampler кладёт монетку через глобальный генератор случайных чисел — тоже потокобезопасно, но счётчика у него нет вовсе. И не забывайте про value-семантику: Sample возвращает новую копию, base продолжает писать без прореживания — включать и выключать семплер можно на отдельных ветках дерева логгеров. Порядок проверок в should при этом стоит держать в голове при отладке: если события пропали после подъёма глобального порога, семплер ни при чём — до него очередь не дошла.
BasicSampler: каждый десятый
GOpackage main
import (
"os"
"github.com/rs/zerolog"
)
func main() {
base := zerolog.New(os.Stdout).With().Timestamp().Logger()
sampled := base.Sample(&zerolog.BasicSampler{N: 10})
for i := 1; i <= 25; i++ {
sampled.Info().Int("i", i).Msg("heartbeat")
}
}
// выйдут записи i=1, i=11, i=21 — каждая десятаяBasicSampler с полем N пропускает каждое N-е событие по атомарному счётчику: сработало условие счётчик%N==1 — запись ушла. N=0 отбрасывает всё, N=1 пропускает всё. Счётчик общий для логгера, поэтому при параллельных вызовах «каждый десятый» соблюдается глобально, а не по горутине.
BasicSampler и RandomSampler: детерминированно или случайно#
Два простых семплера отвечают на два разных вопроса. BasicSampler — детерминированный: каждый N-й, ровно и предсказуемо; удобно, когда важна равномерность потока — heartbeat-ы, статистика цикла. RandomSampler — вероятностный: zerolog.RandomSampler(chance) — преобразование uint32-а в семплер, который пропускает событие с шансом один из chance; никаких счётчиков и равномерности — чистая монетка. В пакете есть готовые ярлыки: zerolog.Often, zerolog.Sometimes и zerolog.Rarely — это RandomSampler(10), RandomSampler(100) и RandomSampler(1000) соответственно. Имена честные до прямолинейности: Often пропускает примерно каждый десятый, Rarely — примерно один из тысячи; док-комментарии пишут «~» не зря — вероятность не обещает ровности. Выбор между ними — вопрос следствия ошибки выборки: детерминированный семплер на периодическом сигнале может войти в резонанс и всегда попадать на одну и ту же фазу цикла; случайный резонансом не болеет, но при малом шансе короткий всплеск может пролететь мимо ленты целиком. Для мониторинга частоты событий детерминированный честнее — объём предсказуем; для генерации уникальных сэмплов отладки — случайный живее. Оба не смотрят на уровень: что прошло порог, то и прореживается — к последствиям этого мы вернёмся в «живом месте».
BurstSampler: всплеск пропустить, хвост отрезать#
BurstSampler — самый полезный из семьи: он пропускает первые Burst событий за окно Period, а дальше передаёт решение следующему семплеру NextSampler. Структура с тремя полями: Burst uint32, Period time.Duration, NextSampler Sampler — и собирается литералом: &zerolog.BurstSampler{Burst: 5, Period: time.Minute, NextSampler: nil}. Окно фиксированное, не скользящее: при первом событии после истечения Period счётчик обнуляется, и начало окна переносится на этот момент — «пять событий в минуту» считается от начала текущего окна, а сброс выполняется атомарным сравнением-и-заменой, без блокировок. NextSampler — место для второго правила: nil означает «после всплеска всё отбрасывать», а &zerolog.BasicSampler{N: 100} — «после всплеска писать одну из ста». Двухступенчатая схема покрывает классику: всплеск при старте или деградации попадает в ленту целиком — вы видите все первые ошибки, — а устойчивый поток прореживается, и диск живёт. Нюанс, о котором мало кто читает док-комментарий: всплеск срабатывает только когда и Burst, и Period больше нуля; при Period 0 семплер сразу передаёт решение NextSampler, а без него — отбрасывает всё. Проверяйте конфигурацию двумя событиями рядом, прежде чем доверять ей прод. Док-комментарий обещает ровно это: «BurstSampler lets Burst events pass per Period then pass the decision to NextSampler. If Sampler is not set, all subsequent events are rejected» — источник правды, когда конфигурация разошлась с поведением.
Семплер на базовом логгере прореживает всё, что прошло порог, — включая Error. Поставили BasicSampler{N: 100} на base «чтобы debug не топил диск» — и получили ленту, где из ста упавших запросов один дошёл до записи; алерт по ошибкам молчит, а инцидент идёт полным ходом. Правило: ошибки, Warn и аудиторские записи не прореживаются никогда — прореживать можно только debug и, с осторожностью, Info. Инструмент для этого — LevelSampler из кода ниже: он вешает семплер на конкретные уровни, а остальные пропускает нетронутыми. Диагностика, если подозрение уже есть: сравните счётчик ошибок метрик с количеством error-записей в ленте — расхождение с вменяемым объяснением указывает на семплер где-то на пути. Проверка при настройке: отправьте десять подряд Error на копию с семплером и убедитесь, что в ленте все десять.
LevelSampler: проредить только debug
GOsampled := log.Sample(zerolog.LevelSampler{
DebugSampler: &zerolog.BurstSampler{
Burst: 5,
Period: 1 * time.Second,
NextSampler: &zerolog.BasicSampler{N: 100},
},
})
sampled.Debug().Msg("hello world")LevelSampler держит по семплеру на уровень: поля TraceSampler, DebugSampler, InfoSampler, WarnSampler и ErrorSampler. Здесь прорежен только debug — пять событий в секунду, дальше одна из сотен, — а Info, Warn и Error проходят без семплера. Это пример из README zerolog и готовый рецепт для debug-пилы в цикле: всплеск виден, объём под контролем.
Sampling или Disabled: выбор по ситуации#
Три инструмента объёма решают разные задачи, и путать их дорого. Disabled — когда события не нужны вообще: известный горячий путь, debug-трассы в проде — нулевая цена и нулевой объём из урока про производительность. Sampling — когда события нужны, но не все: репрезентативная выборка статистики, всплески при деградации, шумные ветки, чей код вы не контролируете. Порог уровня — когда не нужен целый класс: Debug выключен глобально. Семплер выбирают, когда класс нужен, а частота — нет. У LevelSampler нет полей для Fatal и Panic: не заданный явно семплер уровня возвращает true, поэтому самые срочные события проходят даже поверх настроек прореживания. Стоп-кран для всего этого — zerolog.DisableSampling(true): глобальный переключатель, который при разборе инцидента выключает все семплеры во всех логгерах разом, и вы на время видите полный поток без переконфигурации; вернуть как было — одной строкой. Расписывайте семплеры в конфигурации рядом с уровнями из урока про окружение: «debug — всплеск 5 в секунду, дальше 1 из 100» — это читаемое правило, которое ревьюер понимает без исходников. И помните про value-семантику: Sample возвращает копию, поэтому семплер вешается на ветку — на модуль, на воркер, а не обязательно на весь сервис. Копии при этом делят один семплер-указатель: несколько веток, собранных из одной Sample-копии, считают общее окно — иногда это то, что нужно, а иногда сюрприз, который виден только под нагрузкой.
Правила sampling одним списком#
- порог уровня срабатывает раньше семплера: прореживается только то, что прошло порог
- ошибки, Warn и аудит не прореживаются: LevelSampler — для debug и Info
- BurstSampler{Burst: 5, Period: time.Minute} — всплеск целиком, дальше NextSampler
- NextSampler nil — после всплеска всё отбрасывается; BasicSampler{N: 100} — хвост 1 из 100
- RandomSampler(chance) — шанс 1 из chance; Often, Sometimes, Rarely — 10, 100, 1000
- zerolog.DisableSampling(true) — стоп-кран всех семплеров на время инцидента
- счётчики семплеров атомарные: одна копия с семплером безопасна для горутин
- Sample возвращает копию: семплер живёт на ветке дерева логгеров, не обязательно на base
Встроенные семплеры zerolog#
| Семплер | Настройка | Поведение |
|---|---|---|
| BasicSampler | N uint32 | каждый N-й; N=0 — ничего, N=1 — всё |
| RandomSampler | chance uint32 | шанс 1 из chance; Often, Sometimes, Rarely |
| BurstSampler | Burst, Period, NextSampler | всплеск Burst за Period, потом NextSampler |
| LevelSampler | семплер на каждый уровень | прореживает только заданные уровни |
| DisableSampling(true) | глобальный переключатель | выключает семплеры у всех логгеров |
Прореженный поток меняет то, что видит система сбора, — и об этом стоит подумать до, а не после. Объём записей становится неравномерным: всплеск дал двадцать строк, дальше минута тишины — графики частоты по количеству записей начинают врать, потому что частота событий и частота записей больше не совпадают. Если по логам что-то считается, считайте по данным внутри записей — счётчик в поле, а не COUNT(*) по ленте; там, где логи переходят в метрики, это правило обсуждается в уроке про логи и метрики. Оповещение по прореженному уровню — всегда компромисс: всплеск видно, устойчивый поток — только выборкой. Если алерт завязан на конкретный уровень, держите для него копию без Sample: разделение логгеров стоит одну строку на старте, а потерянная волна ошибок — разбор с фразой «в ленте этого нет». И последнее: прореживание — не замена уровням. Лента, где debug прорежен 1 из 100, а error не проречен вовсе, читается лучше, чем лента с единым семплером на всё; первым правилом настройки должен быть LevelSampler, и только потом — всплески и случайность для остальных уровней, если без них не обойтись.
Семплеры в песочнице не живут: она показывает тематическую программу на чистом Go — цикл со счётчиком и окном времени, который имитирует правило «пять за минуту, дальше одна из ста». Настоящие семплеры проверяются локальным Go: соберите копию с BurstSampler, выведите тридцать записей подряд и посчитайте в терминале, сколько прошло, — окно атомарное, и цифры сойдутся с правилом.
Проверка: порядок проверок
Что проверяется раньше — порог уровня или семплер?
Проверка: только debug
Как проредить только debug, не трогая ошибки?
Сколько записей пройдёт BasicSampler?
Прочитайте код и выберите, что он выведет:
sampled := base.Sample(&zerolog.BasicSampler{N: 10})
for i := 1; i <= 25; i++ {
sampled.Info().Int("i", i).Msg("heartbeat")
}Сколько пропустит BurstSampler?
Прочитайте код и выберите, что он выведет:
sampled := base.Sample(&zerolog.BurstSampler{
Burst: 2,
Period: time.Minute,
})
for i := 0; i < 5; i++ {
sampled.Info().Int("i", i).Msg("spike")
}Соберите всплесковый семплер
Восстановите конфигурацию: метод, тип семплера и уровень события.
Программа с семплером
Расставьте строки в правильном порядке: импорты, base, копия с семплером и запись.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: sampling
Что дальше по курсу#
Прореживание — последний инструмент объёма до продакшена: дальше курс идёт в горутины — что происходит, когда сотня воркеров пишет в один логгер, — и затем в конфигурацию по окружению, где уровни и семплеры собираются из переменных окружения. Справочное закрепление: найдите в своём сервисе самый шумный цикл, повесьте на его модуль LevelSampler с BurstSampler для debug и сравните объём ленты до и после — правило «всплеск 5 в секунду, дальше 1 из 100» обычно сокращает debug-объём на два порядка без потери всплесков деградации.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Семплер встаёт после порога уровня: прореживается только то, что прошло порог.
- BasicSampler{N} — каждый N-й по атомарному счётчику; RandomSampler(chance) — шанс 1 из chance.
- BurstSampler{Burst, Period, NextSampler} — всплеск целиком, хвост по правилу NextSampler.
- NextSampler nil отбрасывает всё после всплеска; BasicSampler{N: 100} — хвост 1 из 100.
- LevelSampler прореживает заданные уровни: ошибки и Warn не трогает никогда.
- zerolog.DisableSampling(true) — стоп-кран всех семплеров на время инцидента.
- Счётчики атомарные: копия логгера с семплером безопасна для сотен горутин.
- Прореженный поток ломает графики частоты: считайте по полям, а не по строкам.
Домашнее задание
Соберите копию логгера с BasicSampler{N: 5} и выведите двадцать записей: по терминалу посчитайте, что прошло ровно четыре — первая и каждая пятая. Замените на BurstSampler{Burst: 3, Period: time.Minute, NextSampler: &zerolog.BasicSampler{N: 2}} и проверьте двухступенчатую схему. Добавьте LevelSampler с DebugSampler и убедитесь, что Error-запись доезжает всегда, сколько бы ни было debug-событий рядом. В середине эксперимента вызовите zerolog.DisableSampling(true) и посмотрите на полный поток. Наконец, сломайте конфигурацию — BurstSampler без Period — и запишите своими словами, почему хвост исчез целиком.
Личные заметкиПусто
Вопросы и ответы по уроку
Как проредить debug-лог в zerolog?
LevelSampler с DebugSampler: log.Sample(zerolog.LevelSampler{DebugSampler: &zerolog.BurstSampler{Burst: 5, Period: 1 * time.Second, NextSampler: &zerolog.BasicSampler{N: 100}}}). Debug получает всплеск и хвост 1 из 100, Info, Warn и Error проходят без семплера. Это официальный рецепт из README zerolog. Числа 5 и 100 — отправная точка: подгоняйте их под бюджет диска и типичную длину всплеска.
Чем BurstSampler отличается от BasicSampler?
BasicSampler пишет каждый N-й событие без оглядки на время — поток равномерный. BurstSampler работает окнами: первые Burst событий за Period проходят целиком — видно деградацию в моменте, — а дальше решает NextSampler. Для шумных веток с всплесками берите BurstSampler, для ровной статистики — BasicSampler.
Может ли семплер отбросить Error-запись?
Да, если семплер висит на базовом логгере: BasicSampler и BurstSampler прореживают всё, что прошло порог уровня, включая ошибки. Правильная настройка — LevelSampler: семплер только на DebugSampler или InfoSampler, а WarnSampler и ErrorSampler остаются пустыми, и ошибки доезжают всегда.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Валидация в Go: go-playground/validator
Курс validator для Go: теги required/min/max, email, dive, вложенные структуры, свои валидаторы, Gin и Echo, русские сообщения, тесты. 25 уроков.
- Валидировать структуры тегами binding и validate
- Понимать ValidationErrors и писать понятные сообщения
- Работать с вложенностью, срезами и картами через dive
- Писать свои функции-валидаторы и StructLevel-правила