cabin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []