dlogger 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +19 -0
- data/Guardfile +15 -0
- data/LICENSE +22 -0
- data/README.md +182 -0
- data/README.md.erb +79 -0
- data/Rakefile +25 -0
- data/dlogger.gemspec +21 -0
- data/examples/basic.rb +11 -0
- data/examples/dynamic_context.rb +26 -0
- data/examples/hash_context.rb +16 -0
- data/examples/logstash.rb +22 -0
- data/examples/nested_context.rb +27 -0
- data/examples/threads.rb +29 -0
- data/lib/dlogger.rb +8 -0
- data/lib/dlogger/extension.rb +24 -0
- data/lib/dlogger/logger.rb +134 -0
- data/lib/dlogger/outputs/base.rb +7 -0
- data/lib/dlogger/outputs/em/logstash.rb +36 -0
- data/lib/dlogger/outputs/stdlib_logger.rb +27 -0
- data/lib/dlogger/outputs/stdout.rb +33 -0
- data/lib/dlogger/version.rb +3 -0
- data/specs/common.rb +52 -0
- data/specs/lib/extension_spec.rb +28 -0
- data/specs/lib/logger_spec.rb +76 -0
- data/specs/lib/outputs/stdlib_logger_spec.rb +25 -0
- data/specs/lib/outputs/stdout_spec.rb +17 -0
- metadata +111 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in dlogger.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem 'eventmachine', '~> 1.0.0.beta'
|
7
|
+
gem 'yajl-ruby'
|
8
|
+
|
9
|
+
group(:test) do
|
10
|
+
gem 'schmurfy-bacon', '~> 1.2.1'
|
11
|
+
gem 'mocha', '~> 0.10.0'
|
12
|
+
gem 'simplecov'
|
13
|
+
gem 'schmurfy-em-spec'
|
14
|
+
gem 'guard'
|
15
|
+
gem 'guard-bacon'
|
16
|
+
gem 'rb-fsevent'
|
17
|
+
gem 'growl'
|
18
|
+
end
|
19
|
+
|
data/Guardfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
# parameters:
|
5
|
+
# output => the formatted to use
|
6
|
+
# backtrace => number of lines, nil = everything
|
7
|
+
guard 'bacon', :output => "BetterOutput", :backtrace => 4 do
|
8
|
+
watch(%r{^lib/dlogger/(.+)\.rb$}) { |m| "specs/lib/#{m[1]}_spec.rb" }
|
9
|
+
watch(%r{specs/.+_spec\.rb$})
|
10
|
+
end
|
11
|
+
|
12
|
+
guard 'tilt' do
|
13
|
+
watch(%r{README.md.erb})
|
14
|
+
watch(%r{examples/.*\.rb}){ "README.md.erb" }
|
15
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Julien Ammous
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
# What is this ?
|
2
|
+
|
3
|
+
I was never happy with printf style logs, you rarely know at first what you need to put in them and how to format
|
4
|
+
them so you just drop a ruby logger and log anything. Your log messages quickly become a huge mess unless you
|
5
|
+
revise all of them when the project matures and this is a real pain...
|
6
|
+
|
7
|
+
Since I discovered logstash I started to think about what I would expect from an intelligent ruby logger allowing
|
8
|
+
me to embed the actual context of the log message WITH the message and not inside it so the informations can
|
9
|
+
easily be extracted without stupid operations like regexp matching.
|
10
|
+
|
11
|
+
I first found radar which does something similar but aimed at exception catching then I recently found ruby-cabin,
|
12
|
+
after some considerations I decided to try rolling out my own, I opened a text file and in 10min I had a basic
|
13
|
+
prototype and here we are !
|
14
|
+
|
15
|
+
# Simple example
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
require 'rubygems'
|
19
|
+
require 'bundler/setup'
|
20
|
+
|
21
|
+
require 'dlogger'
|
22
|
+
|
23
|
+
logger = DLogger::Logger.new
|
24
|
+
logger.add_output( DLogger::Output::Stdout.new )
|
25
|
+
|
26
|
+
logger.log("yeah it worked")
|
27
|
+
|
28
|
+
# => yeah it worked : {}
|
29
|
+
```
|
30
|
+
|
31
|
+
|
32
|
+
# Features
|
33
|
+
|
34
|
+
## Contexts
|
35
|
+
|
36
|
+
You can define hash keys and their values, they will be included in any log
|
37
|
+
sent within the block.
|
38
|
+
|
39
|
+
``` ruby
|
40
|
+
require 'rubygems'
|
41
|
+
require 'bundler/setup'
|
42
|
+
|
43
|
+
require 'dlogger'
|
44
|
+
|
45
|
+
logger = DLogger::Logger.new
|
46
|
+
logger.add_output( DLogger::Output::Stdout.new )
|
47
|
+
|
48
|
+
logger.with_context(:user_id => 32) do
|
49
|
+
logger.log("My context is with me")
|
50
|
+
end
|
51
|
+
|
52
|
+
logger.log("but not here")
|
53
|
+
|
54
|
+
# => My context is with me : {:user_id=>32}
|
55
|
+
# => but not here : {}
|
56
|
+
|
57
|
+
```
|
58
|
+
|
59
|
+
## Nested contexts
|
60
|
+
|
61
|
+
You can use nested contexts in this case the inner contexts have a higher priority over
|
62
|
+
the ones above if they define the same keys.
|
63
|
+
|
64
|
+
``` ruby
|
65
|
+
require 'rubygems'
|
66
|
+
require 'bundler/setup'
|
67
|
+
|
68
|
+
require 'dlogger'
|
69
|
+
|
70
|
+
logger = DLogger::Logger.new
|
71
|
+
logger.add_output( DLogger::Output::Stdout.new )
|
72
|
+
|
73
|
+
class TimeExtension < DLogger::Extension
|
74
|
+
def time
|
75
|
+
Time.now
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
logger.with_context(TimeExtension) do
|
80
|
+
logger.with_context(:user_id => 32) do
|
81
|
+
logger.log("My full context is with me")
|
82
|
+
end
|
83
|
+
|
84
|
+
logger.log("I only get the time here")
|
85
|
+
end
|
86
|
+
|
87
|
+
logger.log("but still nothing here")
|
88
|
+
|
89
|
+
# => My full context is with me : {:time=>2012-01-27 11:46:49 +0100, :user_id=>32}
|
90
|
+
# => I only get the time here : {:time=>2012-01-27 11:46:49 +0100}
|
91
|
+
# => but still nothing here : {}
|
92
|
+
|
93
|
+
```
|
94
|
+
|
95
|
+
|
96
|
+
## Dymanic context
|
97
|
+
|
98
|
+
In some cases it may be more handy to use a class rather than a static hash, for
|
99
|
+
example if use the thread local variables to store the user, session or anything.
|
100
|
+
|
101
|
+
``` ruby
|
102
|
+
require 'rubygems'
|
103
|
+
require 'bundler/setup'
|
104
|
+
|
105
|
+
require 'dlogger'
|
106
|
+
|
107
|
+
logger = DLogger::Logger.new
|
108
|
+
logger.add_output( DLogger::Output::Stdout.new )
|
109
|
+
|
110
|
+
# any methods defined in your extension class will become
|
111
|
+
# a key in the resulting hash (except initialize).
|
112
|
+
class UserExtension < DLogger::Extension
|
113
|
+
def user_id
|
114
|
+
rand(43)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
logger.with_context(UserExtension) do
|
119
|
+
logger.log("My context is with me")
|
120
|
+
logger.log("And will change")
|
121
|
+
end
|
122
|
+
|
123
|
+
logger.log("but not here")
|
124
|
+
|
125
|
+
# => My context is with me : {:user_id=>39}
|
126
|
+
# => And will change : {:user_id=>21}
|
127
|
+
# => but not here : {}
|
128
|
+
|
129
|
+
```
|
130
|
+
|
131
|
+
## Threads and Fibers friendly
|
132
|
+
|
133
|
+
Whether you use threads or fibers for concurrency you are good to go, the contexts are
|
134
|
+
store with Thread.current which is local to the current fiber (thread's root fiber if not using
|
135
|
+
Fiber explicitly).
|
136
|
+
|
137
|
+
``` ruby
|
138
|
+
require 'rubygems'
|
139
|
+
require 'bundler/setup'
|
140
|
+
|
141
|
+
require 'dlogger'
|
142
|
+
|
143
|
+
logger = DLogger::Logger.new
|
144
|
+
logger.add_output( DLogger::Output::Stdout.new )
|
145
|
+
|
146
|
+
th1 = Thread.new do
|
147
|
+
logger.with_context(:user_id => "alice", :thread_id => 1) do
|
148
|
+
logger.log("I did it !")
|
149
|
+
sleep 0.2
|
150
|
+
logger.log("completed")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
th2 = Thread.new do
|
155
|
+
logger.with_context(:user_id => "bob", :thread_id => 2) do
|
156
|
+
sleep 0.1
|
157
|
+
logger.log("Done !")
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
[th1, th2].each(&:join)
|
163
|
+
|
164
|
+
# => I did it ! : {:user_id=>"alice", :thread_id=>1}
|
165
|
+
# => Done ! : {:user_id=>"bob", :thread_id=>2}
|
166
|
+
# => completed : {:user_id=>"alice", :thread_id=>1}
|
167
|
+
|
168
|
+
```
|
169
|
+
|
170
|
+
|
171
|
+
# Want to contribute ?
|
172
|
+
|
173
|
+
Fork and clone the repository then:
|
174
|
+
|
175
|
+
``` bash
|
176
|
+
$ bundle
|
177
|
+
$ bundle exec guard
|
178
|
+
```
|
179
|
+
|
180
|
+
After this any change to a file will automatically run the associated spec file, any change
|
181
|
+
to the README.md.erb or any of the examples will trigger a rebuild of the README.md file.
|
182
|
+
|
data/README.md.erb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# What is this ?
|
2
|
+
|
3
|
+
I was never happy with printf style logs, you rarely know at first what you need to put in them and how to format
|
4
|
+
them so you just drop a ruby logger and log anything. Your log messages quickly become a huge mess unless you
|
5
|
+
revise all of them when the project matures and this is a real pain...
|
6
|
+
|
7
|
+
Since I discovered logstash I started to think about what I would expect from an intelligent ruby logger allowing
|
8
|
+
me to embed the actual context of the log message WITH the message and not inside it so the informations can
|
9
|
+
easily be extracted without stupid operations like regexp matching.
|
10
|
+
|
11
|
+
I first found radar which does something similar but aimed at exception catching then I recently found ruby-cabin,
|
12
|
+
after some considerations I decided to try rolling out my own, I opened a text file and in 10min I had a basic
|
13
|
+
prototype and here we are !
|
14
|
+
|
15
|
+
# Simple example
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
<%= File.read('examples/basic.rb') %>
|
19
|
+
|
20
|
+
```
|
21
|
+
|
22
|
+
|
23
|
+
# Features
|
24
|
+
|
25
|
+
## Contexts
|
26
|
+
|
27
|
+
You can define hash keys and their values, they will be included in any log
|
28
|
+
sent within the block.
|
29
|
+
|
30
|
+
``` ruby
|
31
|
+
<%= File.read('examples/hash_context.rb') %>
|
32
|
+
|
33
|
+
```
|
34
|
+
|
35
|
+
## Nested contexts
|
36
|
+
|
37
|
+
You can use nested contexts in this case the inner contexts have a higher priority over
|
38
|
+
the ones above if they define the same keys.
|
39
|
+
|
40
|
+
``` ruby
|
41
|
+
<%= File.read('examples/nested_context.rb') %>
|
42
|
+
|
43
|
+
```
|
44
|
+
|
45
|
+
|
46
|
+
## Dymanic context
|
47
|
+
|
48
|
+
In some cases it may be more handy to use a class rather than a static hash, for
|
49
|
+
example if use the thread local variables to store the user, session or anything.
|
50
|
+
|
51
|
+
``` ruby
|
52
|
+
<%= File.read('examples/dynamic_context.rb') %>
|
53
|
+
|
54
|
+
```
|
55
|
+
|
56
|
+
## Threads and Fibers friendly
|
57
|
+
|
58
|
+
Whether you use threads or fibers for concurrency you are good to go, the contexts are
|
59
|
+
store with Thread.current which is local to the current fiber (thread's root fiber if not using
|
60
|
+
Fiber explicitly).
|
61
|
+
|
62
|
+
``` ruby
|
63
|
+
<%= File.read('examples/threads.rb') %>
|
64
|
+
|
65
|
+
```
|
66
|
+
|
67
|
+
|
68
|
+
# Want to contribute ?
|
69
|
+
|
70
|
+
Fork and clone the repository then:
|
71
|
+
|
72
|
+
``` bash
|
73
|
+
$ bundle
|
74
|
+
$ bundle exec guard
|
75
|
+
```
|
76
|
+
|
77
|
+
After this any change to a file will automatically run the associated spec file, any change
|
78
|
+
to the README.md.erb or any of the examples will trigger a rebuild of the README.md file.
|
79
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
task :readme do
|
7
|
+
require 'tilt'
|
8
|
+
require 'erb'
|
9
|
+
|
10
|
+
Dir.chdir( File.dirname(__FILE__) ) do
|
11
|
+
template = Tilt.new('README.md.erb')
|
12
|
+
File.write('README.md', template.render)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
task :test do
|
18
|
+
require 'bacon'
|
19
|
+
ENV['COVERAGE'] = "1"
|
20
|
+
Dir[File.expand_path('../specs/**/*_spec.rb', __FILE__)].each do |file|
|
21
|
+
load(file)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
data/dlogger.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/dlogger/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Julien Ammous"]
|
6
|
+
gem.email = ["schmurfy@gmail.com"]
|
7
|
+
gem.description = %q{Advanced logger allowing you to include metadata with your messages}
|
8
|
+
gem.summary = %q{Dynamic logger: add a context to your log messages}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "dlogger"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Dlogger::VERSION
|
17
|
+
|
18
|
+
gem.add_development_dependency('rake')
|
19
|
+
gem.add_development_dependency('tilt')
|
20
|
+
gem.add_development_dependency('guard-tilt')
|
21
|
+
end
|
data/examples/basic.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'dlogger'
|
5
|
+
|
6
|
+
logger = DLogger::Logger.new
|
7
|
+
logger.add_output( DLogger::Output::Stdout.new )
|
8
|
+
|
9
|
+
# any methods defined in your extension class will become
|
10
|
+
# a key in the resulting hash (except initialize).
|
11
|
+
class UserExtension < DLogger::Extension
|
12
|
+
def user_id
|
13
|
+
rand(43)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
logger.with_context(UserExtension) do
|
18
|
+
logger.log("My context is with me")
|
19
|
+
logger.log("And will change")
|
20
|
+
end
|
21
|
+
|
22
|
+
logger.log("but not here")
|
23
|
+
|
24
|
+
# => My context is with me : {:user_id=>39}
|
25
|
+
# => And will change : {:user_id=>21}
|
26
|
+
# => but not here : {}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'dlogger'
|
5
|
+
|
6
|
+
logger = DLogger::Logger.new
|
7
|
+
logger.add_output( DLogger::Output::Stdout.new )
|
8
|
+
|
9
|
+
logger.with_context(:user_id => 32) do
|
10
|
+
logger.log("My context is with me")
|
11
|
+
end
|
12
|
+
|
13
|
+
logger.log("but not here")
|
14
|
+
|
15
|
+
# => My context is with me : {:user_id=>32}
|
16
|
+
# => but not here : {}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'yajl'
|
4
|
+
|
5
|
+
require 'bundler/setup'
|
6
|
+
|
7
|
+
require 'dlogger'
|
8
|
+
require 'dlogger/outputs/em/logstash'
|
9
|
+
|
10
|
+
logger = DLogger::Logger.new
|
11
|
+
logger.add_output( DLogger::Output::Stdout.new )
|
12
|
+
|
13
|
+
EM::run do
|
14
|
+
|
15
|
+
logger.add_output( DLogger::Output::LogStash.new('127.0.0.1', 10000) )
|
16
|
+
|
17
|
+
n = 0
|
18
|
+
EM::add_periodic_timer(2) do
|
19
|
+
logger.log("yeah it worked", :n => n+= 1)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'dlogger'
|
5
|
+
|
6
|
+
logger = DLogger::Logger.new
|
7
|
+
logger.add_output( DLogger::Output::Stdout.new )
|
8
|
+
|
9
|
+
class TimeExtension < DLogger::Extension
|
10
|
+
def time
|
11
|
+
Time.now
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
logger.with_context(TimeExtension) do
|
16
|
+
logger.with_context(:user_id => 32) do
|
17
|
+
logger.log("My full context is with me")
|
18
|
+
end
|
19
|
+
|
20
|
+
logger.log("I only get the time here")
|
21
|
+
end
|
22
|
+
|
23
|
+
logger.log("but still nothing here")
|
24
|
+
|
25
|
+
# => My full context is with me : {:time=>2012-01-27 11:46:49 +0100, :user_id=>32}
|
26
|
+
# => I only get the time here : {:time=>2012-01-27 11:46:49 +0100}
|
27
|
+
# => but still nothing here : {}
|
data/examples/threads.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'dlogger'
|
5
|
+
|
6
|
+
logger = DLogger::Logger.new
|
7
|
+
logger.add_output( DLogger::Output::Stdout.new )
|
8
|
+
|
9
|
+
th1 = Thread.new do
|
10
|
+
logger.with_context(:user_id => "alice", :thread_id => 1) do
|
11
|
+
logger.log("I did it !")
|
12
|
+
sleep 0.2
|
13
|
+
logger.log("completed")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
th2 = Thread.new do
|
18
|
+
logger.with_context(:user_id => "bob", :thread_id => 2) do
|
19
|
+
sleep 0.1
|
20
|
+
logger.log("Done !")
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
[th1, th2].each(&:join)
|
26
|
+
|
27
|
+
# => I did it ! : {:user_id=>"alice", :thread_id=>1}
|
28
|
+
# => Done ! : {:user_id=>"bob", :thread_id=>2}
|
29
|
+
# => completed : {:user_id=>"alice", :thread_id=>1}
|
data/lib/dlogger.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module DLogger
|
2
|
+
|
3
|
+
class Extension
|
4
|
+
##
|
5
|
+
# Class attribute, each child class
|
6
|
+
# with have its own.
|
7
|
+
def self.properties
|
8
|
+
@properties ||= []
|
9
|
+
end
|
10
|
+
|
11
|
+
##
|
12
|
+
# Called by ruby each time a new method is added
|
13
|
+
# to this class or any of its children
|
14
|
+
#
|
15
|
+
# @param [Symbol] m method name
|
16
|
+
#
|
17
|
+
def self.method_added(m)
|
18
|
+
unless m == :initialize
|
19
|
+
self.properties << m
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module DLogger
|
4
|
+
class Logger
|
5
|
+
def initialize(name = "_default")
|
6
|
+
@name = name
|
7
|
+
@mutex = Mutex.new
|
8
|
+
@outputs = []
|
9
|
+
|
10
|
+
# initialize context
|
11
|
+
context
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Main entry point, log a message with
|
16
|
+
# its metadata.
|
17
|
+
#
|
18
|
+
# @param [String] msg the message
|
19
|
+
# @param [Hash] metadata Additional data
|
20
|
+
#
|
21
|
+
def log(msg, metadata = {})
|
22
|
+
@mutex.synchronize do
|
23
|
+
# clearing a small hash is slightly faster than creating a new
|
24
|
+
# one each time.
|
25
|
+
merged_metadata.clear
|
26
|
+
|
27
|
+
# first load context data
|
28
|
+
context.each do |ctx_data|
|
29
|
+
case ctx_data
|
30
|
+
when Hash
|
31
|
+
merged_metadata.merge!(ctx_data)
|
32
|
+
|
33
|
+
when Extension
|
34
|
+
ctx_data.class.properties.each do |attr_name|
|
35
|
+
merged_metadata[attr_name] = ctx_data.send(attr_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# then add our data
|
42
|
+
merged_metadata.merge!(metadata)
|
43
|
+
|
44
|
+
# and dispatch the result
|
45
|
+
dispatch(msg, merged_metadata)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Helper methods to mimic the standard ruby logger interface.
|
50
|
+
%w(debug info error warn).each do |level|
|
51
|
+
define_method(level) do |msg, metadata = {}|
|
52
|
+
log(msg, metadata.merge(:severity => level.to_sym))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Register a new output, the only requirement is that
|
58
|
+
# the object passed reponds to the "dispatch" method.
|
59
|
+
#
|
60
|
+
# @param [Object] handler the handler
|
61
|
+
#
|
62
|
+
def add_output(handler)
|
63
|
+
@outputs << handler
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Usually called by #with_context method but you can use
|
68
|
+
# it directly to push a context on the stack, it may be useful
|
69
|
+
# for asynchronous handling or just to push a global context.
|
70
|
+
#
|
71
|
+
# @param [Hash, Dlogger::Extension] context_data additional informations
|
72
|
+
# to include in logs
|
73
|
+
#
|
74
|
+
def push_context(context_data)
|
75
|
+
if context_data.is_a?(Class)
|
76
|
+
context << context_data.new
|
77
|
+
else
|
78
|
+
context << context_data
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# The exact opposite of #push_context, if you called it by hand
|
84
|
+
# you can remove the context for the stack by calling this method.
|
85
|
+
def pop_context
|
86
|
+
context.pop
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# add context data for any log sent within the given block.
|
91
|
+
#
|
92
|
+
# @param [Hash, Dlogger::Extension] context_data additional informations
|
93
|
+
# to include in logs
|
94
|
+
#
|
95
|
+
def with_context(context_data)
|
96
|
+
push_context(context_data)
|
97
|
+
yield
|
98
|
+
ensure
|
99
|
+
# remove context
|
100
|
+
pop_context
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
##
|
107
|
+
# Store the context in fiber local variables, each
|
108
|
+
# Thread/Fiber gets its own.
|
109
|
+
def context
|
110
|
+
Thread.current["#{@name}_dlogger_contexts"] ||= []
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Store the temporary hash used to merge contexts.
|
115
|
+
#
|
116
|
+
def merged_metadata
|
117
|
+
Thread.current["#{@name}_dlogger_merged_metadata"] ||= {}
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Dispatch messages to all registered outputs.
|
122
|
+
#
|
123
|
+
# @param [String] msg the log message
|
124
|
+
# @param [Hash] metadata a hash including all the
|
125
|
+
# additional informations you want to make available
|
126
|
+
#
|
127
|
+
def dispatch(msg, metadata)
|
128
|
+
@outputs.each do |out|
|
129
|
+
out.dispatch(msg, metadata)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
gem 'eventmachine'; require 'eventmachine'
|
2
|
+
gem 'yajl-ruby'; require 'yajl'
|
3
|
+
|
4
|
+
module DLogger
|
5
|
+
module Output
|
6
|
+
|
7
|
+
module LogStashHandler
|
8
|
+
def initialize(master)
|
9
|
+
@master = master
|
10
|
+
end
|
11
|
+
|
12
|
+
def unbind
|
13
|
+
EM::add_timer(0.2){ @master.connect }
|
14
|
+
@master = nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class LogStash < Base
|
19
|
+
def initialize(host, port)
|
20
|
+
@host = host
|
21
|
+
@port = port
|
22
|
+
connect
|
23
|
+
end
|
24
|
+
|
25
|
+
def connect
|
26
|
+
@socket = EM::connect(@host, @port, LogStashHandler, self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def dispatch(msg, metadata)
|
30
|
+
metadata = metadata.merge(:message => msg)
|
31
|
+
@socket.send_data( Yajl::Encoder.encode(metadata) + "\n" )
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module DLogger
|
2
|
+
module Output
|
3
|
+
|
4
|
+
##
|
5
|
+
# Output data to a standard ruby logger.
|
6
|
+
class StdlibLogger < Base
|
7
|
+
|
8
|
+
##
|
9
|
+
# @param [Logger] logger a ruby logger
|
10
|
+
#
|
11
|
+
def initialize(logger, dump_metadata = true)
|
12
|
+
@logger = logger
|
13
|
+
@dump_metadata = dump_metadata
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# @see Logger::dispatch
|
18
|
+
#
|
19
|
+
def dispatch(msg, metadata)
|
20
|
+
severity = metadata.delete(:severity) || :debug
|
21
|
+
msg = @dump_metadata ? "#{msg} : #{metadata.inspect}" : msg
|
22
|
+
@logger.send(severity, msg)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module DLogger
|
4
|
+
module Output
|
5
|
+
|
6
|
+
##
|
7
|
+
# Outputs data to stdout.
|
8
|
+
#
|
9
|
+
class Stdout < Base
|
10
|
+
|
11
|
+
##
|
12
|
+
# @param [Boolean] dump_metadata if true the output will
|
13
|
+
# include the metadata dumped with inspect
|
14
|
+
#
|
15
|
+
def initialize(dump_metadata = true)
|
16
|
+
@dump_metadata = dump_metadata
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# @see Logger::dispatch
|
21
|
+
#
|
22
|
+
def dispatch(msg, metadata)
|
23
|
+
if @dump_metadata
|
24
|
+
Kernel.puts("#{msg} : #{metadata.inspect}")
|
25
|
+
else
|
26
|
+
Kernel.puts(msg)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/specs/common.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
|
2
|
+
require 'rubygems'
|
3
|
+
require 'bundler/setup'
|
4
|
+
|
5
|
+
if (RUBY_VERSION >= "1.9") && ENV['COVERAGE']
|
6
|
+
require 'simplecov'
|
7
|
+
|
8
|
+
puts "[[ SimpleCov enabled ]]"
|
9
|
+
|
10
|
+
SimpleCov.start do
|
11
|
+
add_filter '/specs'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'bacon'
|
16
|
+
require 'mocha'
|
17
|
+
|
18
|
+
$LOAD_PATH.unshift( File.expand_path('../../lib', __FILE__) )
|
19
|
+
require 'dlogger'
|
20
|
+
|
21
|
+
|
22
|
+
Bacon.summary_at_exit
|
23
|
+
|
24
|
+
module Bacon
|
25
|
+
module MochaRequirementsCounter
|
26
|
+
def self.increment
|
27
|
+
Counter[:requirements] += 1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Context
|
32
|
+
include Mocha::API
|
33
|
+
|
34
|
+
alias_method :it_before_mocha, :it
|
35
|
+
|
36
|
+
def it(description)
|
37
|
+
it_before_mocha(description) do
|
38
|
+
# TODO: find better than that...
|
39
|
+
1.should == 1
|
40
|
+
begin
|
41
|
+
mocha_setup
|
42
|
+
yield
|
43
|
+
mocha_verify(MochaRequirementsCounter)
|
44
|
+
rescue Mocha::ExpectationError => e
|
45
|
+
raise Error.new(:failed, "#{e.message}\n#{e.backtrace[0...10].join("\n")}")
|
46
|
+
ensure
|
47
|
+
mocha_teardown
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative '../common'
|
2
|
+
|
3
|
+
describe 'Extension' do
|
4
|
+
should 'record methods added' do
|
5
|
+
ext_class = Class.new(DLogger::Extension) do
|
6
|
+
def user; "user"; end
|
7
|
+
def var; 34; end
|
8
|
+
end
|
9
|
+
|
10
|
+
ext_class.properties.should == [:user, :var]
|
11
|
+
end
|
12
|
+
|
13
|
+
should 'keep separate lists for each subclass' do
|
14
|
+
ext_class1 = Class.new(DLogger::Extension) do
|
15
|
+
def var1; 1; end
|
16
|
+
def var2; 2; end
|
17
|
+
end
|
18
|
+
|
19
|
+
ext_class2 = Class.new(DLogger::Extension) do
|
20
|
+
def var45; 45; end
|
21
|
+
end
|
22
|
+
|
23
|
+
ext_class1.properties.should == [:var1, :var2]
|
24
|
+
ext_class2.properties.should == [:var45]
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative "../common"
|
2
|
+
|
3
|
+
describe "Logger" do
|
4
|
+
before do
|
5
|
+
@logger = DLogger::Logger.new
|
6
|
+
end
|
7
|
+
|
8
|
+
should "dispatch msg with its to registered outputs" do
|
9
|
+
|
10
|
+
handler = mock('Handler')
|
11
|
+
handler.expects(:dispatch).with("the message", :id => 43, :user => 'bob')
|
12
|
+
|
13
|
+
@logger.add_output(handler)
|
14
|
+
|
15
|
+
@logger.log("the message", :id => 43, :user => "bob")
|
16
|
+
end
|
17
|
+
|
18
|
+
should 'mimic standard ruby logger interface' do
|
19
|
+
@logger.expects(:dispatch).with('the message', :severity => :debug)
|
20
|
+
@logger.debug("the message")
|
21
|
+
|
22
|
+
@logger.expects(:dispatch).with('the message', :severity => :info)
|
23
|
+
@logger.info("the message")
|
24
|
+
|
25
|
+
@logger.expects(:dispatch).with('the message', :severity => :warn)
|
26
|
+
@logger.warn("the message")
|
27
|
+
|
28
|
+
@logger.expects(:dispatch).with('the message', :severity => :error)
|
29
|
+
@logger.error("the message")
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'with hash context' do
|
33
|
+
should 'dispatch msg and merged data' do
|
34
|
+
@logger.expects(:dispatch).with('msg', :id => 43, :user => 'bob')
|
35
|
+
|
36
|
+
@logger.with_context(:user => 'bob') do
|
37
|
+
@logger.log('msg', :id => 43)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'with extension context' do
|
44
|
+
should 'dispatch msg and merged data' do
|
45
|
+
ext = Class.new(DLogger::Extension) do
|
46
|
+
def user
|
47
|
+
'alice'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
@logger.expects(:dispatch).with('msg', :id => 56, :user => 'alice')
|
52
|
+
|
53
|
+
@logger.with_context(ext) do
|
54
|
+
@logger.log('msg', :id => 56)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'with multiple contexts' do
|
61
|
+
should 'merge the contexts in defined order (last defined has greater priority)' do
|
62
|
+
@logger.expects(:dispatch).with('msg', :operation => 'destroy', :user => 'billy')
|
63
|
+
|
64
|
+
@logger.with_context(:user => 'bob') do
|
65
|
+
@logger.with_context(:user => 'billy') do
|
66
|
+
@logger.log('msg', :operation => "destroy")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
@logger.expects(:dispatch).with('some text', {})
|
71
|
+
@logger.log('some text')
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative "../../common"
|
2
|
+
|
3
|
+
describe "Stdlib Logger" do
|
4
|
+
before do
|
5
|
+
@out = DLogger::Output::StdlibLogger.new( Logger.new($stdout) )
|
6
|
+
end
|
7
|
+
|
8
|
+
should 'honor severity level' do
|
9
|
+
Logger.any_instance.expects(:info).with('message : {}')
|
10
|
+
@out.dispatch("message", :severity => :info)
|
11
|
+
end
|
12
|
+
|
13
|
+
should 'use debug as default for severity level' do
|
14
|
+
# default = debug
|
15
|
+
Logger.any_instance.expects(:debug).with('message : {}')
|
16
|
+
@out.dispatch("message", {})
|
17
|
+
end
|
18
|
+
|
19
|
+
should 'not log metadata when asked' do
|
20
|
+
out = DLogger::Output::StdlibLogger.new( Logger.new($stdout), false )
|
21
|
+
Logger.any_instance.expects(:debug).with('message')
|
22
|
+
out.dispatch("message", {})
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative "../../common"
|
2
|
+
|
3
|
+
describe "Logger" do
|
4
|
+
|
5
|
+
should 'write messages to stdout' do
|
6
|
+
out = DLogger::Output::Stdout.new
|
7
|
+
Kernel.expects(:puts).with('message : {}')
|
8
|
+
out.dispatch("message", {})
|
9
|
+
end
|
10
|
+
|
11
|
+
should 'write messages to stdout without metadata' do
|
12
|
+
out = DLogger::Output::Stdout.new(false)
|
13
|
+
Kernel.expects(:puts).with('message')
|
14
|
+
out.dispatch("message", {})
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dlogger
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Julien Ammous
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-29 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: &70201269940540 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70201269940540
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: tilt
|
27
|
+
requirement: &70201269939560 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70201269939560
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: guard-tilt
|
38
|
+
requirement: &70201269938640 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70201269938640
|
47
|
+
description: Advanced logger allowing you to include metadata with your messages
|
48
|
+
email:
|
49
|
+
- schmurfy@gmail.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- Gemfile
|
56
|
+
- Guardfile
|
57
|
+
- LICENSE
|
58
|
+
- README.md
|
59
|
+
- README.md.erb
|
60
|
+
- Rakefile
|
61
|
+
- dlogger.gemspec
|
62
|
+
- examples/basic.rb
|
63
|
+
- examples/dynamic_context.rb
|
64
|
+
- examples/hash_context.rb
|
65
|
+
- examples/logstash.rb
|
66
|
+
- examples/nested_context.rb
|
67
|
+
- examples/threads.rb
|
68
|
+
- lib/dlogger.rb
|
69
|
+
- lib/dlogger/extension.rb
|
70
|
+
- lib/dlogger/logger.rb
|
71
|
+
- lib/dlogger/outputs/base.rb
|
72
|
+
- lib/dlogger/outputs/em/logstash.rb
|
73
|
+
- lib/dlogger/outputs/stdlib_logger.rb
|
74
|
+
- lib/dlogger/outputs/stdout.rb
|
75
|
+
- lib/dlogger/version.rb
|
76
|
+
- specs/common.rb
|
77
|
+
- specs/lib/extension_spec.rb
|
78
|
+
- specs/lib/logger_spec.rb
|
79
|
+
- specs/lib/outputs/stdlib_logger_spec.rb
|
80
|
+
- specs/lib/outputs/stdout_spec.rb
|
81
|
+
homepage: ''
|
82
|
+
licenses: []
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
hash: -1333174357779250362
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
segments:
|
103
|
+
- 0
|
104
|
+
hash: -1333174357779250362
|
105
|
+
requirements: []
|
106
|
+
rubyforge_project:
|
107
|
+
rubygems_version: 1.8.15
|
108
|
+
signing_key:
|
109
|
+
specification_version: 3
|
110
|
+
summary: ! 'Dynamic logger: add a context to your log messages'
|
111
|
+
test_files: []
|