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 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 instances that are wrapped smartly to
12
- # log JSON and call the right Logger method (Logger#info, etc).
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
- # Get a channel for a given identifier. If this identifier has never been
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
- :timestamp => Time.now.strftime("%Y-%m-%dT%H:%M:%S.%6N%z")
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 log(level, message, data={})
6
- if message.is_a?(Hash)
7
- data.merge!(message)
8
- else
9
- data[:message] = message
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
- # CAPITALIZE ALL THE STRINGS
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
@@ -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
- log(level, message, data) if send(predicate)
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 log(level, message, data={})
76
+ def log_with_level(level, message, data={})
75
77
  # Invoke 'info?' etc to ask if we should act.
76
- if message.is_a?(Hash)
77
- data.merge!(message)
78
- else
79
- data[:message] = message
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
- private
92
- def debugharder(callstack, data)
93
- path, line, method = callstack[1].split(/(?::in `|:|')/)
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
@@ -3,35 +3,84 @@ require "json"
3
3
 
4
4
  # Wrap IO objects with a reasonable log output.
5
5
  #
6
- # If the IO instance is attached to a TTY, the output will try to be a bit more
7
- # human-friendly in this format:
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 {json data}
9
+ # { "timestamp": ..., "message": message, ... }
10
10
  #
11
- # If the IO instance is not attached to a TTY, the output will be the JSON
12
- # representation of the event:
11
+ # If the IO is attached to a TTY, there are # human-friendly in this format:
13
12
  #
14
- # { "timestamp": ..., "message": message, ... }
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
- public
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 @io.tty?
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.2
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-02-29 00:00:00.000000000 Z
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: &17771920 !ruby/object:Gem::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: *17771920
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