Повышаем информативность ошибок в Go – github.com/ztrue/tracerr

    После многолетнего опыта работы с php и js, я привык иметь в ошибках стектрейс и смотреть на место, где произошла ошибка прямо из эррор-репорта. Пересев на Go пару лет назад, я был несколько удивлен, что в Go другие правила и нужно угадывать стектрейс по какой-нибудь строке типа `invalid character`. А если она произошла на проде и не известно, как ее воспроизвести, то это превращалось в целый аттракцион.

    Поскольку я уверен, что не один от этого страдал, то сделал пакет, который умеет так:

    golang error output

    GitHub

    Все, что он делает, это:

    1. Добавляет к ошибкам стектрейс.
    2. Отображает стектрейс и фрагменты исходников, где эта ошибка произошла (при наличии исходников, конечно).

    Добавление стектрейса


    Создать ошибку со стектрейсом можно одним из нескольких способов:

    // создать новую
    err := tracerr.New("some error")
    
    // можно методом Errorf, который работает так же, как и fmt.Errorf
    err := tracerr.Errorf("some error %d", num)
    
    // или обернуть существующую ошибку, добавив ей стектрейс
    err = tracerr.Wrap(err)
    

    При повторном оборачивании ошибки стектрейс останется прежним и затираться не будет, это удобно в случае, если неизвестно, есть ли уже у ошибки стектрейс или нет.

    Код может выглядеть как-то так:

    func decodeFile(path string, data interface{}) error {
    	b, err := ioutil.ReadFile(path)
    	if err != nil {
    		return tracerr.Wrap(err)
    	}
    	err = json.Unmarshal(b, data)
    	// если err = nil, то останется nil
    	return tracerr.Wrap(err)
    }
    

    Отображение стектрейса


    После того, как ошибка через 100500 if err != nil { return err } вернется на Родину в main() (или там, где она обрабатывается), скорее всего Вы захотите ее отобразить или залогировать.
    Для это есть несколько опций: все работают, как Print (выводит текст) или Sprint (возвращает текст):

    1) Отобразить текст ошибки и стектрейс:

    tracerr.Print(err)
    

    2) Отобразить текст ошибки, стектрейс и фрагмент исходников (6 строк по дефолту):

    tracerr.PrintSource(err)
    

    3) То же, но в цвете, обычно более информативно:

    tracerr.PrintSourceColor(err)
    

    4) Можно передать параметром, сколько строк кода отобразить:

    tracerr.PrintSource(err, 9)
    tracerr.PrintSourceColor(err, 9)
    

    5) Или передать 2 опциональных параметра, сколько до и сколько после строки с ошибкой отобразить:

    tracerr.PrintSource(err, 5, 2)
    tracerr.PrintSourceColor(err, 5, 2)
    

    Вопросы


    Я уже получил некоторый фидбек, поэтому позволю себе ответить заранее на некоторые вопросы, которые уже задавали.

    В: Это подходит только для дебага? Есть дебаггер.
    О: Это подходит не только для дебага, можно логировать ошибки с информацией о стектрейсе, и даже с фрагментами исходников, на проде, как по моему опыту, это существенно упростит потом эти ошибки разбирать.

    В: Есть супер пакет pkg/errors, почему не использовать его?
    О: Есть, я сам его вполне использовал и рад, но он не подошел мне по этим причинам:
    1) Там нет простого способа отобразить стектрейс сразу с исходниками.
    2) При повторном оборачивании ошибки (например, на уровень выше), стектрейс затирается менее информативным.
    3) При каждом оборачивании обязательно передавать дополнительный текст ошибки, что мне кажется некоторым оверхедом при написании/чтении кода.

    В: В Go ошибки это не исключения и так делать вообще нельзя.
    О: Согласен, в Go ошибки это не исключения. Если Вам удобнее обрабатывать тысячи if err != nil { return err } каким-то другим способом — это Ваш выбор, конечно. Вы можете оборачивать только те ошибки, которые обрабатываете как исключения.

    В: Стектрейс добавляет оверхед на производительность.
    О: Да, добавляет, но это актуально только для мест, где ошибки создаются в огромных количествах, просто не добавляйте туда стектрейс, если это критично (уверен, что в большинстве случаев этот оверхед ничтожен).

    В общем, надеюсь, этот пакет несколько облегчит Вашу гоферскую жизнь, буду рад любому фидбеку, спасибо.
    Поделиться публикацией

    Похожие публикации

    Комментарии 30

      0
      Отображает стектрейс и фрагменты исходников, где эта ошибка произошла (при наличии исходников, конечно).


      Не очень понятно, где при этом должны быть исходники?

      При каждом оборачивании обязательно передавать дополнительный текст ошибки, что мне кажется некоторым оверхедом при написании/чтении кода.


      Ну тут на любителя. Наверное, хорошо иметь возможность делать и так, и эдак.
        0
        Исходники должны быть в среде выполнения.
        Насчет и так и так, могу только согласиться. Пожалуй, надо добавить опциональным параметром.
          +2
          Исходники должны быть в среде выполнения.

          Нет, не должны. В Go сборка выполняется в статический бинарник, который можно запустить, даже если Go в системе вообще не установлен. Что уж говорить про исходники.

            0
            «Должны» я говорю в контексте того, что Вы хотите их отображать. Если достаточно стектрейса, то исходники не нужны.
            0
            Все-таки непонятно про исходники — они должны быть рядом с исполняемым файлом или же в GOPATH?

            Про параметр — может быть, там сделать не строку, а сразу ...inteface{}?
              0
              В стектрейсе будет абсолютный путь до файла, где он был во время сборки. Там его будет искать этот пакет.
              У меня есть план сделать, чтобы можно было подменять путь до $GOPATH, но пока не сделал.

              Про парметр, как мне кажется, должны поддерживаться такие сценарии использования:
              tracerr.Wrap(err)
              tracerr.Wrap(err, "some info")
              tracerr.Wrap(err, "some info: %#v %#v", param1, param2)
              


              Как я понимаю, Вы имеете ввиду так?
              tracerr.Wrap(err, param1)
              
                0
                В стектрейсе будет абсолютный путь до файла, где он был во время сборки


                Ну, в принципе, я уже вплотную подхожу к идее собирать содержимое контейнеров по месту развертывания… :)

                Насчет параметров я имею ввиду так:

                tracerr.Wrap(err, "some info", info1, info2...)


                Т.е. функция Error() должна это все как-то универсально соединять, чтобы не мучиться уже с форматированием типа «some info: %#v %#v», а то каждый раз думаешь, как же это сформулировать…
                  0
                  Я думаю, я понял суть, спасибо за пояснение)
                  Выше я описал привычное для Sprintf поведение, но по идее можно сделать разные методы.
                    0
                    Ну, в принципе, я уже вплотную подхожу к идее собирать содержимое контейнеров по месту развертывания… :)


                    Какой в этом бенефит? ИМХО, самое классное — это собрать в pipeline во время ci, и потом только бинарник положить в `FROM scratch` docker-образ. Таким образом у вас вообще ничего, кроме бинарника в докере не будет :) Дальше останется только, управляя переменными окружения, настроить данный образ и всё. Ибо, ИМХО, не очень осмотрительно держать исходники возле бинарника на продукции, если этого можно не делать.
                      0
                      Чем это чревато (почему не осмотрительно)?
                        0
                        Ну, теоретически при проникновении в вашу инфраструктуру злоумышленника, вы можете получить утечку исходников, что в свою очередь может открыть дальнейшие векторы атаки (хотя, конечно, куда уж хуже, но тем не менее). Так зачем ему давать эту (хоть и весьма гипотетическую) возможность? А в случае других неосмотрительностей (конкретно при использовании вашей библиотеки) злоумышленник может получить больше внутренней информации об ошибках: много раз видел, как разработчики допускают отображение стек-трейса при возникновении ошибки в ответе от http-сервера. Все это с точки зрения безопасности весьма плохо. Чем меньше находится лишнего на продакшне, тем лучше.
                          0
                          Чем меньше, тем лучше – согласен. Неосмотрительно – не согласен. Большая часть сайтов, любого масштаба, написана на интерпритируемых языках и, подозреваю, исходники вполне есть на проде.
                          Думаю, что гораздо быстрее и проще исходники можно получить через сотрудника или бывшего сотрудника.
                          Можно код написать и так, что любой `ioutil.ReadFile()` будет опасен.

                          И очевидно, я не сравниваю, что лучше, иметь или не иметь сорсы на проде, а сравниваю, покрывают ли приобретаемые бенефиты дополняющие их недостатки (например, в виде описанной Вами истории, где на сервере завелся хакер с доступом в систему). Если не покрывают (как, вероятно, в Вашем случае, поскольку Вам хватает просто правильно обрабатывать ошибки), то определенно не зачем их там держать.
                          В моем опыте не было ситуаций, когда компания, в которой я работал, переживала бы за то, чтобы держать исходники на своем собственно сервере.
                            0
                            на своем собственно сервере

                            Если сервер действительно свой, и вот он стоит, можно подойти потрогать руками, то гипотетически это один уровень угрозы, а если это сервер в дата-центре, инстанс в AWS, etc — словом, у вас/компании нет физического доступа к серверу — совсем другой.

            +1
            Спасибо, интересная статья!
            Я недавно на C# такое решил сделать: вместо стектрейса — показывать исходники, подсвечивать нужные строки, показывать коментарии, ревизии (+ автор, комментарий и линки на задачи в Jira) + еще немного «поиграв», получилось имена параметров методов с типами и значениями на момент возникновения исключения делать для всего стектрейса (кроме системного), но пока прототип, коллеги не особо оценили, мол а какая от этого польза..))) не был бы я таким «ленивым», объяснил бы доходчиво что это дает, но на мой взгляд тут подумать и все сам поймешь… Потихоньку доделаю рабочий вариант, была идея статейку написать, но врядли)
            Буду признателен если кто-нибудь распишет плюсы и минусы этой идеи, самому совсем не хочется тратить на это время. Сама идея очень понравилась.
              0
              Пилите статью!
                0
                Возможно, прозвучит не убедительно, но времени пока нет даже прототип доделать, не то что статью… Да и статьи ни разу не писал, заминусуют, расстроюсь..))
                  +1
                  Дорогу осилит идущий ;)
              +1

              Не нашел (плохо читал?) как сделать Unwrap ошибке, чтобы узнать ее оригинальный тип/значение за интерфейсом error.


              Согласен, в Go ошибки это не исключения. Если Вам удобнее обрабатывать тысячи if err != nil { return err } каким-то другим способом — это Ваш выбор, конечно

              В том и дело, что в Go ошибки — это не исключения и это правильная обработка значения err приходится кстати, если обрабатывать как можно ближе к месту возникновения, а не пробрасывать наверх по стеку чуть ли не до main.


              уверен, что в большинстве случаев этот оверхед ничтожен

              Без бенчмарка ни в чем нельзя быть уверенным, увы.

                0
                Тут не описывал, но можно получить оригинальную ошибку без стектрейса так: err.Err

                Где обрабатывать ошибки, я не навязываю, как собственно и написал. Описанный способ подойдет для ошибок, которые пробрасываются наверх и для тех людей, кто считает это приемлимым.

                Бенчмарки на сравнение что с чем Вы предлагаете?
                0
                Добавлены Unwrap() и Error.Unwrap().
                –2
                известный в узких кругах Dave Cheney такую библиотеку сделал уже несколько лет назад и даже дал по ней выступление:
                youtu.be/lsBF58Q-DnY
                github.com/pkg/errors
                  +4
                  Оличная библиотека, я написал, почему она мне не подошла, спасибо, что дочитали до конца.
                  0
                  FYI В будущих версиях Go собираются решить проблему отсутствия стек-трейсов. Proposal: Go 2 Error Inspection
                    0
                    Таких библиотек весьма много на самом деле. Нет проблем для себя это написать. Я вот тоже как-то делал: github.com/go-extras/kasoro (да, она попримитивнее, но и сделана была только для себя). Но в конечном итоге использую её только в случае каких-то трудноуловимых ошибок. В обычное время, как тут уже писали выше, своевременная обработка ошибки возле места её возникновения решает большинство проблем.
                      +1
                      «Таких» это каких, которые отображают стектрейс и сорсы? Библиотека по ссылке, насколько я понимаю, может только стектрейс. Да, «таких» которые умею стектрейс полно, я не претендую на новизну)
                      Таких, которые отображают сорсы, я не встречал.
                        0
                        Ну, имхо, главное — это стектрейс, а сорсы — это так, хотя и любопытная, но плюшечка. Да, её вы позиционируете, как основное преимущество библиотеки, но лично я в нем особого смысла не вижу — ведь если дебаг ведется рядом с исходниками, то там можно всегда подноценно отдебагать с брейкпоинтами в нужных местах. А если софт расположен на сервере, то зачем там иметь исходники?

                        P.S. Вы не воспринимайте, если что, как обиду или что-то личное. Я пытаюсь понять use-case, где действительно была бы нужна возможность показать исходники.
                          0
                          Я не воспринимаю как обиду, спасибо, что уделили время и поделились своими мыслями.

                          Мне лично сорсы помогают добавить контекста к эррор-репорту, тем самым ускорить процесс понимания того, что пошло не так. Я допускаю, что не только мне одному. Если кому-то удобно получить стек трейс, затем идти по файлам, чтобы понять контекст, то pkg/errors подойдет гораздо лучше, я думаю.

                          Касательно юз-кейсов на проде, все то же самое – это добавляет контекста к ошибкам, не требует открывать код во многих случаях вообще, чтобы понять, в чем корень. Предлагаю посмотреть такие продукты, как: Sentry и Elastic APM. Они оба используют сорсы в стектрейсах, показывают сорсы и стектрейсы в ошибках, имеют овер 9000 поклонников, и указывают на своих сайтах сорсы в ошибках, как классную фичу: `Sentry shows your source code right in the stacktrace, so you don’t need to find it yourself.` Если этого не достаточно, то наверное, Вам это просто не нужно и у Вас нет такой проблемы.
                        0
                        То что нет проблем для себя это написать – у меня вот есть проблема писать для себя это каждый раз. Но нет проблемы написать 1 раз для себя и других.
                        Я думаю, leftpad (привет, npm) тоже нет проблемы написать для себя, или math.Round (привет, го 1.9) – тем не менее, многие этим не занимаются (как и я, в частности).

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое