DEV Community

Cover image for Using Rails secret weapon: ActiveSupport::Notifications
Hugo Dias
Hugo Dias

Posted on • Updated on

Using Rails secret weapon: ActiveSupport::Notifications

Originally posted in my blog

Ruby on Rails has a powerful weapon that not all developers knows about its existence. ActiveSupport Instrumentations is used by Rails framework to handle events that happen when the application is running.

The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the Rails framework. With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code.

Lucky we can use this tool at our disposal.

How it works

It looks like a PUB/SUB pattern.

In software architecture, publish–subscribe is a messaging pattern where senders of messages, called publishers, do not program the messages to be sent directly to specific receivers, called subscribers. Instead, published messages are characterized into classes, without knowledge of what, if any, subscribers there may be. Similarly, subscribers express interest in one or more classes, and only receive messages that are of interest, without knowledge of what, if any, publishers there are. - Wikipedia

Subscribing to an event is pretty easy:

# config/initializers/events.rb
ActiveSupport::Notifications.subscribe "my_custom.event" do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)
end
Enter fullscreen mode Exit fullscreen mode

The event variable will contain something like this

=> #<ActiveSupport::Notifications::Event:0x0000559258433df8
 @children=[],
 @duration=nil,
 @end=2018-07-03 12:47:46 +0000,
 @name="my_custom.event",
 @payload={:foo=>"bar"},
 @time=2018-07-03 12:47:46 +0000,
 @transaction_id="37bb01f75132e0c0505publisher6">
Enter fullscreen mode Exit fullscreen mode

Notice the @payload? We can send additional data within the event.

Instrumenting an event

There is a method called instrument that publish the event.

You can use it like this:

ActiveSupport::Notifications.instrument "my_custom.event", { foo: "bar" }
Enter fullscreen mode Exit fullscreen mode

Easy right?

Real world example

Let's say that you have an external service that handles application metrics, such as Segment.IO, Keen IO, and many others.

You can use this pattern to send events to one of this services.

# config/initializers/events.rb
# We can use regex to subscribe!
ActiveSupport::Notifications.subscribe /metrics/ do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)
  MyMetricsService.send(event.name, event.payload)
end

# app/controllers/my_controller.rb
def create
  # some code
  ActiveSupport::Notifications.instrument "metrics.item_purchased", { uid: item.id }
end
Enter fullscreen mode Exit fullscreen mode

You can read more about ActiveSupport Instrumentation here

Hope it helps :)


Photo by Ciprian Boiciuc on Unsplash

Top comments (14)

Collapse
 
fonzai profile image
Timo Saloranta

One gotcha: the current implementation is not asynchronous and all subscribers are updated sequentially. So if you want to do anything expensive inside subscribe hook, it's best to offload that part into a separate process by using queues (such as Sidekiq). That way the instrumentation won't slow down the customer phasing web process.

Collapse
 
pjmartorell profile image
Pere Joan Martorell

I don't understand why is synchronous, since it's an event that is listened by others I would expect it to be asynchronous. This has some implications, for example if I run an ActiveJob job and their events are listened by a subscriber and the subscriber raises an exception, this makes the job to fail.. I don't like it

Collapse
 
acuppy profile image
Adam Cuppy

The synchronous part is that the Notification event is run in sequence - just like a call to ActiveJob to enqueue a job would be an event in a sequence. However, unlike an ActiveJob enqueued job, where it's pushed to a datastore for another process to pick up and run, these Notification events are run fully at the time they're called.

As Timo mentioned, if you use that event to do some expensive processing, it would be in sequence and hold up the process until it was complete.

Collapse
 
hugodias profile image
Hugo Dias

Amazing tip @fonzai . Thanks for that!

Collapse
 
andy profile image
Andy Zhao (he/him)

Hmm interesting! I'm thinking of ActiveSupport::Notifications for a notification system, but I wonder if it would eat up a lot of memory. I've been back and forth between whether or not it makes more sense to use something like ActiveSupport or have a database table specifically for the notifications.

Is there a way to render the payload data in a view? I'm guessing you could do something like:

class NotificationsController
  def index
    @notifications = ActiveSupport::Notifications::Event.all
   # or whatever the syntax is
  end
end
Enter fullscreen mode Exit fullscreen mode

Also I wonder if there's a way to expire events. Thanks for sharing this! Now my mind is brewing...

Collapse
 
andy profile image
Andy Zhao (he/him)

Ah, that makes sense. I don't have any solid ideas yet about a notification system, but I've always wanted to know what sort of implementation options there are. Definitely looking forward to the post! I'll message you on DEV Connect some time, too. :)

Collapse
 
chalmagean profile image
Cezar Halmagean

One small thing to note:

You should follow Rails conventions when defining your own events. The format is: event.library. If your application is sending Tweets, you should create an event named tweet.twitter.

Collapse
 
hamled profile image
Charles Ellis

I'm unclear on why the real world example provided is preferable to an implementation that calls MyMetricsService.send directly (or indirectly through dependency injection).

Could you elaborate?

Collapse
 
hugodias profile image
Hugo Dias • Edited

Of course, Charles, and sorry if the example was confusing.

This API is a good use if you want to keep up with what is going on with your application under the hood.

Assuming that MyMetricsService.send is a service that sends a request to segment, now you need also to save this data to a log.

In this example you can add another line in that same event:

ActiveSupport::Notifications.subscribe /metrics/ do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)
  MyMetricsService.send(event.name, event.payload)
  MyLogger.save(event)
end

You can also subscribe to a specific type of events and handle differently.

ActiveSupport::Notifications.subscribe /metrics.purchase/ do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)
  NotifySales.call(event)
end

As you can see, you would need to change this in just one place.

The idea is basically to publish that something happened on the application and you can subscribe to this event.

Collapse
 
thorstenhirsch profile image
Thorsten Hirsch

Very interesting. Until now I have always been using ActiveRecord callbacks like after_commit in order to trigger stuff. But the callbacks seem to be not 100% reliable recently on my production system and so far I couldn't figure out why.

Guess I'll migrate the callbacks to notifications, which seem to be more flexible anyway.

Collapse
 
rickychilcott profile image
Ricky Chilcott

I've wondered the same thing as Andy. Did you ever write your post about a notifications system? I don't see it on your dev.to profile -- though I do see the one about Postgres Notifications.

I've been wondering if it would make sense to use the ActiveSupport::Notifications API to trigger events when certain objects have had a CRUD operation performed on them, and then have various subscribers trigger events such as ActionCable notifications, Emails to specific users, extended logging, storage of notification in a real notifications table, etc.

That way all logic associated with a specific event is in a single location as opposed to scattered all over.

Maybe it feels like too much indirection, and instead, you should just create a service class that triggers all of these operations after a successful change.

Collapse
 
codebeautify profile image
Code Beautify

Great reading

Collapse
 
kirillshevch profile image
Kirill Shevchenko • Edited

Thanks!
Used this thing for build notifications about time-consuming SQL queries.
github.com/kirillshevch/query_track

Collapse
 
ben profile image
Ben Halpern

Just came across this and found it really helpful. Thanks Hugo!