When it comes to logging or tracking changes in Ruby on Rails models, We typically tend to use either paper_trail or audited gems as these are most popular and widely used gems in Ruby on Rails applications for tracking changes in Rails models, but what if you want to track the custom events on controller level(like following) in addition to model changes?

User login and logout

Password change success or fail

User login failed or succeeded

OR any other user activity

In this post I will share my learnings on how we can track controller level custom events with the help of model changes tracking gem audited and ActiveSupport::Notifications.

Brief about ActiveSupport::Notifications

ActiveSupport::Notifications gives an APIs for event instrumentation which works on pub/sub model i.e. you can publish an event notification/trigger for the particular subscriber which in turn takes necessary actions for the given event trigger.

For example, publishing and subscribing of a ‘login_success‘ event

Publishing an event

ActiveSupport::Notifications.instrument('login_failed', event_data) #This triggers the notification for login_success event

Subscribing to an event

ActiveSupport::Notifications.subscribe('login_failed') do |*args| event = ActiveSupport::Notifications::Event.new(*args) #Take necessary actions on this event end

Using Audited for saving custom events

I have created an EventHandler service which publishes/creates events and is used by controllers and subscribers.

#app/services/event_handler.rb class EventHandler def initialize(event) @event = event end def process! audit_data = { action: @event.name, username: @event.payload['email'] || @event.payload['username'], comment: @event.payload[:comment] } audit_data[:auditable] = @event.payload[:auditable] if @event.payload[:auditable] audit_data[:user] = @event.payload[:user] if @event.payload[:user] Audited::Audit.create!(audit_data) end def self.trigger_event_audit(event_name, params, resource, comment=nil) event_data = {} event_data.merge!(params[:user]) event_data.merge!(auditable: resource, user: resource, comment: comment) ActiveSupport::Notifications.instrument(event_name, event_data) end end

Using this in controller action

Let’s say I have the following piece of code in my controller which allows the user to login.

class Users::SessionsController < Devise::SessionsController ... #POST /resource/sign_in def create self.resource = warden.authenticate(auth_options) if resource.present? ... EventHandler.trigger_event_audit('login_success', params, resource) respond_with resource, location: after_sign_in_path_for(resource) else EventHandler.trigger_event_audit('login_failed', params, resource, 'invalid email/username or password address') respond_with resource, location: root_path end end ... end

Subscribing to events

#config/initializers/event_trigger_subscriber.rb ActiveSupport::Notifications.subscribe('login_failed') do |*args| event = ActiveSupport::Notifications::Event.new(*args) EventHandler.new(event).process! end ActiveSupport::Notifications.subscribe('login_success') do |*args| event = ActiveSupport::Notifications::Event.new(*args) EventHandler.new(event).process! end

Conclusion

Rails ActiveSupport Instrumentation is very handy whenever we need to write the event based triggers for notifications, event tracking, data logging etc. The aim of this post is to demonstrate how we can use the Rails ActiveSupport Instrumentation with audited gem, Specially if implementation of audited gem is in place and on top of it we need to track the custom events without creating any new database schema.

If you have any suggestions or feedback, please feel free to add them in comments section.