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

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

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

Урок 1 из 25 ~16 мин +50 XP

Знакомство с zerolog: логгер без аллокаций

Zerolog в Go — логгер, который собирает JSON-событие без единой аллокации: поля дописываются прямо в байтовый буфер, а запись уходит в writer за десятки наносекунд. Если ваш сервис до сих пор живёт на fmt.Println или на логгере, который выделяет память на каждой строке, знакомство с zerolog покажет, как выглядят логи, спроектированные для горячего пути. В этом уроке — модель события, устройство цепочечного API и честный разбор бенчмарков, без которого выбор логгера превращается в гадание.

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

Три часа ночи, на графике 5xx, и вы открываете логи сервиса, который писали год. Логи — это вывод fmt.Println, оставшийся со времён прототипа: строки без времени, без уровня, без единого поля. Что было до ошибки, что после, когда именно случилось — не понять, потому что datetime в выводе нет вовсе: Println пишет то, что ему дали, и в том порядке, в котором вызвали. Классическая эволюция этой боли: сначала человек добавляет время руками через fmt.Println(time.Now(), "..."), потом вспоминает про стандартный пакет log, где время есть, а уровней и полей нет, потом ставит logrus — и вдруг сервис на пике тратит проценты CPU на собственные логи. Каждый шаг лечит одну рану и открывает следующую, потому что все эти инструменты писали строку для человека, а искать по ней придётся машине. Хуже всего, что такая лента врёт задним числом: без полей вы не отличите повторную попытку от первичной, а две одинаковые строки подряд — это два события или один дубль после ретрая? На ревью три вызова Println выглядят безобидно, в проде каждая из этих строк становится шумом, который дежурный вычитывает глазами, — и так на каждом инциденте.

Идея zerolog: событие вместо строки#

Знакомство с zerolog начинается с другой модели: лог — это не строка, а событие. Каждая запись — JSON-объект с фиксированными ключами level, time, message и любым числом ваших полей: user_id, order_id, elapsed. Поля типизированные: число пишется числом, строка строкой, длительность числом миллисекунд — для системы сбора это данные, по которым строятся фильтры и графики, а не текст, из которого регуляркой надо выковыривать значения. Zerolog появился в 2017 году — Оливье Пуатре, автор библиотеки, в то же время, когда Uber собирал zap, — и с самого начала был спроектирован вокруг одной цели: сериализация JSON без рефлексии, без промежуточных структур и без аллокаций на горячем пути. При этом API остался компактнее, чем у конкурентов: одна запись собирается цепочкой методов в одну строку, и читать её можно почти как предложение. Поля — это ещё и контракт со следующей версией вашего сервиса: ключ, однажды попавший в записи, обрастает дашбордами и алертами, и убрать его без последствий уже не выйдет. Поэтому события проектируют как схему данных: немного стабильных ключей, понятные типы, никаких одноразовых полей на каждый чих.

Первый взгляд: цепочечный API

GO
package main

import (
        "github.com/rs/zerolog/log"
)

func main() {
        log.Info().
                Str("user", "ann").
                Int("age", 30).
                Bool("admin", false).
                Msg("user logged in")
}

// Output (stderr):
// {"level":"info","time":"2024-04-06T12:00:00+03:00","user":"ann","age":30,"admin":false,"message":"user logged in"}

log — пакет github.com/rs/zerolog/log с готовым глобальным логгером. Info() начинает событие уровня info, дальше цепочка типизированных полей: Str для строки, Int для числа, Bool для флага. Msg завершает сборку и пишет запись. Обратите внимание на JSON: age и admin пришли числом и булевым значением, а не строками в кавычках, — это и есть структурное событие, которое система сбора разберёт без единой регулярки.

Ноль аллокаций: как zerolog устроен внутри#

Главное инженерное решение zerolog спрятано в слове «без аллокаций». Пока вы собираете событие, zerolog держит его не в структуре с полями, а в байтовом буфере: каждый метод цепочки дописывает в буфер готовые JSON-байты — ключ, кавычки, значение. Никакой рефлексии, никаких промежуточных слайсов полей, как в zap, и никаких map. Буфер берётся из sync.Pool: после вызова Msg он уходит в writer и возвращается в пул для следующего события, поэтому steady-state не создаёт мусора для сборщика. А если уровень события отключён — например, глобальный уровень Info, а вы вызвали Debug, — zerolog возвращает nil-событие, и вся цепочка превращается в no-op: методы проверяют receiver на nil и сразу возвращаются. Для сравнения: подход «собери структуру и отдай json.Marshal» платит рефлексией на каждом поле и аллоцирует результат на каждую запись — на тысячах RPS это уже график. Почему аллокации важны сами по себе: каждая из них — работа для сборщика мусора, а GC в Go стартует чаще, чем растёт куча. Логгер с десятками аллокаций на запись при десятках тысяч RPS стабильно подогревает кучу, и сборщик отбирает процессор у бизнес-кода — не паузами, которые видно на отдельном графике, а общим временем ответа. Ноль аллокаций — не трюк для бенчмарков, а способ вынести логирование из уравнения производительности целиком. Отключённая запись стоит единицы наносекунд, и это меняет привычку: отладочные вызовы можно не вычищать из горячего пути. Разбор с бенчмарками по этой теме — в уроке про производительность.

Логгеры Go: где место zerolog#

ИнструментФорматАллокации на записьAPIНиша
fmt.Printlnтекст без времени и уровнейнесколько на строкуPrintf-шаблоныскрипты и отладка, не прод
log (стандартный)текст с временем2 на записьPrint-семействоутилиты, простой вывод
logrusтекст или JSONдесятки на записьпары ключ-значениестарые проекты
zapJSON или consoleноль-двеконструкторы полейвысоконагруженный прод
slogтекст или JSONноль-двеAttrsстандарт по умолчанию
zerologтолько JSONнольцепочки вызововгорячий путь, CLI, embedded

Запись об ошибке: Err в действии

GO
package main

import (
        "errors"

        "github.com/rs/zerolog/log"
)

func main() {
        err := errors.New("connection refused")

        log.Error().Err(err).Str("op", "dial").Msg("connect failed")
}

// Output (stderr):
// {"level":"error","time":"2024-04-06T12:00:00+03:00","error":"connection refused","op":"dial","message":"connect failed"}

Error начинает событие третьего уровня, Err кладёт ключ error с текстом ошибки — а при nil не добавил бы ключ вовсе. Str добавляет контекст операции. Запись уходит в stderr глобального логгера, потому что время здесь из коробки. По полю error система сбора строит частоты провалов — ошибки как данные, а не как кусок текста.

Бенчмарки: что реально показывает README#

Собственные бенчмарки zerolog из README дают такой порядок величин: пустое событие — около 19 наносекунд, Info с сообщением — около 42, событие с полями — около 184 наносекунд, и во всех случаях ноль аллокаций. В бенчмарке самого zap на записи статичной строки zerolog выигрывает у zap в разы (50 против 236 наносекунд), а у logrus — на порядок (1244 наносекунды и 27 аллокаций). На событии с десятью полями разрыв с zap сжимается до процентов: 767 против 848 наносекунд — здесь у zerolog появляются единичные аллокации из-за упаковки значений в interface{}, которую требует сам бенчмарк. Числа — синтетические, зависят от машины и версии, и на вашем железе они будут другими; прогоните бенчмарки из репозитория сами, если решение стоит денег. Практический вывод не в цифрах, а в форме кривой: zerolog предсказуем на горячем пути — одинаковая цена на каждую запись без пиков на GC. Отдельно читайте колонку аллокаций, а не только время: logrus в том же прогоне отдаёт сборщику 27 объектов на каждую запись, zerolog — ноль. На коротком прогоне разница невидима, на длинном именно аллокации решают, как сервис ведёт себя под затяжной нагрузкой: куча не растёт, GC срабатывает реже, хвосты латентности не плывут.

Цепочечный API zerolog против zap и slog#

Чем цепочечный API отличается от соседей — на этом держится половина впечатления от библиотеки. В zap то же событие выглядит как logger.Info("user logged in", zap.String("user", "ann"), zap.Int("age", 30)): конструкторы полей перечисляются аргументами. В slog — атрибуты: slog.Info("user logged in", "user", "ann", "age", 30). В zerolog поля вплетены в цепочку: log.Info().Str("user", "ann").Int("age", 30).Msg("user logged in"). Мне цепочка читается легче: ключ и значение стоят рядом, тип виден в имени метода, и длинная запись не превращается в простыню аргументов. У этого вкуса есть и цена, о которой честно предупреждают в уроке про первый лог: цепочку надо чем-то завершить — вызовом Msg, — иначе запись не случится вовсе. Есть и практический бонус: порядок ключей в JSON совпадает с порядком вызовов, так что при ревью видно, как запись будет выглядеть в системе сбора, а автодополнение в редакторе предлагает ровно те типы полей, которые валидны для значения. Если после zap вам удобнее перечисление полей аргументами, это нормально; выбор логгера — командное решение, а не религиозное, и умение читать оба API окупается при миграциях.

Когда zerolog — удачный выбор#

  • HTTP-сервис с высокими RPS, где логирование живёт на горячем пути каждого запроса
  • CLI-утилиты: JSON-вывод парсится пайплайнами, а скорость сборки радует в мелких командах
  • агенты и демоны на машинах с малым ресурсом — ноль мусора для GC на постоянной записи
  • проекты, где уже выбран zerolog, — миграция на другой логгер дороже его особенностей
  • команда хочет один формат записи (JSON) и не возится с плагинами и форматтерами
  • неудачный момент: нужен экзотический формат или энкодер — zap с zapcore гибче

Проверка: формат записи

Что пишет zerolog на каждый вызов Info().Msg()?

Проверка: цена отключённого уровня

Сколько стоит цепочка log.Debug().Str("k", "v").Msg("x"), если глобальный уровень — Info?

Какой JSON выйдет из цепочки?

Прочитайте код и выберите, что он выведет:

log.Info().Str("foo", "bar").Msg("hello")

Что напечатает отключённый Debug?

Прочитайте код и выберите, что он выведет:

zerolog.SetGlobalLevel(zerolog.InfoLevel)

log.Debug().Str("key", "user:42").Msg("cache lookup")

Восстановите первую запись

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

package import ( "github.com/rs/zerolog/" ) func main() { log.(). Str("order_id", "A-1024"). ("amount", 1490). Msg("order created") }

Соберите программу целиком

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

Строки

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

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

Карточки: словарь zerolog

Термин

zerolog

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

JSON-логгер для Go (2017, автор Olivier Poitrey). Событие собирается цепочкой методов и сериализуется в буфер без рефлексии и аллокаций.

1 / 6

Переезд с zap на zerolog короче, чем кажется, потому что идеи совпадают: уровни, типизированные поля, JSON-вывод, контекстные поля. Меняется форма: zap.NewProduction() превращается в zerolog.New(os.Stdout).With().Timestamp().Logger(), zap.Error(err) — в .Err(err), а logger.Sync() исчезает вовсе — в zerolog записи уходят в writer сразу, буферизовать нечего. С подробной картой перехода можно сверяться с курсом по zap, а если вы логировали через slog, остаётся знакомой сама структура записи. Разница, о которой стоит знать заранее: у zap есть готовые пресеты NewProduction и NewDevelopment, у zerolog пресетов нет — соберите два логгера сами при старте и выбирайте по окружению, это десять строк в main. Зато исчезает целый класс проблем: нечему терять хвост записей, нечего сбрасывать при выходе, а формат записи не зависит от пресета.

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

Во втором уроке — первый лог в zerolog: zerolog.New(os.Stdout), ключ time, глобальный логгер из zerolog/log и честный ответ на вопрос, почему здесь нет Sync. Третий — уровни логов: от Trace до Fatal и SetGlobalLevel. Четвёртый — поля события во всей красе: Str, Int, Err и цена Any. Дальше ConsoleWriter, контекстные поля и вся библиотека до итогового проекта. Пока — установите zerolog локально и запустите пример из этого урока: первая JSON-строка в терминале объясняет идею библиотеки лучше любого текста.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Zerolog пишет логи событиями: одна запись — JSON с level, time, message и типизированными полями.
  • Событие собирается цепочкой: log.Info().Str("user", "ann").Int("age", 30).Msg("...").
  • Ноль аллокаций: поля дописываются байтами в буфер из sync.Pool, без рефлексии и промежуточных структур.
  • Отключённый уровень возвращает nil-событие — цепочка становится no-op стоимостью в единицы наносекунд.
  • По бенчмаркам README zerolog обгоняет logrus на порядок и стабильно быстрее zap на коротких записях.
  • Цепочка без финализатора Msg ничего не пишет — проверяйте её в ревью.
  • Пресетов у zerolog нет: writer и время настраиваются при старте руками — зато формат записи одинаков везде.

Домашнее задание

Установите zerolog локально: go mod init demo, затем go get github.com/rs/zerolog/log. Запустите пример из урока и рассмотрите JSON по ключам. Потом допишите событию ещё два поля разных типов — Str и Float64 — и проверьте по выводу, что каждое сериализовалось своим JSON-типом. Наконец, закомментируйте вызов Msg и убедитесь, что в терминале не появится ничего. Заодно загляните в go.mod: зависимостей у zerolog почти нет — ради этого его любят в утилитах и CLI.

Личные заметкиПусто
Хранятся только в вашем браузере (zerolog · Знакомство с zerolog: логгер без аллокаций)

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

Что такое zerolog простыми словами?

Библиотека логирования для Go: каждый лог — это JSON-объект с уровнем, временем, сообщением и полями. Особенность — сборка записи без аллокаций, поэтому zerolog один из самых быстрых логгеров в экосистеме.

Zerolog или zap — что выбрать?

Оба быстрые и оба пишут JSON. Zerolog компактнее в API и чуть быстрее на типовых записях, zap гибче в конфигурации через zapcore и умеет готовые пресеты. Для нового сервиса с горячим путём zerolog — самый экономный выбор, а переезд между ними внутри приложения — день работы: идеи совпадают, меняется форма вызовов.

Почему zerolog быстрее logrus и fmt.Println-подхода?

Logrus на каждой записи проходит по полям рефлексией и собирает строку с аллокациями. Zerolog дописывает JSON-байты в переиспользуемый буфер: без рефлексии, без map, без мусора для сборщика — на высоких RPS это читается на графике CPU. Бонусом идёт компактная зависимость: zerolog не тянет цепочки пакетов, как это делают фреймворки.

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

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

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

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

Валидация в Go: go-playground/validator

Курс validator для Go: теги required/min/max, email, dive, вложенные структуры, свои валидаторы, Gin и Echo, русские сообщения, тесты. 25 уроков.

  • Валидировать структуры тегами binding и validate
  • Понимать ValidationErrors и писать понятные сообщения
  • Работать с вложенностью, срезами и картами через dive
  • Писать свои функции-валидаторы и StructLevel-правила