Структурированные логи + локальный стек вызовов: эволюция обработки ошибок в Go

от автора

Каждый 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 }

Проблемы такого подхода:

  • Дублирование названий методов в сообщениях об ошибках
  • Ручное добавление метаданных (аргументы, переменные) в каждое место
  • Сложность отслеживания места возникновения ошибки при нескольких точках выхода
  • Засорение кода — повторяющийся boilerplate
  • Отсутствие структуры — все метаданные упакованы в одной строке

Что если объединить мощь структурированного логирования (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 }

Ключевые возможности

1. Автоматический стек вызовов

Больше не нужно вручную указывать названия методов — стек собирается автоматически:

level=ERROR msg="other error" val=nil  stack[0]="lib.go:26" stack[1]="lib.go:22"

2. Умная идентификация точек ошибок

При нескольких возможных местах возникновения ошибки:

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"

3. Создание новых структурированных ошибок

err = log.NewError("text", "key1", "value1", "key2", "value2")

4. Умное обёртывание

При многократном обёртывании стек сохраняется от первого вызова:

err = log.WrapError(err, "arg1", "val1") // стек записывается здесь err = log.WrapError(err, "arg2", "val2") // стек сохраняется

5. Защита от забытых обёрток

Если забыли обернуть ошибку, DebugOrError всё равно добавит стек:

func (*SomeObject).SomeMethod() (err error) {   // стек всё равно добавится, но только на этот вызов   defer log.DebugOrError("debug description", &err)     return errors.New("unwrapped error")  }

6. Накопление метаданных

Аргументы из всех уровней обёртывания объединяются:

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
  • Показывает только релевантный код (исключает stdlib и зависимости)
  • Предоставляет читаемые пути относительно корня проекта
  • Кеширует метаинформацию для производительности

API для логирования

// Логирование с автоматическим переключением уровня при ошибке 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. Это эксперимент, направленный на упрощение повседневной работы с ошибками. Автор открыт для обратной связи и предложений по дальнейшему развитию идеи.


ссылка на оригинал статьи https://habr.com/ru/articles/915660/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *