Custom event tracking with ActiveSupport::Notifications and Audited

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_failed 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 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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.