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

  • Tutorial
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 тоже ничего не знает о моей библиотеке, но тем не менее мой гем подключен к общей системе логгирования.

Естественно, что логгирование это не единственная область применения уведомлений, но на этом примере проще показать, то зачем они нужны.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 8

    0
    Шикарно!
    Был бы очень полезен пост-дайджест по составу AS, чтобы не писать велосипеды и вообще далеко не ходить. Там ещё куча всего интересного.
      0
      Да там много чего, но все подробно описать одного поста не хватит.
      И все это можно использовать не только в Rails, просто подключаешь гем activesupport
        0
        Pub/Sub на все приложение конечно хорошо, но боюсь, что использование таких с виду интересных инструментов, может подтолкнуть к написанию очень рваного кода. Callbacks вообще трудно отлаживать, и использовать, так как исполнение кода нелинейно, но рельсы предоставили Observer и callbacks внутри моделей для того, чтобы сделать такой код более менее понимаемым.

        Хотя наверное, в других приложениях на Ruby такие штуки можно будет использовать (=
          0
          Конечно, не нужно сразу бросаться все переписывать на pub/sub. Но знать и понимать эти инструменты необходимо.
            0
            Да. Знать необходимо, и за статью вам спасибо. Много раз в доках бегал мимо Notifications, а оно оказалось весьма полезным Pub/Sub. (=
          0
          Возможно в benchmark_it стоит добавить проверку на текущее окружение? Ведь код бенчмарка не нужен на продакшене и при автоматическом тестировании.
            0
            Да, вы правы. Но я не писал рабочий код для продакшена, я писал это, исключительно, как иллюстрацию к статье. Метод benchmark_it стоит вынести в отдельный класс, туда же и подписчика и много чего можно улучшить.

            А, вообще, лучший пример — это код самих рельс в данном случае. Наверное надо было его использовать, но сначало показалось это глупым, так как код рельс и так многие посмотрят после прочтения статьи. Поэтому написал пример сам, но сложно придумать задачу под какой-то паттерн, проще как-то паттерн подобрать к задаче.

            Only users with full accounts can post comments. Log in, please.