cabin 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
1
+ require "rubygems"
2
+ require "cabin"
3
+ require "logger"
4
+
5
+ # Logging::... is something I'm implemented and experimenting with.
6
+ @logger = Cabin::Channel.new
7
+
8
+ # A logging channel can have any number of subscribers.
9
+ # Any subscriber is simply expected to respond to '<<' and take a single
10
+ # argument (the event)
11
+ # Special case of stdlib Logger instances that are wrapped smartly to
12
+ # log JSON and call the right Logger method (Logger#info, etc).
13
+ @logger.subscribe(Logger.new(STDOUT))
14
+
15
+ # You can store arbitrary key-value pairs in the logging channel.
16
+ # These are emitted with every event.
17
+ @logger[:program] = "sample program"
18
+
19
+ def foo(val)
20
+ # A context is something that lets you modify key-value pieces in the
21
+ # logging channel and gives you a trivial way to undo the changes later.
22
+ context = @logger.context()
23
+ context[:foo] = val
24
+ context[:example] = 100
25
+
26
+ # The point of the context above is to save context so that the bar() method
27
+ # and it's logging efforts can include said context.
28
+ timer = @logger.time("Timing bar")
29
+ bar()
30
+ timer.stop # logs the result.
31
+
32
+ @logger.time("Another bar timer") do
33
+ bar()
34
+ end
35
+
36
+ # Clearing this context will exactly undo the changes made to the logger by
37
+ # this context.
38
+ context.clear()
39
+ end
40
+
41
+ def bar
42
+ @logger.info("bar bar bar!")
43
+ sleep(rand * 2)
44
+ end
45
+
46
+ foo("Hello")
47
+ @logger.info("All done.")
48
+
@@ -0,0 +1,27 @@
1
+ require "rubygems"
2
+ require "sinatra"
3
+ $: << "./lib"
4
+ require "cabin"
5
+ require "logger"
6
+
7
+ $logger = Cabin::Channel.new
8
+ $logger.subscribe(Cabin.new(STDOUT))
9
+
10
+ def serve_it_up(arg)
11
+ $logger.info("Serving it up")
12
+ sleep 2
13
+ "Hello, #{arg}!"
14
+ end
15
+
16
+ get "/hello/:name" do
17
+ context = $logger.context
18
+ context[:name] = params[:name]
19
+ context[:verb] = "GET"
20
+ timer = $logger.time("serve_it_up latency")
21
+ result = serve_it_up(params[:name])
22
+ timer.stop
23
+
24
+ # Clear the context so that the next request doesn't have tainted context.
25
+ context.clear
26
+ return result
27
+ end
data/lib/cabin.rb ADDED
@@ -0,0 +1 @@
1
+ require "cabin/channel"
@@ -0,0 +1,142 @@
1
+ require "cabin/logger"
2
+ require "cabin/namespace"
3
+ require "cabin/timer"
4
+ require "cabin/context"
5
+ require "cabin/outputs/stdlib-logger"
6
+ require "logger"
7
+
8
+ # A wonderful channel for logging.
9
+ #
10
+ # You can log normal messages through here, but you should be really
11
+ # shipping structured data. A message is just part of your data.
12
+ # "An error occurred" - in what? when? why? how?
13
+ #
14
+ # Logging channels support the usual 'info' 'warn' and other logger methods
15
+ # provided by Ruby's stdlib Logger class
16
+ #
17
+ # It additionally allows you to store arbitrary pieces of data in it like a
18
+ # hash, so your call stack can do be this:
19
+ #
20
+ # @logger = Cabin::Channel.new
21
+ # rubylog = Logger.new(STDOUT) # ruby's stlib logger
22
+ # @logger.subscribe(rubylog)
23
+ #
24
+ # def foo(val)
25
+ # context = @logger.context()
26
+ # context[:foo] = val
27
+ # context[:example] = 100
28
+ # bar()
29
+ #
30
+ # # Clear any context we just wanted bar() to know about
31
+ # context.clear()
32
+ #
33
+ # @logger.info("Done in foo")
34
+ # end
35
+ #
36
+ # def bar
37
+ # @logger.info("Fizzle")
38
+ # end
39
+ #
40
+ # The result:
41
+ #
42
+ # I, [2011-10-11T01:00:57.993200 #1209] INFO -- : {:timestamp=>"2011-10-11T01:00:57.992353-0700", :foo=>"Hello", :example=>100, :message=>"Fizzle", :level=>:info}
43
+ # I, [2011-10-11T01:00:57.993575 #1209] INFO -- : {:timestamp=>"2011-10-11T01:00:57.993517-0700", :message=>"Done in foo", :level=>:info}
44
+ #
45
+ class Cabin::Channel
46
+ include Cabin::Logger
47
+
48
+ # Create a new logging channel.
49
+ # The default log level is 'info'
50
+ public
51
+ def initialize
52
+ @outputs = []
53
+ @data = {}
54
+ @level = :info
55
+ end # def initialize
56
+
57
+ # Subscribe a new input
58
+ public
59
+ def subscribe(output)
60
+ # Wrap ruby stdlib Logger if given.
61
+ if output.is_a?(::Logger)
62
+ output = Cabin::Outputs::StdlibLogger.new(output)
63
+ end
64
+ @outputs << output
65
+ # TODO(sissel): Return a method or object that allows you to easily
66
+ # unsubscribe?
67
+ end # def subscribe
68
+
69
+ # Set some contextual map value
70
+ public
71
+ def []=(key, value)
72
+ @data[key] = value
73
+ end # def []=
74
+
75
+ # Get a context value by name.
76
+ public
77
+ def [](key)
78
+ @data[key]
79
+ end # def []
80
+
81
+ # Remove a context value by name.
82
+ public
83
+ def remove(key)
84
+ @data.delete(key)
85
+ end # def remove
86
+
87
+ # Publish data to all outputs. The data is expected to be a hash or a string.
88
+ #
89
+ # A new hash is generated based on the data given. If data is a string, then
90
+ # it will be added to the new event hash with key :message.
91
+ #
92
+ # A special key :timestamp is set at the time of this method call. The value
93
+ # is a string ISO8601 timestamp with microsecond precision.
94
+ public
95
+ def publish(data)
96
+ event = {
97
+ :timestamp => Time.now.strftime("%Y-%m-%dT%H:%M:%S.%6N%z")
98
+ }
99
+ event.merge!(@data)
100
+ # TODO(sissel): need to refactor string->hash shoving.
101
+ if data.is_a?(String)
102
+ event[:message] = data
103
+ else
104
+ event.merge!(data)
105
+ end
106
+
107
+ @outputs.each do |out|
108
+ out << event
109
+ end
110
+ end # def publish
111
+
112
+ # Start timing something.
113
+ # Returns an instance of Cabin::Timer bound to this Cabin::Channel.
114
+ # To stop the timer and immediately emit the result to this channel, invoke
115
+ # the Cabin::Timer#stop method.
116
+ public
117
+ def time(data, &block)
118
+ # TODO(sissel): need to refactor string->hash shoving.
119
+ if data.is_a?(String)
120
+ data = { :message => data }
121
+ end
122
+
123
+ timer = Cabin::Timer.new do |duration|
124
+ # TODO(sissel): Document this field
125
+ data[:duration] = duration
126
+ publish(data)
127
+ end
128
+
129
+ if block_given?
130
+ block.call
131
+ return timer.stop
132
+ else
133
+ return timer
134
+ end
135
+ end # def time
136
+
137
+ public
138
+ def context
139
+ ctx = Cabin::Context.new(self)
140
+ return ctx
141
+ end # def context
142
+ end # class Cabin::Channel
@@ -0,0 +1,45 @@
1
+ require "cabin/namespace"
2
+
3
+ # Logging context exists to make it easy to add and later undo any changes made
4
+ # to the context data associated with a given Logging::Channel
5
+ #
6
+ # Usage:
7
+ #
8
+ # context = channel.context
9
+ # context["foo"] = "Hello world!"
10
+ # channel.info("Sample log") # output includes { "foo" => "Hello world!" }
11
+ # context.clear
12
+ # channel.info("Sample log 2") # context cleared, key "foo" removed.
13
+ #
14
+ class Cabin::Context
15
+ def initialize(channel)
16
+ @changes = []
17
+ @channel = channel
18
+ end # def initialize
19
+
20
+ def on_clear(&block)
21
+ @clear_callback = block
22
+ end # def on_clear
23
+
24
+ def []=(key, value)
25
+ # Maintain a record of what was changed so clear() can undo this context.
26
+ # This record is in reverse order so it can be undone in reverse later.
27
+ @changes.unshift([key, value, @channel[key]])
28
+ @channel[key] = value
29
+ end # def []=
30
+
31
+ def [](key)
32
+ @channel[key]
33
+ end # def []
34
+
35
+ # Undo any changes made to the channel by this context.
36
+ def clear
37
+ @changes.each do |key, value, original|
38
+ if original.nil?
39
+ @channel.remove(key)
40
+ else
41
+ @channel[key] = original
42
+ end
43
+ end
44
+ end # def clear
45
+ end # class Cabin::Context
@@ -0,0 +1,29 @@
1
+ require "cabin/namespace"
2
+
3
+ module Cabin::Logger
4
+ attr_accessor :level
5
+ LEVELS = {
6
+ :fatal => 0,
7
+ :error => 1,
8
+ :warn => 2,
9
+ :info => 3,
10
+ :debug => 4
11
+ }
12
+
13
+ # Define the usual log methods: info, fatal, etc.
14
+ # Each level-based method accepts both a message and a hash data.
15
+ %w(fatal error warn info debug).each do |level|
16
+ level = level.to_sym
17
+ # def info, def warn, etc...
18
+ define_method(level) do |message, data={}|
19
+ next unless LEVELS[@level] >= LEVELS[level]
20
+ if message.is_a?(Hash)
21
+ data.merge!(message)
22
+ else
23
+ data[:message] = message
24
+ end
25
+ data[:level] = level
26
+ publish(data)
27
+ end
28
+ end # end defining level-based log methods
29
+ end # module Cabin::Logger
@@ -0,0 +1,3 @@
1
+ module Cabin
2
+ module Outputs; end
3
+ end
@@ -0,0 +1,20 @@
1
+ require "cabin"
2
+ require "json"
3
+
4
+ # Wrap Ruby stdlib's logger. This allows you to output to a normal ruby logger
5
+ # with Cabin. Since Ruby's Logger has a love for strings alone, this
6
+ # wrapper will convert the data/event to json before sending it to Logger.
7
+ class Cabin::Outputs::StdlibLogger
8
+ public
9
+ def initialize(logger)
10
+ @logger = logger
11
+ end # def initialize
12
+
13
+ # Receive an event
14
+ public
15
+ def <<(data)
16
+ method = data[:level] || "info"
17
+ # This will call @logger.info(data) or something similar.
18
+ @logger.send(method, data.to_json)
19
+ end # def <<
20
+ end # class Cabin::Outputs::StdlibLogger
@@ -0,0 +1,20 @@
1
+ require "cabin/namespace"
2
+
3
+ # A simple timer class for timing events like a stop watch. Normally you don't
4
+ # invoke this yourself, but you are welcome to do so.
5
+ #
6
+ # See also: Cabin::Channel#time
7
+ class Cabin::Timer
8
+ def initialize(&block)
9
+ @start = Time.now
10
+ @callback = block if block_given?
11
+ end # def initialize
12
+
13
+ # Stop the clock and call the callback with the duration.
14
+ # Also returns the duration of this timer.
15
+ def stop
16
+ duration = Time.now - @start
17
+ @callback.call(duration) if @callback
18
+ return duration
19
+ end # def stop
20
+ end # class Cabin::Timer
@@ -0,0 +1,13 @@
1
+ require "rubygems"
2
+ require "minitest/spec"
3
+ # XXX: This code stolen from logstash's test bits.
4
+
5
+ # I don't really like monkeypatching, but whatever, this is probably better
6
+ # than overriding the 'describe' method.
7
+ class MiniTest::Spec
8
+ class << self
9
+ # 'it' sounds wrong, call it 'test'
10
+ alias :test :it
11
+ end
12
+ end
13
+
@@ -0,0 +1,101 @@
1
+ $: << File.dirname(__FILE__)
2
+ $: << File.join(File.dirname(__FILE__), "..", "lib")
3
+
4
+ require "rubygems"
5
+ require "minitest-patch"
6
+ require "cabin"
7
+ require "stringio"
8
+ require "minitest/autorun" if __FILE__ == $0
9
+
10
+ describe Cabin::Channel do
11
+ class Receiver
12
+ attr_accessor :data
13
+
14
+ public
15
+ def initialize
16
+ @data = []
17
+ end
18
+
19
+ public
20
+ def <<(data)
21
+ @data << data
22
+ end
23
+ end # class Receiver
24
+
25
+ before do
26
+ @logger = Cabin::Channel.new
27
+ @target = Receiver.new
28
+ @logger.subscribe(@target)
29
+ end
30
+
31
+ test "simple string publishing" do
32
+ @logger.publish("Hello world")
33
+ assert_equal(1, @target.data.length)
34
+ assert_equal("Hello world", @target.data[0][:message])
35
+ end
36
+
37
+ test "simple context data" do
38
+ @logger[:foo] = "bar"
39
+ @logger.publish("Hello world")
40
+ assert_equal(1, @target.data.length)
41
+ assert_equal("Hello world", @target.data[0][:message])
42
+ assert_equal("bar", @target.data[0][:foo])
43
+ end
44
+
45
+ test "time something" do
46
+ timer = @logger.time("some sample")
47
+ timer.stop
48
+
49
+ event = @target.data[0]
50
+ assert_equal("some sample", event[:message])
51
+ assert(event[:duration].is_a?(Numeric))
52
+ end
53
+
54
+ test "double subscription" do
55
+ @logger.subscribe(@target)
56
+ @logger.publish("Hello world")
57
+ assert_equal(2, @target.data.length)
58
+ assert_equal("Hello world", @target.data[0][:message])
59
+ assert_equal("Hello world", @target.data[1][:message])
60
+ end
61
+
62
+ test "context values" do
63
+ context = @logger.context
64
+ context["foo"] = "hello"
65
+ @logger.publish("testing")
66
+ assert_equal(1, @target.data.length)
67
+ assert_equal("hello", @target.data[0]["foo"])
68
+ assert_equal("testing", @target.data[0][:message])
69
+ end
70
+
71
+ test "context values clear properly" do
72
+ context = @logger.context
73
+ context["foo"] = "hello"
74
+ context.clear
75
+ @logger.publish("testing")
76
+ assert_equal(1, @target.data.length)
77
+ assert(!@target.data[0].has_key?("foo"))
78
+ assert_equal("testing", @target.data[0][:message])
79
+ end
80
+
81
+ %w(fatal error warn info debug).each do |level|
82
+ level = level.to_sym
83
+ test "standard use case, '#{level}' logging when enabled" do
84
+ @logger.level = level
85
+ @logger.send(level, "Hello world")
86
+ event = @target.data[0]
87
+ assert_equal("Hello world", event[:message])
88
+ assert_equal(level, event[:level])
89
+ end
90
+ end
91
+
92
+ %w(error warn info debug).each do |level|
93
+ level = level.to_sym
94
+ test "standard use case, '#{level}' logging when wrong level" do
95
+ @logger.level = :fatal
96
+ # Should not log since log level is :fatal and we are above that.
97
+ @logger.send(level, "Hello world")
98
+ assert_equal(0, @target.data.length)
99
+ end
100
+ end
101
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cabin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jordan Sissel
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-11 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: &10650540 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *10650540
25
+ description: This is an experiment to try and make logging more flexible and more
26
+ consumable. Plain text logs are bullshit, let's emit structured and contextual logs.
27
+ email:
28
+ - jls@semicomplete.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/cabin.rb
34
+ - lib/cabin/outputs/stdlib-logger.rb
35
+ - lib/cabin/channel.rb
36
+ - lib/cabin/namespace.rb
37
+ - lib/cabin/timer.rb
38
+ - lib/cabin/context.rb
39
+ - lib/cabin/logger.rb
40
+ - examples/sinatra-logging.rb
41
+ - examples/sample.rb
42
+ - test/minitest-patch.rb
43
+ - test/test_logging.rb
44
+ homepage: https://github.com/jordansissel/ruby-cabin
45
+ licenses:
46
+ - Apache License (2.0)
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 1.8.10
67
+ signing_key:
68
+ specification_version: 3
69
+ summary: Experiments in structured and contextual logging
70
+ test_files: []