Что такое ActiveSupport::Notifications и зачем нужны?

в 9:24, , рубрики: notifications, ruby, ruby on rails, метки: , ,

ActiveSupport::Notifications – это встроенная в рельсы система уведомлений. Вы можете подписаться на определенные уведомления в Rails и вызывать свой код когда они будут посланы. Это чем-то похоже на ActiveSupport::Callbacks, но работают во всем проекте и события не нужно заранее объявлять.

К примеру, мы можем подписаться на уведомление 'render':

ActiveSupport::Notifications.subscribe("render") do |*args|
  # Этот блок будет вызван при получении уведомления render
end

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

Послать уведомление можно методом ActiveSupport::Notifications.publish:

ActiveSupport::Notifications.publish('render', 'arg1', 'arg2')

В блок subscribe будет переданы 'render', 'arg1' и 'arg2'.

Я не нашел в Rails кода который напрямую использует эти возможности, вероятно, данный фунцианал предпологается использовать пользователям фреймворка для прикладных задач. Но в ActiveSupport::Notifications есть весьма полезный метод instrument. Он принимает блок и после выполнения которого отправляет уведомление с временными метками начала и конца выполнения блока, а так же хэш с дополнительными данными. Rails использует этот механизм, а опосредованно, и методы subscribe и publish, для создания подробных логов в девелоперском окружении (на продакшене используется те же “измерители”, но не все пишется в лог)

Для тех же целей эти “измерители” можно использовать в своем приложении. Можно обернуть код который вы хотите проверить в этот метод и результат выполнения писать в лог. Например, у нас есть метод скорость выполнения которого нам бы хотелось отслеживать.

def do_something
  # Тут наши вычисления
end

Просто оборачиваем его в блок ActiveSupport::Notifications.instrument

def do_something
  ActiveSupport::Notifications.instrument('benchmark.do_something', desc: 'Some description') do
    # Тут наши вычисления
  end
end

В каком-нибудь месте, например, config/initializers/benchmarking.rb подписываемся на все уведомления с строкой 'benchmark.'.

logger = Logger.new(File.join(Rails.root, 'log', "benchmarks.log"))
ActiveSupport::Notifications.subscribe(%r/benchmark.*/) do |name, start, ending, transaction_id, payload|
  method = name.split(?.).last
  duration = (1000.0 * (ending - start))
  message = if payload[:exception].present?
    payload[:exception].join(' ')
  else
    payload[:desc].to_s
  end
  logger.info("Benchmark:%s: %.0fms - %s" % method, duration, message)
end

В блок будут переданы следующие переменные: name, start, ending, transaction_id, payload.

  • name – имя пойманного уведомления
  • start – время начала выполнения блока
  • ending – время конца выполнения блока
  • transaction_id – уникальный id, как правило уникальный в пределах одного треда
  • payload – дополнительные данные переданные в “измеритель”

Далее просто записываем время исполнения в лог. При возникновении исключительной ситуации в payload[:exception] будет записан масив с именем исключения и сообщением об ошибке. Это тоже нужно учесть.

Более подробно роль ActiveSupport::Notifications в логгировании Rails можно посмотреть в модулях ActiveSupport::LogSubscriber, ActiveRecord::LogSubscriber, ActionController::LogSubscriber и ActionMailer::LogSubscriber.

Есть еще один метод, который временно подписывается на уведомления, но только пока выполняется блок переданный ему.

callback = lambda do|*args|
  # Это блок который выполнится в момент посылки 
  # уведомления "event.name"
end

ActiveSupport::Notifications.subscribed(callback, "event.name") do
  # Блок в течении которого подписка будет действительна
end

Чтобы отписаться от события вызовите метод unsubscribe и передайте в него ссылку на подписчика.

ActiveSupport::Notifications.unsubscribe(subscriber)

Сам механизм рассылки уведомлений скрыт в классе ActiveSupport::Notifications::Fanout, так же будет интересно посмотреть на класс ActiveSupport::Notifications::Instrumenter, который отвечает за измерение времени выполнения блока в методе instrument

В качестве примера, монкипатч для реалтаймового подсчета времени выполнения методов, использующий ActiveSupport::Notifications:

class Module
  def benchmark_it *names
    options, names = benchmark_options_and_names(*names)
    names.each do |name|
      target, punctuation = name.to_s.sub(/([?!=])$/, ''), $1
      define_method "#{target}_with_benchmark#{punctuation}" do |*args|
        ActiveSupport::Notifications.instrument("benchmark.#{self.name}.#{name}", options) do
          send("#{target}_without_benchmark#{punctuation}", *args)
        end
      end
      alias_method_chain name, :benchmark
    end
  end

  protected
  def benchmark_options_and_names *args
    options = args.last.is_a?(Hash) ? args.pop : {}
    [{desc: ''}.merge(options), args]
  end
end

ActiveSupport::Notifications.subscribe(%r/benchmark.*/) do |name, start, ending, transaction_id, payload|
  _, classname, method = name.split(?.)
  duration = (1000.0 * (ending - start))
  message = if payload[:exception].present?
    payload[:exception].join(' ')
  else
    payload[:desc].to_s
  end
  Rails.logger.info("Benchmark: %s.%s: %.0fms - %s" % classname, method, duration, message)
end

Используется так:

class MyClass
  def my_method
    # Делаем тут чего-нибудь
  end
  
  # Указываем что хотим протестить этот метод
  benchmark_it :my_method, desc: 'Сообщение для логгера'
end

Для чего все это нужно? На примере тех же Rails видно, что если Ваш код состоит из разных слабо связанных компонентов, то можно наладить взаимодействие с ними с помощью таких уведомлений. Соответственно, я могу написать свою ORM или еще какую-нибудь библиотеку, к примеру MyORM, и задачу логгирования повесить на класс MyORM::LogSubscriber отнаследованный от ActiveSupport::LogSubscriber. Весть код, отвечающий за отображение логов не размазан по приложению, а находится в одном месте. Ну, естественно, нужно расставить датчики по всей библеотеке. Кстати, эти же самые датчики можно использовать для чего угодно еще помимо логгирования.

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

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

Автор: undr

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js