Gallus

Codeship Status for jobandtalent/gallus

Gallus Anonymus (Polonized variant: Gall Anonim) is the name traditionally given to the anonymous author of Gesta principum Polonorum (Deeds of the Princes of the Poles), composed in Latin about 1115. Gallus is generally regarded as the first historian to have described Poland. His Chronicles are an obligatory text for university courses in Poland's history.

Gallus Anonymus

Picture stolen from: https://www.flickr.com/photos/maxcuo/15601437990

Gallus is a logger for Ruby apps.

Q: Why write yet another logger for ruby?
A: Because culture of logging among ruby developers is very low and tools used are either prehistoric (Log4r) or messed up and insufficient (Standard library logger, logging library, etc.)

Q: What's so special about Gallus?
A: Nothing really, it's just a collection of best practices from loggers of different technologies (like Log4j & Slf4j, Python logging, etc.)

Q: So why would I use Gallus over Log4r for example.
A: Because it's simpler to customize, it's more powerful by default, it's more robust in working with context variables.

Q: Is it faster than Log4r or standard logger?
A: It is not. It's about 30-60% slower depending on the log level (check hacking/benchmarks.rb for comparison with Log4r).

Q: What? It's slower even than Log4r, why would I event want to use it?
A: Log4r provides much more limited way to work with contexts, and this is where the overhead comes from. If you'd wrap Log4r with adapter that works the same way as Gallus, performance would be the same. On a side note, if you consider performance of logging as your bottleneck then you evidently do something wrong.

Q: Yeah? So logger can be slow?
A: Gallus ain't no slow, it's slower than Log4r, a tradeoff of convenience in usage. It's still blazingly fast comparing with all your business logic operations.

Installation

Add this line to your application's Gemfile:

gem 'gallus', '0.1.2'

And then execute:

$ bundle

Or install it yourself as:

$ gem install gallus --version 0.1.2

Usage

Start off from configuring root logger:

require 'gallus'

Gallus::Log.configure do |log|
  log.level = :INFO
  log.output << Gallus::Output::Stderr.new(Gallus::Format::SimpleLog.new)
end

Simple logging:

Gallus::Log.root.info("Hello, this is info message")
# => I @ 2016-01-15T16:32:56+01:00Z $ root > Hello, this is info message

Gallus::Log.root.info("With context", foo: 1, bar: "baz")
# => I @ 2016-01-15T16:32:56+01:00Z $ root > With context; foo=1 bar="baz"

Gallus::Log.root.info("With lazy context", foo: 1, bar: -> { 100 * 2 })
# => I @ 2016-01-15T16:32:56+01:00Z $ root > With context; foo=1 bar=200

Nothing fancy so far. Lets try injecting loggers into classes:

class User < Struct.new(:name)
  include Gallus::Logging

  def greet
    log.info("Greeted", name: name)
    "Hello, I'm #{name}"
  end
end

User.new("Jon Snow").greet
# => I @ 2016-01-15T16:32:56+01:00Z $ User > Greeted; name="Jon Snow"

Wow, did you see that? What did just happened? We have logger injected and configured with one include. And those neat context variables:

log.info("Multi level contexts", foo: 1, bar: 2)
# => I @ 2016-01-15T16:32:56+01:00Z $ root > Multi level context; foo=1 bar=2

What about global contexts?

Gallus::Log.global_context { |ctx| ctx[:location] = "Castle Black" }

SamwellTarly.log.info("I always wanted to be a Wizard", name: "Samwell Tarly")
# => I always wanted to be a Wizard; name="Samwell Tarly" location="Castle Black"

JonSnow.log.info("I know nothing", name: "Jon Snow")
# => I know nothing; name="Jon Snow" location="Castle Black"

Q: Why block in global_context call?
A: Because it's thread-safe this way.

Speaking of threads...

t1 = Thread.new do
  Gallus::Log.current_thread_context { |ctx| ctx[:location] = "Castle Black" }
  SamwellTarly.log.info("I always wanted to be a Wizard", name: "Samwell Tarly")
end

t2 = Thread.new do
  Gallus::Log.current_thread_context { |ctx| ctx[:location] = "Beyond the Wall" }
  JonSnow.log.info("I know nothing", name: "Jon Snow")
end

t1.join
t2.join

# => I always wanted to be a Wizard; name="Samwell Tarly" location="Castle Black"
# => I know nothing; name="Jon Snow" location="Beyond the Wall"

Yeap, you can have context variables per thread too.

Hacking

Finally, you can customize pretty much everything here. Output, serialization and formatting handlers are all callables (Proc interfaces). So you can do something like this:


Gallus::Log.configure do |log|
  custom_format = -> (event) { "#{event.level} (#{event.payload[:pid]}): #{event.message}" }
  log.output << Gallus::Output::Stderr.new(custom_format)
end

Or even like this:

Gallus::Log.configure do |log|
  log.output << -> (event) { puts event.inspect }
end

Configuration inheritance

Given logger Foo and Foo::Bar, and Foo::Bar::Baz - Foo inherits from root, Foo::Bar from Foo, etc... You can override configuration manually:

Gallus::Log.configure do |log|
  log.level = :INFO
end

Gallus::Log.configure("Foo") do |log|
  log.level = :ERROR
end

Gallus::Log.configure("Foo::Bar") do |log|
  log.level = :DEBUG
end

NOTE: Configuration must be executed before creation of the logger. At this point configuration is frozen and child loggers can't be reconfigured. You can reconfigure particular logger though.

Development

You have two options to work with this project. The docker flow is suggested since solves problems of compatibility of tools.

Manual Setup

First off, make sure you have Ruby 2.2+ and latest version of Bundler on your machine. After checking out the repo, you can install dependencies and prepare the project with:

$ bin/setup

Now you can run tests:

$ bundle exec rake spec

You can also connect to interactive prompt that will allow you to experiment. To do this, run:

$ bundle exec bin/console

To install this gem onto your local machine, run:

$ bundle exec rake install

To run all example files, use following rake task:

$ bundle exec rake examples

Setup with Docker

If you're lazy and don't wanna get into how the setup works, here's something for you. This project comes fully dockerized. Install docker toolchain and then go for:

$ docker-compose build

All done, you can do testing and fiddling around:

$ docker-compose run gallus bash
root@xyyyyxx:/usr/local/src/gallus# bundle exec rake spec
root@xyyyyxx:/usr/local/src/gallus# bundle exec bin/console

Releasing new version

This project is powered by rake-bump. To release gem version, follow this continuous releasing guide.

NOTE: This gem is a dependency for rake-bump, so to avoid circular dependency issues you should invoke bump tasks follow:

$ rake -r rake/bump/tasks bump
$ rake -r rake/bump/tasks release:rubygems

Contributing

Bug reports and pull requests are welcome here.