[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