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

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

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

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

Ошибки: Err, Stack и AnErr

Ошибки в zerolog — это поля события: Err кладёт их под ключ error, AnErr — под любой ключ, а Stack() добавляет stacktrace, если настроен ErrorStackMarshaler. Разберём вложенные ошибки, поле stack и момент, когда err.Error() не то, что вы хотите увидеть в логе.

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

Ошибка в проде, а в логе — одна строка error":"unexpected EOF. Где она случилась, кто её обёртывал, какая причина под верхним слоем — неизвестно: текст ошибки без контекста превращает разбор в археологию. Структурный логгер решает это полями: ошибка кладётся в JSON как данные, а не как кусок текста сообщения, причину выносят в отдельный ключ, а стек вызовов включают там, где ошибка — действительно сбой. У zerolog для этого четыре вызова: Err для поля по умолчанию, AnErr для своего ключа, Errs для массива и Stack() для стека — и у каждого есть негласный контракт, который стоит прочитать до инцидента, а не после. Контракт проще, чем кажется: Err и AnErr молча пропускают nil, Errs оформляет слайс массивом, а стек — единственный, кто требует глобальной настройки до первого использования. Логирование ошибок в Go — это всегда два вопроса: что положить в запись и где именно её логировать. Первый вопрос разбирается по вызовам ниже; второй — дисциплина, без которой даже правильно размеченные ошибки утонут в тройных записях одного и того же сбоя.

Err в zerolog: поле error из коробки#

Метод Event.Err(err) — главный способ положить ошибку в запись: поле получает имя zerolog.ErrorFieldName, по умолчанию error, и значение err.Error(). Нулевая ошибка пропускается целиком — поля error":null в ленте не появится, и это поведение стоит помнить при цепочках, где err может быть nil. Внутри метода стоит проверка на nil, поэтому вызов безопасен в любом месте цепочки и не стоит ничего. Работает Err и на других уровнях: Warn().Err(err) — нормальная форма для «упало, но пережили». Сокращение Logger.Err(err) сразу поднимает уровень до error и добавляет поле одним вызовом — удобно наверху обработки. Нюанс из док-комментария: «error-уровень с полем, если err не nil, иначе — info», то есть при nil-ошибке отдаётся Info-событие без поля, и ветка остаётся читаемой записью даже без сбоя. Главная ловушка — err.Error(): метод возвращает склеенную цепочку «outer: middle: root», и в JSON уезжает вся строка целиком. Для поиска по корню это боль: фильтр error"deadline не найдёт запись с текстом charge" timeout" deadline. Поэтому либо кладите причину отдельным ключом через AnErr, либо разматывайте цепочку errors.Is и errors.As и логируйте её явно. Для глобальной настройки есть zerolog.ErrorMarshalFunc — функция, через которую проходит каждая ошибка перед записью; ей можно вернуть структуру с типом и кодом вместо сырого текста. Сама механика полей события разобрана в уроке про поля, а правило «не склеивать ошибку в текст сообщения» — из антипаттернов логов: строка в Msgf не фильтруется, поле фильтруется.

Err, AnErr и Errs в одной записи

GO
package main

import (
        "errors"
        "fmt"
        "os"

        "github.com/rs/zerolog"
)

var errQuota = errors.New("quota exceeded")

func charge(amount int) error {
        if amount > 100 {
                return fmt.Errorf("charge %d: %w", amount, errQuota)
        }
        return nil
}

func main() {
        logger := zerolog.New(os.Stdout).With().Timestamp().Logger()

        err := charge(150)
        logger.Error().
                Err(err).
                AnErr("cause", errors.Unwrap(err)).
                Msg("payment rejected")

        logger.Warn().
                Errs("problems", []error{errQuota, errors.New("disk full")}).
                Msg("deferred")
}

// Output:
// {"level":"error","error":"charge 150: quota exceeded","cause":"quota exceeded","message":"payment rejected",...}
// {"level":"warn","problems":["quota exceeded","disk full"],"message":"deferred",...}

Err пишет поле error — текст содержит всю цепочку, потому что fmt.Errorf с %w склеивает err.Error(). AnErr кладёт причину под собственным ключом cause: errors.Unwrap достаёт следующее звено, и фильтр по cause ловит корень без разбора текста. Errs сериализует слайс ошибок JSON-массивом под ключом problems — место для мульти-ошибок валидации.

AnErr и Errs: когда одного поля мало#

AnErr(key, err) — тот же Err, но под вашим ключом: cause, db_err, remote_err. Зачем второй ключ: фильтры по error ловят всё подряд, а по cause — только то, что вы пометили как причину; текст верхнего уровня и корень живут раздельно, и дашборд по cause не всплывает от посторонних ошибок. Нулевая ошибка пропускается: AnErr("cause", nil) не оставит в записи следа, поэтому условная цепочка AnErr без if работает безопасно. Errs(key, errs) — для коллекций: слайс ошибок уезжает JSON-массивом под заданным ключом, и в v1.34.0 nil-элементы сериализуются как null, а пустой слайс — пустым массивом; учитывайте это, собирая мульти-ошибку из валидации. Сценарий у Errs один — «набор проблем одного события»: список причин отказа, накопленные ошибки ретраев, итог батча. errors.Join из стандартной библиотеки вписывается в эту картину с оговоркой: он склеивает ошибки в одну, и Err покажет весь список текстом — если нужна структура массивом, держите слайс и пишите Errs. Для одиночной ошибки Err и AnErr остаются основными вызовами, а Logger.Err стоит держать в голове как короткую форму: уровень и поле одним вызовом, без отдельной цепочки.

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

  • Err — ключ error по умолчанию; глобально имя меняет zerolog.ErrorFieldName
  • AnErr — тот же Err под своим ключом: cause, db_err; nil пропускается
  • Errs — слайс ошибок массивом; в v1.34.0 nil-элементы становятся null
  • err.Error() — вся цепочка текстом «outer: middle: root»; фильтруйте с оглядкой
  • причину доставайте errors.Is, errors.As или errors.Unwrap и кладите отдельным полем
  • не склеивайте ошибку в Msgf — строка не фильтруется, поле фильтруется
  • Logger.Err(err) — уровень error и поле одним вызовом; при nil отдаёт Info

Stack: stacktrace в поле stack#

Стек вызовов в лог zerolog не пишет сам: у события есть метод Stack(), но он лишь ставит флаг, а поле добавляет глобальный маркер zerolog.ErrorStackMarshaler. По умолчанию маркер nil, и Stack() без настройки ничего не делает — самая тихая ловушка урока. Маршрут из коробки: zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack из пакета github.com/rs/zerolog/pkgerrors — он работает с ошибками github.com/pkg/errors, которые захватывают стек в момент создания. Тип маркера — func(err error) interface{}: собственная реализация на runtime.Callers или debug.Stack встраивается одной функцией, если тянуть pkg/errors в проект не хочется. MarshalStack разматывает цепочку через Unwrap, находит ошибку с методом StackTrace() и сериализует кадры массивом объектов с ключами source, line и func под именем zerolog.ErrorStackFieldName — по умолчанию stack. Порядок вызовов важен: Stack() ставит флаг, Err читает его, поэтому цепочка собирается как Error().Stack().Err(err) — наоборот флаг опоздает. Стек переживает обёртывание: fmt.Errorf("%w") поверх pkg/errors-ошибки не рвёт цепочку, и MarshalStack докопается до оригинала — потому стек захватывается там, где ошибка рождается, а сколько слоёв её потом обернули — неважно.

Стек через pkgerrors.MarshalStack

GO
package main

import (
        "github.com/pkg/errors"
        "github.com/rs/zerolog/log"
        "github.com/rs/zerolog/pkgerrors"

        "github.com/rs/zerolog"
)

func middle() error {
        return errors.New("connection refused")
}

func main() {
        zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack

        err := middle()
        log.Error().Stack().Err(err).Msg("dial failed")
}

// Output:
// {"level":"error","error":"connection refused","stack":[{"source":"main.go","line":15,"func":"main.middle"},...],"message":"dial failed",...}

Ошибка создана errors.New из pkg/errors — в момент создания захвачен стек. ErrorStackMarshaler настроен на pkgerrors.MarshalStack, поэтому Error().Stack().Err(err) пишет и поле error с текстом, и поле stack: массив кадров с source, line и func. Порядок Stack() до Err() обязателен: Err читает флаг стека в момент вызова, а не при финализации.

Когда стек нужен, а когда мусор#

Стек — инструмент программных ошибок: паника, нарушенный инвариант, невозможная ветка. Для доменных отказов он почти всегда шум: «пользователь не найден» — это данные ответа, а не сбой, и кадры на двадцать строк лишь раздувают лог. Спорное мнение, которое сэкономит вам гигабайты: если ошибка — ожидаемая часть бизнес-логики, стек ей не нужен, хватит текста и причин в отдельных полях. Условие, при котором я прав: ожидаемые ошибки вы различаете типом или errors.Is, а не разбором строки — иначе без стека не поймёте, откуда взялся unexpected EOF. Вторая ловушка — переиспользуемые ошибки: синглтон errNotFound из пакета захватил стек при инициализации, и MarshalStack честно покажет кадры package init — правды о месте отказа там нет. Поэтому стеки имеет смысл включать там, где ошибки создаются локально на каждый вызов, а для сентинелов — ошибок-констант — стек бессмыслен с рождения. Практический компромисс: Stack() на Error-событиях, Warn и ниже — без кадров, и объём лога под контролем. Кадр стека весит заметно больше остальной записи — объект с source, line и func на каждый кадр, — и на шумном Error-потоке эти сотни байт на событие превращаются в гигабайты за месяц.

Двойное логирование — типовая коза: нижний слой пишет Warn, средний Error, наверху ещё один Error с той же ошибкой — в ленте три строки на один сбой, и алерт срабатывает трижды. Договоритесь на одном месте: ошибка логируется там, где её обрабатывают, — где принимается решение откатить, отретраить или отдать 500. Внизу — возврат с обёрткой fmt.Errorf("%w") и контекстом того, что делали, наверху — одна запись. Уровень выбирается по последствию: не доехало до клиента и пережито — Warn, упал запрос — Error. К этой дисциплине возвраты без логов относятся так же строго, как и к тройным записям: замолчанная ошибка не лучше тройной. Промежуточный слой, которому нечего добавить, просто возвращает ошибку дальше — обёртка ради обёртки удлиняет текст, не добавляя смысла. Если ошибка уходит в ответ клиенту, поле error и request_id из урока про HTTP-логи дают дежурному всё сразу: что случилось и в каком запросе искать подробности — остальное собирается фильтром по идентификатору.

Способы положить ошибку в запись#

ВызовКлюч в JSONnil на входе
Event.Err(err)error (ErrorFieldName)поле не добавляется
Event.AnErr(key, err)ваш ключполе не добавляется
Event.Errs(key, errs)ваш ключ — массивnil-элементы становятся null
Event.Stack().Err(err)error + stackбез ErrorStackMarshaler стек не пишется
Logger.Err(err)error, уровень errornil даёт Info-событие без поля

Проверка: имя поля

Какое имя поля по умолчанию заполняет Err()?

Проверка: условие стека

Что нужно, чтобы Stack() добавил поле stack?

Проверка: nil в AnErr

Что сделает AnErr("cause", nil)?

Что уедет в поле error?

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

root := errors.New("deadline")
err := fmt.Errorf("charge: %w", root)

log.Error().Err(err).Msg("failed")

Включите стеки

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

func main() { zerolog. = pkgerrors. err := errors.New("boom") log.().().Err(err).Msg("failed") }

Программа со стеком

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

Строки

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

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

Карточки: ошибки

Термин

Event.Err

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

Пишет ошибку под ключом error (zerolog.ErrorFieldName). nil пропускает; значение — err.Error(), вся цепочка текстом.

1 / 6

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

Ошибка в JSON — уже половина диагностики; курс доводит её до конца. Производительность покажет, сколько стоит событие с Err на горячем пути и где включать Disabled, урок про структуру JSON — как устроены вложенные объекты и массивы вроде поля stack, а тесты логов научат проверять error и cause в тестах буфером. Справочное закрепление: соберите три слоя — репозиторий, сервис, хендлер — и проведите одну ошибку через все, обёртывая в каждом fmt.Errorf с %w; в access-логе должен остаться один Error с текстом цепочки и причинами в отдельных полях.

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

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

Вывод

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

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

  • Err — ключ error (zerolog.ErrorFieldName); AnErr — свой ключ; Errs — массив.
  • nil у Err и AnErr пропускается; в Errs nil-элементы становятся null.
  • err.Error() склеивает цепочку «outer: middle: root» — фильтруйте с оглядкой.
  • Stack() ставит флаг; поле stack пишет ErrorStackMarshaler, например pkgerrors.MarshalStack.
  • Порядок: Error().Stack().Err(err) — флаг должен быть поставлен до Err.
  • Стек — для программных ошибок; ожидаемые доменные отказы живут текстом и причинами.
  • Ошибку логируем раз — там, где обрабатываем; внизу обёртка, наверху запись.
  • Сентинел-ошибки захватывают стек при init — кадры init в логе ничего не значат.

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

Соберите программу урока и убедитесь, что в JSON уехали и error, и cause. Перепишите сценарий на errors.Join и сравните: Err покажет склеенный текст, Errs — массивом. Включите pkgerrors.MarshalStack, создайте ошибку через pkg/errors глубоко в функции-внуке и проверьте в поле stack кадры с её именем. Сломайте порядок — Stack() после Err() — и запишите своими словами, чего не хватило в записи. Наконец, добавьте Warn-событие с AnErr("cause", nil) и найдите его в ленте: поле cause не появится — это и есть контракт nil. Замерьте размер записи со стеком и без — разница в байтах станет аргументом, когда кто-нибудь предложит включить кадры на Warn.

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

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

Как залогировать ошибку под своим ключом, а не error?

AnErr: logger.Error().AnErr("db_err", err).Msg("query failed") — значение то же, ключ ваш. Обычный Err продолжает писать в error, поэтому верхний текст и конкретика живут раздельно; nil в AnErr пропускается целиком, и условные цепочки работают без if.

Почему поле stack пустое, хотя Stack() вызван?

Три причины по порядку проверки: Stack() вызван после Err(), маркер zerolog.ErrorStackMarshaler не настроен, ошибка создана без захвата стека — errors.New из stdlib или сентинел-константа. Настройте pkgerrors.MarshalStack и создавайте ошибки через pkg/errors там, где стек нужен. Четвёртая причина экзотичнее: имя поля переименовано через zerolog.ErrorStackFieldName, и вы ищете в JSON stack, хотя запись содержит другое имя.

Куда девать ошибки из errors.Join?

Join возвращает одну ошибку, чей Error() склеивает все звенья, — Err покажет текст целиком. Для структуры удобнее Errs: передайте слайс ошибок массивом под одним ключом, но помните про nil-элементы — в v1.34.0 они сериализуются как null, а пустой слайс — пустым массивом.

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

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

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

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

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

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

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