Пользователь
0,0
рейтинг
29 ноября 2012 в 13:24

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

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

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (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 стоит вынести в отдельный класс, туда же и подписчика и много чего можно улучшить.

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

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