Simple User Request Tracking Using Redis

par budu

Logging is an important part of any web application. Rails has a great logging mechanism, which is ideal to see what’s happening during development. It can also be useful in many other situations, but it has its limitations. Being text based, its output is meant to be human readable, but sometimes it’s better to keep track of requests in a way that’s readily available programmatically.

Although saving requests data into a SQL database using Active Record seems like a no-brainer, it’s not necessarily a good idea. As tracking requests can quickly become a write intensive operation, I’ve took the habit of using Redis for that purpose. It happen to be really easy to add basic tracking capability to your Rails application. We’ll look at how to accomplish this cleanly and easily.

We first need to create a non-AR-backed model (maybe we should start using the PORO acronym) using the Redis::Objects gem.

class Tracker

  include Redis::Objects

  list :requests, marshal: true, maxlength: 1000

We’ve defined a Redis-backed attribute called requests which will act as a normal Ruby list. The marshal option tells Redis::Objects to marshal the content of the list so that you can append any kind of Ruby objects. The maxlength option limit the number of items in the list so that you don’t fill all available memory, but you still have to take into account the number of user using your application.

The actual key that will be used inside Redis will be a combination of three things: the name of the class, the name of the attribute and the value returned by the id method of the instance.

  def initialize(user = nil)
    @user = user
  end

  def id
    @user.try(:id) || -1
  end

Here we took an optional user parameter in the initialize method and that user’s id attribute will be used. If no user is given then we’ll return -1 to represent anonymous users.

We then just need a method to actually perform the tracking. In this example we’ll just take the URL, the request’s parameters, the user’s IP and add a timestamp.

  def track(request)
    requests << {
      url:    request.url,
      params: request.params,
      ip:     request.ip,
      time:   Time.now
    }
  end

Now it’s a simple matter of adding a filter in the application controller.

before_filter :track unless Rails.env.test?

# ...

def track
  Tracker.new(current_user).track request
end

Two things to note about the above snippet. Firstly, we are preventing this filter to run in the test environment. Secondly, it assumes there’s a current_user method that will return nil when no user is logged in, so beware if you’re using Sorcery.

With the Tracker model, we can easily get the last request made by a user:

Tracker.new(current_user).requests.last