cabin 0.4.2 → 0.4.3
Sign up to get free protection for your applications and to get access to all the features.
- data/examples/sample.rb +2 -4
- data/lib/cabin/channel.rb +47 -50
- data/lib/cabin/mixins/CAPSLOCK.rb +11 -19
- data/lib/cabin/mixins/colors.rb +24 -0
- data/lib/cabin/mixins/logger.rb +29 -11
- data/lib/cabin/mixins/timer.rb +25 -0
- data/lib/cabin/mixins/timestamp.rb +13 -0
- data/lib/cabin/outputs/io.rb +67 -18
- metadata +7 -4
data/examples/sample.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require "rubygems"
|
2
2
|
require "cabin"
|
3
|
-
require "logger"
|
4
3
|
|
5
4
|
# Logging::... is something I'm implemented and experimenting with.
|
6
5
|
@logger = Cabin::Channel.new
|
@@ -8,9 +7,8 @@ require "logger"
|
|
8
7
|
# A logging channel can have any number of subscribers.
|
9
8
|
# Any subscriber is simply expected to respond to '<<' and take a single
|
10
9
|
# argument (the event)
|
11
|
-
# Special case of stdlib Logger
|
12
|
-
|
13
|
-
@logger.subscribe(Logger.new(STDOUT))
|
10
|
+
# Special case handling of stdlib Logger and IO objects comes for free, though.
|
11
|
+
@logger.subscribe(STDOUT)
|
14
12
|
|
15
13
|
# You can store arbitrary key-value pairs in the logging channel.
|
16
14
|
# These are emitted with every event.
|
data/lib/cabin/channel.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require "cabin/mixins/logger"
|
2
|
+
require "cabin/mixins/timestamp"
|
3
|
+
require "cabin/mixins/timer"
|
2
4
|
require "cabin/namespace"
|
3
|
-
require "cabin/timer"
|
4
5
|
require "cabin/context"
|
5
6
|
require "cabin/outputs/stdlib-logger"
|
6
7
|
require "cabin/outputs/io"
|
@@ -45,26 +46,42 @@ require "logger"
|
|
45
46
|
# I, [2011-10-11T01:00:57.993575 #1209] INFO -- : {:timestamp=>"2011-10-11T01:00:57.993517-0700", :message=>"Done in foo", :level=>:info}
|
46
47
|
#
|
47
48
|
class Cabin::Channel
|
49
|
+
class << self
|
50
|
+
# Get a channel for a given identifier. If this identifier has never been
|
51
|
+
# used, a new channel is created for it.
|
52
|
+
# The default identifier is the application executable name.
|
53
|
+
#
|
54
|
+
# This is useful for using the same Cabin::Channel across your
|
55
|
+
# entire application.
|
56
|
+
def get(identifier=$0)
|
57
|
+
@channels ||= Hash.new { |h,k| h[k] = Cabin::Channel.new }
|
58
|
+
return @channels[identifier]
|
59
|
+
end # def Cabin::Channel.get
|
60
|
+
|
61
|
+
# Get a list of filters included in this class.
|
62
|
+
def filters
|
63
|
+
@filters ||= []
|
64
|
+
end # def Cabin::Channel.filters
|
65
|
+
|
66
|
+
# Register a new filter. The block is passed the event. It is expected to
|
67
|
+
# modify that event or otherwise do nothing.
|
68
|
+
def filter(&block)
|
69
|
+
@filters ||= []
|
70
|
+
@filters << block
|
71
|
+
end
|
72
|
+
end # class << self
|
73
|
+
|
48
74
|
include Cabin::Mixins::Logger
|
75
|
+
include Cabin::Mixins::Timestamp
|
76
|
+
include Cabin::Mixins::Timer
|
49
77
|
|
50
78
|
# All channels come with a metrics provider.
|
51
79
|
attr_accessor :metrics
|
52
|
-
|
53
|
-
|
54
|
-
# used, a new channel is created for it.
|
55
|
-
# The default identifier is the application executable name.
|
56
|
-
#
|
57
|
-
# This is useful for using the same Cabin::Channel across your
|
58
|
-
# entire application.
|
59
|
-
public
|
60
|
-
def self.get(identifier=$0)
|
61
|
-
@channels ||= Hash.new { |h,k| h[k] = Cabin::Channel.new }
|
62
|
-
return @channels[identifier]
|
63
|
-
end # def Cabin::Channel.get
|
80
|
+
|
81
|
+
private
|
64
82
|
|
65
83
|
# Create a new logging channel.
|
66
84
|
# The default log level is 'info'
|
67
|
-
public
|
68
85
|
def initialize
|
69
86
|
@outputs = []
|
70
87
|
@data = {}
|
@@ -76,7 +93,6 @@ class Cabin::Channel
|
|
76
93
|
# Subscribe a new input
|
77
94
|
# New events will be sent to the subscriber using the '<<' method
|
78
95
|
# foo << event
|
79
|
-
public
|
80
96
|
def subscribe(output)
|
81
97
|
# Wrap ruby stdlib Logger if given.
|
82
98
|
if output.is_a?(::Logger)
|
@@ -90,19 +106,16 @@ class Cabin::Channel
|
|
90
106
|
end # def subscribe
|
91
107
|
|
92
108
|
# Set some contextual map value
|
93
|
-
public
|
94
109
|
def []=(key, value)
|
95
110
|
@data[key] = value
|
96
111
|
end # def []=
|
97
112
|
|
98
113
|
# Get a context value by name.
|
99
|
-
public
|
100
114
|
def [](key)
|
101
115
|
@data[key]
|
102
116
|
end # def []
|
103
117
|
|
104
118
|
# Remove a context value by name.
|
105
|
-
public
|
106
119
|
def remove(key)
|
107
120
|
@data.delete(key)
|
108
121
|
end # def remove
|
@@ -114,52 +127,36 @@ class Cabin::Channel
|
|
114
127
|
#
|
115
128
|
# A special key :timestamp is set at the time of this method call. The value
|
116
129
|
# is a string ISO8601 timestamp with microsecond precision.
|
117
|
-
public
|
118
130
|
def publish(data)
|
119
|
-
event = {
|
120
|
-
|
121
|
-
|
122
|
-
event.merge!(@data)
|
123
|
-
# TODO(sissel): need to refactor string->hash shoving.
|
131
|
+
event = {}
|
132
|
+
event.merge!(@data) # Merge any logger context
|
133
|
+
|
124
134
|
if data.is_a?(String)
|
125
135
|
event[:message] = data
|
126
136
|
else
|
127
137
|
event.merge!(data)
|
128
138
|
end
|
129
139
|
|
140
|
+
self.class.filters.each do |filter|
|
141
|
+
filter.call(event)
|
142
|
+
end
|
143
|
+
|
130
144
|
@outputs.each do |out|
|
131
145
|
out << event
|
132
146
|
end
|
133
147
|
end # def publish
|
134
148
|
|
135
|
-
# Start timing something.
|
136
|
-
# Returns an instance of Cabin::Timer bound to this Cabin::Channel.
|
137
|
-
# To stop the timer and immediately emit the result to this channel, invoke
|
138
|
-
# the Cabin::Timer#stop method.
|
139
|
-
public
|
140
|
-
def time(data, &block)
|
141
|
-
# TODO(sissel): need to refactor string->hash shoving.
|
142
|
-
if data.is_a?(String)
|
143
|
-
data = { :message => data }
|
144
|
-
end
|
145
|
-
|
146
|
-
timer = Cabin::Timer.new do |duration|
|
147
|
-
# TODO(sissel): Document this field
|
148
|
-
data[:duration] = duration
|
149
|
-
publish(data)
|
150
|
-
end
|
151
|
-
|
152
|
-
if block_given?
|
153
|
-
block.call
|
154
|
-
return timer.stop
|
155
|
-
else
|
156
|
-
return timer
|
157
|
-
end
|
158
|
-
end # def time
|
159
|
-
|
160
|
-
public
|
161
149
|
def context
|
162
150
|
ctx = Cabin::Context.new(self)
|
163
151
|
return ctx
|
164
152
|
end # def context
|
153
|
+
|
154
|
+
def dataify(data)
|
155
|
+
if data.is_a?(String)
|
156
|
+
data = { :message => data }
|
157
|
+
end
|
158
|
+
return data
|
159
|
+
end # def dataify
|
160
|
+
|
161
|
+
public(:initialize, :context, :subscribe, :[]=, :[], :remove, :publish, :time, :context)
|
165
162
|
end # class Cabin::Channel
|
@@ -2,23 +2,15 @@ require "cabin/namespace"
|
|
2
2
|
|
3
3
|
# ALL CAPS MEANS SERIOUS BUSINESS
|
4
4
|
module Cabin::Mixins::CAPSLOCK
|
5
|
-
def
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
def self.extended(instance)
|
6
|
+
self.included(instance.class)
|
7
|
+
end
|
8
|
+
def self.included(klass)
|
9
|
+
klass.filter do |event|
|
10
|
+
# CAPITALIZE ALL THE STRINGS
|
11
|
+
event.each do |key, value|
|
12
|
+
event[key] = value.upcase if value.respond_to?(:upcase)
|
13
|
+
end
|
10
14
|
end
|
11
|
-
|
12
|
-
|
13
|
-
data.each do |key, value|
|
14
|
-
value.upcase! if value.respond_to?(:upcase!)
|
15
|
-
end
|
16
|
-
|
17
|
-
# Add extra debugging bits (file, line, method) if level is debug.
|
18
|
-
debugharder(caller.collect { |c| c.upcase }, data) if @level == :debug
|
19
|
-
|
20
|
-
data[:level] = level.upcase
|
21
|
-
|
22
|
-
publish(data)
|
23
|
-
end # def log
|
24
|
-
end # module Cabin::Mixins::CAPSLOCK
|
15
|
+
end
|
16
|
+
end # MODULE CABIN::MIXINS::CAPSLOCK
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "cabin/namespace"
|
2
|
+
require "cabin/mixins/logger"
|
3
|
+
|
4
|
+
# Colorful logging.
|
5
|
+
module Cabin::Mixins::Colors
|
6
|
+
def included(klass)
|
7
|
+
klass.extend(Cabin::Mixins::Logger)
|
8
|
+
end
|
9
|
+
|
10
|
+
COLORS = [ :black, :red, :green, :yellow, :blue, :magenta, :cyan, :white ]
|
11
|
+
|
12
|
+
COLORS.each do |color|
|
13
|
+
# define the color first
|
14
|
+
define_method(color) do |message, data={}|
|
15
|
+
log(message, data.merge(:color => color))
|
16
|
+
end
|
17
|
+
|
18
|
+
# Exclamation marks mean bold. You should probably use bold all the time
|
19
|
+
# because it's awesome.
|
20
|
+
define_method("#{color}!".to_sym) do |message, data={}|
|
21
|
+
log(message, data.merge(:color => color, :bold => true))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end # module Cabin::Mixins::Colors
|
data/lib/cabin/mixins/logger.rb
CHANGED
@@ -12,6 +12,8 @@ module Cabin::Mixins::Logger
|
|
12
12
|
:debug => 4
|
13
13
|
}
|
14
14
|
|
15
|
+
BACKTRACE_RE = /([^:]+):([0-9]+):in `(.*)'/
|
16
|
+
|
15
17
|
def level=(value)
|
16
18
|
if value.respond_to?(:downcase)
|
17
19
|
@level = value.downcase.to_sym
|
@@ -59,7 +61,7 @@ module Cabin::Mixins::Logger
|
|
59
61
|
"a #{data.class.name}, I require a Hash.")
|
60
62
|
end
|
61
63
|
|
62
|
-
|
64
|
+
log_with_level(level, message, data) if send(predicate)
|
63
65
|
end
|
64
66
|
|
65
67
|
# def info?, def warn? ...
|
@@ -71,26 +73,40 @@ module Cabin::Mixins::Logger
|
|
71
73
|
end # end defining level-based log methods
|
72
74
|
|
73
75
|
private
|
74
|
-
def
|
76
|
+
def log_with_level(level, message, data={})
|
75
77
|
# Invoke 'info?' etc to ask if we should act.
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
78
|
+
data[:level] = level
|
79
|
+
_log(message, data)
|
80
|
+
end # def log_with_level
|
81
|
+
|
82
|
+
def log(message, data={})
|
83
|
+
_log(message, data)
|
84
|
+
end
|
85
|
+
|
86
|
+
def _log(message, data={})
|
87
|
+
case message
|
88
|
+
when Hash
|
89
|
+
data.merge!(message)
|
90
|
+
when Exception
|
91
|
+
# message is an exception
|
92
|
+
data[:message] = message.to_s
|
93
|
+
data[:exception] = message.class
|
94
|
+
data[:backtrace] = message.backtrace
|
95
|
+
else
|
96
|
+
data[:message] = message
|
80
97
|
end
|
81
98
|
|
82
99
|
# Add extra debugging bits (file, line, method) if level is debug.
|
83
|
-
debugharder(caller, data) if @level == :debug
|
100
|
+
debugharder(caller[2], data) if @level == :debug
|
84
101
|
|
85
|
-
data[:level] = level
|
86
102
|
publish(data)
|
87
103
|
end # def log
|
88
104
|
|
89
105
|
# This method is used to pull useful information about the caller
|
90
106
|
# of the logging method such as the caller's file, method, and line number.
|
91
|
-
|
92
|
-
|
93
|
-
path, line, method =
|
107
|
+
def debugharder(callinfo, data)
|
108
|
+
m = BACKTRACE_RE.match(callinfo)
|
109
|
+
path, line, method = m[1..3]
|
94
110
|
whence = $:.detect { |p| path.start_with?(p) }
|
95
111
|
if whence
|
96
112
|
# Remove the RUBYLIB path portion of the full file name
|
@@ -104,4 +120,6 @@ module Cabin::Mixins::Logger
|
|
104
120
|
data[:line] = line
|
105
121
|
data[:method] = method
|
106
122
|
end # def debugharder
|
123
|
+
|
124
|
+
public(:log)
|
107
125
|
end # module Cabin::Mixins::Logger
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "cabin/namespace"
|
2
|
+
require "cabin/timer"
|
3
|
+
|
4
|
+
module Cabin::Mixins::Timer
|
5
|
+
# Start timing something.
|
6
|
+
# Returns an instance of Cabin::Timer bound to this Cabin::Channel.
|
7
|
+
# To stop the timer and immediately emit the result to this channel, invoke
|
8
|
+
# the Cabin::Timer#stop method.
|
9
|
+
def time(data, &block)
|
10
|
+
# TODO(sissel): need to refactor string->hash shoving.
|
11
|
+
data = dataify(data)
|
12
|
+
|
13
|
+
timer = Cabin::Timer.new do |duration|
|
14
|
+
data[:duration] = duration
|
15
|
+
publish(data)
|
16
|
+
end
|
17
|
+
|
18
|
+
if block_given?
|
19
|
+
block.call
|
20
|
+
return timer.stop
|
21
|
+
else
|
22
|
+
return timer
|
23
|
+
end
|
24
|
+
end # def time
|
25
|
+
end # module Cabin::Mixins::Timer
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "cabin/namespace"
|
2
|
+
|
3
|
+
# Timestamp events before publishing.
|
4
|
+
module Cabin::Mixins::Timestamp
|
5
|
+
def self.extended(instance)
|
6
|
+
self.included(instance.class)
|
7
|
+
end
|
8
|
+
def self.included(klass)
|
9
|
+
klass.filter do |event|
|
10
|
+
event[:timestamp] = Time.now.strftime("%Y-%m-%dT%H:%M:%S.%6N%z")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/cabin/outputs/io.rb
CHANGED
@@ -3,35 +3,84 @@ require "json"
|
|
3
3
|
|
4
4
|
# Wrap IO objects with a reasonable log output.
|
5
5
|
#
|
6
|
-
# If the IO
|
7
|
-
#
|
6
|
+
# If the IO is *not* attached to a tty (io#tty? returns false), then
|
7
|
+
# the event will be written in json format terminated by a newline:
|
8
8
|
#
|
9
|
-
# message
|
9
|
+
# { "timestamp": ..., "message": message, ... }
|
10
10
|
#
|
11
|
-
# If the IO
|
12
|
-
# representation of the event:
|
11
|
+
# If the IO is attached to a TTY, there are # human-friendly in this format:
|
13
12
|
#
|
14
|
-
#
|
13
|
+
# message {json data}
|
14
|
+
#
|
15
|
+
# Additionally, colorized output may be available. If the event has :level,
|
16
|
+
# :color, or :bold. Any of the Cabin::Mixins::Logger methods (info, error, etc)
|
17
|
+
# will result in colored output. See the LEVELMAP for the mapping of levels
|
18
|
+
# to colors.
|
15
19
|
class Cabin::Outputs::IO
|
16
|
-
|
20
|
+
# Mapping of color/style names to ANSI control values
|
21
|
+
CODEMAP = {
|
22
|
+
:normal => 0,
|
23
|
+
:bold => 1,
|
24
|
+
:black => 30,
|
25
|
+
:red => 31,
|
26
|
+
:green => 32,
|
27
|
+
:yellow => 33,
|
28
|
+
:blue => 34,
|
29
|
+
:magenta => 35,
|
30
|
+
:cyan => 36,
|
31
|
+
:white => 37
|
32
|
+
}
|
33
|
+
|
34
|
+
# Map of log levels to colors
|
35
|
+
LEVELMAP = {
|
36
|
+
:fatal => :red,
|
37
|
+
:error => :red,
|
38
|
+
:warn => :yellow,
|
39
|
+
:info => :green, # default color
|
40
|
+
:debug => :cyan,
|
41
|
+
}
|
42
|
+
|
17
43
|
def initialize(io)
|
18
44
|
@io = io
|
19
45
|
end # def initialize
|
20
46
|
|
21
47
|
# Receive an event
|
22
|
-
public
|
23
48
|
def <<(event)
|
24
|
-
if
|
25
|
-
data = event.clone
|
26
|
-
# delete things from the 'data' portion that's not really data.
|
27
|
-
data.delete(:message)
|
28
|
-
data.delete(:timestamp)
|
29
|
-
message = "#{event[:message]} #{data.to_json}"
|
30
|
-
|
31
|
-
@io.puts(message)
|
32
|
-
@io.flush if @io.tty?
|
33
|
-
else
|
49
|
+
if !@io.tty?
|
34
50
|
@io.puts(event.to_json)
|
51
|
+
else
|
52
|
+
tty_write(event)
|
35
53
|
end
|
36
54
|
end # def <<
|
55
|
+
|
56
|
+
private
|
57
|
+
def tty_write(event)
|
58
|
+
# The io is attached to a tty, so make pretty colors.
|
59
|
+
# delete things from the 'data' portion that's not really data.
|
60
|
+
data = event.clone
|
61
|
+
data.delete(:message)
|
62
|
+
data.delete(:timestamp)
|
63
|
+
|
64
|
+
color = data.delete(:color)
|
65
|
+
# :bold is expected to be truthy
|
66
|
+
bold = data.delete(:bold) ? :bold : nil
|
67
|
+
|
68
|
+
# Make 'error' and other log levels have color
|
69
|
+
if color.nil? and data[:level]
|
70
|
+
color = LEVELMAP[data[:level]]
|
71
|
+
end
|
72
|
+
|
73
|
+
if data.empty?
|
74
|
+
message = [event[:message]]
|
75
|
+
else
|
76
|
+
message = ["#{event[:message]} #{data.to_json}"]
|
77
|
+
end
|
78
|
+
message.unshift("\e[#{CODEMAP[color.to_sym]}m") if !color.nil?
|
79
|
+
message.unshift("\e[#{CODEMAP[bold]}m") if !bold.nil?
|
80
|
+
message.push("\e[#{CODEMAP[:normal]}m") if !(bold.nil? and color.nil?)
|
81
|
+
@io.puts(message.join(""))
|
82
|
+
@io.flush
|
83
|
+
end # def <<
|
84
|
+
|
85
|
+
public(:initialize, :<<)
|
37
86
|
end # class Cabin::Outputs::StdlibLogger
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cabin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-03-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: json
|
16
|
-
requirement: &
|
16
|
+
requirement: &17116580 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,7 +21,7 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *17116580
|
25
25
|
description: This is an experiment to try and make logging more flexible and more
|
26
26
|
consumable. Plain text logs are bullshit, let's emit structured and contextual logs.
|
27
27
|
Metrics, too!
|
@@ -40,8 +40,11 @@ files:
|
|
40
40
|
- lib/cabin/publisher.rb
|
41
41
|
- lib/cabin/channel.rb
|
42
42
|
- lib/cabin/mixins/logger.rb
|
43
|
+
- lib/cabin/mixins/colors.rb
|
43
44
|
- lib/cabin/mixins/dragons.rb
|
44
45
|
- lib/cabin/mixins/CAPSLOCK.rb
|
46
|
+
- lib/cabin/mixins/timer.rb
|
47
|
+
- lib/cabin/mixins/timestamp.rb
|
45
48
|
- lib/cabin/timer.rb
|
46
49
|
- lib/cabin/metric.rb
|
47
50
|
- lib/cabin/metrics.rb
|