on programming Rails for fun and profit...

📖 Understanding the Instrumentation API in Rails

The instrumentation API in ActiveSupport serves a dual purpose. You can use it to implement the publish-subscribe pattern, as well as benchmark how long it took to execute some action.

Rails Instrumentation API
Rails Instrumentation API

The instrumentation API in Rails provides a simple observer (pub-sub) pattern implementation, allowing you to subscribe and listen to various events that occur within your application or the Rails framework. You can also use it to benchmark a piece of code.

This article explains how to use the instrumentation API and some practical examples of how the Rails framework uses it.

Here's a list of topics this article covers:

  1. How to publish an event
  2. How to subscribe to an event
  3. Publishing an event to multiple subscribers
  4. Subscribing to multiple events
  5. Performance benchmarking
  6. Example: Instrumenting Render

Events are a great way to decouple various aspects of your application. A single event can have multiple listeners in unrelated parts of the codebase, and a subscriber can listen to multiple events. The instrumentation API in Rails lets you create and publish such events to subscribers.

The Rails framework provides several of these events itself. For example,

  • Active Record fires an event named sql.active_record every time it uses a SQL query on a database. You can subscribe to this event to keep track of the total number of queries made during an action.
  • Action controller fires an event process_action.action_controller after processing a controller action, allowing you to track how long it took to process that action.

How to Publish an Event

To publish an event that other parts of the code can listen to, call the ActiveSupport::Notifications.instrument method, passing the event's name, payload (a hash containing information about the event), and an optional block. The payload is used to pass additional data to the event's subscribers.

ActiveSupport::Notifications.instrument "publish.post", { title: "hello world" } do
  puts "Creating and publishing the post"
end
  • If you pass a block, ActiveSupport will execute the block and then call all the event subscribers with the provided payload and the time taken to execute the block.
  • If you don't pass the block, ActiveSupport will simply notify the subscribers.

Hence, the instrumentation API serves a dual purpose. You can use it to benchmark how long it took to execute some action, as well as to notify other parts of the codebase that a certain event occurred.

How to Subscribe to an Event

To listen to any custom events that you created, or the ones triggered by the Rails framework, call the ActiveSupport::Notifications.subscribe method, providing the name of the event you're subscribing to and passing a block that will be called whenever this event occurs.  

ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, started, finished, id, data|
  name    # => String, name of the event (such as 'render' from above)
  start   # => Time, when the instrumented block started execution
  finish  # => Time, when the instrumented block ended execution
  id      # => String, unique ID for the instrumenter that fired the
  data    # => Hash, the payload
end

Receive an Event

If you don't want to type all those arguments each time you subscribe to an event, don't worry. Passing a block with a single argument will ensure it receives an ActiveSupport::Notifications::Event object.

ActiveSupport::Notifications.subscribe "process_action.action_controller" do |event|
  puts event.class # ActiveSupport::Notifications::Event
  puts event.name  # process_action.action_controller
  puts event.duration  # 0.03 (in ms)
  puts event.payload
end

To continue our earlier example, you can subscribe to the event publish.post using the following code:

ActiveSupport::Notifications.subscribe "publish.post" do |event|
  puts "Published the post"
end

Note: The code that subscribes to an event must run before the code that publishes the event. If no one is listening when the event is published, future subscribers won't be notified. As an analogy, if you subscribe to my blog today, you won't receive any articles I published last month. Get it?

Publishing to Multiple Subscribers

Multiple subscribers can listen to a single event, allowing you to decouple your code. For example, you'd like to publish a newsletter as well as take a backup when you publish a post. You can accomplish this by dispatching an event after publishing a post.

# newsletter service
ActiveSupport::Notifications.subscribe "publish.post" do |event|
  # send newsletter
end

# backup service
ActiveSupport::Notifications.subscribe "publish.post" do |event|
  # take backup
end

# blogging-related code
ActiveSupport::Notifications.instrument "publish.post", { id: post.id } do
  # publish the post
end

Subscribing to Multiple Events

Instead of the event name, you can pass a regular expression to the subscribe method above. This lets you subscribe to multiple events at once.

Here's the code to subscribe to all the events from ActiveRecord library.

ActiveSupport::Notifications.subscribe /active_record/ do |*args|
  # inspect all ActionController events
end

Alternatively, the subscriber can simply listen to each event separately.

Performance Benchmarking

Sometimes, you're only interested in measuring how long it took to execute a code block. The duration property on the event object received by the subscriber returns the difference in milliseconds between when the execution of the event started and when it ended.

ActiveSupport::Notifications.subscribe "publish.post" do |event|
  puts "#{event.duration} ms"  # 3001.50 ms
end

ActiveSupport::Notifications.instrument "publish.post" do
  sleep 3
  puts "instrument: publishing the post"
end

This provides a simple way to figure out the parts of the codebase that are slowing the application down and optimize them.

Example: Instrumenting Render

Let's say you would like to measure how long it took to render an action. In your Rails controller, you'd wrap the call to the render method in the instrument block as follows:

ActiveSupport::Notifications.instrument('render', extra: :information) do
  render 'index'
end

Rails will first execute the provided block by calling render, and then notify all the subscribers.

You can listen to this event by registering a subscriber.

ActiveSupport::Notifications.subscribe('render') do |name, start, finish, id, payload|
  # do something with the information provided.
end

This lets you measure how long it took to render the view as well as get notified whenever rendering happens.


I hope you found this article useful and that you learned something new.

If you have any questions or feedback, or didn't understand something, please leave a comment below or send me an email. I look forward to hearing from you.

Please subscribe to my blog below if you'd like to receive future articles directly in your email. If you're already a subscriber, thank you.

Subscribe to Akshay's Blog

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe