18 Aug 2011

PID numbers for Rails 3 logs

tl;dr: Copy the code into lib/pid_logger.rb and edit your application.rb. Now your Rails 3 app has PID numbers on each log line. Tada.

I recently signed up for Papertrail, and starting sending all of my production Rails logs to a single consolidated place for tailing and searching. Papertrail is pretty great, but having all my logs in one place revealed a pretty big problem with default Passenger configurations. All the Passenger processes log to the same production.log file. This means that log lines from completely unrelated requests can get intermingled, making it impossible to tell which log lines belong to which requests. This makes the production log nearly useless for reconstructing what happened while investigating something.

A typical solution to this issue, especially for Mongrel or Unicorn setups, is to have each Rails process log to a separate file. Since I'm using Passenger, though, I came up with a simpler solution: prepend the PID of the current process to each log line. This means I can use Papertrail to filter the production log by PID number, and then I can focus on a single request as needed.

There are a lot of patches floating around the internet that change how Rails logs things, and I wasn't feeling great about copying someone else's monkepatch into my app. Ultimately, I wrote a subclass of ActiveSupport::BufferedLogger that uses some of the tricks from patches floating around the web and some of my own code. Once you install it, you'll be able to see both the PID and the log level of each line written to the production logs, greatly improving things. I suggest putting this file into lib/pid_logger.rb.

# You must require this file in application.rb, above the Application
# definition, for this to work. For example:
#
#   # PIDs prepended to logs
#   if Rails.env.production?
#     require File.expand_path('../../lib/pid_logger', __FILE__)
#   end
#
#   module MyApp
#     class Application < Rails::Application

require 'active_support/buffered_logger'

class PidLogger < ActiveSupport::BufferedLogger

  SEVERITIES = Severity.constants.sort_by{|c| Severity.const_get(c) }

  def add(severity, message = nil, progname = nil, &block)
    return if @level > severity
    message = (message || (block && block.call) || progname).to_s
    # Prepend pid and severity to the written message
    log = "[#{$$}] #{SEVERITIES[severity]} #{message.gsub(/^\n+/, '')}"
    # If a newline is necessary then create a new message ending with a newline.
    log << "\n" unless log[-1] == ?\n
    buffer << log
    auto_flush
    message
  end

  class Railtie < ::Rails::Railtie
    initializer "swap in PidLogger" do
      Rails.logger = PidLogger.new(Rails.application.config.paths.log.first)
      ActiveSupport::Dependencies.logger = Rails.logger
      Rails.cache.logger = Rails.logger
      ActiveSupport.on_load(:active_record) do
        ActiveRecord::Base.logger = Rails.logger
      end
      ActiveSupport.on_load(:action_controller) do
        ActionController::Base.logger = Rails.logger
      end
      ActiveSupport.on_load(:action_mailer) do
        ActionMailer::Base.logger = Rails.logger
      end
    end
  end

end

I've also posted the code on Github in a gist, for forking, commentary, and improvement. Thanks!