---
title: "Sampling: прореживание потока"
description: "Sampling в zerolog: BurstSampler пропускает всплеск целиком, BasicSampler пишет каждый N-й, RandomSampler режет случайно, LevelSampler — только debug."
course: "Zerolog в Go: быстрое JSON-логирование"
url: "https://golangmaster.ru/courses/zerolog/lessons/14-sampling/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Sampling: прореживание потока

*Курс «Zerolog в Go: быстрое JSON-логирование», урок 14 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/zerolog/lessons/14-sampling/*

Sampling в zerolog — это семплер между уровнем и writer-ом: событие прошло порог, и семплер решает, доехать ли ему до записи. Разберём BurstSampler с всплеском и окном, BasicSampler с каждым N-м, RandomSampler, LevelSampler для debug-пилы и случай, когда семплер съедает ваши алерты.

Цикл, который пишет 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 из [урока про уровни](/courses/zerolog/lessons/03-urovni-logov/), и только потом вызывает семплер — то есть прореживаются только те события, которые уже прошли порог. Уровень при этом передаётся в Sample первым аргументом, и поуровневый семплер из середины урока именно им пользуется. Счётчики детерминированных семплеров — BasicSampler и BurstSampler — крутятся на атомарных операциях, поэтому одна копия логгера с семплером безопасна для сотен горутин: счётчик общий, а не у каждого воркера. RandomSampler кладёт монетку через глобальный генератор случайных чисел — тоже потокобезопасно, но счётчика у него нет вовсе. И не забывайте про value-семантику: Sample возвращает новую копию, base продолжает писать без прореживания — включать и выключать семплер можно на отдельных ветках дерева логгеров. Порядок проверок в should при этом стоит держать в голове при отладке: если события пропали после подъёма глобального порога, семплер ни при чём — до него очередь не дошла.

### BasicSampler: каждый десятый

```go
package 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

```go
sampled := 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-трассы в проде — нулевая цена и нулевой объём из [урока про производительность](/courses/zerolog/lessons/13-log-proizvoditelnost/). 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(*) по ленте; там, где логи переходят в метрики, это правило обсуждается в [уроке про логи и метрики](/courses/zerolog/lessons/21-logi-i-metriki/). Оповещение по прореженному уровню — всегда компромисс: всплеск видно, устойчивый поток — только выборкой. Если алерт завязан на конкретный уровень, держите для него копию без Sample: разделение логгеров стоит одну строку на старте, а потерянная волна ошибок — разбор с фразой «в ленте этого нет». И последнее: прореживание — не замена уровням. Лента, где debug прорежен 1 из 100, а error не проречен вовсе, читается лучше, чем лента с единым семплером на всё; первым правилом настройки должен быть LevelSampler, и только потом — всплески и случайность для остальных уровней, если без них не обойтись.

> **Про песочницу.** Семплеры в песочнице не живут: она показывает тематическую программу на чистом Go — цикл со счётчиком и окном времени, который имитирует правило «пять за минуту, дальше одна из ста». Настоящие семплеры проверяются локальным Go: соберите копию с BurstSampler, выведите тридцать записей подряд и посчитайте в терминале, сколько прошло, — окно атомарное, и цифры сойдутся с правилом.

### Проверка: порядок проверок

**Вопрос:** Что проверяется раньше — порог уровня или семплер?

- ✅ Порог уровня: семплер видит только события, прошедшие порог
- Семплер: он решает до сравнения с уровнем
- Оба одновременно
- Зависит от уровня события

Метод should сначала проверяет локальный порог и глобальный уровень, и только потом зовёт семплер. Событие ниже порога до семплера не доезжает — поднимите глобальный порог, и семплер останется без работы.

### Проверка: только debug

**Вопрос:** Как проредить только debug, не трогая ошибки?

- BasicSampler{N: 100} на базовом логгере
- ✅ LevelSampler с полем DebugSampler
- Поднять глобальный порог до Error
- RandomSampler на копии с Info

LevelSampler держит семплер на каждый уровень: DebugSampler прореживает debug, остальные уровни проходят без изменений. BasicSampler на базовом логгере режет и ошибки — дефект из «живого места».

### Сколько записей пройдёт BasicSampler?

```go
sampled := base.Sample(&zerolog.BasicSampler{N: 10})

for i := 1; i <= 25; i++ {
        sampled.Info().Int("i", i).Msg("heartbeat")
}
```

**Что выведет программа?**

- ✅ 3 — i=1, i=11, i=21: сработало счётчик%10==1
- 25 — семплер не трогает Info
- 2 — первая и последняя
- 10 — по одной на каждую десятку

Атомарный счётчик растёт на каждом событии, запись уходит при счётчик%N==1: события 1, 11 и 21 из двадцати пяти. Первое событие всегда проходит — счётчик стартует с единицы.

### Сколько пропустит BurstSampler?

```go
sampled := base.Sample(&zerolog.BurstSampler{
        Burst:  2,
        Period: time.Minute,
})

for i := 0; i < 5; i++ {
        sampled.Info().Int("i", i).Msg("spike")
}
```

**Что выведет программа?**

- ✅ 2 — первые два за окно, дальше NextSampler nil отбрасывает
- 5 — всплеск всегда пишется целиком
- 1 — только первое событие
- 0 — BurstSampler без NextSampler запрещён

За окно Period проходят первые Burst событий; без NextSampler всё после всплеска отбрасывается. Из пяти событий за минуту доедут первые два — ровно это и есть контракт «всплеск целиком, хвост отрезать».

### Соберите всплесковый семплер

Восстановите конфигурацию: метод, тип семплера и уровень события.

```go
func main() {
        base := zerolog.New(os.Stdout).With().Timestamp().Logger()

        sampled := base.___(&zerolog.___{Burst: 5, Period: time.Minute})

        sampled.___().Msg("heartbeat")
}
```

Подсказка: Семплер вешается методом Sample, всплесковый тип — BurstSampler, штатное событие начинает Info.

Правильные ответы в пропусках: `Sample`, `BurstSampler`, `Info`

### Программа с семплером

Расставьте строки в правильном порядке: импорты, base, копия с семплером и запись.

Правильный порядок строк:

1. `package main`
2. `import (`
3. `  "os"`
4. `  "time"`
5. `  "github.com/rs/zerolog"`
6. `)`
7. `func main() {`
8. `  base := zerolog.New(os.Stdout).With().Timestamp().Logger()`
9. `  sampled := base.Sample(&zerolog.BurstSampler{Burst: 5, Period: time.Minute})`
10. `  sampled.Info().Msg("heartbeat")`
11. `}`

### Карточки: sampling

- **Sampler** — Интерфейс zerolog с методом Sample(lvl Level) bool: true — событие пишется, false — отбрасывается. Вызывается после проверки порога уровня.
- **BasicSampler** — Поле N uint32: каждый N-й по атомарному счётчику. N=0 отбрасывает всё, N=1 пропускает всё.
- **RandomSampler** — Шанс один из chance; ярлыки Often, Sometimes, Rarely — 1 из 10, 100, 1000. Без счётчиков, чистая вероятность.
- **BurstSampler** — Поля Burst, Period, NextSampler: первые Burst событий за окно Period, дальше решает NextSampler; nil — отбрасывать всё. Окно атомарное.
- **LevelSampler** — Семплер на каждый уровень: DebugSampler, WarnSampler и соседи. Остальные уровни проходят без прореживания — способ резать только debug.
- **DisableSampling** — zerolog.DisableSampling(true) — глобальный стоп-кран: семплеры перестают работать у всех логгеров до обратного вызова.

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

Прореживание — последний инструмент объёма до продакшена: дальше курс идёт в горутины — что происходит, когда сотня воркеров пишет в один логгер, — и затем в конфигурацию по окружению, где уровни и семплеры собираются из переменных окружения. Справочное закрепление: найдите в своём сервисе самый шумный цикл, повесьте на его модуль LevelSampler с BurstSampler для debug и сравните объём ленты до и после — правило «всплеск 5 в секунду, дальше 1 из 100» обычно сокращает debug-объём на два порядка без потери всплесков деградации.

## Коротко: что запомнить

- Семплер встаёт после порога уровня: прореживается только то, что прошло порог.
- 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 остаются пустыми, и ошибки доезжают всегда.
