Rails console snoopery with Papertrail

Keep an eagle eye on your fellow engineers in a Rails project πŸ‘€

Keep an eagle eye on your fellow engineers in a Rails project πŸ‘€.

The console is a powerful tool for interacting with your Rails app. When you've deployed via a service like Heroku, you can also access the console in production, something like this:

This is great for investigating your data and even performing one-off maintenance tasks. But... who's using the console, and for what? You don't get an audit trail out of the box!

Projects like Basecamp's console1984 provide full-featured security and an audit trail. But if you want something a more lightweight, or you're building it yourself, the below approach might be interesting.

Our goal will be simply to log each console command as it's executed, to our preferred log management service.

The guts of the console: IRB

Rails uses IRB by default as its console interface. It looks like this inside:

irb.rb[1]

module IRB
  class Irb
    # Evaluates input for this session.
    def eval_input
      # βœ„ ...
      @scanner.each_top_level_statement do |line, line_no|
          # βœ„ ...
          @context.evaluate(line, line_no, exception: exc)

eval_input is a REPL. @context is responsible for evaluating commands.

We want to override, aka "monkeypatch", IRB::Context#evaluate. There's more than one way to do it, but Module.prepend seems nice if we don't want to be coupled too strongly to the internals of how IRB builds its context.

By prepending to the IRB::Context class we will put our own behaviour around IRB's; something like this:

# .irbrc
IRB::Context.prepend(Module.new do
    def evaluate(line, ...)
        puts "About to execute #{ line.length } characters of Ruby code!"
        # Call super to evaluate the expression
        super
    end
end)

Which of course results in this:

Logging to Logplex

Okay, we can find what commands are being executed. How about integrating with a logging setup? This part is specific to your environment, but here's what we did to get logs into Papertrail running on Heroku.

Ordinarily we might write directly to Papertrail via its token-based HTTP log destination, but Heroku's Papertrail addon works only via their dedicated Logplex logging service. The service accepts syslog format, so here's one way to produce that:[2]


# Logs a single message to a Logplex drain.
def logplex_log(message, url, token, app: 'rails', process: 'console')
  msg = "<134>1 #{ DateTime.now.rfc3339 } "\
    "#{ Socket.gethostname } #{ app } #{ process } " \
    "- #{ message }\n".encode('utf-8')
  framed_msg = "#{ msg.bytes.length } #{ msg }"
  conn = Faraday.new(
    url: url,
    headers: {
      'Logplex-Drain-Token' => token,
      'Content-Type' => 'application/logplex-1',
    }
  )
  conn.post('', framed_msg)
  conn.close
end

You might have noticed some magic numbers:

  • 134 is the sum of
    • 16*8 for "local0 facility"
    • 6 for Informational logging level
  • 1 for syslog protocol version 1

Logplex endpoint configuration

Heroku configures syslog drains for logging addons such as Papertrail. you can find the url and token like this:

% heroku drains --json
[
  {
    "token": "d.01234567-8901-2345-6789-012345678901",
    "url": "https://collector.papertrail.com/v1/logplex",
    # βœ„ ...
  }
]

Putting it together

You can keep the code in .irbrc, or add a rails initializer:

# config/initializers/console.rb
DRAIN_TOKEN = 'd.01234567-8901-2345-6789-012345678901'
DRAIN_URL = 'https://collector.papertrail.com/v1/logplex' 

if Rails.env.production?
  require 'irb'
  IRB::Context.prepend(Module.new do
    def evaluate(line, ...)
      begin
        logplex_log "CONSOLE[#{ Rails.env }]> #{ line }", DRAIN_URL, DRAIN_TOKEN
      rescue StandardError
        # do nothing
      end
      super
    end
  end)
end

There we have it

Now you can spy on all the juicy stuff your team is doing! And you might be able to remember how you fixed that bug last Thursday too.


  1. βœ„ snipped for clarity, from here: https://github.com/ruby/irb/blob/v1.4.2/lib/irb.rb#L529 β†©οΈŽ

  2. The logplex gem is an alternative, see here how it formats and frames the syslog message: https://github.com/heroku/logplex-gem/blob/v0.0.6/lib/logplex/message.rb β†©οΈŽ