Содержание урока · 10 разделов
Событие редко знает о себе всё заранее. У профиля может не быть email, у операции — ошибки, у запроса — заголовков авторизации, и первое, что делает человек на zerolog, — пишет пустые значения на всякий случай: email с пустой строкой, error с нулём, код со статусом 0. Через неделю в системе сбора появляется фильтр «профили без email», который считает записи email="", а вместе с ними — половину записей, где email был, но кавычки потерялись на пути. Дашборд по коду ответа радостно рисует нули, хотя нулей в трафике не было. Мусорные ключи размывают самое ценное свойство структурированного лога: отсутствие значения — это тоже данные, и запись без ключа email говорит правду, а запись с email="" — нет. Вторая крайность — ветвления на каждый вызов: писать одно событие для удачного случая и другое для неудачного, дублируя общий каркас. В zerolog для обеих крайностей есть аккуратное решение: событие собирается пошагово, и каждый шаг можно сделать по условию, а для ошибок условность встроена прямо в библиотеку. Ни пустых значений, ни дублей каркаса — и ровно один способ объяснить системе сбора, почему у записи нет ключа. Дальше разберём механику пошаговой сборки, цену универсального Any и guard, который не даёт считать тяжёлые значения для записей, которые всё равно не уедут.
Условное логирование: собираем событие по условию#
Каждый метод уровня в zerolog возвращает *zerolog.Event — мутируемый аккумулятор записи. Цепочка из одного урока про поля — это просто короткая форма; ничто не мешает разбить её на шаги и пропускать шаги по условию. Паттерн условного логирования выглядит так: evt := logger.Info(), затем evt.Str("user", id), затем сколько угодно проверок с evt.Str или evt.Err внутри if, и в конце — evt.Msg(...), который завершает сборку и пишет запись. До Msg ничего никуда не уходит: буфер события живёт в памяти, порядок полей в JSON остаётся порядком вызовов, а пропущенные шаги не оставляют в записи следов. На практике паттерн применяется шире, чем кажется: условным бывает не только поле, но и уровень — единая точка сборки позволяет выбрать Info или Warn до добавления полей, и каркас при этом один. Часть условий zerolog делает сам: Err и AnErr молча пропускают nil — запись с ошибкой и без неё собирается одной и той же цепочкой, и условие писать не нужно. Errs устроен иначе: он пишет слайс ошибок массивом под заданным ключом, и nil-элементы внутри этого массива сериализуются как null. Поэтому для одиночной необязательной ошибки Err и AnErr остаются главным инструментом, а Errs берут, когда ошибок действительно несколько и их состав известен заранее. Для всего остального — строки, числа, флаги — условие на поле пишется обычным if, и это честная плата за то, что в записи оказываются только существующие значения. Различие между типизированными методами и универсальным Any, который часто тянется сюда же, — тема следующего раздела; база из урока про поля события предполагается знакомой.
Одно событие, два варианта полей
GOpackage main
import (
"errors"
"os"
"github.com/rs/zerolog"
)
type profile struct {
ID string
Email string
}
func save(logger zerolog.Logger, p profile, err error) {
evt := logger.Info()
evt.Str("user", p.ID)
if p.Email != "" {
evt.Str("email", p.Email)
}
if err != nil {
evt.AnErr("save_error", err)
}
evt.Msg("profile processed")
}
func main() {
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
save(logger, profile{ID: "u-1", Email: "ann@example.com"}, nil)
save(logger, profile{ID: "u-2"}, errors.New("email not verified"))
}
// Output:
// {"level":"info","time":"...","user":"u-1","email":"ann@example.com","message":"profile processed"}
// {"level":"info","time":"...","user":"u-2","save_error":"email not verified","message":"profile processed"}Обе записи собраны одной функцией: каркас события — Info, user, message — общий, а email и save_error добавляются только при наличии значения. AnErr с nil пропустил бы ключ сам, но явный if читается одинаково для строк и ошибок, поэтому в этой функции он у всех полей. В JSON нет ни пустых строк, ни нулей: отсутствие email в первом вызове видно по отсутствию ключа — именно этого хотят фильтры и дашборды.
zerolog any: цена универсального поля#
Метод zerolog any — обёртка над Interface: он принимает значение произвольного типа и маршалит его рефлексией, через тот же json.Marshal, что и стандартная библиотека. Отсюда цена: типизированные поля вроде Str и Int дописывают в буфер готовые байты без единой аллокации, а Any проходит по значению рефлексией и аллоцирует — на порядок и больше, в зависимости от глубины структуры. Есть и обходной путь: если тип реализует интерфейс LogObjectMarshaler с методом MarshalZerologObject, Any направит его в Object, который пишет поля без рефлексии. Когда Any оправдан: разовые дампы на Debug — слепок конфига, содержимое сообщения из очереди, паника с картой состояния. Когда нет: горячий путь, где типы полей известны на этапе компиляции — там Str, Int и Float64 и дешевле, и честнее по типам в системе сбора. Отдельный случай — значение уже сериализовано где-то ещё: RawJSON принимает байты готового JSON и дописывает их в запись как есть, без повторного маршалинга; валидировать байты приходится вам — zerolog вставит их в запись доверчиво. Оценка цены на конкретных числах — тема урока про производительность, там Any и типизированные поля встают в один бенчмарк.
Для дорогих значений есть guard точнее, чем уровень: метод Enabled(). Вызов if evt := logger.Debug(); evt.Enabled() возвращает событие только если оно переживёт фильтр — глобальный уровень, уровень логгера или сэмплер из урока о прореживании — и внутри блока можно спокойно строить тяжёлый аргумент: собирать слепок, читать статистику, маршалить карту. Сравните с наивной формой logger.Debug().Any("dump", snapshot()).Msg(...): аргумент snapshot() вычисляется при каждом вызове, до того как zerolog поймёт, что Debug отключён, — затраты на построение слепка никуда не деваются, экономится только сериализация. Это общее правило Go: аргументы вычисляются до вызова метода, и логгер не может отсрочить вашу функцию. Enabled() возвращает false и для nil-события, поэтому guard безопасен и в ветках, где уровень неизвестен заранее. Точной формулировки держитесь такой: отключённая запись должна стоить не больше единиц наносекунд — значит, всё, что дороже, обязано жить за Enabled(). Тот же guard работает и для средних по цене полей: конкатенация больших строк или поиск по карте на горячем пути тоже лучше прятать — микросекунды на каждый вызов на пике превращаются в процент CPU.
Правила условных полей одним списком#
- значения нет — ключа нет: пустая строка и ноль в записи врут фильтрам и дашбордам
- ошибки добавляйте Err и AnErr: nil они пропускают сами, условие писать не нужно
- опциональные строки — под if s != ""; счётчики с нулём — либо всегда, либо if n > 0, но по одному правилу в сервисе
- Any — для Debug-дампов и единичных мест; на горячем пути — типизированные поля
- дорогой payload — только за if evt.Enabled(): аргументы вычисляются до вызова метода
- готовый JSON сериализуйте один раз и вставляйте RawJSON, а не прогоняйте через Any второй раз
- булевы флаги — Bool, а не строки "true" и "yes": тип поля важен для фильтров
Bool-хелперы и честные флаги#
Bool(key, b) дописывает в запись JSON-значение true или false — тип события в системе сбора станет булевым, и фильтр admin=true отработает без приведений. Вокруг этого метода выросла маленькая дисциплина, потому что флаги логируют тремя разными способами. Первый — Bool: честно, типизировано, фильтруется везде. Второй — строка: Str("admin", "1") или "yes" — тогда каждый потребитель описывает своё преобразование, а админство внезапно оказывается и строкой, и числом в разных местах логов. Третий — числовые маяки вроде Int("admin", 1): они конфликтуют с настоящими счётчиками, у которых 0 и 1 — значения, а не флаги. Я за Bool везде, где значение действительно бинарное, и за именование без отрицаний: флаг not_admin=true читается на инциденте как ловушка, admin=false — мгновенно. Хелперы есть и на контексте: Context.Bool задаёт флаг всем событиям логгера — например, dry_run для режима примерки, — а Bools добавляет слайс булевых значений целиком. По цене Bool не отличается от Str: один append байтов, ноль аллокаций. И последнее: bool-хелперы закрывают и обратную задачу — читать флаги из конфига в контекст, поэтому признак окружения или фича-флага задаётся один раз на base и едет во все записи сервиса без единого повторного вызова.
История из типового ретро: для отладки добавили logger.Info().Any("payload", req) — полный объект запроса с токенами и персональными данными. Через месяц аудитория логов выросла втрое, в систему сбора доехали пароли в открытом виде, а вычистить их из ретеншена уже нельзя. Лечится тремя правилами: Any — только на Debug и только за Enabled(); на Info — узкие поля нужных значений, а не объект целиком; на ревью Any в Info, Warn и Error — красный флаг, требующий объяснения. Токены и секреты — отдельная тема, ей посвящён урок о полях и секретах.
Каким методом добавить значение#
| Метод | Когда подходит | Цена | Особенность |
|---|---|---|---|
| Str, Int, Bool, Float64 | тип известен на этапе компиляции | базовая: без аллокаций | тип в JSON честный |
| Any / Interface | тип значения произвольный, дампы | рефлексия и аллокации | LogObjectMarshaler уводит в Object |
| Object / EmbedObject | структура реализует LogObjectMarshaler | как у типизированных полей | поля структуры пишутся вручную |
| RawJSON | JSON уже собран другой системой | почти нулевая | байты вставляются без валидации |
| Dict | вложенный объект из цепочки | как у полей | группировка без своей структуры |
| Err / AnErr | ошибка, которой может не быть | базовая | nil пропускает ключ автоматически |
Сборка события как код: Dict и Fields#
Как только событие стало последовательностью вызовов, к нему применимы обычные конструкции языка. Вложенность собирает Dict: logger.Info().Dict("http", zerolog.Dict().Str("method", "POST").Int("status", 201)).Msg(...) — внутри записи появится объект http с полями method и status; Dict не требует собственной структуры и живёт целиком в вызове. Слайд значений добавляет Array с zerolog.Arr() или типом, реализующим LogArrayMarshaler. Набор пар, известный только в рантайме, — Event.Fields: он принимает map или слайс чередующихся ключей и значений и добавляет их все разом; ключи карты сортируются по алфавиту, слайс — в порядке следования. Это удобно для метаданных, которые пришли конфигом или ответом системы: не размечать каждое поле руками. Держите рамку: условная сборка — это то же самое событие с тем же контрактом ключей, а не способ менять схему записи от вызова к вызову. Потребители логов должны знать заранее, какие ключи вообще бывают в записях этого логгера, поэтому набор условных полей фиксируют в документации модуля так же, как фиксировали бы схему ответа API. Дёшево и полезно сверяться со списком ключей на ревью: пять минут против сюрприза в системе сбора через месяц.
Enabled, Any и Dict в одной программе
GOpackage main
import (
"os"
"github.com/rs/zerolog"
)
func main() {
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
if evt := logger.Debug(); evt.Enabled() {
evt.Any("payload", map[string]int{"items": 3, "retries": 1}).
Msg("debug dump")
}
logger.Info().
Dict("http", zerolog.Dict().Str("method", "POST").Int("status", 201)).
Msg("order created")
}
// Output:
// {"level":"debug","time":"...","payload":{"items":3,"retries":1},"message":"debug dump"}
// {"level":"info","time":"...","http":{"method":"POST","status":201},"message":"order created"}Guard Enabled() пропускает сборку дамп-события только при включённом Debug: поставьте глобальный уровень Info — и карта вообще не построится. Any сериализует карту рефлексией, и encoding/json выводит ключи карты отсортированными. Info-запись показывает Dict: объект http собран цепочкой без собственной структуры, вложенные поля стоят внутри объекта в порядке добавления.
Песочница сайта — тематическая программа на чистом Go: она собирает JSON-событие вручную и пропускает поля по условию, показывая механику без сторонних модулей. Настоящие Any, Enabled и Dict запускайте локально: go mod init demo, go get github.com/rs/zerolog/log, go run — и сравните вывод двух вызовов из этого урока с разными уровнями.
Что дальше по курсу#
Условные поля стоят вплотную к трём темам курса. Основа — типизированные методы из урока про поля события: условная сборка использует их же, просто под if. Экономика подсказана в уроке про производительность: там Any и Enabled встречаются в бенчмарках, и видно, сколько наносекунд стоит универсальное поле. А если поток записей с условными полями разрастается в спам — например, ретраи с полным payload на каждой попытке, — прореживание разберёт урок про sampling. Практический тест после этого урока: откройте свои вызовы логирования и найдите записи, которые пишут пустые строки и нули. Их нет? Отлично. Есть — вы знаете, что с ними делать: убрать пустые значения под условие, ошибки собрать через AnErr и перепроверить, что ни один тяжёлый payload не считается зря без Enabled.
Проверка: условные ошибки
Какой метод добавит поле только тогда, когда ошибка не nil?
Проверка: цена Any
Чем Any(key, value) отличается от Str(key, value)?
Что выведет Err(nil)?
Прочитайте код и выберите, что он выведет:
logger.Info().Str("op", "sync").Err(nil).Msg("done")Сколько раз вызовется snapshot()?
Прочитайте код и выберите, что он выведет:
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if evt := logger.Debug(); evt.Enabled() {
evt.Any("dump", snapshot()).Msg("debug")
}Соберите условное событие
Восстановите программу: пакет, вывод, начало события, ошибка для примера и условное добавление.
Пошаговая сборка события
Расставьте строки в правильном порядке: от объявления пакета до финализатора.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: условные поля
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Событие собирается пошагово: evt := logger.Info(), поля под if, финализатор Msg.
- Err, AnErr и Errs пропускают nil: условие для ошибок писать не нужно.
- Any — обёртка Interface: рефлексия и аллокации; на горячем пути — только типизированные поля.
- Guard if evt := logger.Debug(); evt.Enabled() не даёт вычислять дорогие значения для отключённых записей.
- Пустая строка и ноль в поле — ложь фильтрам: значения нет — ключа нет.
- Bool пишет честный JSON true/false; строковые и числовые флаги ломают агрегации.
- Dict собирает вложенный объект в вызове, RawJSON вставляет готовые байты без валидации.
Домашнее задание
Возьмите свой обработчик и соберите одно событие пошагово: обязательные поля сразу, опциональные — под if, ошибку — через AnErr. Запустите два варианта данных и сравните JSON: в записи без опциональных значений ключей быть не должно. Затем добавьте Enabled-guard для тяжёлого слепка: установите SetGlobalLevel(InfoLevel) и убедитесь по отладочной печати, что функция слепка не вызывается. Наконец, замените один строковый флаг на Bool и посмотрите, как изменился JSON.
Личные заметкиПусто
Вопросы и ответы по уроку
Когда использовать Any, а когда типизированные поля?
Any — для значений с типом, известным только в рантайме: дампы конфига, содержимое сообщений, отладочные слепки, и лучше на Debug. Типизированные Str, Int, Float64 — там, где тип известен при написании кода: они без аллокаций и дают системе сбора стабильные типы.
Почему не стоит писать пустые значения в лог?
Пустая строка и ноль — валидные значения, и потребитель не отличит их от настоящих: фильтры и дашборды посчитают мусор как данные. Отсутствие ключа честно говорит, что значения не было. Если пустое значение значимо, придумайте ключ с явным смыслом вместо подмены.
Enabled() вызывать на каждом событии?
Нет: guard нужен только там, где за полем стоит дорогая функция — слепок, запрос, маршалинг большой структуры. Обычные поля из аргументов функции дешевле самого условия. Правило простое: всё, что дороже пары наносекунд на построение значения, живёт за Enabled.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Валидация в Go: go-playground/validator
Курс validator для Go: теги required/min/max, email, dive, вложенные структуры, свои валидаторы, Gin и Echo, русские сообщения, тесты. 25 уроков.
- Валидировать структуры тегами binding и validate
- Понимать ValidationErrors и писать понятные сообщения
- Работать с вложенностью, срезами и картами через dive
- Писать свои функции-валидаторы и StructLevel-правила