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 of16*8
for "local0
facility"6
forInformational
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.
β snipped for clarity, from here: https://github.com/ruby/irb/blob/v1.4.2/lib/irb.rb#L529 β©οΈ
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 β©οΈ