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

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

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

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

Условные поля и Any

Условное логирование в zerolog — про события, состав которых зависит от данных: email есть не всегда, ошибка не всегда, payload тяжёлый. Разберём сборку события по условию через промежуточную переменную, цену zerolog any, bool-хелперы и guard Enabled(), который не даёт считать дорогие значения впустую.

Содержание урока · 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, который часто тянется сюда же, — тема следующего раздела; база из урока про поля события предполагается знакомой.

Одно событие, два варианта полей

GO
package 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 и едет во все записи сервиса без единого повторного вызова.

Каким методом добавить значение#

МетодКогда подходитЦенаОсобенность
Str, Int, Bool, Float64тип известен на этапе компиляциибазовая: без аллокацийтип в JSON честный
Any / Interfaceтип значения произвольный, дампырефлексия и аллокацииLogObjectMarshaler уводит в Object
Object / EmbedObjectструктура реализует LogObjectMarshalerкак у типизированных полейполя структуры пишутся вручную
RawJSONJSON уже собран другой системойпочти нулеваябайты вставляются без валидации
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 в одной программе

GO
package 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 собран цепочкой без собственной структуры, вложенные поля стоят внутри объекта в порядке добавления.

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

Условные поля стоят вплотную к трём темам курса. Основа — типизированные методы из урока про поля события: условная сборка использует их же, просто под 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")
}

Соберите условное событие

Восстановите программу: пакет, вывод, начало события, ошибка для примера и условное добавление.

package import ( "errors" "os" "github.com/rs/zerolog" ) func main() { logger := zerolog.New(os.).With().Timestamp().Logger() evt := logger.() evt.Str("op", "sync") err := errors.("disk full") if err != nil { evt.(err) } evt.Msg("job finished") }

Пошаговая сборка события

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

Строки

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

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

Карточки: условные поля

Термин

Условное логирование

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

Сборка события пошагово: evt := logger.Info(), поля под if, финализатор Msg. Пропущенные шаги не оставляют в записи следов.

1 / 6

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

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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.

Личные заметкиПусто
Хранятся только в вашем браузере (zerolog · Условные поля и Any)

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

Когда использовать Any, а когда типизированные поля?

Any — для значений с типом, известным только в рантайме: дампы конфига, содержимое сообщений, отладочные слепки, и лучше на Debug. Типизированные Str, Int, Float64 — там, где тип известен при написании кода: они без аллокаций и дают системе сбора стабильные типы.

Почему не стоит писать пустые значения в лог?

Пустая строка и ноль — валидные значения, и потребитель не отличит их от настоящих: фильтры и дашборды посчитают мусор как данные. Отсутствие ключа честно говорит, что значения не было. Если пустое значение значимо, придумайте ключ с явным смыслом вместо подмены.

Enabled() вызывать на каждом событии?

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

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

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

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

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

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

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

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