Support for ActiveSupport::Notifications
Rails and other gems provide lots of instrumentation data through ActiveSupport::Notifications subscriptions.
For now I am mainly interested in instrumenting ViewComponent which already has instrumentation built in and provides all the data using ActiveSupport::Notifications. The notifications from ViewComponent are documented here. Right now, when we use lots of components all their time is recorded under View without any granularity visible in Scout UI.
Basic code to consume notifications looks like this:
ActiveSupport::Notifications.subscribe("render.view_component") do |event| # or !render.view_component
event.name # => "render.view_component"
event.duration # => 10 (in milliseconds)
event.allocations # => 1826 (objects)
event.payload # => { name: "MyComponent", identifier: "/Users/mona/project/app/components/my_component.rb" }
end
Look at ActiveSupport::Notifications::Event to find all information that is available.
It would be great if Scout agent would provide a way to easily ingest data from ActiveSupport::Notifications. It could probably even subscribe to some of them by default when gems are detected (like ViewComponent).
I would be happy to try to implement proof of concept with some guidance or try any testing version you would have.
From what I can tell and from discussion with @lancetarn on Discord I adding children ScoutApm::Layers under the View one as components are rendered could be possible. Is this the way to go or should I explore anything else?
I've managed to make this work with existing Scout Agent API with a simple monkey patch for ActiveSupport::Notifications. Screenshots from our production env (without the monkey patch) and from my development env (with the monkey patch):
Here is the monkey patch. It allows for easy integration of other notifications as well. It seems to integrate well with the UI for both Time Breakdown and Memory Allocation Breakdown.
# frozen_string_literal: true
module ScoutApmNotifications
INSTRUMENTED_NOTIFICATIONS = { 'render.view_component' => 'ViewComponent' }.freeze
module ActiveSupportNotificationsInstrumenterMonkeyPatch
def instrument(name, payload = {})
# ignore Rails private events
# https://github.com/rails/rails/blob/528b62e3869288b5f3ffbd1491f10bad00958c08/actionview/lib/action_view/template.rb#L384
# https://x.com/rafaelfranca/status/1289978377550442496
should_instrument = !name.start_with?('!')
# only instrument notifications we are interested in
should_instrument &&= INSTRUMENTED_NOTIFICATIONS.key?(name)
return super unless should_instrument
scout_apm_type = INSTRUMENTED_NOTIFICATIONS[name]
scout_apm_name = payload[:name]
::ScoutApm::Tracer.instrument(scout_apm_type, scout_apm_name) do
super
end
end
end
class Instrument
def self.setup
::ActiveSupport::Notifications.class_eval do
class << self
def instrument(name, payload = {})
# skip check for notifier.listening? in ActiveSupport::Notifications.instrument to receive everything
# https://github.com/rails/rails/blob/3235827585d87661942c91bc81f64f56d710f0b2/activesupport/lib/active_support/notifications.rb#L208
instrumenter.instrument(name, payload) { yield payload if block_given? }
end
end
end
ActiveSupport::Notifications::Instrumenter.prepend(ActiveSupportNotificationsInstrumenterMonkeyPatch)
end
end
end
ScoutApmNotifications::Instrument.setup
That is so fancy! Thank you for the suggestion and the POC. I imagine we would want to make instrumented notifications configurable; looks like that wouldn't be too difficult. Not sure if there are any other dragons lurking there. We can certainly bring it to the team.
Thanks. I didn't find any dragons lurking anywhere for now. I will deploy this to production and keep you posted if I find any issues. Looking forward to Scout implementing this officially in the future.