About Travis CI

In the Ruby community it's very popular to just append to a file in log/ directory in the current app. In many frameworks the developer can't even change the file. Damn it guys, we can do better!

Why Should I Care?

  • You might want to have the log files in /var/log for simpler log rotation.
  • You might not want to use files for logging at all. Not on the app server anyway. Especially considering that for security reasons it's better to send logs to a different server.
  • You might want to aggregate logs from multiple servers.
  • You might want to filter logs based on given pattern. Give me all error messages from all applications logs.#.error, all log items for database layer of testapp logs.testapp.db.*, all error messages for testapp logs.testapp.*.error etc.
  • Isn't ssh & tail -f really, really, I mean really lame? With AMQP, just subscribe to any pattern on any server you want from comfort of your own dev machine. Rock'n'roll!

Readable Logs (If You Want)

Besides, logs should be easy to read for the developers. That's why logging4hackers provides colourful formatter which uses colours instead of displaying log level as text and Logger#inspect for showing objects as syntax-highlighted JSON.

require 'logging'
require 'logging/code'

logger = Logging::Logger.new do |logger|
  logger.io = Logging::IO::Raw.new('testapp.logs.db')
  logger.formatter = Logging::Formatters::Colourful.new
end

logger.info("Starting the app.")
logger.inspect({method: 'GET', path: '/ideas.json', response: '200'})
logger.warn("Now I'm a tad bored ...")
logger.error("OK, gotta sleep now.")

Note: Actually the screenshot shows how would you inspect messages published into RabbitMQ, whereas in the code I'm using the IO::Raw which only prints to console. Example with AMQP is longer.

About @botanicus (blog)

botanicus I'm a launch-addict, creating stuff that matters is my biggest passion. I dropped out of high school and learnt programming before I'd end up on a street. In just a few months I moved from middle of nowhere to London where I worked as a freelancer for companies like VMware on the RabbitMQ team for which I, alongside great hacker michaelklishin, rewrote the AMQP gem.

I contributed to many famous OSS projects including RubyGems, rSpec and back in the g'd old days also to Merb. When EY decided to abandon Merb I wrote my own web framework, Rango (now discontinued), the only framework in Ruby with template inheritance.

My other hobbies include travelling, learning languages (你好!) and personal development. My 3 + 2 rule was featured on LifeHacker.

My only goal for this year is to launch a successful start-up. Could MatcherApp be it?

Use-Cases

Logging Into RabbitMQ (Local or Remote)

TODO: Disconnect the AMQP, stop EM & terminate.

  • You can connect to RabbitMQ on localhost or remote server.
  • So far it requires some setup. In the future I might provide helpers for this.
  • It's the most powerful setup. You can filter patterns, you can discard messages just by not subscribing to those you're not interested in, you can consume given message multiple times, so you can for instance duplicate logs on two servers etc.
  • Instead writing directly to AMQP you can write to a named pipe and have a daemon which reroutes messages to RabbitMQ as described below.
require 'logging'
require 'logging/code'
require 'eventmachine'

EM.run do
  require 'amq/client'

  AMQ::Client.connect(adapter:  'eventmachine') do |connection|
    channel = AMQ::Client::Channel.new(connection, 1)

    channel.open

    exchange = AMQ::Client::Exchange.new(connection, channel, 'amq.topic', :topic)

    logger = Logging::Logger.new do |logger|
      logger.io = Logging::IO::AMQP.new('testapp.logs.db', exchange)
      logger.formatter = Logging::Formatters::Colourful.new
    end

    logger.info('Starting the app.')
    logger.inspect({method: 'GET', path: '/ideas.json', response: '200'})
    logger.error('Whops, no app defined, terminating.')
  end
end

Client/Server on Localhost using Named Pipe

  • You might not want to run EventMachine.
  • Setting up the Pipe logger on the client side requires much less setup, hence much less stuff can wrong.
  • It's easy to write a daemon to publish those messages from the pipe into RabbitMQ. In the future I might provide one.
# Create a named pipe.
mkfifo /tmp/loggingd.pipe

# Listen for messages coming to /tmp/loggingd.pipe.
#tail -f /tmp/loggingd.pipe

# Listen for new messages on the pipe and forward them to RabbitMQ.
./bin/loggingd.rb
logger = Logging::Logger.new do |logger|
  logger.io = Logging::IO::Pipe.new('testapp.logs.db', '/tmp/loggingd.pipe')
  logger.formatter = Logging::Formatters::Serialised.new(Logging::Formatters::Colourful.new)
end

Inspecting Remote Server

Often you want to figure out what's going on on server. This is how you do it:

./bin/logs_listen.rb 'logs.myapp.#' amqp://user:pass@remote_server/vhost

It creates temporary queue which it binds to the amq.topic exchange which exists by default in any RabbitMQ installation. Then it binds the temporary queue to this exchange with pattern we provide (in this case it's logs.myapp.#). This makes sure all the subscribers gets all the messages they're interested in.

Logging Best Practices

Don't Use Just One Logger Per App

In Ruby community people often don't use loggers at all. If they do, they work with only one instance. One logger instance for database, web server, application code and metrics. That doesn't scale.

If you use one logger instance per each module you can very easily filter based on specific pattern.

class DB
  def self.logger
    @logger ||= Logging::Logger.new do |logger|
      logger.io = Logging::IO::Pipe.new('testapp.logs.db', '/tmp/loggingd.pipe')
      logger.formatter = Logging::Formatters::Colourful.new
    end
  end
end

class App
  def self.logger
  @logger ||= Logging::Logger.new do |logger|
    logger.io = Logging::IO::Pipe.new('testapp.app.db', '/tmp/loggingd.pipe')
    logger.formatter = Logging::Formatters::Colourful.new
  end
end

Links