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.
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.}" }
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.