Отчёт об ошибках в приложениях Rails

Это руководство представляет способы управления ошибками, которые случаются в приложениях Ruby on Rails: использование репортера об ошибках, создание пользовательских получателей и обогащение контекста через middleware.

Это руководство представляет способы управления ошибками, которые случаются в приложениях Ruby on Rails.

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

  • Как использовать репортер об ошибках Rails, чтобы отлавливать и сообщать об ошибках.
  • Как создавать пользовательских получателей (subscribers) для вашего сервиса отчёта об ошибках.
  • Как добавлять общий контекст ошибок с помощью middleware.

Отчёт об ошибках

Репортер об ошибках Rails предоставляет стандартный способ собирать ошибки, случающиеся в вашем приложении, и отправлять их в предпочитаемый вами сервис или место (например, можно отправлять ошибки в сервис мониторинга, такой как Sentry).

Репортер об ошибках нацелен заменить шаблонный код обработки ошибок наподобие:

begin
  do_something
rescue SomethingIsBroken => error
  MyErrorReportingService.notify(error)
end

на единообразный интерфейс:

Rails.error.handle(SomethingIsBroken) do
  do_something
end

Rails оборачивает все выполнения (такие как HTTP-запросы, задачи и вызовы rails runner) в репортер об ошибках, поэтому любые необработанные ошибки, вызванные в вашем приложении, автоматически будут отправлены вашему сервису отчёта об ошибках через его получателей.

Rails автоматически добавляет в контекст отчёта 2 ключа:

  • :controller — значением которого является экземпляр контроллера для отчётов на основе запроса;
  • :job — значением которого является экземпляр задачи для отчётов на основе Active Job.

NOTE: Для HTTP-запросов ошибки, присутствующие в ActionDispatch::ExceptionWrapper.rescue_responses, не отправляются, так как они не приводят к ошибкам сервера (500) и обычно не являются багами, требующими исправления.

Это означает, что сторонним библиотекам отчёта об ошибках больше не нужно вставлять Rack middleware или делать monkey-patching, чтобы захватить необработанные ошибки. Библиотеки, использующие Active Support, также могут это использовать, чтобы ненавязчиво отчитываться о предупреждениях, которые раньше терялись в логах.

NOTE: Использование репортера об ошибках Rails не является обязательным, поскольку другие средства отлова ошибок всё ещё работают.

Подписка на репортер

Чтобы использовать репортер об ошибках со сторонним сервисом, необходим получатель (subscriber). Получателем может быть любой Ruby-объект с методом report. Когда в вашем приложении происходит ошибка или о ней сообщается вручную, репортер об ошибках Rails вызовет этот метод с объектом ошибки и некоторыми опциями.

NOTE: Некоторые библиотеки для отчёта об ошибках, такие как Sentry и Honeybadger, автоматически регистрируют для вас получателя.

Также можно создать пользовательского получателя. Например:

# config/initializers/error_subscriber.rb
class ErrorSubscriber
  def report(error, handled:, severity:, context:, source: nil)
    MyErrorReportingService.report_error(error, context: context, handled: handled, level: severity)
  end
end

После определения класса получателя, зарегистрируйте его, вызвав метод Rails.error.subscribe:

Rails.error.subscribe(ErrorSubscriber.new)

Можно зарегистрировать сколько угодно получателей. Rails вызовет их по очереди в порядке, в котором они были зарегистрированы.

Также можно отменить регистрацию получателя, вызвав Rails.error.unsubscribe. Это может быть полезно, если необходимо заменить или убрать получателя, добавленного одной из ваших зависимостей. И subscribe, и unsubscribe могут принимать как сам получатель, так и его класс:

subscriber = ErrorSubscriber.new
Rails.error.unsubscribe(subscriber)
# или
Rails.error.unsubscribe(ErrorSubscriber)

NOTE: Репортер об ошибках Rails всегда вызывает зарегистрированных получателей независимо от окружения. Однако многие сервисы отчёта об ошибках по умолчанию сообщают об ошибках только в production. Вам следует настроить и протестировать вашу конфигурацию во всех окружениях по необходимости.

Использование репортера об ошибках

У репортера об ошибках Rails есть четыре метода, позволяющих отчитываться об ошибках разными способами:

  • Rails.error.handle
  • Rails.error.record
  • Rails.error.report
  • Rails.error.unexpected

Отчёт и проглатывание ошибок

Метод Rails.error.handle отчитается о любой ошибке, вызванной в блоке. Затем он проглотит ошибку, и остальной ваш код вне блока будет выполняться как обычно.

result = Rails.error.handle do
  1 + "1" # вызовет TypeError
end
result # => nil
1 + 1 # Это будет выполнено

Если в блоке не вызвана ошибка, Rails.error.handle вернёт результат блока, в противном случае вернёт nil. Это поведение можно переопределить, предоставив fallback:

user = Rails.error.handle(fallback: -> { User.anonymous }) do
  User.find(params[:id])
end

Отчёт и повторный вызов ошибок

Метод Rails.error.record отчитается об ошибках всем зарегистрированным получателям, а затем повторно вызовет ошибку, что означает, что остальной ваш код не будет выполнен.

Rails.error.record do
  1 + "1" # вызовет TypeError
end
1 + 1 # Это не будет выполнено

Если в блоке не вызвана ошибка, Rails.error.record вернёт результат блока.

Отчёт об ошибках вручную

Также можно вручную отчитаться об ошибках, вызвав Rails.error.report:

begin
  # код
rescue StandardError => e
  Rails.error.report(e)
end

Любые переданные опции будут переданы получателям ошибки.

Отчёт о неожиданных ошибках

Можно сообщить о любой неожиданной ошибке, вызвав Rails.error.unexpected.

Если этот метод вызван в production, он вернёт nil после того, как об ошибке будет сообщено, и выполнение вашего кода продолжится.

Если этот метод вызван в development или test, ошибка будет обёрнута в новый класс ошибки (чтобы убедиться, что она не будет перехвачена выше по стеку) и показана разработчику для отладки.

Например:

def edit
  if published?
    Rails.error.unexpected("[BUG] Attempting to edit a published article, that shouldn't be possible")
    false
  end
  # ...
end

NOTE: Этот метод предназначен для аккуратной обработки любых ошибок, которые могут возникнуть в production, но не ожидаются как результат типичного использования.

Опции отчёта об ошибках

API отчётов #handle, #record и #report поддерживают следующие опции, которые затем передаются всем зарегистрированным получателям:

  • handled: Boolean, указывающий, была ли обработана ошибка. По умолчанию true. #record устанавливает значение false.
  • severity: Symbol, описывающий серьёзность ошибки. Ожидаемые значения: :error, :warning и :info. #handle устанавливает значение :warning, а #record устанавливает значение :error.
  • context: Hash для предоставления большего контекста об ошибке, например подробностей о запросе или пользователе.
  • source: String об источнике ошибки. Источник по умолчанию — "application". Ошибки, вызываемые внутренними библиотеками, могут устанавливать другие источники; например, библиотека Redis-кэша может использовать "redis_cache_store.active_support". Ваш получатель может использовать источник, чтобы игнорировать ошибки, в которых он не заинтересован.
Rails.error.handle(context: { user_id: user.id }, severity: :info) do
  # ...
end

Установка глобального контекста

В дополнение к установке контекста с помощью опции context, можно использовать Rails.error.set_context. Например:

Rails.error.set_context(section: "checkout", user_id: @user.id)

Любой установленный таким образом контекст будет объединён с опцией context:

Rails.error.set_context(a: 1)
Rails.error.handle(context: { b: 2 }) { raise }
# Контекст отчёта будет: {:a=>1, :b=>2}
Rails.error.handle(context: { b: 3 }) { raise }
# Контекст отчёта будет: {:a=>1, :b=>3}

Установка контекста с помощью Error Context Middleware

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

Например, предположим, у вас работает несколько получателей ошибок (один для вашего APM, один для сервиса логирования) и вы хотите, чтобы каждая отчётная ошибка содержала текущий ID запроса и тенанта. Без middleware вам пришлось бы добавлять этот поиск в каждый получатель. С middleware контекста вы обогащаете контекст один раз, и каждый получатель его получает. Так же гем может добавить общий контекст к сообщениям об ошибках, не требуя от хост-приложения изменять своих получателей ошибок. Задачи «установить контекст ошибки» и «отправить данные в трекер ошибок» должны быть разделены между middleware и получателями.

Middleware контекста получает те же параметры, что и Rails.error.report. Стек middleware возвращает хэш после выполнения всех middleware — каждый middleware может изменять хэш контекста и должен возвращать новый хэш контекста.

class MyErrorContextMiddleware
  def call(error, context:, handled:, severity:, source:)
    context.merge({ foo: :bar })
  end
end

Rails.error.add_middleware(MyErrorContextMiddleware.new) # Добавить middleware в конец стека контекста ошибок

Rails.error.report(error, context: { bar: :baz }) # Отправить отчёт об ошибке с некоторым контекстом
# Отчётный контекст будет содержать { bar: :baz, foo: :bar }

Middleware контекста выполняется после применения к хэшу контекста глобального контекста, установленного через Rails.error.set_context.

Фильтрация по классам ошибок

С помощью Rails.error.handle и Rails.error.record также можно выбрать, чтобы сообщать только об ошибках определённых классов. Например:

Rails.error.handle(IOError) do
  1 + "1" # вызовет TypeError
end
1 + 1 # TypeError не является IOError, поэтому это *не* будет выполнено

Здесь TypeError не будет захвачен репортером об ошибках Rails. Будут отправлены только экземпляры IOError и его потомков. Любые другие ошибки будут вызваны как обычно.

Отключение уведомлений

Можно предотвратить уведомление получателя об ошибках на время выполнения блока, вызвав Rails.error.disable. Аналогично subscribe и unsubscribe, можно передать как сам получатель, так и его класс.

Rails.error.disable(ErrorSubscriber) do
  1 + "1" # TypeError не будет отправлен через ErrorSubscriber
end

NOTE: Это также может быть полезно для сторонних сервисов отчёта об ошибках, которые могут захотеть управлять обработкой ошибок другим способом или выше по стеку.

Библиотеки для отчёта об ошибках

Библиотеки для отчёта об ошибках могут регистрировать своих получателей в Railtie:

module MySdk
  class Railtie < ::Rails::Railtie
    initializer "my_sdk.error_subscribe" do
      Rails.error.subscribe(MyErrorSubscriber.new)
    end
  end
end

NOTE: Если вы зарегистрировали получатель ошибки, но всё ещё имеете другие механизмы для ошибок, такие как Rack middleware, может получиться, что ошибки сообщаются несколько раз. Следует либо убрать ваши другие механизмы, либо настроить функциональность отчёта так, чтобы она пропускала сообщения об ошибке, которую она уже видела.

On this page