Rails Instrumentation API

Understanding the Instrumentation API in Rails

The Instrumentation API in ActiveSupport serves a dual purpose. You can use it to implement the observer (pub-sub) pattern, as well as benchmark how long it took to execute some action. In this post, we'll learn almost everything you need to know about the Rails Instrumentation API.

5 min read

The observer design pattern (also called pub-sub) allows one or more subscribers to register with and receive notifications from a publisher. It's great for scenarios that need push-based notifications, instead of continuous polling. A real-world example is this blog. Whenever a new post is published, it sends an email to all the subscribers of the blog. As a result, the readers don't have to constantly refresh the blog to see if a new post was published.

The pattern defines a publisher (also known as a provider or an observable) and zero or more subscribers (also known as observers or listeners). Subscribers register with the publisher, and whenever a predefined condition, event, or state change happens (e.g. a new post is published), the publisher automatically notifies all observers, passing any additional data to provide current state information to observers.

The Instrumentation (or Notification) API is a simple implementation of the observer (pub-sub) pattern in Rails. It allows you to subscribe and listen to various events that occur within your Rails application or even the Rails framework. In addition, 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 Rails (the framework) uses it internally.

What we'll learn:

  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

Too many arguments? Receive an event instead...

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
💡
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. For example, 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 ActiveRecord 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.


That's a wrap. I hope you found this article helpful and you learned something new.

As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.

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