Mike Clark showed off a cool Capistrano trick at RailsConf 2006 and I needed (okay, wanted) to do it today. He showed us that, using Capistrano, you can essentially tail -f multiple log files on multiple servers with a single command. Genius!
Thanks to Jamis Buck (as always), I’ve cobbled together a Capistrano task:
desc "tail production log files" task :tail_logs, :roles => :app do run "tail -f #{shared_path}/log/production.log" do |channel, stream, data| puts # for an extra line break before the host name puts "#{channel[:host]}: #{data}" break if stream == :err end end
Now all I need to do is type $ cap tail_logs from my shell and bam, I’m tailing the production logs from all my app servers in one console window. It’s really quite great. How does it work?
As documented in the Capistrano manual, the run command can optionally take a block. If present, your block is passed three variables. Going with the above example: channel represents the ssh channel opened between you and the host, stream is whether you’re grabbing from $stderr or $stdout, and data is the output of the run‘d command.
Remember: everything in the block happens on your localhost. My puts is putting to my shell screen, not some sort of remote trickery.
So I’m running tail -f on my production log on each server at the same time with (in my case) three open ssh connections. All of this is done automatically by Capistrano, behind the proverbial scenes. I’m then printing the name of the host I’m connected to and the data being sent. What I see is something like this:
app-server-1: Processing HubController#front_door (for 0.0.0.0 at 2006-08-25 18:58:59) [GET] Parameters: {"action"=>"front_door", "controller"=>"hub"} Redirected to http://www.chow.com/account/login Filter chain halted as [login_required] returned false Completed in 0.00050 (1987 reqs/sec) | DB: 0.00011 (22%) | 302 Found [http://www.chow.com/] app-server-3: Processing AccountController#lost_password (for 0.0.0.0 at 2006-08-25 19:01:29) [GET] Parameters: {"action"=>"lost_password", "controller"=>"account"} Rendering within layouts/simple Rendering account/lost_password Completed in 0.06520 (15 reqs/sec) | Rendering: 0.06387 (97%) | DB: 0.00054 (0%) | 200 OK [http://www.chow.com/account/lost_password]
Of course this also works on a single server setup and is useful for tailing your production log file without actually sshing into your production box. We sure the love the laziness, don’t we?
Oh and while we’re here, this may or may not be useful: here’s a task to grab the last 500 lines of each of your log files and view them all in TextMate.
desc "check production log files in textmate(tm)" task :mate_logs, :roles => :app do require 'tempfile' tmp = Tempfile.open('w') logs = Hash.new { |h,k| h[k] = '' } run "tail -n500 #{shared_path}/log/production.log" do |channel, stream, data| logs[channel[:host]] << data break if stream == :err end logs.each do |host, log| tmp.write("--- #{host} ---\n\n") tmp.write(log + "\n") end exec "mate -w #{tmp.path}" tmp.close end
I’m a Vim dude, but TextMate still shines in millions of areas. This is one of them. Bonus points for the nice blue formatting of the hostname line.
You got any cool uses of run’s stream functionality? Lay it on me.
Update: Okay, Brandon threw down the gauntlet. He wants a Capistrano task to start up script/console on a remote server.
desc "remotely console" task :console, :roles => :app do input = '' run "cd #{current_path} && ./script/console #{ENV['RAILS_ENV']}" do |channel, stream, data| next if data.chomp == input.chomp || data.chomp == '' print data channel.send_data(input = $stdin.gets) if data =~ /^(>|\?)>/ end end
It’s simple to run this bad boy:
$ cap console
Use it with your production DB, too:
$ RAILS_ENV=production cap console
A fair bit of warning: I have no idea what will happen if your :role has more than one machine in it. I tested this against an :app role which only contained one machine. But if, say, your role is :db and you only have one db server… console away!
Chris,
That’s great! I’ve been wanting to do something similar since I saw Mike’s RaisConf presentation, but haven’t gotten around to it. Thanks for sharing.
I also want a capistrano task to start up script/console on the db server. I spent a few minutes on it without success.
Nice work. I loved it so much that I’ve wrapped it in a gem, I blogged about it here: http://tinyurl.com/yt4og3
hi, great stuff. however i had to change two things in the mate_logs task before it worked (rails 2.0.2, ruby 1.8.6, os x 10.5.1)
tmp.flush #otherwise the last part of the log file was not included #exec “mate -w #{tmp.path}” #this gave an error `mate -w #{tmp.path}` #this works
i also had to install the symlink to the mate command in my /bin/ folder
Chime in.