Rails Logging With MongoDB

I've been playing with MongoDB a bit lately, and have been pretty impressed both with its simplicity and usefulness. There seem to be quite a few uses for a document-oriented DB, and, as the MongoDB team mentioned on their blog, logging is one of those things that MongoDB is perfectly suited for.

EDIT: 9/28/2009 I’ve fixed the Github repo link

Log files can be kind of a pain to peruse in the first place, but things become especially annoying when you’ve got multiple servers on which your app is running, and you have to tail / grep multiple Rails logs just to find a single request. Not only that, but there’s no concept of a “request record” in a flat text file, it’s just a stream of log messages, one after the other. AND, if (in the case of Rails) you’re running multiple mongrels (which you probably are), all of which are logging to that single log file, then things become even more frustrating as request messages can potentially be intertwined with each other. I can tell you from experience, grep-ing a bunch of HUGE log files in an attempt to find a single request can be a bit of a nightmare.

Enter MongoDB.

I’m not going to spend too much time in this post going into the details of Mongo – that’s what their website is for. And, there are also plenty of introductory articles and presentations outlining the basics of MongoDB / Ruby interaction.

Now that we’ve gotten that out of the way, let’s move on to how MongoDB can cure all your Rails log-perusing headaches.

I just finished up a Rails plugin to allow apps to log to a centralized MongoDB. To start, the code’s here on Github: http://github.com/peburrows/mongo_db_logger. Usage is pretty simple – once you’ve installed the plugin (script/plugin install git://github.com/peburrows/mongo_db_logger.git), the first thing to do is put the following line in your ApplicationController:


include MongoDBLogging

And then, to actually use it in your different environments, add the following line to your app’s "config/#{environment}.rb" file so Rails knows to use the MongoLogger:


config.logger = MongoLogger.new

Finally, in your config/database.yml file, you’ll need to add mongo settings for each environment in which you want to use MongoDB for logging, like so:


development:
  adapter: mysql
  database: my_app_development
  user: root
  mongo:
    database: my_app
    capsize: <%= 10.megabytes %>
    host: localhost
    port: 27017

The only required setting is database (probably just the name of the app) – everything else will just choose a sensible default if not specified. capsize determines the size of the capped collection (think of a capped collection as a db table that has a max size and auto-flushes old records as necessary to make room for new records) and defaults to 250.megabytes in production and 100.megabytes in all other environments (if you’re testing locally, there’s probably no need for it to be 100.megabytes, so you can change that in your config/database.yml file as shown above).

And that’s all. With that in place, a new MongoDB document (record) will be created for each request and, by default will record the following information: Runtime, IP Address, Request Time, Controller, Action, Params and All messages sent to the logger. The structure of the Mongo document looks something like this:


{
  'controller'    : controller_name,
  'action'        : action_name,
  'ip'            : ip_address,
  'runtime'       : runtime,
  'request_time'  : time_of_request,
  'params'        : { }
  'messages'      : {
                      'info'  : [ ],
                      'debug' : [ ],
                      'error' : [ ],
                      'warn'  : [ ],
                      'fatal' : [ ]
                    }
}

Beyond that, if you want to add extra information to the base of the document (let’s say something like user_guid on every request that it’s available), you can just call the Rails.logger.add_metadata method on your logger like so:


# make sure we're using the MongoLogger in this environment
if Rails.logger.respond_to?(:add_metadata)
  Rails.logger.add_metadata(:user_guid => @user_guid)
end
The benefit of using MongoDB for logging (especially when adding extra, custom data like @user_guid to documents) is pretty significant:
  1. you have a centralized logging system for all application servers, so you’re no longer required to look in multiple places for log messages
  2. that centralized log DB can be indexed and searched using MongoDB’s [simplistic but pretty powerful] query language.
  3. each request is a single record, so ALL the information about the request in contained in that one “document”

And now, for a couple quick examples on getting ahold of this log data…

First, here’s how to get a handle on the MongoDB from within a Rails console:


>> db = MongoLogger.mongo_connection
=> #<Mongo::DB:0x102f19ac0 @slave_ok=nil, @name="my_app" ... >

>> collection = db[MongoLogger.mongo_collection_name]
=> #<Mongo::Collection:0x1031b3ee8 @name="development_log" ... >

Once you’ve got the collection, you can…

  1. Find all requests for a specific user (with guid):
    
    >> cursor = collection.find(:user_guid => '12355')
    => #<Mongo::Cursor:0x1031a3e30 ... >
    >> cursor.count
    => 5
    
    
    Then, you can just iterate over the records returned by the query like any other collection.
  2. Find all requests that took more that one second to complete:
    
    >> collection.find({:runtime => {'$gt' => 1000}}).count
    => 3
    
  3. Find all requests that passed a parameter with a certain value:
    
    >> collection.find({'params.currency' => 'USD'}).count
    => 22
    
  4. Perform any other query on the data that you like.

Feel free to play around with it and let me know if you have any questions / suggestions. Also, the mongoDB site has some good resources and the documentation (although slightly lacking in some places) is pretty straightforward as well.

One other thing that I should mention: currently, this does NOT replace the normal Rails logging that happens in "log/#{environment}.log", but is just a supplemental addition. There’s no real reason not to replace the file-based logging, except that with MongoDB capped collections, it will only store a certain number of requests – a flat file doesn’t really have a limit on the number of requests it will store. Sure, it’ll get HUGE if you don’t roll it / truncate it occasionally, but it could [potentially] keep a record of every request ever if that’s what you want.

Posted by Phil Burrows on Sep 28, 2009

Comments

  1. Chris Bailey Friday, October 02, 2009

    AWESOME! I was planning to do this myself, as I've wanted to centralize logging, and have it be queryable, plus leverage Mongo for this exact thing. And now you've gone and done it for me, brilliant! I will check it out soon.

    One thing I may look into is actually tee'ing the output - to maintain existing Rails log files, as well as use Mongo. Many discussions on the net about using a DB for logging cite the issue where if there is a problem with the DB, you may lose your logging data, so having both is nice.

    I've been following your Mongo work, and it's great, keep it up. I'll try to remember to post back my experience once I have this going (probably be a while before I try it even in our staging environment, but hey).

  2. Chris Bailey Friday, October 02, 2009

    Phil, realized that I followed this link from Jon Nunemaker's tweet or something, and was thinking that's where I was, which is what my reference to "following your Mongo work" is (his Mongo Mapper). So, just thought I'd explain that so it didn't sound like some weird thing :) Regardless, planning to try out MongoDBLogging shortly, and will indeed give feedback. Thanks.

  3. phil76 Wednesday, December 23, 2009

    This is great, we'll try it with our application! Thanks for sharing!

    The source code formatting in this blog entry is broken, though ...

    Phillip

  4. Elise Thursday, January 28, 2010

    I've got a lot of projects that require keeping a log of every single thing that anyone does. so they can point fingers later. which also means keep the data they changed. that just got a lot simpler. Thanks!

Post a comment