[DRE-commits] [ruby-cabin] 01/06: Imported Upstream version 0.6.1

Tim Potter tpot-guest at moszumanska.debian.org
Tue Aug 5 01:13:25 UTC 2014


This is an automated email from the git hooks/post-receive script.

tpot-guest pushed a commit to branch master
in repository ruby-cabin.

commit 61976b0fcf7453dc3d136de3073f39056dbbf6e3
Author: Tim Potter <tpot at hp.com>
Date:   Fri Jul 25 15:25:58 2014 +1000

    Imported Upstream version 0.6.1
---
 CHANGELIST                            |  16 +++
 LICENSE                               |  14 +++
 bin/rubygems-cabin-test               |   5 +
 examples/fibonacci-timing.rb          |  28 +++++
 examples/metrics.rb                   |  41 ++++++++
 examples/pipe-spoon.rb                |  25 +++++
 examples/pipe.rb                      |  14 +++
 examples/sample.rb                    |  46 ++++++++
 examples/sinatra-logging.rb           |  27 +++++
 lib/cabin.rb                          |   1 +
 lib/cabin/channel.rb                  | 191 ++++++++++++++++++++++++++++++++++
 lib/cabin/context.rb                  |  50 +++++++++
 lib/cabin/inspectable.rb              |  56 ++++++++++
 lib/cabin/metric.rb                   |  22 ++++
 lib/cabin/metrics.rb                  | 124 ++++++++++++++++++++++
 lib/cabin/metrics/counter.rb          |  42 ++++++++
 lib/cabin/metrics/gauge.rb            |  24 +++++
 lib/cabin/metrics/histogram.rb        |  66 ++++++++++++
 lib/cabin/metrics/meter.rb            |  39 +++++++
 lib/cabin/metrics/timer.rb            |  39 +++++++
 lib/cabin/mixins/CAPSLOCK.rb          |  16 +++
 lib/cabin/mixins/colors.rb            |  24 +++++
 lib/cabin/mixins/dragons.rb           |  34 ++++++
 lib/cabin/mixins/logger.rb            | 126 ++++++++++++++++++++++
 lib/cabin/mixins/pipe.rb              |  50 +++++++++
 lib/cabin/mixins/timer.rb             |  25 +++++
 lib/cabin/mixins/timestamp.rb         |  13 +++
 lib/cabin/namespace.rb                |   8 ++
 lib/cabin/outputs/em/stdlib-logger.rb |  42 ++++++++
 lib/cabin/outputs/io.rb               |  89 ++++++++++++++++
 lib/cabin/outputs/stdlib-logger.rb    |  33 ++++++
 lib/cabin/outputs/zeromq.rb           |  96 +++++++++++++++++
 lib/cabin/publisher.rb                |  20 ++++
 lib/cabin/timer.rb                    |  20 ++++
 metadata.yml                          | 106 +++++++++++++++++++
 test/all.rb                           |  17 +++
 test/minitest-patch.rb                |  13 +++
 test/test_logging.rb                  | 152 +++++++++++++++++++++++++++
 test/test_metrics.rb                  |  97 +++++++++++++++++
 test/test_zeromq.rb                   |  99 ++++++++++++++++++
 40 files changed, 1950 insertions(+)

diff --git a/CHANGELIST b/CHANGELIST
new file mode 100644
index 0000000..353709c
--- /dev/null
+++ b/CHANGELIST
@@ -0,0 +1,16 @@
+v0.1.7 (2011-11-07)
+  - Add support for ruby 1.8
+
+v0.1.6 (2011-11-07)
+  - Add 'Dragons' mixin for times when info/debug/fatal log levels don't cut it
+    and you'd rather deal in D&D alignments instead.
+  - Add Cabin::Outputs::EmStdlibLogger which wraps ruby's stdlib logger but in
+    a way smartly usable with EventMachine. (Contributed by Sean Porter)
+  - Moved mixins to Cabin::Mixins like Logger and the new Dragon mixin.
+
+v0.1.3 through v0.1.5 were not documented here. Sad.
+
+v0.1.2 (2011-10-12)
+  - Added file+line+method context if the logger level is debug
+
+!!date +\%Y-\%m-\%d
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c98d54c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,14 @@
+Copyright 2011 Jordan Sissel
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
diff --git a/bin/rubygems-cabin-test b/bin/rubygems-cabin-test
new file mode 100755
index 0000000..37f3af2
--- /dev/null
+++ b/bin/rubygems-cabin-test
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+
+require "minitest/autorun"
+test_dir = File.join(File.dirname(__FILE__), "..", "test")
+load File.join(test_dir, "test_logging.rb")
diff --git a/examples/fibonacci-timing.rb b/examples/fibonacci-timing.rb
new file mode 100644
index 0000000..574862b
--- /dev/null
+++ b/examples/fibonacci-timing.rb
@@ -0,0 +1,28 @@
+require "rubygems"
+require "cabin"
+require "logger"
+
+def fib(n)
+  return 1 if n < 2
+  return fib(n - 1) + fib(n - 2)
+end
+
+# Logging::... is something I'm implemented and experimenting with.
+ at logger = Cabin::Channel.new
+
+# A logging channel can have any number of subscribers.
+# Any subscriber is simply expected to respond to '<<' and take a single
+# argument (the event)
+# Special case of stdlib Logger instances that are wrapped smartly to 
+# log JSON and call the right Logger method (Logger#info, etc).
+ at logger.subscribe(Logger.new(STDOUT))
+
+# You can store arbitrary key-value pairs in the logging channel. 
+# These are emitted with every event.
+
+n = 35
+ at logger[:input] = n
+ at logger.time("fibonacci latency") do
+  fib(n)
+end
+
diff --git a/examples/metrics.rb b/examples/metrics.rb
new file mode 100644
index 0000000..363ca39
--- /dev/null
+++ b/examples/metrics.rb
@@ -0,0 +1,41 @@
+require "rubygems"
+require "cabin"
+require "logger"
+
+# Logging::... is something I'm implemented and experimenting with.
+ at logger = Cabin::Channel.new
+
+# Metrics can be subscribed-to as well.
+ at logger.subscribe(Logger.new(STDOUT))
+
+counter = @logger.metrics.counter("mycounter")
+counter.incr
+counter.incr
+counter.incr
+counter.decr
+
+meter = @logger.metrics.meter("something", "hello-world")
+meter.mark
+meter.mark
+meter.mark
+
+# If nil is passed as the 'instance' then the metric class name will be
+# used instead.
+timer = @logger.metrics.timer("ticktock")
+5.times do
+  timer.time do
+    sleep rand * 2
+  end
+end
+
+3.times do
+  # Another way to do timing.
+  clock = timer.time
+  sleep rand * 2
+  clock.stop
+end
+
+# Loop through all metrics:
+ at logger.metrics.each do |metric|
+  @logger.info(metric.inspect)
+end
diff --git a/examples/pipe-spoon.rb b/examples/pipe-spoon.rb
new file mode 100644
index 0000000..6c16cf7
--- /dev/null
+++ b/examples/pipe-spoon.rb
@@ -0,0 +1,25 @@
+require "cabin"
+require "spoon"
+
+
+logger = Cabin::Channel.get
+logger.subscribe(STDOUT)
+logger.level = :info
+
+fileactions = Spoon::FileActions.new
+fileactions.close(0)
+cor, cow = IO.pipe # child stdout 
+cer, cew = IO.pipe # child stderr
+fileactions.dup2(cow.fileno, 1)
+fileactions.dup2(cew.fileno, 2)
+
+spawn_attr = Spoon::SpawnAttributes.new
+pid = Spoon.posix_spawn("/bin/sh", fileactions, spawn_attr, ["sh", "-c"] + ARGV)
+puts pid
+cow.close
+cew.close
+
+logger.pipe(cor => :info, cer => :error)
+pid, status = Process.waitpid2(pid)
+
+puts status.inspect
diff --git a/examples/pipe.rb b/examples/pipe.rb
new file mode 100644
index 0000000..bc28e5c
--- /dev/null
+++ b/examples/pipe.rb
@@ -0,0 +1,14 @@
+require "cabin"
+require "open4"
+
+cmd = 'strace -e trace=write date'
+
+logger = Cabin::Channel.get
+logger.subscribe(STDOUT)
+logger.level = :info
+
+status = Open4::popen4(cmd) do |pid, stdin, stdout, stderr|
+  stdin.close
+  logger.pipe(stdout => :info, stderr => :error)
+end
+
diff --git a/examples/sample.rb b/examples/sample.rb
new file mode 100644
index 0000000..88b02ca
--- /dev/null
+++ b/examples/sample.rb
@@ -0,0 +1,46 @@
+require "rubygems"
+require "cabin"
+
+# Logging::... is something I'm implemented and experimenting with.
+ at logger = Cabin::Channel.new
+
+# A logging channel can have any number of subscribers.
+# Any subscriber is simply expected to respond to '<<' and take a single
+# argument (the event)
+# Special case handling of stdlib Logger and IO objects comes for free, though.
+ at logger.subscribe(STDOUT)
+
+# You can store arbitrary key-value pairs in the logging channel. 
+# These are emitted with every event.
+ at logger[:program] = "sample program"
+
+def foo(val)
+  # A context is something that lets you modify key-value pieces in the
+  # logging channel and gives you a trivial way to undo the changes later.
+  context = @logger.context()
+  context[:foo] = val
+  context[:example] = 100
+
+  # The point of the context above is to save context so that the bar() method 
+  # and it's logging efforts can include said context.
+  timer = @logger.time("Timing bar")
+  bar()
+  timer.stop   # logs the result.
+
+  @logger.time("Another bar timer") do
+    bar()
+  end
+
+  # Clearing this context will exactly undo the changes made to the logger by
+  # this context.
+  context.clear()
+end
+
+def bar
+  @logger.info("bar bar bar!")
+  sleep(rand * 2)
+end
+
+foo("Hello")
+ at logger.info("All done.")
+
diff --git a/examples/sinatra-logging.rb b/examples/sinatra-logging.rb
new file mode 100644
index 0000000..a56e80b
--- /dev/null
+++ b/examples/sinatra-logging.rb
@@ -0,0 +1,27 @@
+require "rubygems"
+require "sinatra"
+$: << "./lib"
+require "cabin"
+require "logger"
+
+$logger = Cabin::Channel.new
+$logger.subscribe(Logger.new(STDOUT))
+
+def serve_it_up(arg)
+  $logger.info("Serving it up")
+  sleep 2
+  "Hello, #{arg}!"
+end
+
+get "/hello/:name" do
+  context = $logger.context
+  context[:name] = params[:name]
+  context[:verb] = "GET"
+  timer = $logger.time("serve_it_up latency")
+  result = serve_it_up(params[:name])
+  timer.stop
+
+  # Clear the context so that the next request doesn't have tainted context.
+  context.clear
+  return result
+end
diff --git a/lib/cabin.rb b/lib/cabin.rb
new file mode 100644
index 0000000..ca6b2b1
--- /dev/null
+++ b/lib/cabin.rb
@@ -0,0 +1 @@
+require "cabin/channel"
diff --git a/lib/cabin/channel.rb b/lib/cabin/channel.rb
new file mode 100644
index 0000000..0340958
--- /dev/null
+++ b/lib/cabin/channel.rb
@@ -0,0 +1,191 @@
+require "cabin/mixins/logger"
+require "cabin/mixins/pipe"
+require "cabin/mixins/timestamp"
+require "cabin/mixins/timer"
+require "cabin/namespace"
+require "cabin/context"
+require "cabin/outputs/stdlib-logger"
+require "cabin/outputs/io"
+require "cabin/metrics"
+require "logger" # stdlib
+require "thread"
+
+# A wonderful channel for logging.
+#
+# You can log normal messages through here, but you should be really
+# shipping structured data. A message is just part of your data.
+# "An error occurred" - in what? when? why? how?
+#
+# Logging channels support the usual 'info' 'warn' and other logger methods
+# provided by Ruby's stdlib Logger class
+#
+# It additionally allows you to store arbitrary pieces of data in it like a
+# hash, so your call stack can do be this:
+#
+#     @logger = Cabin::Channel.new
+#     rubylog = Logger.new(STDOUT) # ruby's stlib logger
+#     @logger.subscribe(rubylog)
+#
+#     def foo(val)
+#       context = @logger.context()
+#       context[:foo] = val
+#       context[:example] = 100
+#       bar()
+#
+#       # Clear any context we just wanted bar() to know about
+#       context.clear()
+#
+#       @logger.info("Done in foo")
+#     end
+#
+#     def bar
+#       @logger.info("Fizzle")
+#     end
+#
+# The result:
+#
+#     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}
+#     I, [2011-10-11T01:00:57.993575 #1209]  INFO -- : {:timestamp=>"2011-10-11T01:00:57.993517-0700", :message=>"Done in foo", :level=>:info}
+#
+class Cabin::Channel
+  @channel_lock = Mutex.new
+  @channels = Hash.new { |h,k| h[k] = Cabin::Channel.new }
+
+  class << self
+    # Get a channel for a given identifier. If this identifier has never been
+    # used, a new channel is created for it.
+    # The default identifier is the application executable name.
+    #
+    # This is useful for using the same Cabin::Channel across your
+    # entire application.
+    def get(identifier=$0)
+      return @channel_lock.synchronize { @channels[identifier] }
+    end # def Cabin::Channel.get
+
+    def set(identifier, channel)
+      return @channel_lock.synchronize { @channels[identifier] = channel }
+    end # def Cabin::Channel.set
+
+    def each(&block)
+      @channel_lock.synchronize do
+        @channels.each do |identifier, channel|
+          yield identifier, channel
+        end
+      end
+    end # def Cabin::Channel.each
+
+    # Get a list of filters included in this class.
+    def filters
+      @filters ||= []
+    end # def Cabin::Channel.filters
+    
+    # Register a new filter. The block is passed the event. It is expected to
+    # modify that event or otherwise do nothing.
+    def filter(&block)
+      @filters ||= []
+      @filters << block
+    end
+  end # class << self
+
+  include Cabin::Mixins::Timestamp
+  include Cabin::Mixins::Logger
+  include Cabin::Mixins::Pipe
+  include Cabin::Mixins::Timer
+
+  # All channels come with a metrics provider.
+  attr_accessor :metrics
+  
+  private
+
+  # Create a new logging channel.
+  # The default log level is 'info'
+  def initialize
+    @subscribers = {}
+    @data = {}
+    @level = :info
+    @metrics = Cabin::Metrics.new
+    @metrics.channel = self
+    @subscriber_lock = Mutex.new
+  end # def initialize
+
+  # Subscribe a new input
+  # New events will be sent to the subscriber using the '<<' method
+  #   foo << event
+  #
+  # Returns a subscription id you can use later to unsubscribe
+  def subscribe(output)
+    # Wrap ruby stdlib Logger if given.
+    if output.is_a?(::Logger)
+      output = Cabin::Outputs::StdlibLogger.new(output)
+    elsif output.is_a?(::IO)
+      output = Cabin::Outputs::IO.new(output)
+    end
+    @subscriber_lock.synchronize do
+      @subscribers[output.object_id] = output
+    end
+    return output.object_id
+  end # def subscribe
+
+  # Unsubscribe. Takes a 'subscription id' as returned by the subscribe method
+  def unsubscribe(id)
+    @subscriber_lock.synchronize do
+      @subscribers.delete(id)
+    end
+  end # def unsubscribe
+ 
+  # Set some contextual map value
+  def []=(key, value)
+    @data[key] = value
+  end # def []= 
+
+  # Get a context value by name.
+  def [](key)
+    @data[key]
+  end # def []
+
+  # Remove a context value by name.
+  def remove(key)
+    @data.delete(key)
+  end # def remove
+
+  # Publish data to all outputs. The data is expected to be a hash or a string. 
+  #
+  # A new hash is generated based on the data given. If data is a string, then
+  # it will be added to the new event hash with key :message.
+  #
+  # A special key :timestamp is set at the time of this method call. The value
+  # is a string ISO8601 timestamp with microsecond precision.
+  def publish(data)
+    event = {}
+    self.class.filters.each do |filter|
+      filter.call(event)
+    end
+
+    if data.is_a?(String)
+      event[:message] = data
+    else
+      event.merge!(data)
+    end
+    event.merge!(@data) # Merge any logger context
+
+    @subscriber_lock.synchronize do
+      @subscribers.each do |id, output|
+        output << event
+      end
+    end
+  end # def publish
+
+  def context
+    ctx = Cabin::Context.new(self)
+    return ctx
+  end # def context
+
+  def dataify(data)
+    if data.is_a?(String)
+      data = { :message => data }
+    end
+    return data
+  end # def dataify
+
+  public(:initialize, :context, :subscribe, :unsubscribe, :[]=, :[], :remove, :publish, :time, :context)
+end # class Cabin::Channel
diff --git a/lib/cabin/context.rb b/lib/cabin/context.rb
new file mode 100644
index 0000000..40d4ee6
--- /dev/null
+++ b/lib/cabin/context.rb
@@ -0,0 +1,50 @@
+require "cabin/namespace"
+
+# Logging context exists to make it easy to add and later undo any changes made
+# to the context data associated with a given Logging::Channel
+# 
+# Usage:
+#
+#     context = channel.context
+#     context["foo"] = "Hello world!"
+#     channel.info("Sample log") # output includes { "foo" => "Hello world!" }
+#     context.clear
+#     channel.info("Sample log 2") # context cleared, key "foo" removed.
+#
+# Essentially, a Cabin::Context acts as a transactional proxy on top of a
+# Cabin::Channel. Any changes you make in a context are passed through to
+# the channel while keeping an ordered record of the changes made so
+# you can unroll those changes when the context is no longer needed..
+# 
+class Cabin::Context
+  def initialize(channel)
+    @changes = []
+    @channel = channel
+  end # def initialize
+
+  def on_clear(&block)
+    @clear_callback = block
+  end # def on_clear
+
+  def []=(key, value)
+    # Maintain a record of what was changed so clear() can undo this context.
+    # This record is in reverse order so it can be undone in reverse later.
+    @changes.unshift([key, value, @channel[key]])
+    @channel[key] = value
+  end # def []=
+
+  def [](key)
+    @channel[key]
+  end # def []
+  
+  # Undo any changes made to the channel by this context.
+  def clear
+    @changes.each do |key, value, original|
+      if original.nil?
+        @channel.remove(key)
+      else
+        @channel[key] = original
+      end
+    end
+  end # def clear
+end # class Cabin::Context
diff --git a/lib/cabin/inspectable.rb b/lib/cabin/inspectable.rb
new file mode 100644
index 0000000..86bdaea
--- /dev/null
+++ b/lib/cabin/inspectable.rb
@@ -0,0 +1,56 @@
+require "cabin/namespace"
+
+module Cabin
+  module Inspectable
+    # Provide a saner inspect method that's easier to configure.
+    #
+    # By default, will inspect all instance variables. You can tune
+    # this by setting @inspectables to an array of ivar symbols, like:
+    #     [ :@hello, :@world ]
+    #
+    #     class Foo
+    #       include Cabin::Inspectable
+    #
+    #       def initialize
+    #         @inspectables = [:@foo, :@bar]
+    #         @foo = 123
+    #         @bar = "hello"
+    #         @baz = "ok"
+    #       end
+    #     end
+    #
+    #     foo = Foo.new
+    #     foo.inspect == '<Foo(1) @foo=123 @bar="hello" >'
+    def inspect
+      if instance_variable_defined?(:@inspectables)
+        ivars = @inspectables
+      else
+        ivars = instance_variables
+      end
+      str = "<#{self.class.name}(@#{self.object_id}) "
+      ivars.each do |ivar|
+        str << "#{ivar}=#{instance_variable_get(ivar).inspect} "
+      end
+      str << ">"
+      return str
+    end
+  end
+
+  def self.__Inspectable(*ivars)
+    mod = Module.new
+    mod.instance_eval do
+      define_method(:inspect) do
+        ivars = instance_variables if ivars.empty?
+        str = "<#{self.class.name}(@#{self.object_id}) "
+        ivars.each do |ivar|
+          str << "#{ivar}=#{instance_variable_get(ivar).inspect} "
+        end
+        str << ">"
+        return str
+      end
+    end
+    return mod
+  end # def Cabin.Inspectable
+end # module Cabin
+
+
diff --git a/lib/cabin/metric.rb b/lib/cabin/metric.rb
new file mode 100644
index 0000000..a921c63
--- /dev/null
+++ b/lib/cabin/metric.rb
@@ -0,0 +1,22 @@
+require "cabin/namespace"
+require "cabin/publisher"
+require "cabin/inspectable"
+
+module Cabin::Metric
+  include Cabin::Inspectable
+  include Cabin::Publisher
+
+  def instance=(instance)
+    @instance = instance
+  end # def instance=
+
+  def instance
+    return @instance
+  end # def instance
+
+  def emit
+    if !@channel.nil?
+      @channel.publish({ :metric => instance }.merge(to_hash))
+    end
+  end # def emit
+end # module Cabin::Metric
diff --git a/lib/cabin/metrics.rb b/lib/cabin/metrics.rb
new file mode 100644
index 0000000..d210ad2
--- /dev/null
+++ b/lib/cabin/metrics.rb
@@ -0,0 +1,124 @@
+require "cabin/namespace"
+require "cabin/metrics/gauge"
+require "cabin/metrics/meter"
+require "cabin/metrics/counter"
+require "cabin/metrics/timer"
+require "cabin/metrics/histogram"
+require "cabin/publisher"
+require "cabin/channel"
+
+# What type of metrics do we want?
+#
+# What metrics should come by default?
+# Per-call/transaction/request metrics like:
+#   - hit (count++ type metrics)
+#   - latencies/timings
+#
+# Per app or generally long-lifetime metrics like:
+#   - "uptime"
+#   - cpu usage
+#   - memory usage
+#   - count of active/in-flight actions/requests/calls/transactions
+#   - peer metrics (number of cluster members, etc)
+# ------------------------------------------------------------------
+# https://github.com/codahale/metrics/tree/master/metrics-core/src/main/java/com/yammer/metrics/core
+# Reading what Coda Hale's "Metrics" stuff has, here's my summary:
+#
+#   gauges (callback to return a number)
+#   counters (.inc and .dec methods)
+#   meters (.mark to track each 'hit')
+#     Also exposes 1, 5, 15 minute moving averages
+#   histograms: (.update(value) to record a new value)
+#     like meter, but takes values more than simply '1'
+#     as a result, exposes percentiles, median, etc.
+#   timers
+#     a time-observing interface on top of histogram.
+#
+# With the exception of gauges, all the other metrics are all active/pushed.
+# Gauges take callbacks, so their values are pulled, not pushed. The active
+# metrics can be represented as events since they the update occurs at the
+# time of the change.
+#
+# These active/push metrics can therefore be considered events.
+#
+# All metrics (active/passive) can be queried for 'current state', too,
+# making this suitable for serving to interested parties like monitoring
+# and management tools.
+class Cabin::Metrics
+  include Enumerable
+  include Cabin::Publisher
+
+  # Get us a new metrics container.
+  public
+  def initialize
+    @metrics_lock = Mutex.new
+    @metrics = {}
+  end # def initialize
+
+  private
+  def create(instance, name, metric_object)
+    if !instance.is_a?(String)
+      instance = "#{instance.class.name}<#{instance.object_id}>"
+    end
+
+    if name.nil?
+      # If no name is given, use the class name of the metric.
+      # For example, if we invoke Metrics#timer("foo"), the metric
+      # name will be "foo/timer"
+      metric_name = "#{instance}/#{metric_object.class.name.split("::").last.downcase}"
+    else
+      # Otherwise, use "instance/name" as the name.
+      metric_name = "#{instance}/#{name}"
+    end
+
+    metric_object.channel = @channel
+    metric_object.instance = metric_name
+
+    if @channel
+      @channel.debug("Created metric", :instance => instance, :type => metric_object.class)
+    end
+    return @metrics_lock.synchronize do
+      @metrics[metric_name] = metric_object
+    end
+  end # def create
+
+  # Create a new Counter metric
+  # 'instance' is usually an object owning this metric, but it can be a string.
+  # 'name' is the name of the metric.
+  public
+  def counter(instance, name=nil)
+    return create(instance, name, Cabin::Metrics::Counter.new)
+  end # def counter
+
+  # Create a new Meter metric
+  # 'instance' is usually an object owning this metric, but it can be a string.
+  # 'name' is the name of the metric.
+  public
+  def meter(instance, name=nil)
+    return create(instance, name, Cabin::Metrics::Meter.new)
+  end # def meter
+
+  # Create a new Histogram metric
+  # 'instance' is usually an object owning this metric, but it can be a string.
+  # 'name' is the name of the metric.
+  public
+  def histogram(instance, name=nil)
+    return create(instance, name, Cabin::Metrics::Histogram.new)
+  end # def histogram
+
+  # Create a new Timer metric
+  # 'instance' is usually an object owning this metric, but it can be a string.
+  # 'name' is the name of the metric.
+  public
+  def timer(instance, name=nil)
+    return create(instance, name, Cabin::Metrics::Timer.new)
+  end # def timer
+  
+  # iterate over each metric. yields identifer, metric
+  def each(&block)
+    # delegate to the @metrics hash until we need something fancier
+    @metrics_lock.synchronize do
+      @metrics.each(&block)
+    end
+  end # def each
+end # class Cabin::Metrics
diff --git a/lib/cabin/metrics/counter.rb b/lib/cabin/metrics/counter.rb
new file mode 100644
index 0000000..780dfd0
--- /dev/null
+++ b/lib/cabin/metrics/counter.rb
@@ -0,0 +1,42 @@
+require "cabin/namespace"
+require "cabin/metric"
+require "thread"
+
+class Cabin::Metrics::Counter
+  include Cabin::Metric
+
+  # A new Counter. 
+  #
+  # Counters can be incremented and decremented only by 1 at a time..
+  public
+  def initialize
+    @inspectables = [ :@value ]
+    @value = 0
+    @lock = Mutex.new
+  end # def initialize
+
+  # increment this counter
+  def incr
+    @lock.synchronize { @value += 1 }
+    emit
+  end # def incr
+
+  # decrement this counter
+  def decr
+    @lock.synchronize { @value -= 1 }
+    emit
+  end # def decr
+
+  # Get the value of this metric.
+  public
+  def value
+    return @lock.synchronize { @value }
+  end # def value
+
+  public
+  def to_hash
+    return @lock.synchronize do
+      { :value => @value }
+    end
+  end # def to_hash
+end # class Cabin::Metrics::Counter
diff --git a/lib/cabin/metrics/gauge.rb b/lib/cabin/metrics/gauge.rb
new file mode 100644
index 0000000..f2a03f3
--- /dev/null
+++ b/lib/cabin/metrics/gauge.rb
@@ -0,0 +1,24 @@
+require "cabin/namespace"
+require "cabin/metric"
+
+class Cabin::Metrics::Gauge
+  include Cabin::Metric
+
+  # A new Gauge. The block given will be called every time the metric is read.
+  public
+  def initialize(&block)
+    @inspectables = [ ]
+    @block = block
+  end # def initialize
+
+  # Get the value of this metric.
+  public
+  def value
+    return @block.call
+  end # def value
+
+  public
+  def to_hash
+    return { :value => value }
+  end # def to_hash
+end # class Cabin::Metrics::Gauge
diff --git a/lib/cabin/metrics/histogram.rb b/lib/cabin/metrics/histogram.rb
new file mode 100644
index 0000000..6687654
--- /dev/null
+++ b/lib/cabin/metrics/histogram.rb
@@ -0,0 +1,66 @@
+require "cabin/namespace"
+require "cabin/metric"
+require "thread"
+
+class Cabin::Metrics::Histogram
+  include Cabin::Metric
+
+  # A new Histogram. 
+  public
+  def initialize
+    @lock = Mutex.new
+    @inspectables = [ :@total, :@min, :@max, :@count, :@mean ]
+
+    # Histogram should track many things, including:
+    # - percentiles (50, 75, 90, 95, 99?)
+    # - median
+    # - max
+    # - min
+    # - total sum
+    #
+    # Sliding values of all of these?
+    @total = 0
+    @min = nil
+    @max = nil
+    @count = 0
+    @mean = 0.0
+  end # def initialize
+
+  public
+  def record(value)
+    @lock.synchronize do
+      @count += 1
+      @total += value
+      if @min.nil? or value < @min
+        @min = value
+      end
+      if @max.nil? or value > @max
+        @max = value
+      end
+      @mean = @total / @count
+      # TODO(sissel): median
+      # TODO(sissel): percentiles
+    end
+    emit
+  end # def record
+
+  # This is a very poor way to access the metric data.
+  # TODO(sissel): Need to figure out a better interface.
+  public
+  def value
+    return @lock.synchronize { @count }
+  end # def value
+
+  public
+  def to_hash
+    return @lock.synchronize do
+      { 
+        :count => @count,
+        :total => @total,
+        :min => @min,
+        :max => @max,
+        :mean => @mean,
+      }
+    end
+  end # def to_hash
+end # class Cabin::Metrics::Histogram
diff --git a/lib/cabin/metrics/meter.rb b/lib/cabin/metrics/meter.rb
new file mode 100644
index 0000000..9156e8a
--- /dev/null
+++ b/lib/cabin/metrics/meter.rb
@@ -0,0 +1,39 @@
+require "cabin/namespace"
+require "cabin/metric"
+require "thread"
+
+class Cabin::Metrics::Meter
+  include Cabin::Metric
+
+  # A new Meter 
+  #
+  # Counters can be incremented and decremented only by 1 at a time..
+  public
+  def initialize
+    @inspectables = [ :@value ]
+    @value = 0
+    @lock = Mutex.new
+  end # def initialize
+
+  # Mark an event
+  def mark
+    @lock.synchronize do
+      @value += 1
+      # TODO(sissel): Keep some moving averages?
+    end
+    emit
+  end # def mark
+
+  # Get the value of this metric.
+  public
+  def value
+    return @lock.synchronize { @value }
+  end # def value
+
+  public
+  def to_hash
+    return @lock.synchronize do
+      { :value => @value }
+    end
+  end # def to_hash
+end # class Cabin::Metrics::Meter
diff --git a/lib/cabin/metrics/timer.rb b/lib/cabin/metrics/timer.rb
new file mode 100644
index 0000000..82988fd
--- /dev/null
+++ b/lib/cabin/metrics/timer.rb
@@ -0,0 +1,39 @@
+require "cabin/namespace"
+require "cabin/metrics/histogram"
+
+class Cabin::Metrics::Timer < Cabin::Metrics::Histogram
+  # Start timing something.
+  #
+  # If no block is given
+  # If a block is given, the execution of that block is timed.
+  #
+  public
+  def time(&block)
+    return time_block(&block) if block_given?
+
+    # Return an object we can .stop
+    # Call record(...) when we stop.
+    return TimerContext.new { |duration| record(duration) }
+  end # def time
+
+  private
+  def time_block(&block)
+    start = Time.now
+    block.call
+    record(Time.now - start)
+  end # def time_block
+
+  class TimerContext
+    public
+    def initialize(&stop_callback)
+      @start = Time.now
+      @callback = stop_callback
+    end
+
+    public
+    def stop
+      duration = Time.now - @start
+      @callback.call(duration)
+    end # def stop
+  end # class TimerContext
+end # class Cabin::Metrics::Timer
diff --git a/lib/cabin/mixins/CAPSLOCK.rb b/lib/cabin/mixins/CAPSLOCK.rb
new file mode 100644
index 0000000..b4344a7
--- /dev/null
+++ b/lib/cabin/mixins/CAPSLOCK.rb
@@ -0,0 +1,16 @@
+require "cabin/namespace"
+
+# ALL CAPS MEANS SERIOUS BUSINESS
+module Cabin::Mixins::CAPSLOCK
+  def self.extended(instance)
+    self.included(instance.class)
+  end
+  def self.included(klass)
+    klass.filter do |event|
+      # CAPITALIZE ALL THE STRINGS
+      event.each do |key, value|
+        event[key] = value.upcase if value.respond_to?(:upcase)
+      end
+    end
+  end
+end # MODULE CABIN::MIXINS::CAPSLOCK
diff --git a/lib/cabin/mixins/colors.rb b/lib/cabin/mixins/colors.rb
new file mode 100644
index 0000000..d9317cb
--- /dev/null
+++ b/lib/cabin/mixins/colors.rb
@@ -0,0 +1,24 @@
+require "cabin/namespace"
+require "cabin/mixins/logger"
+
+# Colorful logging.
+module Cabin::Mixins::Colors
+  def included(klass)
+    klass.extend(Cabin::Mixins::Logger)
+  end
+
+  COLORS = [ :black, :red, :green, :yellow, :blue, :magenta, :cyan, :white ]
+
+  COLORS.each do |color|
+    # define the color first
+    define_method(color) do |message, data={}|
+      log(message, data.merge(:color => color))
+    end
+
+    # Exclamation marks mean bold. You should probably use bold all the time
+    # because it's awesome.
+    define_method("#{color}!".to_sym) do |message, data={}|
+      log(message, data.merge(:color => color, :bold => true))
+    end
+  end
+end # module Cabin::Mixins::Colors
diff --git a/lib/cabin/mixins/dragons.rb b/lib/cabin/mixins/dragons.rb
new file mode 100644
index 0000000..b6da4a2
--- /dev/null
+++ b/lib/cabin/mixins/dragons.rb
@@ -0,0 +1,34 @@
+require "cabin/namespace"
+
+# An experiment to use AD&D-style log levels, because why not? 'info' and
+# 'fatal' and other log levels are pretty lame anyway.
+#
+# Plus, now you can 'include Dragons' in your logger, which means it
+# has +2 against Knights and a special fire breathing attack..
+module Cabin::Mixins::Dragons
+  orders = %w(lawful ambivalent chaotic)
+  desires = %w(good neutral evil)
+
+  orders.each do |order|
+    desires.each do |desire|
+      # alignment will be like 'lawful_good' etc
+      alignment = "#{order} #{desire}"
+      define_method(alignment.gsub(" ", "_")) do |message, data={}|
+        log(alignment, message, data)
+      end
+    end # desires
+  end # orders
+
+  private
+  def log(alignment, message, data={})
+    # Invoke 'info?' etc to ask if we should act.
+    if message.is_a?(Hash)
+      data.merge!(message)
+    else
+      data[:message] = message
+    end
+
+    data[:alignment] = alignment
+    publish(data)
+  end # def log
+end # module Cabin::Dragons
diff --git a/lib/cabin/mixins/logger.rb b/lib/cabin/mixins/logger.rb
new file mode 100644
index 0000000..8e6271b
--- /dev/null
+++ b/lib/cabin/mixins/logger.rb
@@ -0,0 +1,126 @@
+require "cabin/namespace"
+
+# This module implements methods that act somewhat like Ruby's Logger class
+# It is included in Cabin::Channel
+module Cabin::Mixins::Logger
+  attr_accessor :level
+  LEVELS = {
+    :fatal => 0,
+    :error => 1,
+    :warn => 2,
+    :info => 3,
+    :debug => 4
+  }
+
+  BACKTRACE_RE = /([^:]+):([0-9]+)(?::in `(.*)')?/
+
+  def level=(value)
+    if value.respond_to?(:downcase)
+      @level = value.downcase.to_sym
+    else
+      @level = value.to_sym
+    end
+  end # def level
+
+  # Define the usual log methods: info, fatal, etc.
+  # Each level-based method accepts both a message and a hash data.
+  #
+  # This will define methods such as 'fatal' and 'fatal?' for each
+  # of: fatal, error, warn, info, debug
+  #
+  # The first method type (ie Cabin::Channel#fatal) is what logs, and it takes a
+  # message and an optional Hash with context.
+  #
+  # The second method type (ie; Cabin::Channel#fatal?) returns true if
+  # fatal logs are being emitted, false otherwise.
+  %w(fatal error warn info debug).each do |level|
+    level = level.to_sym
+    predicate = "#{level}?".to_sym
+
+    # def info, def warn, etc...
+
+    # Arguments: message, data, data is assumed to be {} if nil
+    # This hack is necessary because ruby 1.8 doesn't support default arguments
+    # on blocks.
+    define_method(level) do |*args| #|message, data={}|
+      if args.size < 1
+        raise ::ArgumentError.new("#{self.class.name}##{level}(message, " \
+                                  "data={}) requires at least 1 argument")
+      end
+      if args.size > 2
+        raise ::ArgumentError.new("#{self.class.name}##{level}(message, " \
+                                  "data={}) takes at most 2 arguments")
+      end
+
+      message = args[0]
+      data = args[1] || {}
+
+      if not data.is_a?(Hash)
+        raise ::ArgumentError.new("#{self.class.name}##{level}(message, " \
+                                  "data={}) - second argument you gave me was" \
+                                  "a #{data.class.name}, I require a Hash.")
+      end
+
+      log_with_level(level, message, data) if send(predicate)
+    end
+
+    # def info?, def warn? ...
+    # these methods return true if the loglevel allows that level of log.
+    define_method(predicate) do 
+      @level ||= :info
+      return LEVELS[@level] >= LEVELS[level]
+    end # def info?, def warn? ...
+  end # end defining level-based log methods
+
+  private
+  def log_with_level(level, message, data={})
+    # Invoke 'info?' etc to ask if we should act.
+    data[:level] = level
+    _log(message, data)
+  end # def log_with_level
+
+  def log(message, data={})
+    _log(message, data)
+  end
+
+  def _log(message, data={})
+    case message
+      when Hash
+        data.merge!(message)
+      when Exception
+        # message is an exception
+        data[:message] = message.to_s
+        data[:exception] = message.class
+        data[:backtrace] = message.backtrace
+      else
+        data = { :message => message }.merge(data)
+    end
+
+    # Add extra debugging bits (file, line, method) if level is debug.
+    debugharder(caller[2], data) if @level == :debug
+
+    publish(data)
+  end # def log
+
+  # This method is used to pull useful information about the caller
+  # of the logging method such as the caller's file, method, and line number.
+  def debugharder(callinfo, data)
+    m = BACKTRACE_RE.match(callinfo)
+    return unless m
+    path, line, method = m[1..3]
+    whence = $:.detect { |p| path.start_with?(p) }
+    if whence
+      # Remove the RUBYLIB path portion of the full file name 
+      file = path[whence.length + 1..-1]
+    else
+      # We get here if the path is not in $:
+      file = path
+    end
+    
+    data[:file] = file
+    data[:line] = line
+    data[:method] = method if data[:method]
+  end # def debugharder
+
+  public(:log)
+end # module Cabin::Mixins::Logger
diff --git a/lib/cabin/mixins/pipe.rb b/lib/cabin/mixins/pipe.rb
new file mode 100644
index 0000000..5ff0f82
--- /dev/null
+++ b/lib/cabin/mixins/pipe.rb
@@ -0,0 +1,50 @@
+require "cabin/namespace"
+
+# This module provides a 'pipe' method which instructs cabin to pipe anything
+# read from the IO given to be logged.
+module Cabin::Mixins::Pipe
+
+  # Pipe IO objects to method calls on a logger.
+  #
+  # The argument is a hash of IO to method symbols. 
+  #
+  #     logger.pipe(io => :the_method)
+  #
+  # For each line read from 'io', logger.the_method(the_line) will be called.
+  #
+  # Example:
+  #
+  #     cmd = "strace -e trace=write date"
+  #     Open4::popen4(cmd) do |pid, stdin, stdout, stderr|
+  #       stdin.close
+  #
+  #       # Make lines from stdout be logged as 'info'
+  #       # Make lines from stderr be logged as 'error'
+  #       logger.pipe(stdout => :info, stderr => :error)
+  #     end
+  #
+  # Output:
+  #
+  #     write(1, "Fri Jan 11 22:49:42 PST 2013\n", 29) = 29 {"level":"error"}
+  #     Fri Jan 11 22:49:42 PST 2013 {"level":"info"}
+  #     +++ exited with 0 +++ {"level":"error"}
+  def pipe(io_to_method_map)
+    fds = io_to_method_map.keys
+
+    while !fds.empty?
+      readers, _, _ = IO.select(fds, nil, nil, nil)
+      readers.each do |fd|
+        begin
+          line = fd.readline.chomp
+        rescue EOFError
+          fd.close rescue nil
+          fds.delete(fd)
+          next
+        end
+
+        method_name = io_to_method_map[fd]
+        send(method_name, line)
+      end # readers.each
+    end # while !fds.empty?
+  end # def pipe
+end # module Cabin::Mixins::Logger
diff --git a/lib/cabin/mixins/timer.rb b/lib/cabin/mixins/timer.rb
new file mode 100644
index 0000000..9875149
--- /dev/null
+++ b/lib/cabin/mixins/timer.rb
@@ -0,0 +1,25 @@
+require "cabin/namespace"
+require "cabin/timer"
+
+module Cabin::Mixins::Timer
+  # Start timing something.
+  # Returns an instance of Cabin::Timer bound to this Cabin::Channel.
+  # To stop the timer and immediately emit the result to this channel, invoke
+  # the Cabin::Timer#stop method.
+  def time(data, &block)
+    # TODO(sissel): need to refactor string->hash shoving.
+    data = dataify(data)
+
+    timer = Cabin::Timer.new do |duration|
+      data[:duration] = duration
+      publish(data)
+    end
+
+    if block_given?
+      block.call
+      return timer.stop
+    else
+      return timer
+    end
+  end # def time
+end # module Cabin::Mixins::Timer
diff --git a/lib/cabin/mixins/timestamp.rb b/lib/cabin/mixins/timestamp.rb
new file mode 100644
index 0000000..43c20a0
--- /dev/null
+++ b/lib/cabin/mixins/timestamp.rb
@@ -0,0 +1,13 @@
+require "cabin/namespace"
+
+# Timestamp events before publishing.
+module Cabin::Mixins::Timestamp
+  def self.extended(instance)
+    self.included(instance.class)
+  end
+  def self.included(klass)
+    klass.filter do |event|
+      event[:timestamp] = Time.now.strftime("%Y-%m-%dT%H:%M:%S.%6N%z")
+    end
+  end
+end
diff --git a/lib/cabin/namespace.rb b/lib/cabin/namespace.rb
new file mode 100644
index 0000000..1df7cb3
--- /dev/null
+++ b/lib/cabin/namespace.rb
@@ -0,0 +1,8 @@
+module Cabin
+  module Outputs
+    module EM; end
+  end
+  module Mixins; end
+  module Emitters; end
+  class Metrics; end
+end
diff --git a/lib/cabin/outputs/em/stdlib-logger.rb b/lib/cabin/outputs/em/stdlib-logger.rb
new file mode 100644
index 0000000..7c85ea8
--- /dev/null
+++ b/lib/cabin/outputs/em/stdlib-logger.rb
@@ -0,0 +1,42 @@
+require "cabin"
+require "eventmachine"
+
+# Wrap Ruby stdlib's logger and make it EventMachine friendly. This
+# allows you to output to a normal ruby logger with Cabin.
+class Cabin::Outputs::EM::StdlibLogger
+  public
+  def initialize(logger)
+    @logger_queue = EM::Queue.new
+    @logger = logger
+    # Consume log lines from a queue and send them with logger
+    consumer
+  end
+
+  def consumer
+    line_sender = Proc.new do |line|
+      # This will call @logger.info(data) or something similar
+      @logger.send(line[:method], line[:message])
+      EM::next_tick do
+        # Pop another line off the queue and do it again
+        @logger_queue.pop(&line_sender)
+      end
+    end
+    # Pop a line off the queue and send it with logger
+    @logger_queue.pop(&line_sender)
+  end
+
+  # Receive an event
+  public
+  def <<(data)
+    line = Hash.new
+    line[:method] = data[:level] || "info"
+    line[:message] = "#{data[:message]} #{data.inspect}"
+    if EM::reactor_running?
+      # Push line onto queue for later sending
+      @logger_queue.push(line)
+    else
+      # This will call @logger.info(data) or something similar
+      @logger.send(line[:method], line[:message])
+    end
+  end
+end
diff --git a/lib/cabin/outputs/io.rb b/lib/cabin/outputs/io.rb
new file mode 100644
index 0000000..e6448ae
--- /dev/null
+++ b/lib/cabin/outputs/io.rb
@@ -0,0 +1,89 @@
+require "cabin"
+require "thread"
+
+# Wrap IO objects with a reasonable log output. 
+#
+# If the IO is *not* attached to a tty (io#tty? returns false), then
+# the event will be written in ruby inspect format terminated by a newline:
+#
+#     { "timestamp" => ..., "message" => message, ... }
+#
+# If the IO is attached to a TTY, there are # human-friendly in this format:
+#
+#     message { event data }
+#
+# Additionally, colorized output may be available. If the event has :level,
+# :color, or :bold. Any of the Cabin::Mixins::Logger methods (info, error, etc)
+# will result in colored output. See the LEVELMAP for the mapping of levels
+# to colors.
+class Cabin::Outputs::IO
+  # Mapping of color/style names to ANSI control values
+  CODEMAP = {
+    :normal => 0,
+    :bold => 1,
+    :black => 30,
+    :red => 31,
+    :green => 32,
+    :yellow => 33,
+    :blue => 34,
+    :magenta => 35,
+    :cyan => 36,
+    :white => 37
+  }
+
+  # Map of log levels to colors
+  LEVELMAP = {
+    :fatal => :red,
+    :error => :red,
+    :warn => :yellow,
+    :info => :green, # default color
+    :debug => :cyan,
+  }
+
+  def initialize(io)
+    @io = io
+    @lock = Mutex.new
+  end # def initialize
+
+  # Receive an event
+  def <<(event)
+    @lock.synchronize do
+      if !@io.tty?
+        @io.puts(event.inspect)
+      else
+        tty_write(event)
+      end
+    end
+  end # def <<
+
+  private
+  def tty_write(event)
+    # The io is attached to a tty, so make pretty colors.
+    # delete things from the 'data' portion that's not really data.
+    data = event.clone
+    data.delete(:message)
+    data.delete(:timestamp)
+
+    color = data.delete(:color)
+    # :bold is expected to be truthy
+    bold = data.delete(:bold) ? :bold : nil
+
+    # Make 'error' and other log levels have color
+    if color.nil? and data[:level]
+      color = LEVELMAP[data[:level]]
+    end
+
+    if data.empty?
+      message = [event[:message]]
+    else
+      message = ["#{event[:message]} #{data.inspect}"]
+    end
+    message.unshift("\e[#{CODEMAP[color.to_sym]}m") if !color.nil?
+    message.unshift("\e[#{CODEMAP[bold]}m") if !bold.nil?
+    message.push("\e[#{CODEMAP[:normal]}m") if !(bold.nil? and color.nil?)
+    @io.puts(message.join(""))
+    @io.flush
+  end # def <<
+
+  public(:initialize, :<<)
+end # class Cabin::Outputs::StdlibLogger
diff --git a/lib/cabin/outputs/stdlib-logger.rb b/lib/cabin/outputs/stdlib-logger.rb
new file mode 100644
index 0000000..10c0b2d
--- /dev/null
+++ b/lib/cabin/outputs/stdlib-logger.rb
@@ -0,0 +1,33 @@
+require "cabin"
+
+# Wrap Ruby stdlib's logger. This allows you to output to a normal ruby logger
+# with Cabin. Since Ruby's Logger has a love for strings alone, this 
+# wrapper will convert the data/event to ruby inspect format before sending it
+# to Logger.
+class Cabin::Outputs::StdlibLogger
+  public
+  def initialize(logger)
+    @logger = logger
+    @logger.level = logger.class::DEBUG
+  end # def initialize
+
+  # Receive an event
+  public
+  def <<(event)
+    if !event.include?(:level)
+      event[:level] = :info
+    end
+    method = event[:level].downcase.to_sym || :info
+    event.delete(:level)
+
+    data = event.clone
+    # delete things from the 'data' portion that's not really data.
+    data.delete(:message)
+    data.delete(:timestamp)
+    message = "#{event[:message]} #{data.inspect}"
+
+    #p [@logger.level, logger.class::DEBUG]
+    # This will call @logger.info(data) or something similar.
+    @logger.send(method, message)
+  end # def <<
+end # class Cabin::Outputs::StdlibLogger
diff --git a/lib/cabin/outputs/zeromq.rb b/lib/cabin/outputs/zeromq.rb
new file mode 100644
index 0000000..3010fc2
--- /dev/null
+++ b/lib/cabin/outputs/zeromq.rb
@@ -0,0 +1,96 @@
+require 'cabin'
+require 'ffi-rzmq'
+
+# Output to a zeromq socket.
+class Cabin::Outputs::ZeroMQ
+  DEFAULTS = {
+    :topology => "pushpull",
+    :hwm => 0, # zeromq default: no limit
+    :linger => -1, # zeromq default: wait until all messages are sent.
+    :topic => ""
+  }
+
+  CONTEXT = ZMQ::Context.new
+
+  attr_reader :socket, :topology, :topic
+
+  # Create a new ZeroMQ output.
+  #
+  # arguments:
+  # addresses A list of addresses to connect to. These are round-robined by zeromq.
+  # 
+  # :topology Either 'pushpull' or 'pubsub'. Specifies which zeromq socket type to use. Default pushpull.
+  # :hwm Specifies the High Water Mark for the socket. Default 0, which means there is none.
+  # :linger Specifies the linger time in milliseconds for the socket. Default -1, meaning wait forever for the socket to close.
+  # :topic Specifies the topic for a pubsub topology. This can be a string or a proc with the event as the only argument.
+  def initialize(addresses, options={})
+    options = DEFAULTS.merge(options)
+
+    @topology = options[:topology].to_s
+    case @topology
+    when "pushpull"
+      socket_type = ZMQ::PUSH
+    when "pubsub"
+      socket_type = ZMQ::PUB
+    end
+
+    @topic = options[:topic]
+    @socket = CONTEXT.socket(socket_type)
+    
+    Array(addresses).each do |address|
+      error_check @socket.connect(address), "connecting to #{address}"
+    end
+
+    error_check @socket.setsockopt(ZMQ::LINGER, options[:linger]), "while setting ZMQ::LINGER to #{options[:linger]}"
+    error_check @socket.setsockopt(ZMQ::HWM, options[:hwm]), "while setting ZMQ::HWM to #{options[:hwm]}"
+
+    #TODO use cabin's teardown when it exists
+    at_exit do
+      teardown
+    end
+
+    #define_finalizer
+  end
+
+  def linger
+    array = []
+    error_check @socket.getsockopt(ZMQ::LINGER, array), "while getting ZMQ::LINGER"
+    array.first
+  end
+
+  def hwm
+    array = []
+    error_check @socket.getsockopt(ZMQ::HWM, array), "while getting ZMQ::HWM"
+    array.first
+  end
+
+  def <<(event)
+    if @socket.name == "PUB"
+      topic = @topic.is_a?(Proc) ? @topic.call(event) : @topic
+      error_check @socket.send_string(topic, ZMQ::SNDMORE), "in topic send_string"
+    end
+    error_check @socket.send_string(event.inspect), "in send_string"
+  end
+
+  def teardown
+    @socket.close if @socket
+  end
+
+  private
+  def error_check(rc, doing)
+    unless ZMQ::Util.resultcode_ok?(rc)
+      raise "ZeroMQ Error while #{doing}"
+    end
+  end
+
+  # This causes the following message on exit:
+  # File exists (epoll.cpp:69)
+  # [1]    26175 abort      bundle exec irb
+  # def define_finalizer
+  #   ObjectSpace.define_finalizer(self, self.class.finalize(@socket))
+  # end
+
+  # def self.finalize(socket)
+  #   Proc.new { puts "finalizing"; socket.close unless socket.nil?; puts "done" }
+  # end
+end
diff --git a/lib/cabin/publisher.rb b/lib/cabin/publisher.rb
new file mode 100644
index 0000000..d1c37e4
--- /dev/null
+++ b/lib/cabin/publisher.rb
@@ -0,0 +1,20 @@
+require "cabin/namespace"
+
+# This mixin allows you to easily give channel and publish features
+# to a class.
+module Cabin::Publisher
+  # Set the channel
+  def channel=(channel)
+    @channel = channel
+  end # def channel=
+
+  # Get the channel
+  def channel
+    return @channel
+  end # def channel
+
+  # Publish to the channel
+  def publish(object)
+    @channel << object
+  end # def publish
+end # module Cabin::Publisher
diff --git a/lib/cabin/timer.rb b/lib/cabin/timer.rb
new file mode 100644
index 0000000..cb3fe32
--- /dev/null
+++ b/lib/cabin/timer.rb
@@ -0,0 +1,20 @@
+require "cabin/namespace"
+
+# A simple timer class for timing events like a stop watch. Normally you don't
+# invoke this yourself, but you are welcome to do so.
+#
+# See also: Cabin::Channel#time
+class Cabin::Timer
+  def initialize(&block)
+    @start = Time.now
+    @callback = block if block_given?
+  end # def initialize
+
+  # Stop the clock and call the callback with the duration.
+  # Also returns the duration of this timer.
+  def stop
+    duration = Time.now - @start
+    @callback.call(duration) if @callback
+    return duration
+  end # def stop
+end # class Cabin::Timer
diff --git a/metadata.yml b/metadata.yml
new file mode 100644
index 0000000..1c38624
--- /dev/null
+++ b/metadata.yml
@@ -0,0 +1,106 @@
+--- !ruby/object:Gem::Specification 
+name: cabin
+version: !ruby/object:Gem::Version 
+  hash: 5
+  prerelease: 
+  segments: 
+  - 0
+  - 6
+  - 1
+  version: 0.6.1
+platform: ruby
+authors: 
+- Jordan Sissel
+autorequire: 
+bindir: bin
+cert_chain: []
+
+date: 2013-06-18 00:00:00 Z
+dependencies: []
+
+description: This is an experiment to try and make logging more flexible and more consumable. Plain text logs are bullshit, let's emit structured and contextual logs. Metrics, too!
+email: 
+- jls at semicomplete.com
+executables: 
+- rubygems-cabin-test
+extensions: []
+
+extra_rdoc_files: []
+
+files: 
+- lib/cabin/metrics/timer.rb
+- lib/cabin/metrics/meter.rb
+- lib/cabin/metrics/histogram.rb
+- lib/cabin/metrics/gauge.rb
+- lib/cabin/metrics/counter.rb
+- lib/cabin/outputs/stdlib-logger.rb
+- lib/cabin/outputs/em/stdlib-logger.rb
+- lib/cabin/outputs/io.rb
+- lib/cabin/outputs/zeromq.rb
+- lib/cabin/metric.rb
+- lib/cabin/context.rb
+- lib/cabin/timer.rb
+- lib/cabin/inspectable.rb
+- lib/cabin/publisher.rb
+- lib/cabin/mixins/logger.rb
+- lib/cabin/mixins/timer.rb
+- lib/cabin/mixins/colors.rb
+- lib/cabin/mixins/dragons.rb
+- lib/cabin/mixins/timestamp.rb
+- lib/cabin/mixins/CAPSLOCK.rb
+- lib/cabin/mixins/pipe.rb
+- lib/cabin/namespace.rb
+- lib/cabin/metrics.rb
+- lib/cabin/channel.rb
+- lib/cabin.rb
+- examples/sinatra-logging.rb
+- examples/fibonacci-timing.rb
+- examples/sample.rb
+- examples/pipe-spoon.rb
+- examples/metrics.rb
+- examples/pipe.rb
+- test/all.rb
+- test/minitest-patch.rb
+- test/test_metrics.rb
+- test/test_zeromq.rb
+- test/test_logging.rb
+- LICENSE
+- CHANGELIST
+- bin/rubygems-cabin-test
+homepage: https://github.com/jordansissel/ruby-cabin
+licenses: 
+- Apache License (2.0)
+post_install_message: 
+rdoc_options: []
+
+require_paths: 
+- lib
+- lib
+required_ruby_version: !ruby/object:Gem::Requirement 
+  none: false
+  requirements: 
+  - - ">="
+    - !ruby/object:Gem::Version 
+      hash: 3
+      segments: 
+      - 0
+      version: "0"
+required_rubygems_version: !ruby/object:Gem::Requirement 
+  none: false
+  requirements: 
+  - - ">="
+    - !ruby/object:Gem::Version 
+      hash: 3
+      segments: 
+      - 0
+      version: "0"
+requirements: []
+
+rubyforge_project: 
+rubygems_version: 1.8.25
+signing_key: 
+specification_version: 3
+summary: Experiments in structured and contextual logging
+test_files: []
+
+has_rdoc: 
diff --git a/test/all.rb b/test/all.rb
new file mode 100644
index 0000000..c2676b3
--- /dev/null
+++ b/test/all.rb
@@ -0,0 +1,17 @@
+$: << File.join(File.dirname(__FILE__), "..", "lib")
+
+require "rubygems"
+require "minitest/autorun"
+require "simplecov"
+
+SimpleCov.start
+
+dir = File.dirname(File.expand_path(__FILE__))
+Dir.glob(File.join(dir, "**", "test_*.rb")).each do |path|
+  puts "Loading tests from #{path}"
+  if path =~ /test_zeromq/
+    puts "Skipping zeromq tests because they force ruby to exit if libzmq is not found"
+    next
+  end
+  require path
+end
diff --git a/test/minitest-patch.rb b/test/minitest-patch.rb
new file mode 100644
index 0000000..737fd7a
--- /dev/null
+++ b/test/minitest-patch.rb
@@ -0,0 +1,13 @@
+require "rubygems"
+require "minitest/spec"
+# XXX: This code stolen from logstash's test bits.
+
+# I don't really like monkeypatching, but whatever, this is probably better
+# than overriding the 'describe' method.
+class MiniTest::Spec
+  class << self
+    # 'it' sounds wrong, call it 'test'
+    alias :test :it
+  end
+end
+
diff --git a/test/test_logging.rb b/test/test_logging.rb
new file mode 100644
index 0000000..fdd0636
--- /dev/null
+++ b/test/test_logging.rb
@@ -0,0 +1,152 @@
+$: << File.dirname(__FILE__)
+$: << File.join(File.dirname(__FILE__), "..", "lib")
+
+require "rubygems"
+require "minitest-patch"
+require "cabin"
+require "stringio"
+require "minitest/autorun" if __FILE__ == $0
+
+describe Cabin::Channel do
+
+  # Cabin::Channel is a subscription thing, so implement
+  # a simple receiver that just stores events in an array
+  # for later access - this lets us see exactly what is
+  # logged, in order.
+  class Receiver
+    attr_accessor :data
+
+    public
+    def initialize
+      @data = []
+    end
+
+    public
+    def <<(data)
+      @data << data
+    end
+  end # class Receiver
+
+  before do
+    @logger = Cabin::Channel.new
+    @target = Receiver.new
+    @logger.subscribe(@target)
+  end
+
+  test "simple string publishing" do
+    @logger.publish("Hello world")
+    assert_equal(1, @target.data.length)
+    assert_equal("Hello world", @target.data[0][:message])
+  end
+
+  test "simple context data" do
+    @logger[:foo] = "bar"
+    @logger.publish("Hello world")
+    assert_equal(1, @target.data.length)
+    assert_equal("Hello world", @target.data[0][:message])
+    assert_equal("bar", @target.data[0][:foo])
+  end
+
+  test "time something" do
+    timer = @logger.time("some sample")
+    timer.stop
+
+    event = @target.data[0]
+    assert_equal("some sample", event[:message])
+    assert(event[:duration].is_a?(Numeric))
+  end
+
+  test "double subscription should still only subscribe once" do
+    @logger.subscribe(@target)
+    @logger.publish("Hello world")
+    assert_equal(1, @target.data.length)
+    assert_equal("Hello world", @target.data[0][:message])
+  end 
+
+  test "context values" do
+    context = @logger.context
+    context["foo"] = "hello"
+    @logger.publish("testing")
+    assert_equal(1, @target.data.length)
+    assert_equal("hello", @target.data[0]["foo"])
+    assert_equal("testing", @target.data[0][:message])
+  end
+
+  test "context values clear properly" do
+    context = @logger.context
+    context["foo"] = "hello"
+    context.clear
+    @logger.publish("testing")
+    assert_equal(1, @target.data.length)
+    assert(!@target.data[0].has_key?("foo"))
+    assert_equal("testing", @target.data[0][:message])
+  end
+
+  %w(fatal error warn info debug).each do |level|
+    level = level.to_sym
+    test "standard use case, '#{level}' logging when enabled" do
+      @logger.level = level
+      @logger.send(level, "Hello world")
+      event = @target.data[0]
+      assert(@logger.send("#{level}?"),
+             "At level #{level}, Channel##{level}? should return true.")
+      assert_equal("Hello world", event[:message])
+      assert_equal(level, event[:level])
+    end
+  end
+
+  %w(error warn info debug).each do |level|
+    level = level.to_sym
+    test "standard use case, '#{level}' logging when wrong level" do
+      @logger.level = :fatal
+      # Should not log since log level is :fatal and we are above that.
+      @logger.send(level, "Hello world")
+      assert_equal(0, @target.data.length)
+    end
+  end
+
+  test "extra debugging data" do 
+    @logger.level = :debug
+    @logger.info("Hello world")
+    event = @target.data[0]
+    assert(event.include?(:file), "At debug level, there should be a :file attribute")
+    assert(event.include?(:line), "At debug level, there should be a :line attribute")
+    assert(event.include?(:method), "At debug level, there should be a :method attribute")
+  end
+
+  test "extra debugging data absent if log level is not debug" do 
+    @logger.level = :info
+    @logger.info("Hello world")
+    event = @target.data[0]
+    assert(!event.include?(:file), "At non-debug level, there should not be a :file attribute")
+    assert(!event.include?(:line), "At non-debug level, there should not be a :line attribute")
+    assert(!event.include?(:method), "At non-debug level, there should not be a :method attribute")
+  end
+
+  test "invalid arguments to logger.info raises ArgumentError" do
+    assert_raises(ArgumentError, "logger.info() should raise ArgumentError") do
+      @logger.info()
+    end
+
+    assert_raises(ArgumentError, "logger.info('foo', 'bar') should raise " \
+                  "ArgumentError because 'bar' is not a Hash.") do
+      @logger.info("foo", "bar")
+    end
+
+    assert_raises(ArgumentError, "logger.info('foo', { 'foo': 'bar' }, 'baz')" \
+                  "should raise ArgumentError for too many arguments") do
+      @logger.info("foo", { "foo" => "bar" }, "bar")
+    end
+  end
+
+  test "output to queue" do
+    require "thread"
+    queue = Queue.new
+    @logger.subscribe(queue)
+
+    @logger.info("Hello world")
+    event = queue.pop
+    assert_equal("Hello world", event[:message])
+    assert_equal(:info, event[:level])
+  end
+end # describe Cabin::Channel do
diff --git a/test/test_metrics.rb b/test/test_metrics.rb
new file mode 100644
index 0000000..8b70de9
--- /dev/null
+++ b/test/test_metrics.rb
@@ -0,0 +1,97 @@
+$: << File.dirname(__FILE__)
+$: << File.join(File.dirname(__FILE__), "..", "lib")
+
+require "rubygems"
+require "minitest-patch"
+require "cabin"
+require "cabin/metrics"
+require "minitest/autorun" if __FILE__ == $0
+
+describe Cabin::Metrics do
+  before do
+    @metrics = Cabin::Metrics.new
+  end
+
+  #test "gauge" do
+    #gauge = @metrics.gauge(self) { 3 }
+    #assert_equal(3, gauge.value)
+    ## metrics.first == [identifier, Gauge]
+    #assert_equal(3, @metrics.first.last.value)
+  #end
+
+  test "counter" do
+    counter = @metrics.counter(self)
+    0.upto(30) do |i|
+      assert_equal(i, counter.value)
+      assert_equal({ :value => i }, counter.to_hash)
+      counter.incr
+    end
+    31.downto(0) do |i|
+      assert_equal(i, counter.value)
+      assert_equal({ :value => i }, counter.to_hash)
+      counter.decr
+    end
+  end
+
+  test "meter counter" do
+    meter = @metrics.meter(self)
+    30.times do |i|
+      assert_equal(i, meter.value)
+      assert_equal({ :value => i }, meter.to_hash)
+      meter.mark
+    end
+  end
+
+  test "meter time-based averages" # TODO(sissel): implement
+
+  test "timer first-run has max == min" do
+    timer = @metrics.timer(self)
+    timer.time { true }
+    assert_equal(timer.to_hash[:min], timer.to_hash[:max],
+                 "With a single event, min and max must be equal")
+  end
+
+  test "timer counter" do
+    timer = @metrics.timer(self)
+    30.times do |i|
+      assert_equal(i, timer.value)
+      assert_equal(i, timer.to_hash[:count])
+      timer.time { sleep(0.01) }
+      assert(timer.to_hash[:total] > 0, "total should be nonzero")
+      assert(timer.to_hash[:mean] > 0, "mean should be nonzero")
+      assert(timer.to_hash[:max] > 0, "max should be nonzero")
+    end
+  end
+
+  test "timer.time without block" do
+    timer = @metrics.timer(self)
+    30.times do |i|
+      assert_equal(i, timer.value)
+      assert_equal(i, timer.to_hash[:count])
+      t = timer.time
+      sleep(0.01)
+      t.stop
+      assert(timer.to_hash[:total] > 0, "total should be nonzero")
+      assert(timer.to_hash[:mean] > 0, "mean should be nonzero")
+      assert(timer.to_hash[:max] > 0, "max should be nonzero")
+    end
+  end
+
+  test "metrics from Cabin::Metrics" do
+    # Verify the Metrics api for creating new metrics.
+    metrics = Cabin::Metrics.new
+    assert(metrics.timer(self).is_a?(Cabin::Metrics::Timer))
+    assert(metrics.counter(self).is_a?(Cabin::Metrics::Counter))
+    assert(metrics.histogram(self).is_a?(Cabin::Metrics::Histogram))
+    assert(metrics.meter(self).is_a?(Cabin::Metrics::Meter))
+  end
+
+  test "metrics from logger" do
+    logger = Cabin::Channel.new
+    meter = logger.metrics.meter(self)
+    assert_equal(0, meter.value)
+  end
+
+  test "timer histogram" # TODO(sissel): implement
+  test "histogram" # TODO(sissel): implement
+end # describe Cabin::Channel do
diff --git a/test/test_zeromq.rb b/test/test_zeromq.rb
new file mode 100644
index 0000000..0074aff
--- /dev/null
+++ b/test/test_zeromq.rb
@@ -0,0 +1,99 @@
+$: << File.dirname(__FILE__)
+$: << File.join(File.dirname(__FILE__), "..", "lib")
+
+require "rubygems"
+require "minitest-patch"
+require "cabin/outputs/zeromq"
+require "minitest/autorun" if __FILE__ == $0
+
+describe Cabin::Outputs::ZeroMQ do
+
+  def error_check(rc, doing)
+    unless ZMQ::Util.resultcode_ok?(rc)
+      raise "ZeroMQ Error while #{doing}"
+    end
+  end
+
+  NonBlockingFlag = (ZMQ::LibZMQ.version2? ? ZMQ::NOBLOCK : ZMQ::DONTWAIT) unless defined?(NonBlockingFlag)
+  def receive(socket)
+    received = ""
+    error_check socket.recv_string(received, NonBlockingFlag), "receiving"
+    received
+  end
+
+  before do
+    @logger = Cabin::Channel.new
+    @address = "inproc://zeromq-output"
+    @pull   = Cabin::Outputs::ZeroMQ::CONTEXT.socket(ZMQ::PULL)
+    @sub    = Cabin::Outputs::ZeroMQ::CONTEXT.socket(ZMQ::SUB)
+  end
+
+  after do
+    @pull.close
+    @sub.close
+    @output.teardown if @output
+  end
+
+  test 'push messages' do
+    @pull.bind(@address); sleep 0.1 # sleeps are necessary for inproc transport
+    @output = Cabin::Outputs::ZeroMQ.new(@address)
+    @logger.subscribe(@output)
+    @logger.info("hello")
+    @logger.info("hello2")
+    assert_equal "hello", JSON.parse(receive(@pull))['message']
+    assert_equal "hello2", JSON.parse(receive(@pull))['message']
+  end
+
+  test "pub messages" do
+    @sub.bind(@address); sleep 0.1
+    error_check @sub.setsockopt(ZMQ::SUBSCRIBE, ""), "subscribing"
+    @output = Cabin::Outputs::ZeroMQ.new(@address, :topology => "pubsub")
+    @logger.subscribe(@output)
+    @logger.info("hi")
+    assert_equal "", receive(@sub)
+    assert_equal "hi", JSON.parse(receive(@sub))['message']
+  end
+
+  test "pub messages on a topic" do
+    @sub.bind(@address); sleep 0.1
+    error_check @sub.setsockopt(ZMQ::SUBSCRIBE, "topic"), "subscribing"
+    @output = Cabin::Outputs::ZeroMQ.new(@address, :topology => "pubsub", :topic => "topic")
+    @logger.subscribe(@output)
+    @logger.info("hi")
+    assert_equal "topic", receive(@sub)
+    assert_equal "hi", JSON.parse(receive(@sub))['message']
+  end
+
+  test "topic proc" do
+    @sub.bind(@address); sleep 0.1
+    error_check @sub.setsockopt(ZMQ::SUBSCRIBE, "topic2"), "subscribing"
+    @output = Cabin::Outputs::ZeroMQ.new(@address, :topology => "pubsub", :topic => Proc.new { |event| event[:message] })
+    @logger.subscribe(@output)
+    @logger.info("topic1")
+    @logger.info("topic2")
+    assert_equal "topic2", receive(@sub)
+    assert_equal "topic2", JSON.parse(receive(@sub))['message']
+  end
+
+  test "multiple addresses" do
+    @pull.bind(@address); sleep 0.1
+    @pull2 = Cabin::Outputs::ZeroMQ::CONTEXT.socket(ZMQ::PULL)
+    @pull2.bind(@address.succ); sleep 0.1
+
+    @output = Cabin::Outputs::ZeroMQ.new([@address, @address.succ])
+    @logger.subscribe(@output)
+    @logger.info("yo")
+    @logger.info("yo")
+
+    assert_equal "yo", JSON.parse(receive(@pull))['message']
+    assert_equal "yo", JSON.parse(receive(@pull2))['message']
+  end
+
+  test "options" do
+    @pull.bind(@address); sleep 0.1
+    @output = Cabin::Outputs::ZeroMQ.new(@address, :hwm => 10, :linger => 100)
+
+    assert_equal 10, @output.hwm
+    assert_equal 100, @output.linger
+  end
+end

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-cabin.git



More information about the Pkg-ruby-extras-commits mailing list