gallus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 411a743f66a68e2f25e37c20ff108513e9b4baf6
4
+ data.tar.gz: ea6360329a24846efa92d86cf3a1250afdb116cd
5
+ SHA512:
6
+ metadata.gz: 14e6d3d70973ca0fcc6d194782100c16dc926e89458d61d200c0ad577f96082f876c38a5fd213e9f64e164ffe09b2ba717ea96c43b01b12c44f4488de82efeb0
7
+ data.tar.gz: e81bae8f87270af9a4297c0237b4017463ecc8a580cf4ed8ebadbfa74cc348f7e3337bdf76546163ecfcc2e30dfea7814b68ee11c33b7d4609a12e41658efb43
data/.editorconfig ADDED
@@ -0,0 +1,12 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+ This project adheres to [Semantic Versioning](http://semver.org/).
4
+
5
+ ## [Unreleased]
6
+
7
+ ...
8
+
9
+ ## [0.1.0]
10
+
11
+ ...
12
+
13
+ ### Added
14
+
15
+ - Project extracted from other library and moved into this standalone gem.
16
+ - Documentation.
data/Dockerfile ADDED
@@ -0,0 +1,11 @@
1
+ FROM ruby:2.2
2
+
3
+ ENV GEM_NAME gallus
4
+ ENV WORKDIR /usr/local/src/$GEM_NAME
5
+
6
+ RUN mkdir -p $WORKDIR
7
+ WORKDIR $WORKDIR
8
+ ADD . $WORKDIR
9
+ RUN bundle install
10
+
11
+ CMD bundle exec rake spec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in gallus.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,227 @@
1
+ # Gallus
2
+
3
+ [![Codeship Status for jobandtalent/gallus](https://codeship.com/projects/3c59b1b0-9dd1-0133-8d35-5e859e904c15/status?branch=master)](https://codeship.com/projects/127615)
4
+
5
+ _**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.
6
+
7
+ ![Gallus Anonymus](http://i.imgur.com/QpQ2g7B.jpg)
8
+
9
+ Picture stolen from: https://www.flickr.com/photos/maxcuo/15601437990
10
+
11
+ Gallus is a logger for Ruby apps.
12
+
13
+ Q: **Why write yet another logger for ruby?**<br />
14
+ A: Because culture of logging among ruby developers is very low and tools used are either
15
+ prehistoric (Log4r) or messed up and insufficient (Standard library logger, logging library, etc.)
16
+
17
+ Q: **What's so special about Gallus?**<br />
18
+ A: Nothing really, it's just a collection of best practices from loggers of different technologies (like Log4j & Slf4j, Python logging, etc.)
19
+
20
+ Q: **So why would I use Gallus over Log4r for example.**<br />
21
+ A: Because it's simpler to customize, it's more powerful by default, it's more robust in working with context variables.
22
+
23
+ Q: **Is it faster than Log4r or standard logger?**<br />
24
+ A: It is not. It's about 30-60% slower depending on the log level (check `hacking/benchmarks.rb` for comparison with Log4r).
25
+
26
+ Q: **What? It's slower even than Log4r, why would I event want to use it?**<br />
27
+ 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.
28
+
29
+ Q: **Yeah? So logger can be slow?**<br />
30
+ 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.
31
+
32
+ ## Installation
33
+
34
+ Add this line to your application's Gemfile:
35
+
36
+ ```ruby
37
+ gem 'gallus', '0.1.0'
38
+ ```
39
+
40
+ And then execute:
41
+
42
+ $ bundle
43
+
44
+ Or install it yourself as:
45
+
46
+ $ gem install gallus --version 0.1.0
47
+
48
+ ## Usage
49
+
50
+ Start off from configuring root logger:
51
+
52
+ ```ruby
53
+ require 'gallus'
54
+
55
+ Gallus::Log.configure do |log|
56
+ log.level = :INFO
57
+ log.output << Gallus::Output::Stderr.new(Gallus::Format::SimpleLog.new)
58
+ end
59
+ ```
60
+
61
+ Simple logging:
62
+
63
+ ```ruby
64
+ Gallus::Log.root.info("Hello, this is info message")
65
+ # => I @ 2016-01-15T16:32:56+01:00Z $ root > Hello, this is info message
66
+
67
+ Gallus::Log.root.info("With context", foo: 1, bar: "baz")
68
+ # => I @ 2016-01-15T16:32:56+01:00Z $ root > With context; foo=1 bar="baz"
69
+
70
+ Gallus::Log.root.info("With lazy context", foo: 1, bar: -> { 100 * 2 })
71
+ # => I @ 2016-01-15T16:32:56+01:00Z $ root > With context; foo=1 bar=200
72
+ ```
73
+
74
+ Nothing fancy so far. Lets try injecting loggers into classes:
75
+
76
+ ```ruby
77
+ class User < Struct.new(:name)
78
+ include Gallus::Logging
79
+
80
+ def greet
81
+ log.info("Greeted", name: name)
82
+ "Hello, I'm #{name}"
83
+ end
84
+ end
85
+
86
+ User.new("Jon Snow").greet
87
+ # => I @ 2016-01-15T16:32:56+01:00Z $ User > Greeted; name="Jon Snow"
88
+ ```
89
+
90
+ Wow, did you see that? What did just happened? We have logger injected and configured with one `include`.
91
+ And those neat context variables:
92
+
93
+ ```ruby
94
+ log.info("Multi level contexts", foo: 1, bar: 2)
95
+ # => I @ 2016-01-15T16:32:56+01:00Z $ root > Multi level context; foo=1 bar=2
96
+ ```
97
+
98
+ What about global contexts?
99
+
100
+ ```ruby
101
+ Gallus::Log.global_context { |ctx| ctx[:location] = "Castle Black" }
102
+
103
+ SamwellTarly.log.info("I always wanted to be a Wizard", name: "Samwell Tarly")
104
+ # => I always wanted to be a Wizard; name="Samwell Tarly" location="Castle Black"
105
+
106
+ JonSnow.log.info("I know nothing", name: "Jon Snow")
107
+ # => I know nothing; name="Jon Snow" location="Castle Black"
108
+ ```
109
+
110
+ Q: Why block in `global_context` call?<br/>
111
+ A: Because it's thread-safe this way.
112
+
113
+ Speaking of threads...
114
+
115
+ ```ruby
116
+ t1 = Thread.new do
117
+ Gallus::Log.current_thread_context { |ctx| ctx[:location] = "Castle Black" }
118
+ SamwellTarly.log.info("I always wanted to be a Wizard", name: "Samwell Tarly")
119
+ end
120
+
121
+ t2 = Thread.new do
122
+ Gallus::Log.current_thread_context { |ctx| ctx[:location] = "Beyond the Wall" }
123
+ JonSnow.log.info("I know nothing", name: "Jon Snow")
124
+ end
125
+
126
+ t1.join
127
+ t2.join
128
+
129
+ # => I always wanted to be a Wizard; name="Samwell Tarly" location="Castle Black"
130
+ # => I know nothing; name="Jon Snow" location="Beyond the Wall"
131
+ ```
132
+
133
+ Yeap, you can have context variables per thread too.
134
+
135
+ ### Hacking
136
+
137
+ Finally, you can customize pretty much everything here. Output, serialization and formatting handlers are
138
+ all callables (Proc interfaces). So you can do something like this:
139
+
140
+ ```ruby
141
+
142
+ Gallus::Log.configure do |log|
143
+ custom_format = -> (event) { "#{event.level} (#{event.payload[:pid]}): #{event.message}" }
144
+ log.output << Gallus::Output::Stderr.new(custom_format)
145
+ end
146
+ ```
147
+
148
+ Or even like this:
149
+
150
+ ```ruby
151
+ Gallus::Log.configure do |log|
152
+ log.output << -> (event) { puts event.inspect }
153
+ end
154
+ ```
155
+
156
+ ### Configuration inheritance
157
+
158
+ Given logger `Foo` and `Foo::Bar`, and `Foo::Bar::Baz` - `Foo` inherits from `root`, `Foo::Bar` from `Foo`, etc...
159
+ You can override configuration manually:
160
+
161
+ ```ruby
162
+ Gallus::Log.configure do |log|
163
+ log.level = :INFO
164
+ end
165
+
166
+ Gallus::Log.configure("Foo") do |log|
167
+ log.level = :ERROR
168
+ end
169
+
170
+ Gallus::Log.configure("Foo::Bar") do |log|
171
+ log.level = :DEBUG
172
+ end
173
+ ```
174
+
175
+ **NOTE**: Configuration must be executed before creation of the logger. At this point configuration is frozen and
176
+ child loggers can't be reconfigured. You can reconfigure particular logger though.
177
+
178
+ ## Development
179
+
180
+ You have two options to work with this project. The [docker flow](#setup-with-docker) is suggested since solves problems of compatibility of tools.
181
+
182
+ ### Manual Setup
183
+
184
+ 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:
185
+
186
+ $ bin/setup
187
+
188
+ Now you can run tests:
189
+
190
+ $ bundle exec rake spec
191
+
192
+ You can also connect to interactive prompt that will allow you to experiment. To do this, run:
193
+
194
+ $ bundle exec bin/console
195
+
196
+ To install this gem onto your local machine, run:
197
+
198
+ $ bundle exec rake install
199
+
200
+ To run all example files, use following rake task:
201
+
202
+ $ bundle exec rake examples
203
+
204
+ ### Setup with Docker
205
+
206
+ If you're lazy and don't wanna get into how the setup works, here's something for you. This project comes fully [dockerized](http://docker.io/). Install docker toolchain and then go for:
207
+
208
+ $ docker-compose build
209
+
210
+ All done, you can do testing and fiddling around:
211
+
212
+ $ docker-compose run gallus bash
213
+ root@xyyyyxx:/usr/local/src/gallus# bundle exec rake spec
214
+ root@xyyyyxx:/usr/local/src/gallus# bundle exec bin/console
215
+
216
+ ### Releasing new version
217
+
218
+ This project is powered by rake-bump. To release gem version, follow [this continuous releasing guide](https://github.com/jobandtalent/rake-bump#continuous-releasing).
219
+
220
+ **NOTE**: This gem is a dependency for `rake-bump`, so to avoid circular dependency issues you should invoke bump tasks follow:
221
+
222
+ $ bundle exec rake -r rake/bump/tasks bump
223
+ $ bundle exec rake -r rake/bump/tasks release:rubygems
224
+
225
+ ## Contributing
226
+
227
+ Bug reports and pull requests are welcome [here](https://github.com/jobandtalent/gallus/issues).
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ require "rake/testtask"
2
+
3
+ if defined?(Rake::Bump)
4
+ require File.expand_path("../lib/gallus/version", __FILE__)
5
+
6
+ Rake::Bump::Tasks.new do |t|
7
+ t.gem_name = 'gallus'
8
+ t.gem_current_version = Gallus::VERSION
9
+ end
10
+ end
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << "test"
14
+ t.libs << "lib"
15
+ t.test_files = FileList['test/**/*_test.rb']
16
+ end
17
+
18
+ desc "Run all examples in sequence"
19
+ task :examples do
20
+ Pathname.new(Dir.pwd).join('examples').each_child do |f|
21
+ puts "-----> Executing example: #{f.basename}"
22
+ system("ruby #{f.to_s}") if f.file?
23
+ puts
24
+ end
25
+ end
26
+
27
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "gallus"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+
3
+ set -euo pipefail
4
+ IFS=$'\n\t'
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,4 @@
1
+ gallus:
2
+ build: .
3
+ volumes:
4
+ - .:/usr/local/src/gallus:rw
@@ -0,0 +1,28 @@
1
+ require 'gallus'
2
+
3
+ include Gallus
4
+
5
+ Log.configure do |log|
6
+ log.output << Output::Stderr.new(Format::SimpleLog.new)
7
+ end
8
+
9
+ class SamwellTarly
10
+ include Logging
11
+ end
12
+
13
+ class JonSnow
14
+ include Logging
15
+ end
16
+
17
+ t1 = Thread.new do
18
+ Log.current_thread_context { |ctx| ctx[:location] = "Castle Black" }
19
+ SamwellTarly.log.info("I always wanted to be a Wizard", name: "Samwell Tarly")
20
+ end
21
+
22
+ t2 = Thread.new do
23
+ Log.current_thread_context { |ctx| ctx[:location] = "Beyond the Wall" }
24
+ JonSnow.log.info("I know nothing", name: "Jon Snow")
25
+ end
26
+
27
+ t1.join
28
+ t2.join
@@ -0,0 +1,10 @@
1
+ require 'gallus'
2
+
3
+ include Gallus
4
+
5
+ logger = Log.configure('test') do |log|
6
+ custom_format = -> (event) { "#{event.level} (#{event.payload[:pid]}): #{event.message}" }
7
+ log.output << Output::Stderr.new(custom_format)
8
+ end
9
+
10
+ logger.info('Custom format here...', pid: Process.pid)
@@ -0,0 +1,9 @@
1
+ require 'gallus'
2
+
3
+ include Gallus
4
+
5
+ logger = Log.configure('test') do |log|
6
+ log.output << -> (event) { puts event.inspect }
7
+ end
8
+
9
+ logger.info('Context as JSON again, but custom serialization used this time', foo: 1, bar: 'baz')
@@ -0,0 +1,10 @@
1
+ require 'gallus'
2
+
3
+ include Gallus
4
+
5
+ logger = Log.configure('test') do |log|
6
+ custom_json_serialization = -> (event) { event.to_json }
7
+ log.output << Output::Stderr.new(Format::SimpleLog.new(custom_json_serialization))
8
+ end
9
+
10
+ logger.info('Context as JSON again, but custom serialization used this time', foo: 1, bar: 'baz')
@@ -0,0 +1,19 @@
1
+ require 'gallus'
2
+
3
+ include Gallus
4
+
5
+ Log.configure do |log|
6
+ log.output << Output::Stderr.new(Format::SimpleLog.new)
7
+ end
8
+
9
+ class SamwellTarly
10
+ include Logging
11
+ end
12
+
13
+ class JonSnow
14
+ include Logging
15
+ end
16
+
17
+ Log.global_context { |ctx| ctx[:location] = "Castle Black" }
18
+ SamwellTarly.log.info("I always wanted to be a Wizard", name: "Samwell Tarly")
19
+ JonSnow.log.info("I know nothing", name: "Jon Snow")
@@ -0,0 +1,9 @@
1
+ require 'gallus'
2
+
3
+ include Gallus
4
+
5
+ logger = Log.configure('test') do |log|
6
+ log.output << Output::Stderr.new(Format::SimpleLog.new(Serialization::JSON))
7
+ end
8
+
9
+ logger.info('Context as JSON', foo: 1, bar: 'baz')
@@ -0,0 +1,9 @@
1
+ require 'gallus'
2
+
3
+ include Gallus
4
+
5
+ logger = Log.configure('test') do |log|
6
+ log.output << Output::Stderr.new(Format::SimpleLog.new)
7
+ end
8
+
9
+ logger.info('Testing lambdas', foo: -> { 10 * 20 })
@@ -0,0 +1,13 @@
1
+ require 'gallus'
2
+
3
+ include Gallus
4
+
5
+ logger = Log.configure('test') do |log|
6
+ log.level = :DEBUG
7
+ log.output << Output::Stderr.new(Format::SimpleConsole.new)
8
+ end
9
+
10
+ logger.info('This is console output')
11
+ logger.warn('Everything else than INFO is being prefixed')
12
+ logger.debug('Checking debugs, just in case')
13
+ logger.error('And errors too...')
@@ -0,0 +1,12 @@
1
+ require 'gallus'
2
+
3
+ include Gallus
4
+
5
+ logger = Log.configure('test') do |log|
6
+ log.level = :DEBUG
7
+ log.output << Output::Stderr.new(Format::SimpleLog.new)
8
+ end
9
+
10
+ logger.info('Hello World')
11
+ logger.warn('Not everything is what it seems', planet: 'Earth')
12
+ logger.info('Another info message with context', foo: 1, bar: 2)
data/gallus.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'gallus/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "gallus"
8
+ spec.version = Gallus::VERSION
9
+ spec.authors = ["jobandtalent", "Kris Kovalik"]
10
+ spec.email = ["kris.kovalik@jobandtalent.com", "hi@kkvlk.me"]
11
+
12
+ spec.summary = %q{Production grade logger for Ruby.}
13
+ spec.description = %q{Easy to use and insanely powerfull logger for Ruby applications.}
14
+ spec.homepage = "https://github.com/jobandtalent/gallus"
15
+ spec.license = 'Apache-2.0'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "minitest", "~> 5.8"
25
+ spec.add_development_dependency "minitest-reporters", "~> 1.1"
26
+ spec.add_development_dependency "mocha", "~> 1.1"
27
+ spec.add_development_dependency "log4r", "~> 1.1", ">= 1.0"
28
+ #spec.add_development_dependency "rake-bump", "~> 0.4", ">= 0.4.6"
29
+
30
+ spec.add_dependency "activesupport", "~> 4.2", ">= 3.0"
31
+ end
@@ -0,0 +1,47 @@
1
+ # Usage: LEVEL=INFO N=10000 bundle exec ruby hacking/benchmarks.rb
2
+
3
+ require 'log4r'
4
+ require 'gallus'
5
+ require 'stringio'
6
+ require 'benchmark'
7
+
8
+ LEVEL = ENV.fetch('LEVEL', 'INFO')
9
+
10
+ $gallus = Gallus::Log.configure('test') do |log|
11
+ log.level = LEVEL
12
+ log.output << Gallus::Output::Stream.new(StringIO.new, Gallus::Format::SimpleLog.new)
13
+ end
14
+
15
+ N = ENV.fetch('N', 10000).to_i
16
+ calls = []
17
+
18
+ N.times do
19
+ %w[debug info warn error].each do |level|
20
+ calls << level
21
+ end
22
+ end
23
+
24
+ Log4r::IOOutputter.new('stream', StringIO.new, formatter: Log4r::PatternFormatter.new(pattern: "%l @ %d $ %C - %m"))
25
+
26
+ $log4r = Log4r::Logger.new('test')
27
+ $log4r.level = Log4r.const_get(LEVEL)
28
+ $log4r.add('stream')
29
+
30
+ payload = { one: 1, two: "Two", three: Object.new, four: "Yada! yada! yada!" }
31
+
32
+ Benchmark.bm do |x|
33
+ x.report("Log4r: ") do
34
+ calls.dup.shuffle.each do |level|
35
+ $log4r.send(level) do
36
+ formatted_payload = payload.map { |k,v| "#{k}=#{v.inspect}" }.join(" ")
37
+ "This is fancy little message; #{formatted_payload}"
38
+ end
39
+ end
40
+ end
41
+
42
+ x.report("Gallus: ") do
43
+ calls.dup.shuffle.each do |level|
44
+ $gallus.send(level, "This is fancy little message", payload)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ module Gallus
2
+ # Internal: Every log event is wrapped with this envelope. It holds information about the logger that
3
+ # performed the operation, log level, message (or object passed) and full payload. Also includes information
4
+ # at what time event has been recorded.
5
+ Event = Struct.new(:logger, :level, :message, :payload) do
6
+ attr_reader :recorded_at
7
+
8
+ def initialize(*)
9
+ super
10
+
11
+ self.payload = Payload.new(payload || {})
12
+ @recorded_at = Time.now.utc
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ module Gallus
2
+ module Format
3
+ # Public: This console log format is used for command line apps. Instead of using puts-es and write-s to
4
+ # display stuff use logger! INFO level will be displayed without prefixes, as is - just message and context
5
+ # info. Other levels will be prefixed with level name. Example:
6
+ #
7
+ # Hello, this is info message
8
+ # ERROR: Upps, something went wrong; foo="Bar"
9
+ # Another info message
10
+ # DEBUG: Here's debug information
11
+ # ...
12
+ #
13
+ class SimpleConsole
14
+ def initialize(serialization = Serialization::Inspect)
15
+ @serialization = serialization
16
+ end
17
+
18
+ def call(event)
19
+ parts = [ [ event.message, @serialization.call(event.payload) ].compact.join('; ') ]
20
+ parts.unshift(format("%s:", event.level.name)) unless event.level == Level::INFO
21
+ parts.compact.join(' ')
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ module Gallus
2
+ module Format
3
+ # Public: Simple because of implementation, not because of the output. It's default log line that contains
4
+ # most information needed. Everything presented in a compact and easy to parse form. Example:
5
+ #
6
+ # "I @ 2016-01-15T16:32:56+01:00Z $ Foo > Hello World; foo=1 bar=2"
7
+ class SimpleLog
8
+ def initialize(serialization = Serialization::Inspect)
9
+ @serialization = serialization
10
+ end
11
+
12
+ def call(event)
13
+ message = [ event.message, @serialization.call(event.payload) ].compact.join('; ')
14
+ format("%s @ %s $ %s > %s", event.level.name[0], event.recorded_at.iso8601, event.logger, message)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,50 @@
1
+ module Gallus
2
+ # Internal: Log level represented in a coherent way. You can easily compare log levels between each other.
3
+ # You're also able to get log level by string/symbol name.
4
+ class Level
5
+ include Comparable
6
+
7
+ # Internal: All log levels.
8
+ def self.all
9
+ @all ||= []
10
+ end
11
+
12
+ # Internal: Shorthand to all.each.
13
+ def self.each(&block)
14
+ self.all.each(&block)
15
+ end
16
+
17
+ # Internal: Returns level defined under given name.
18
+ def self.[](name)
19
+ const_get(name.to_s)
20
+ end
21
+
22
+ attr_reader :name, :id
23
+
24
+ # Internal: Constructor. Initializes logger with given name and identifier, then registers it.
25
+ def initialize(name, id)
26
+ @name, @id = name.to_s, id
27
+
28
+ self.class.const_set(@name, self)
29
+ self.class.all << self
30
+ end
31
+
32
+ def <=>(other)
33
+ self.id <=> other.id
34
+ rescue => err
35
+ return nil
36
+ end
37
+
38
+ def to_s
39
+ name
40
+ end
41
+
42
+ # Register available log levels:
43
+
44
+ new :TRACE, 1
45
+ new :DEBUG, 2
46
+ new :INFO, 3
47
+ new :WARN, 4
48
+ new :ERROR, 5
49
+ end
50
+ end
data/lib/gallus/log.rb ADDED
@@ -0,0 +1,162 @@
1
+ module Gallus
2
+ # Public: The brain of logging. All defined loggers inherit from this class.
3
+ class Log
4
+ # Global context requires a semaphore to be thread-safe.
5
+ @@global_context_mutex = Mutex.new
6
+
7
+ # Global context stores context variables that will be printed by any logger alongside with its
8
+ # local information.
9
+ @@global_context = {}
10
+
11
+ class << self
12
+ # Public: Configures logger with given name. When no name specified, root logger will be configured.
13
+ #
14
+ # Example:
15
+ #
16
+ # Gallus::Log.configure do |log|
17
+ # log.level = :INFO
18
+ # log.output << -> (event) { puts event.inspect }
19
+ # end
20
+ #
21
+ # Returns configured logger.
22
+ def configure(name = '', &block)
23
+ Repository.get_or_create_logger(name, &block)
24
+ end
25
+
26
+ # Public: Returns logger registered under given name.
27
+ def [](name)
28
+ configure(name)
29
+ end
30
+
31
+ # Internal: For testing purposes we should be able to remove logger with given name.
32
+ def delete(name)
33
+ Repository.delete_with_children(name)
34
+ end
35
+
36
+ # Internal: Performance magic. It dynamically defines log level methods to avoid overhead of not-logged
37
+ # levels. For example if log level is WARN it'll define empty `trace`, `debug` and `info` methods and fully
38
+ # working `warn` and `error`. Effectively log methods call `log` under the hood.
39
+ def define_log_methods!(log_level)
40
+ Level.each do |level|
41
+ method_name = level.name.downcase
42
+ remove_method(method_name) if method_defined?(method_name)
43
+
44
+ if level >= log_level
45
+ define_method(method_name) do |message = nil, payload = {}, &block|
46
+ log(level, message, payload, &block)
47
+ end
48
+ else
49
+ define_method(method_name) do |message = nil, payload = {}, &block|
50
+ # supressed...
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # Public: Yields global context. Only block calls are allowed since this operation must be thread-safe.
57
+ #
58
+ # Example:
59
+ #
60
+ # log.global_context { |ctx| ctx[:location] = "Castle Black" }
61
+ # samwell_log.info("I always wanted to be a Wizard", name: "Samwell Tarly")
62
+ # # => I always wanted to be a Wizard; name="Samwell Tarly" location="Castle Black"
63
+ # jon_snow_log.info("I know nothing", name: "Jon Snow")
64
+ # # => I know nothing; name="Jon Snow" location="Castle Black"
65
+ #
66
+ def global_context
67
+ @@global_context_mutex.synchronize { yield @@global_context }
68
+ end
69
+
70
+ # Public: Yields context for current thread. Just to conform with the behavior of #global_context we're
71
+ # using block call here as well.
72
+ #
73
+ # Example:
74
+ #
75
+ # log.current_thread_context { |ctx| ctx[:location] = "Castle Black" }
76
+ # log.info("I know nothing!", name: "Jon Snow")
77
+ # # => I know nothing! name="Jon Snow" location="Castle Black"
78
+ #
79
+ def current_thread_context
80
+ yield (Thread.current[:log_context] ||= {})
81
+ end
82
+
83
+ # Public: Returns root logger.
84
+ def root
85
+ @@root
86
+ end
87
+ end
88
+
89
+ # Public: Logger name, log level and context serializtion handler.
90
+ attr_accessor :name, :level
91
+
92
+ # Public: A list with configured outputs.
93
+ attr_reader :output
94
+
95
+ # Internal: Parent logger. Used for testing purposes.
96
+ attr_reader :parent
97
+
98
+ # Internal: Constructs new logger with configuration inherited from given parent. Yields itself so
99
+ # it can be reconfigured with a block. Don't use this method directly. Use Gallus::Log.configure class
100
+ # method if you wanna define or reconfigure a logger.
101
+ def initialize(parent, name, &block)
102
+ @parent, @name = parent, name.to_s
103
+
104
+ if parent
105
+ @output = @parent.output.dup
106
+ self.level = @parent.level
107
+ end
108
+
109
+ @output ||= []
110
+
111
+ yield self if block_given?
112
+ end
113
+
114
+ # Public: Changing log level means that we have to redefine logging methods.
115
+ def level=(level)
116
+ level = level.is_a?(Level) ? level : Level[level.to_s]
117
+ return if @level == level
118
+ self.class.class_eval { define_log_methods!(level) }
119
+ @level = level
120
+ end
121
+
122
+ # Public: Shortand to Gallus::Log.global_context.
123
+ def global_context(&block)
124
+ self.class.global_context(&block)
125
+ end
126
+
127
+ # Public: Shortand to Gallus::Log.current_thread_context.
128
+ def current_thread_context(&block)
129
+ self.class.current_thread_context(&block)
130
+ end
131
+
132
+ private
133
+
134
+ # Internal: Merges data from all contexts and provides them as a single hash. Order:
135
+ #
136
+ # 1. Global context.
137
+ # 2. Curreant thread context.
138
+ # 3. Self context.
139
+ #
140
+ def merged_context
141
+ {}.tap do |merged|
142
+ self.class.global_context { |ctx| merged.merge!(ctx) }
143
+ self.class.current_thread_context { |ctx| merged.merge!(ctx) }
144
+ end
145
+ end
146
+
147
+ # Internal: Actual logging. Takes payload and merges it into contexts, serializes context variables
148
+ # using configured serialization handler and finally sends it to each defined output.
149
+ def log(level, message = nil, payload = {}, &block)
150
+ message ||= block.call if block_given?
151
+ event = Event.new(@name, level, message.to_s, merged_context.merge(payload))
152
+ output.each { |out| out.call(event) }
153
+ end
154
+
155
+ # Root logger configuration (defaults for any logger defined after):
156
+
157
+ @@root = Log.configure do |log|
158
+ log.name = 'root'
159
+ log.level = Level::INFO
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,35 @@
1
+ module Gallus
2
+ # Internal: Programmers are lazy, why to initialize logger manually in language like Ruby, when you can
3
+ # use handy one-liner include to have logger available for your class. Example:
4
+ #
5
+ # class Foo
6
+ # include Gallus::Logging
7
+ #
8
+ # def initialize
9
+ # log.debug("Initializing!")
10
+ # end
11
+ # end
12
+ #
13
+ # Gallus::Log.configure('Foo') do |log|
14
+ # log.level = :DEBUG
15
+ # log.output << Gallus::Output::Stdout.new(Gallus::Format::SimpleConsole.new)
16
+ # end
17
+ #
18
+ # Foo.new # => "DEBUG: Initializing!"
19
+ #
20
+ module Logging
21
+ def self.included(klass)
22
+ klass.extend(ClassMethods)
23
+ end
24
+
25
+ def log
26
+ self.class.log
27
+ end
28
+
29
+ module ClassMethods
30
+ def log
31
+ @log ||= ::Gallus::Log.configure(self.name)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,15 @@
1
+ module Gallus
2
+ module Output
3
+ # Public: A thread safe IO stream output that writes to given file.
4
+ class File < Stream
5
+ def initialize(filename, format)
6
+ @file = ::File.open(filename, 'a+')
7
+ super(@file, format)
8
+ end
9
+
10
+ def close
11
+ @file.close
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ module Gallus
2
+ module Output
3
+ # Public: Prints nothing.
4
+ class Null
5
+ def call
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ module Gallus
2
+ module Output
3
+ # Public: A thread safe IO stream output that writes to standard error.
4
+ class Stderr < Stream
5
+ def initialize(format)
6
+ super($stderr, format)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Gallus
2
+ module Output
3
+ # Public: A thread safe IO stream output that writes to standard output.
4
+ class Stdout < Stream
5
+ def initialize(format)
6
+ super($stdout, format)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ module Gallus
2
+ module Output
3
+ # Public: Simple, thread-safe IO stream output handler. Takes stream and format on input.
4
+ class Stream
5
+ def initialize(stream, format)
6
+ @mutex = Mutex.new
7
+ @stream, @format = stream, format
8
+ end
9
+
10
+ def call(event)
11
+ @mutex.synchronize { call!(event) }
12
+ end
13
+
14
+ def call!(event)
15
+ @stream.write(@format.call(event) + "\n")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Gallus
2
+ # Public: Similarly to Gallus::Logging no point to initialize global logger for your
3
+ # modules/packages. Include this mixin to define LOG constant and log class method shortcut.
4
+ #
5
+ # Often used practice is to define such top level logger as a parent with default outputters,
6
+ # formatters, log level, etc.
7
+ module PackageLogging
8
+ def self.included(klass)
9
+ klass.const_set(:LOG, ::Gallus::Log.configure(klass.name))
10
+ klass.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+ def log
15
+ const_get(:LOG)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ module Gallus
2
+ # Internal: To improve performance, context can provide values wrapped in lambdas. Those will be
3
+ # executed at the time of writing log. Getting the values from lambdas is the job of this simple
4
+ # payload class.
5
+ class Payload
6
+ include Enumerable
7
+ extend Forwardable
8
+
9
+ def_delegators :@h, :empty?, :each
10
+
11
+ def initialize(payload)
12
+ @h = payload.inject({}) do |res,(k,v)|
13
+ res[k.to_sym] = v.is_a?(Proc) ? v.call : v
14
+ res
15
+ end
16
+ end
17
+
18
+ def to_h
19
+ @h
20
+ end
21
+
22
+ def inspect
23
+ @h.map { |k,v| "#{k}=#{v.inspect}" }.join(" ")
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,53 @@
1
+ module Gallus
2
+ # Internal: Wee need a place to keep track of registered loggers and their parents. This little
3
+ # repository class handles this task in a thread-safe manner.
4
+ class Repository
5
+ PARENT_DELIMITER = '::'
6
+
7
+ @mutex = Mutex.new
8
+
9
+ # Internal: Returns all registered loggers.
10
+ def self.all
11
+ @all ||= {}
12
+ end
13
+
14
+ # Internal: There must be a way to find parent logger for given class name. For example, looking up parent
15
+ # for Foo::Bar::Baz logger it'll look up for Foo::Bar, then falling back to Foo and eventually to root logger
16
+ # if nothing found.
17
+ def self.find_parent(name)
18
+ parent, name = nil, name.dup
19
+
20
+ while parent.nil?
21
+ name = name.split(PARENT_DELIMITER)[0..-2].join(PARENT_DELIMITER)
22
+ parent = all[name]
23
+ break if name.empty?
24
+ end
25
+
26
+ parent
27
+ end
28
+
29
+ # Internal: For testing purposes we need to be able to delete logger alongside with all its children.
30
+ def self.delete_with_children(name)
31
+ @mutex.synchronize do
32
+ all.keys.each { |k| all.delete(k) if k.start_with?(name + PARENT_DELIMITER) }
33
+ all.delete(name)
34
+ end
35
+ end
36
+
37
+ # Internal: We obviously need a way to create logger or retrieve it by name if already registered.
38
+ # Creation of logger causes configuration to be inherited from parent (or root) logger.
39
+ def self.get_or_create_logger(name, &block)
40
+ name, log = name.to_s, nil
41
+
42
+ @mutex.synchronize do
43
+ if log = all[name]
44
+ yield log if block_given?
45
+ else
46
+ all[name] = (log = Class.new(Log).new(find_parent(name), name, &block))
47
+ end
48
+ end
49
+
50
+ log
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,11 @@
1
+ module Gallus
2
+ module Serialization
3
+ # Public: Serialization handler that produces inspected key=value data.
4
+ class Inspect
5
+ def self.call(payload)
6
+ return if payload.nil? or payload.empty?
7
+ payload.inspect
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ require 'json'
2
+
3
+ module Gallus
4
+ module Serialization
5
+ # Public: Serialization handler that produces JSON data.
6
+ class JSON
7
+ def self.call(payload)
8
+ return if payload.nil? or payload.empty?
9
+ payload.to_h.to_json
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Gallus
2
+ VERSION = '0.1.0'
3
+ end
data/lib/gallus.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'time'
2
+ require 'thread'
3
+
4
+ module Gallus
5
+ require 'gallus/version'
6
+ require 'gallus/format/simple_console'
7
+ require 'gallus/format/simple_log'
8
+ require 'gallus/output/null'
9
+ require 'gallus/output/stream'
10
+ require 'gallus/output/stdout'
11
+ require 'gallus/output/stderr'
12
+ require 'gallus/output/file'
13
+ require 'gallus/serialization/inspect'
14
+ require 'gallus/serialization/json'
15
+ require 'gallus/level'
16
+ require 'gallus/payload'
17
+ require 'gallus/event'
18
+ require 'gallus/repository'
19
+ require 'gallus/log'
20
+ require 'gallus/logging'
21
+ require 'gallus/package_logging'
22
+ end
metadata ADDED
@@ -0,0 +1,195 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gallus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - jobandtalent
8
+ - Kris Kovalik
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2016-01-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.10'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.10'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '10.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '10.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: minitest
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '5.8'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '5.8'
56
+ - !ruby/object:Gem::Dependency
57
+ name: minitest-reporters
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.1'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.1'
70
+ - !ruby/object:Gem::Dependency
71
+ name: mocha
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '1.1'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '1.1'
84
+ - !ruby/object:Gem::Dependency
85
+ name: log4r
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '1.1'
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '1.0'
94
+ type: :development
95
+ prerelease: false
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '1.1'
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '1.0'
104
+ - !ruby/object:Gem::Dependency
105
+ name: activesupport
106
+ requirement: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '4.2'
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '3.0'
114
+ type: :runtime
115
+ prerelease: false
116
+ version_requirements: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - "~>"
119
+ - !ruby/object:Gem::Version
120
+ version: '4.2'
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '3.0'
124
+ description: Easy to use and insanely powerfull logger for Ruby applications.
125
+ email:
126
+ - kris.kovalik@jobandtalent.com
127
+ - hi@kkvlk.me
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".editorconfig"
133
+ - ".gitignore"
134
+ - CHANGELOG.md
135
+ - Dockerfile
136
+ - Gemfile
137
+ - README.md
138
+ - Rakefile
139
+ - bin/console
140
+ - bin/setup
141
+ - docker-compose.yml
142
+ - examples/current_thread_context.rb
143
+ - examples/custom_format.rb
144
+ - examples/custom_output.rb
145
+ - examples/custom_serialization.rb
146
+ - examples/global_context.rb
147
+ - examples/json_serialization.rb
148
+ - examples/lambda_context_vars.rb
149
+ - examples/simple_console.rb
150
+ - examples/simple_log.rb
151
+ - gallus.gemspec
152
+ - hacking/benchmarks.rb
153
+ - lib/gallus.rb
154
+ - lib/gallus/event.rb
155
+ - lib/gallus/format/simple_console.rb
156
+ - lib/gallus/format/simple_log.rb
157
+ - lib/gallus/level.rb
158
+ - lib/gallus/log.rb
159
+ - lib/gallus/logging.rb
160
+ - lib/gallus/output/file.rb
161
+ - lib/gallus/output/null.rb
162
+ - lib/gallus/output/stderr.rb
163
+ - lib/gallus/output/stdout.rb
164
+ - lib/gallus/output/stream.rb
165
+ - lib/gallus/package_logging.rb
166
+ - lib/gallus/payload.rb
167
+ - lib/gallus/repository.rb
168
+ - lib/gallus/serialization/inspect.rb
169
+ - lib/gallus/serialization/json.rb
170
+ - lib/gallus/version.rb
171
+ homepage: https://github.com/jobandtalent/gallus
172
+ licenses:
173
+ - Apache-2.0
174
+ metadata: {}
175
+ post_install_message:
176
+ rdoc_options: []
177
+ require_paths:
178
+ - lib
179
+ required_ruby_version: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - ">="
182
+ - !ruby/object:Gem::Version
183
+ version: '0'
184
+ required_rubygems_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ requirements: []
190
+ rubyforge_project:
191
+ rubygems_version: 2.5.1
192
+ signing_key:
193
+ specification_version: 4
194
+ summary: Production grade logger for Ruby.
195
+ test_files: []