Структурированные логи + локальный стек вызовов: эволюция обработки ошибок в Go
- четверг, 5 июня 2025 г. в 00:00:06
Каждый Go-разработчик знаком с этим паттерном — создание обёрток для ошибок с дублированием метаданных:
func (*SomeObject).SomeMethod(val any) error {
if err := otherMethod(val); err != nil {
return fmt.Errorf("otherMethod %w with val %v", err, val)
}
return nil
}
Проблемы такого подхода:
Что если объединить мощь структурированного логирования (slog
) с автоматическим сбором локального стека вызовов. Результат — чистый код и информативные логи.
Было:
func (*SomeObject).SomeMethod(val any) error {
if err := otherMethod(val); err != nil {
slog.Error(err, "val", val) // дублирование
return fmt.Errorf("someMethod %w with val %v", err, val)
}
return nil
}
Стало:
func (*SomeObject).SomeMethod(val any) (err error) {
defer log.DebugOrError("debug description", &err)
if err = otherMethod(val); err != nil {
return log.WrapError(err, "val", val)
}
return nil
}
Больше не нужно вручную указывать названия методов — стек собирается автоматически:
level=ERROR msg="other error" val=nil
stack[0]="lib.go:26" stack[1]="lib.go:22"
При нескольких возможных местах возникновения ошибки:
func (*SomeObject).SomeMethod() (err error) {
defer log.DebugOrError("debug description", &err)
if err := otherMethod1(); err != nil {
return log.WrapError(err) // line 25
}
if err := otherMethod2(); err != nil {
return log.WrapError(err) // line 29
}
return nil
}
Получаем точное указание места в логах:
level=ERROR msg="other error 1"
stack[0]="lib.go:25" stack[1]="lib.go:22"
level=ERROR msg="other error 2"
stack[0]="lib.go:29" stack[1]="lib.go:22"
err = log.NewError("text", "key1", "value1", "key2", "value2")
При многократном обёртывании стек сохраняется от первого вызова:
err = log.WrapError(err, "arg1", "val1") // стек записывается здесь
err = log.WrapError(err, "arg2", "val2") // стек сохраняется
Если забыли обернуть ошибку, DebugOrError
всё равно добавит стек:
func (*SomeObject).SomeMethod() (err error) {
// стек всё равно добавится, но только на этот вызов
defer log.DebugOrError("debug description", &err)
return errors.New("unwrapped error")
}
Аргументы из всех уровней обёртывания объединяются:
func (*SomeObject).SomeMethod() (err error) {
defer log.DebugOrError("debug description", &err, "arg3", "val3")
err = log.NewError("my error", "arg1", "val1")
// ...
err = log.WrapError(err, "arg2", "val2")
return err
}
Результат:
level=ERROR msg="my error" arg1=val1 arg2=val2 arg3=val3
stack[0]="lib.go:25" stack[1]="lib.go:22"
type CustomError struct {
error
Args []any // структурированные аргументы
Stack []*CallInfo // локальный стек вызовов
}
go.mod
// Логирование с автоматическим переключением уровня при ошибке
logger.DebugOrError("operation completed", &err, "user_id", 123)
logger.InfoOrError("data processed", &err, "records", count)
logger.WarnOrError("cache miss", &err, "key", cacheKey)
✅ Чистота кода — убрали boilerplate
✅ Автоматические метаданные — стек и аргументы собираются сами
✅ IDE-friendly — клик по ссылке ведёт прямо к проблемному коду
✅ Производительность — кеширование метаданных проекта
✅ Безопасность — защита от забытых обёрток
✅ Масштабируемость — работает с любой структурой проекта
Структурированные ошибки с локальным стеком вызовов — это естественная эволюция обработки ошибок в Go. Они сохраняют философию явности языка, но избавляют от рутинного копипаста, делая код чище, а отладку — приятнее.
Решение особенно эффективно в микросервисной архитектуре, где важно быстро локализовать проблемы, и при работе с командой, где каждая минута, сэкономленная на отладке, на счету.
Важно понимать, что логи — это лишь одна из составляющих полноценной телеметрии приложения наряду с метриками и трейсингом. Однако структурированные логи в связке со структурированными ошибками открывают новые возможности для observability-стека:
Grafana + Loki: Структурированный формат позволяет строить более точные запросы и дашборды. Автоматические поля стека (stack[0]
, stack[1]
) становятся удобными фильтрами для группировки ошибок по местам возникновения.
Алертинг: Постоянная структура метаданных упрощает настройку умных алертов, которые могут анализировать не только факт ошибки, но и её характеристики.
Аналитика: Накопленные структурированные данные позволяют выявлять паттерны в ошибках, горячие точки в коде и тренды деградации производительности.
Таким образом, инвестиции в качественное логирование на уровне кода окупаются на уровне всей инфраструктуры мониторинга.
Полная реализация и её наглядное применение в репозитории пет-проекта
Disclaimer: Предлагаемое решение не претендует на роль панацеи от всех проблем логирования в Go. Это эксперимент, направленный на упрощение повседневной работы с ошибками. Автор открыт для обратной связи и предложений по дальнейшему развитию идеи.