cabin 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/examples/sample.rb +48 -0
- data/examples/sinatra-logging.rb +27 -0
- data/lib/cabin.rb +1 -0
- data/lib/cabin/channel.rb +142 -0
- data/lib/cabin/context.rb +45 -0
- data/lib/cabin/logger.rb +29 -0
- data/lib/cabin/namespace.rb +3 -0
- data/lib/cabin/outputs/stdlib-logger.rb +20 -0
- data/lib/cabin/timer.rb +20 -0
- data/test/minitest-patch.rb +13 -0
- data/test/test_logging.rb +101 -0
- metadata +70 -0
data/examples/sample.rb
ADDED
@@ -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
|
data/lib/cabin/logger.rb
ADDED
@@ -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,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
|
data/lib/cabin/timer.rb
ADDED
@@ -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: []
|