---
title: "Поля события в zerolog: Str, Int, Err, Dur"
description: "Поля события zerolog: типизированные Str, Int, Bool и Dur, Err — ошибки как данные, порядок ключей в JSON, цена Any, единицы Dur и соглашения об именах полей."
course: "Zerolog в Go: быстрое JSON-логирование"
url: "https://golangmaster.ru/courses/zerolog/lessons/04-polya-sobytiya/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

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

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

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

Поле в 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 и по умолчанию пишет значение числом миллисекунд: 317*time.Millisecond превращается в "elapsed":317. За единицу отвечает глобальная переменная zerolog.DurationFieldUnit — по умолчанию time.Millisecond; если вашим дашбордам нужны секунды, поставьте её равной time.Second при старте, а DurationFieldInteger=true отрежет дробную часть у целых значений. Почему за дефолт взяты миллисекунды: это единица латентностей — HTTP-запросы, запросы к базе и кэшу живут в десятках и сотнях миллисекунд, и именно в этих единицах построены графики p50 и p99. Дробные доли миллисекунды Dur тоже пишет: 500*time.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 | Заметка |
| --- | --- | --- | --- |
| Str | string | ваш ключ | идентификаторы, статусы, версии |
| Int / Int64 / Float64 | числа | ваш ключ | без кавычек — готовы к агрегации |
| Bool | bool | ваш ключ | true / false без кавычек |
| Dur | time.Duration | ваш ключ | число миллисекунд по умолчанию |
| Time | time.Time | ваш ключ | формат задаёт TimeFieldFormat |
| Err | error | error | nil не добавляет ключ вовсе |
| AnErr / Errs | error / []error | ваш ключ | ошибка-объект и слайс ошибок |
| Any | interface{} | ваш ключ | рефлексия: медленнее и с аллокациями |
| 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 пишет как есть — маскируйте токены и персональные данные до вызова, а не после; теме секретов посвящён [отдельный урок](/courses/zerolog/lessons/18-log-polya-sekrety/).

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

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

> **Sprintf внутри Msg — минус поля.** Msgf("payment for order %s failed: %v", id, err) греет глаза, но фильтры бессильны: и идентификатор, и ошибка заперты в message. Что сломалось бы: дашборд по order_id, алерт по тексту ошибки, частоты провалов — всё становится grep-ом. Данные — в поля, в message — только факт: payment failed. Ловится это ревью одного вызова: посчитайте плейсхолдеры в Msgf — больше одного, значит данные просятся в поля.

> **Один тип — один метод.** Числа пишите Int и Int64, а не Str: числовое поле агрегируется в ClickHouse и Loki, строковое — нет. Статусы и идентификаторы — Str. Проверяйте типы на ревью: поле, однажды уехавшее строкой, будет строкой во всех дашбордах — переименовать его позже дороже, чем сразу выбрать Int. Исключение — идентификаторы с ведущими нулями и коды вроде 007: они числа только формально, пишите их Str.

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

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

- В JSON появится "error": null
- В JSON появится "error": ""
- ✅ Ключа error не будет вовсе: запись уйдёт без поля
- Паника: Err не принимает nil

Err с nil-ошибкой возвращает событие без поля — zerolog не пишет ни null, ни пустую строку. Поэтому один и тот же вызов лога безопасен и в успешном пути, и в обработчике ошибки: ключ появляется только вместе с настоящей ошибкой.

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

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

- В секундах
- ✅ В миллисекундах, числом
- В наносекундах — как считает time.Duration
- Строкой вида "250ms"

Глобальная переменная zerolog.DurationFieldUnit задаёт единицу — по умолчанию time.Millisecond, поэтому 250*time.Millisecond даёт "elapsed":250. Единицу и целочисленный формат (DurationFieldInteger) меняют один раз при старте.

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

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

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

- ✅ {"level":"info","a":"1","b":"2","message":"hi"}
- {"level":"info","message":"hi","a":"1","b":"2"}
- {"a":"1","b":"2","message":"hi","level":"info"}
- {"level":"info","a":"1","b":"2","message":"hi","a":"1"}

Сначала уровень, потом поля в порядке вызовов, потом message — zerolog дописывает его при финализации последним. Дубликаты ключей zerolog не дедуплицирует, но здесь каждый ключ встречается один раз.

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

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

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

- ✅ "elapsed":250
- "elapsed":0.25
- "elapsed":"250ms"
- "elapsed":250000000

Dur кодирует длительность числом миллисекунд: единица задана DurationFieldUnit по умолчанию. Секунды вышли бы при DurationFieldUnit = time.Second, а строка "250ms" — это то, что вернул бы d.String(), но Dur строк не пишет.

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

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

```go
package main

import (
        "errors"

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

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

        log.___().
                ___(err).
                ___("attempt", 2).
                Msg("connect failed")
}
```

Подсказка: Уровень провала — Error; текст ошибки кладёт Err; число попыток дописывает Int.

Правильные ответы в пропусках: `Error`, `Err`, `Int`

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

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

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

1. `package main`
2. `import (`
3. `  "errors"`
4. `  "github.com/rs/zerolog/log"`
5. `)`
6. `func main() {`
7. `  err := errors.New("connection refused")`
8. `  log.Error().Err(err).Str("op", "dial").Msg("connect failed")`
9. `}`

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

- **Поле события** — Пара ключ-значение, дописанная методом цепочки: .Str("user", "ann"). Тип известен при компиляции — сериализация без рефлексии.
- **Str / Int / Bool** — Базовые типизированные методы: строка, число, флаг. В JSON значение сохраняет свой тип — числа агрегируются системами сбора.
- **.Err(err)** — Ключ error с текстом ошибки. При err == nil ключ не добавляется — вызов безопасен в любом пути кода.
- **.Dur(key, d)** — Длительность числом миллисекунд (DurationFieldUnit). Строковый d.String() ломает сортировку и агрегацию.
- **.Any(key, v)** — Универсальное поле для interface{}: кодирует рефлексией, стоит аллокаций. Уместно на редких путях, не в горячем коде.
- **Дубликаты ключей** — Zerolog не дедуплицирует: два Str("user", ...) дадут два ключа user в JSON. Следите за цепочками при сборке события по частям.

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

Следующий урок — [Logger.With](/courses/zerolog/lessons/06-log-context/): поля, которые есть у каждого события, от имени модуля до request_id запроса. Затем — [условные поля](/courses/zerolog/lessons/07-uslovnye-polya/): собирать событие по частям и платить только за нужное. Для ошибок глубже — урок про логирование ошибок со стеком и обёртками, а про то, как поля едут в Loki и ELK, расскажет урок про структуру JSON.

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

- Поля 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?

Цепочкой после метода уровня: 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 без рефлексии.
