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

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

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

Урок 4 из 25 ~18 мин +50 XP

Поля события в zerolog: Str, Int, Err, Dur

Поля события zerolog — это то, ради чего затевался JSON-лог: Str, Int, Err и Dur дописывают типизированные пары ключ-значение прямо в байтовый буфер записи. В этом уроке разберём словарь методов-полей, узнаем, почему Err(nil) не добавит ключа, в каких единицах Dur пишет длительности и сколько стоит универсальный Any.

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

Типизированные поля: байты вместо рефлексии#

Поле в zerolog — это метод события, который дописывает в буфер готовые JSON-байты: ключ с кавычками, двоеточие и значение в правильном для типа виде. Тип известен на этапе компиляции — метод Int не может принять строку, — поэтому сериализация обходится без рефлексии и без упаковки в interface{}: число ложится в JSON числом, булево значение словами true или false, строка строкой. Порядок ключей в записи равен порядку вызовов: если вам важно, как поля выглядят глазами или в diff-ах, просто вызывайте методы в нужном порядке. Сообщение живёт отдельно от полей — оно дописывается при финализации и оказывается последним ключом. Для систем сбора типизированные поля — главный аргумент: Elasticsearch и ClickHouse получают числовые значения числами и умеют по ним агрегировать, тогда как строка «items: 3» внутри message остаётся мусором, из которого данные приходится доставать регуляркой. Проверить всё это проще всего локально: zerolog в песочнице сайта не исполняется, она показывает тематическую программу на чистом Go, а библиотека ждёт вас в терминале. Цепочка при этом не создаёт объектов: каждый метод поля дописывает байты и возвращает тот же указатель события, поэтому десять полей стоят структурно столько же, сколько одно. Из типизации следует и безопасность рефакторинга: переименование ключа компилятор не заметит — это просто строка, — а вот смену типа значения поймает сразу. И автодополнение работает за типизацию: редактор предлагает ровно те методы полей, что подходят значению, — опечатка в имени метода ловится до запуска, а не на ревью вывода.

Событие с набором полей

GO
package main

import (
        "time"

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

func main() {
        log.Info().
                Str("order_id", "A-1024").
                Int("items", 3).
                Int64("amount_cents", 149000).
                Float64("weight", 1.75).
                Bool("paid", true).
                Dur("elapsed", 317*time.Millisecond).
                Time("due", time.Date(2024, 4, 7, 12, 0, 0, 0, time.UTC)).
                Msg("order updated")
}

// {"level":"info","time":"2024-04-06T12:00:00+03:00","order_id":"A-1024","items":3,
//  "amount_cents":149000,"weight":1.75,"paid":true,"elapsed":317,
//  "due":"2024-04-07T12:00:00Z","message":"order updated"}

Каждый метод дописывает пару ключ-значение и возвращает событие дальше по цепочке. В JSON сохранены типы: items и amount_cents — числа без кавычек, paid — true, elapsed — число миллисекунд, due — строка RFC3339, как задал метод Time. Ключи идут в порядке вызовов, а message дописан последним — так zerolog финализирует каждое событие. Time здесь пишет строкой, а Dur — числом: единицы у каждого метода свои, и смешивать их в дашбордах не стоит.

Dur: длительности в миллисекундах#

Метод Dur принимает time.Duration и по умолчанию пишет значение числом миллисекунд: 317time.Millisecond превращается в "elapsed":317. За единицу отвечает глобальная переменная zerolog.DurationFieldUnit — по умолчанию time.Millisecond; если вашим дашбордам нужны секунды, поставьте её равной time.Second при старте, а DurationFieldInteger=true отрежет дробную часть у целых значений. Почему за дефолт взяты миллисекунды: это единица латентностей — HTTP-запросы, запросы к базе и кэшу живут в десятках и сотнях миллисекунд, и именно в этих единицах построены графики p50 и p99. Дробные доли миллисекунды Dur тоже пишет: 500time.Microsecond даст 0.5. Если дроби не нужны, DurationFieldInteger=true пишет целые миллисекунды без хвоста — для индексов это дешевле. Ошибка новичка — логировать длительность строкой через .Str("elapsed", d.String()): получившаяся «317ms» не сортируется и не агрегируется, а число из миллисекунд — сразу готовая метрика в системе сбора. Канонический замер живёт рядом с операцией: start := time.Now() на входе, дальше Dur("elapsed", time.Since(start)) в записи — поле показывает полную жизнь запроса, а замерять куски удобнее отдельными полями: db_ms и render_ms расскажут больше, чем один elapsed, который придётся объяснять.

Err: ошибка как данные#

Метод Err — самый частый гость в боевом коде. Вызов .Err(err) кладёт в запись ключ error со значением err.Error() — текстом ошибки. Ключевое поведение: если err равен nil, ключ не добавится вовсе — цепочка просто продолжится без поля, поэтому не надо строить if err != nil вокруг логов «на всякий случай». Ошибка в поле — это данные: система сбора строит по error частоты, группирует тексты, ищет всплеск одинаковых провалов, тогда как ошибка, вклеенная в message, остаётся текстом без индекса. Родственники Err закрывают остальные случаи: AnErr кладёт ошибку, умеющую сериализоваться объектом — метод MarshalZerologObject у ошибок с деталями; Errs пишет слайс ошибок одним полем; Stack() добавляет к ошибке стек, если настроен ErrorStackMarshaler — вся механика стека в уроке про логирование ошибок. Рабочее правило: каждый вызов Error несёт .Err(err), иначе запись объясняет, что провалилось, но не объясняет почему. Обёрнутые через %w ошибки Err печатает целиком: err.Error() склеивает тексты цепочки через двоеточие — читаемо, но не индексируемо по частям, поэтому детали для графиков лучше выводить отдельными полями. Если у ошибки есть данные — код ответа шлюза, идентификатор транзакции, — им место рядом, самостоятельными ключами.

Ошибка в логе: правильный вызов

GO
package main

import (
        "errors"

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

func chargeOrder() error {
        return errors.New("card declined")
}

func main() {
        if err := chargeOrder(); err != nil {
                log.Error().
                        Err(err).
                        Str("op", "charge-order").
                        Int("attempt", 2).
                        Msg("payment failed")
        }
}

// {"level":"error","time":"...","error":"card declined","op":"charge-order",
//  "attempt":2,"message":"payment failed"}

log.Error() задаёт уровень, Err(err) приносит ключ error с текстом провала, Str и Int добавляют контекст: какая операция и какая попытка. По такой записи система сбора ответит на вопросы «как часто падает charge-order» и «какие тексты ошибок всплывают» без единой регулярки. Автоматического стека здесь нет — он включается методом Stack() с настроенным ErrorStackMarshaler. if err != nil в примере ради уровня Error, а не ради Err: при nil поле бы просто не появилось.

Словарь методов-полей#

МетодТип значенияКлюч в JSONЗаметка
Strstringваш ключидентификаторы, статусы, версии
Int / Int64 / Float64числаваш ключбез кавычек — готовы к агрегации
Boolboolваш ключtrue / false без кавычек
Durtime.Durationваш ключчисло миллисекунд по умолчанию
Timetime.Timeваш ключформат задаёт TimeFieldFormat
Errerrorerrornil не добавляет ключ вовсе
AnErr / Errserror / []errorваш ключошибка-объект и слайс ошибок
Anyinterface{}ваш ключрефлексия: медленнее и с аллокациями
RawJSON[]byteваш ключготовые JSON-байты без экранирования

Any в zerolog: универсальное поле и его цена#

Метод Any принимает interface{} и умеет закодировать почти всё — но делает это рефлексией в рантайме, а рефлексия стоит: аллокации и десятки раз более медленный путь, чем у типизированных полей. Это не повод запретить Any, а повод понимать, где он уместен: разовые записи со сложными структурами, редкие пути вроде отчётов о панике, прототипы, где сначала смотрят на данные, а потом размечают поля. На горячем пути — в обработчике HTTP или внутреннем цикле — Any заменяется типизированными методами, а вложенные структуры пишутся Dict и Array: они собирают подобъекты и списки без выхода в рефлексию. Средняя позиция — поля, которые пишутся только при условии: zerolog позволяет собирать событие по частям, и этот приём с bool-хелперами разбирается в уроке про условные поля. Каждая аллокация в логе на высоких RPS — аллокация, которая досталась логам, а не бизнесу: тема целиком раскрывается в уроке про производительность. Типичный примиряющий сценарий: Any живёт в обработчике паники и в дампе конфига при старте — два места на весь сервис, оба не на горячем пути. Как только такой вызов переезжает в хендлер запроса, он становится долгом: каждое событие платит рефлексией за данные, у которых были имя и тип ещё на этапе компиляции.

С числовыми методами спешка дороже всего. Int принимает int: на 64-битных платформах это 64 бита, на 32-битных — 32, и счётчик, уехавший за два миллиарда, на слабой машине запишется неверно; для больших значений существует Int64, для явной ширины — Int32 и Int8. Float64 — для весов, долей и коэффициентов, но не для денег: двоичная дробь не умеет 0.1 без хвоста, и сумма в 19.99 рано или поздно появится в логе как 19.990000000000002. Деньги пишите целыми: amount_cents через Int64 — центы, копейки, тиыны; если сумма приходит строкой из формы, конвертируйте до лога, а не после. Для адресов и бинарных значений в zerolog есть готовые методы: IPAddr и MACAddr пишут адрес в привычном виде, Hex и Bytes — сырые байты, Strs и Ints — слайсы. Они типизированы так же, как Str и Int, и стоят столько же — словарь методов стоит пробежать глазами один раз, чтобы потом не городить Any там, где хватало метода по типу. Сумма 19.990000000000002 — не теория: так выглядит сложение двоичных дробей в любом языке, и лог лишь фиксирует то, что уже поехало в платежах.

Ключи полей — контракт, который вы подписываете со всеми, кто будет читать логи после вас. Соглашения простые: snake_case — user_id, а не UserID и не userId; стабильные имена — ключ, однажды попавший в дашборд, переезжать не должен; единица в имени, если она не очевидна из типа — amount_cents вместо amount. Схема полей — входной билет для системы сбора: от неё зависит, что попадёт в колонки Loki и ELK, этому посвящён урок про структуру JSON. Держите в голове и цену переименования: ключ, уехавший в дашборд, переезжает только вместе с ней — новая версия поля означает либо миграцию графиков, либо переходный период с двумя ключами в параллель. Поэтому имена выбирают один раз и в одном месте: константы в пакете логирования вместо строк по всему коду избавят от опечаток, которые молча создадут новое поле. Два подводных камня из этой же темы. Первый: zerolog не дедуплицирует ключи — два вызова Str("user", ...) в одной цепочке дадут два ключа user в JSON, и парсеры возьмут последний, а поведение остальных не гарантировано. Второй: значения полей zerolog пишет как есть — маскируйте токены и персональные данные до вызова, а не после; теме секретов посвящён отдельный урок.

Правила полей одним списком#

  • snake_case: user_id, а не UserID и не userId
  • стабильные имена: ключ из дашборда не переименовывают — миграция графиков дороже
  • единица в имени, если не видна из типа: amount_cents, elapsed_ms
  • числа — Int и Int64, деньги — целыми копейками, Float64 — только для долей и весов
  • каждый Error несёт .Err(err); сообщение — факт без данных внутри
  • секреты маскируются до вызова: zerolog печатает значения как есть

Проверка: Err и nil

Что произойдёт при вызове log.Info().Err(nil).Msg("ok")?

Проверка: единицы Dur

В каких единицах метод Dur пишет time.Duration по умолчанию?

Какой порядок ключей получится?

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

log.Info().Str("a", "1").Int("b", 2).Msg("hi")

Что окажется в поле elapsed?

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

log.Warn().Dur("elapsed", 250*time.Millisecond).Msg("slow query")

Событие ошибки

Восстановите запись о провале: уровень ошибки, сама ошибка в поле, числовой контекст.

package main import ( "errors" "github.com/rs/zerolog/log" ) func main() { err := errors.New("connection refused") log.(). (err). ("attempt", 2). Msg("connect failed") }

Программа с ошибкой в поле

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

Строки

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

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

Карточки: поля события

Термин

Поле события

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

Пара ключ-значение, дописанная методом цепочки: .Str("user", "ann"). Тип известен при компиляции — сериализация без рефлексии.

1 / 6

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

Следующий урок — Logger.With: поля, которые есть у каждого события, от имени модуля до request_id запроса. Затем — условные поля: собирать событие по частям и платить только за нужное. Для ошибок глубже — урок про логирование ошибок со стеком и обёртками, а про то, как поля едут в Loki и ELK, расскажет урок про структуру JSON.

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

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

Вывод

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

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

  • Поля zerolog — типизированные методы цепочки: Str, Int, Int64, Float64, Bool, Dur, Time.
  • Порядок ключей в JSON равен порядку вызовов; message дописывается последним, при финализации.
  • .Err(err) кладёт ключ error; с nil ключ не появляется — if вокруг лога не нужен.
  • Dur пишет миллисекунды числом; единица задаётся zerolog.DurationFieldUnit, целочисленность — DurationFieldInteger.
  • Any кодирует всё через рефлексию — годится для редких путей, на горячем пути типизированные поля и Dict/Array.
  • Int на 32-битных платформах — 32 бита: большие значения и деньги пишите Int64, Float64 — не для денег.
  • Ключи — контракт для систем сбора: snake_case, стабильные имена, никакого Sprintf внутри Msg.

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

Соберите событие из шести полей разных типов — Str, Int, Float64, Bool, Dur и Time — и рассмотрите JSON: проверьте, что каждый тип сериализовался правильно. Потом замените Dur на Str с d.String() и посмотрите, как число превратилось в строку. Наконец, вызовите Err(nil) на событии Info и убедитесь, что ключа error в выводе нет. В конце сделайте два вызова Str("user", ...) в одной цепочке и посмотрите, как выглядит JSON с дубликатом ключа.

Личные заметкиПусто
Хранятся только в вашем браузере (zerolog · Поля события в zerolog: Str, Int, Err, Dur)

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

Как добавить поле в лог zerolog?

Цепочкой после метода уровня: log.Info().Str("user", "ann").Int("age", 30).Msg("..."). Каждый метод дописывает типизированную пару в JSON: строковые — Str, числовые — Int/Int64/Float64, флаги — Bool, длительности — Dur, ошибки — Err. Для слайсов есть Strs и Ints, для адресов — IPAddr и MACAddr, для готовых JSON-байтов — RawJSON.

Что делает zerolog.Err(err) при err = nil?

Ничего: ключ error не добавляется, событие пишется как обычно. Это позволяет вызывать .Err(err) в общем пути кода без проверки на nil — поле появится только при настоящей ошибке, проверку делает сама библиотека.

Почему .Any медленнее других полей?

Any принимает interface{} и определяет тип через рефлексию в рантайме — это аллокации и заметно более медленный путь. На горячем пути используйте типизированные методы: они пишут байты напрямую в буфер события. Если без Any не обойтись, профилируйте: иногда хватает перехода на Dict без рефлексии.

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

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

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

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

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

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

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