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