cabin-0.8.1/0000755000004100000410000000000012672060745012635 5ustar www-datawww-datacabin-0.8.1/bin/0000755000004100000410000000000012672060745013405 5ustar www-datawww-datacabin-0.8.1/bin/rubygems-cabin-test0000755000004100000410000000022712672060745017220 0ustar www-datawww-data#!/usr/bin/env ruby require "minitest/autorun" test_dir = File.join(File.dirname(__FILE__), "..", "test") load File.join(test_dir, "test_logging.rb") cabin-0.8.1/examples/0000755000004100000410000000000012672060745014453 5ustar www-datawww-datacabin-0.8.1/examples/sinatra-logging.rb0000644000004100000410000000105712672060745020070 0ustar www-datawww-datarequire "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 cabin-0.8.1/examples/metrics.rb0000644000004100000410000000144212672060745016447 0ustar www-datawww-datarequire "rubygems" require "cabin" require "logger" # Logging::... is something I'm implemented and experimenting with. @logger = Cabin::Channel.new # Metrics can be subscribed-to as well. @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: @logger.metrics.each do |metric| @logger.info(metric.inspect) end cabin-0.8.1/examples/fibonacci-timing.rb0000644000004100000410000000133212672060745020201 0ustar www-datawww-datarequire "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. @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). @logger.subscribe(Logger.new(STDOUT)) # You can store arbitrary key-value pairs in the logging channel. # These are emitted with every event. n = 35 @logger[:input] = n @logger.time("fibonacci latency") do fib(n) end cabin-0.8.1/examples/pipe.rb0000644000004100000410000000042012672060745015731 0ustar www-datawww-datarequire "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 cabin-0.8.1/examples/sample.rb0000644000004100000410000000236312672060745016265 0ustar www-datawww-datarequire "rubygems" require "cabin" # Logging::... is something I'm implemented and experimenting with. @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. @logger.subscribe(STDOUT) # You can store arbitrary key-value pairs in the logging channel. # These are emitted with every event. @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") @logger.info("All done.") cabin-0.8.1/lib/0000755000004100000410000000000012672060745013403 5ustar www-datawww-datacabin-0.8.1/lib/cabin.rb0000644000004100000410000000003012672060745014775 0ustar www-datawww-datarequire "cabin/channel" cabin-0.8.1/lib/cabin/0000755000004100000410000000000012672060745014457 5ustar www-datawww-datacabin-0.8.1/lib/cabin/namespace.rb0000644000004100000410000000017412672060745016742 0ustar www-datawww-datamodule Cabin module Outputs module EM; end end module Mixins; end module Emitters; end class Metrics; end end cabin-0.8.1/lib/cabin/outputs/0000755000004100000410000000000012672060745016202 5ustar www-datawww-datacabin-0.8.1/lib/cabin/outputs/em/0000755000004100000410000000000012672060745016603 5ustar www-datawww-datacabin-0.8.1/lib/cabin/outputs/em/stdlib-logger.rb0000644000004100000410000000224612672060745021672 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/outputs/stdlib-logger.rb0000644000004100000410000000206612672060745021271 0ustar www-datawww-datarequire "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 if event[:level].is_a?(Symbol) method = event[:level] else method = event[:level].downcase.to_sym || :info end 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 cabin-0.8.1/lib/cabin/outputs/zeromq.rb0000644000004100000410000000534312672060745020051 0ustar www-datawww-datarequire '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 cabin-0.8.1/lib/cabin/outputs/io.rb0000644000004100000410000000444112672060745017141 0ustar www-datawww-datarequire "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 !tty? @io.puts(event.inspect) @io.flush else tty_write(event) end end end # def << def tty? @io.tty? end 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 cabin-0.8.1/lib/cabin/mixins/0000755000004100000410000000000012672060745015766 5ustar www-datawww-datacabin-0.8.1/lib/cabin/mixins/CAPSLOCK.rb0000644000004100000410000000063212672060745017513 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/mixins/dragons.rb0000644000004100000410000000172312672060745017753 0ustar www-datawww-datarequire "cabin/namespace" # An experiment to use AD&D-style log levels, because why not? 'info' and # 'fatal' and other log levels are silly 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 cabin-0.8.1/lib/cabin/mixins/terminal.rb0000644000004100000410000000034212672060745020125 0ustar www-datawww-datarequire "cabin/namespace" module Cabin::Mixins::Terminal def terminal(message) publish(message) do |subscriber, event| output = subscriber.output output.respond_to?(:tty?) && output.tty? end end end cabin-0.8.1/lib/cabin/mixins/logger.rb0000644000004100000410000000772412672060745017604 0ustar www-datawww-datarequire "cabin/namespace" # This module implements methods that act somewhat like Ruby's Logger class # It is included in Cabin::Channel module Cabin::Mixins::Logger def self.included(klass) klass.condition do |event, subscription| if subscription.nil? true else LEVELS[(subscription.options[:level] || :debug)] >= LEVELS[event[:level]].to_i end end end 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 end # def debugharder public(:log) end # module Cabin::Mixins::Logger cabin-0.8.1/lib/cabin/mixins/timer.rb0000644000004100000410000000121212672060745017427 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/mixins/colors.rb0000644000004100000410000000126012672060745017613 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/mixins/timestamp.rb0000644000004100000410000000046712672060745020325 0ustar www-datawww-datarequire "cabin/namespace" # Timestamp events before publishing. module Cabin::Mixins::Timestamp def self.extended(instance) self.included(instance.class) end def self.included(klass) klass.action do |event| event[:timestamp] = Time.now.strftime("%Y-%m-%dT%H:%M:%S.%6N%z") end end end cabin-0.8.1/lib/cabin/mixins/pipe.rb0000644000004100000410000000272512672060745017256 0ustar www-datawww-datarequire "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, &block) 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] block.call(line, method_name) if block_given? send(method_name, line) end # readers.each end # while !fds.empty? end # def pipe end # module Cabin::Mixins::Logger cabin-0.8.1/lib/cabin/metrics/0000755000004100000410000000000012672060745016125 5ustar www-datawww-datacabin-0.8.1/lib/cabin/metrics/counter.rb0000644000004100000410000000144112672060745020131 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/metrics/histogram.rb0000644000004100000410000000252012672060745020446 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/metrics/meter.rb0000644000004100000410000000135012672060745017565 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/metrics/timer.rb0000644000004100000410000000157512672060745017602 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/metrics/gauge.rb0000644000004100000410000000075012672060745017544 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/context.rb0000644000004100000410000000272712672060745016500 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/metric.rb0000644000004100000410000000066712672060745016300 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/channel.rb0000644000004100000410000001424312672060745016420 0ustar www-datawww-datarequire "cabin/mixins/logger" require "cabin/mixins/pipe" require "cabin/mixins/timestamp" require "cabin/mixins/timer" require "cabin/mixins/terminal" require "cabin/namespace" require "cabin/context" require "cabin/outputs/stdlib-logger" require "cabin/outputs/io" require "cabin/subscriber" 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 actions included in this class. def actions @actions ||= [] end # def Cabin::Channel.actions alias_method(:filters, :actions) # DEPRECATED # Register a new action. The block is passed the event. It is expected to # modify that event or otherwise do nothing. def action(&block) actions << block end alias_method(:filter, :action) # DEPRECATED # Get a list of conditions included in this class. def conditions @conditions ||= [] end # Register a new condition. The block must expect an event and a subscription. # It is expected to either return true (allow the event) or false (reject it). def condition(&block) conditions << block end # Decide to publish the event based on conditions and subscription options def allow_event?(event, subscription) conditions.all? { |condition| condition.call(event, subscription) } end end # class << self include Cabin::Mixins::Timestamp include Cabin::Mixins::Logger include Cabin::Mixins::Pipe include Cabin::Mixins::Timer include Cabin::Mixins::Terminal # 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, options = {}) # 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] = Cabin::Subscriber.new(output, options) 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, &block) event = {} self.class.actions.each do |action| action.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 |_, subscriber| append = block_given? ? block.call(subscriber, event) : true if append && self.class.allow_event?(event, subscriber) subscriber << event end 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 cabin-0.8.1/lib/cabin/inspectable.rb0000644000004100000410000000265612672060745017306 0ustar www-datawww-datarequire "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 == '' 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 cabin-0.8.1/lib/cabin/timer.rb0000644000004100000410000000107712672060745016131 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/publisher.rb0000644000004100000410000000064312672060745017004 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/metrics.rb0000644000004100000410000001010612672060745016450 0ustar www-datawww-datarequire "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 cabin-0.8.1/lib/cabin/subscriber.rb0000644000004100000410000000027612672060745017154 0ustar www-datawww-dataclass Cabin::Subscriber attr :output attr :options def initialize(output, options = {}) @output = output @options = options end def <<(data) @output << data end end cabin-0.8.1/test/0000755000004100000410000000000012672060745013614 5ustar www-datawww-datacabin-0.8.1/test/test_helper.rb0000644000004100000410000000022712672060745016460 0ustar www-datawww-datarequire "rubygems" require "support/minitest-patch" require "minitest/autorun" require "cabin" require "stringio" require "simplecov" SimpleCov.start cabin-0.8.1/test/all.rb0000644000004100000410000000052312672060745014711 0ustar www-datawww-datarequire "test_helper" base_dir = File.join(File.expand_path(File.dirname(__FILE__)), "cabin") Dir.glob(File.join(base_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 cabin-0.8.1/test/cabin/0000755000004100000410000000000012672060745014670 5ustar www-datawww-datacabin-0.8.1/test/cabin/test_zeromq.rb0000644000004100000410000000567512672060745017606 0ustar www-datawww-datarequire "test_helper" require "cabin/outputs/zeromq" 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 cabin-0.8.1/test/cabin/test_metrics.rb0000644000004100000410000000513512672060745017726 0ustar www-datawww-datarequire "test_helper" require "cabin/metrics" 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 cabin-0.8.1/test/cabin/test_pipe.rb0000644000004100000410000000345012672060745017213 0ustar www-datawww-datarequire "test_helper" describe Cabin::Channel do class Receiver attr_accessor :data public def initialize @data = [] end def <<(data) @data << data end end # class Receiver before do @logger = Cabin::Channel.new @target = Receiver.new @logger.subscribe(@target) @info_reader, @info_writer = IO.pipe @error_reader, @error_writer = IO.pipe end after do @logger.unsubscribe(@target.object_id) [ @info_reader, @info_writer, @error_reader, @error_writer ].each do |io| io.close unless io.closed? end end test 'Piping one IO' do @info_writer.puts 'Hello world' @info_writer.close @logger.pipe(@info_reader => :info) assert_equal(1, @target.data.length) assert_equal('Hello world', @target.data[0][:message]) end test 'Piping multiple IOs' do @info_writer.puts 'Hello world' @info_writer.close @error_writer.puts 'Goodbye world' @error_writer.close @logger.pipe(@info_reader => :info, @error_reader => :error) assert_equal(2, @target.data.length) assert_equal('Hello world', @target.data[0][:message]) assert_equal(:info, @target.data[0][:level]) assert_equal('Goodbye world', @target.data[1][:message]) assert_equal(:error, @target.data[1][:level]) end test 'Piping with a block' do @info_writer.puts 'Hello world' @info_writer.close @error_writer.puts 'Goodbye world' @error_writer.close info = StringIO.new error = StringIO.new @logger.pipe(@info_reader => :info, @error_reader => :error) do |message, level| info << message if level == :info error << message if level == :error end assert_equal('Hello world', info.string) assert_equal('Goodbye world', error.string) end end cabin-0.8.1/test/cabin/test_logging.rb0000644000004100000410000001251412672060745017705 0ustar www-datawww-datarequire "test_helper" 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 class TtyReceiver < Receiver def tty? true end end 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 "subscribe with a level impacts log publishing" do sub1 = Receiver.new sub2 = Receiver.new @logger.subscribe(sub1, :level => :info) @logger.subscribe(sub2, :level => :error) @logger.debug("test debug") @logger.info("test info") @logger.error("test error") assert_equal("test info", sub1.data[0][:message]) assert_equal("test error", sub1.data[1][:message]) assert_equal("test error", sub2.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 "standard use case, 'terminal' logging when there is no tty" do @logger.terminal('Hello world') assert_equal(true, @target.data.empty?) end test "standard use case, 'terminal' logging when there is a tty" do tty_target = TtyReceiver.new id = @logger.subscribe(tty_target) @logger.terminal('Hello world') assert_equal('Hello world', tty_target.data[0][:message]) @logger.unsubscribe(id) 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 cabin-0.8.1/test/support/0000755000004100000410000000000012672060745015330 5ustar www-datawww-datacabin-0.8.1/test/support/minitest-patch.rb0000644000004100000410000000050012672060745020601 0ustar www-datawww-datarequire "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 cabin-0.8.1/LICENSE0000644000004100000410000000104712672060745013644 0ustar www-datawww-dataCopyright 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. cabin-0.8.1/CHANGELIST0000644000004100000410000000111312672060745014175 0ustar www-datawww-datav0.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 cabin-0.8.1/cabin.gemspec0000644000004100000410000000460212672060745015260 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- Gem::Specification.new do |s| s.name = "cabin" s.version = "0.8.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Jordan Sissel"] s.date = "2016-01-29" s.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!" s.email = ["jls@semicomplete.com"] s.executables = ["rubygems-cabin-test"] s.files = ["CHANGELIST", "LICENSE", "bin/rubygems-cabin-test", "examples/fibonacci-timing.rb", "examples/metrics.rb", "examples/pipe.rb", "examples/sample.rb", "examples/sinatra-logging.rb", "lib/cabin.rb", "lib/cabin/channel.rb", "lib/cabin/context.rb", "lib/cabin/inspectable.rb", "lib/cabin/metric.rb", "lib/cabin/metrics.rb", "lib/cabin/metrics/counter.rb", "lib/cabin/metrics/gauge.rb", "lib/cabin/metrics/histogram.rb", "lib/cabin/metrics/meter.rb", "lib/cabin/metrics/timer.rb", "lib/cabin/mixins/CAPSLOCK.rb", "lib/cabin/mixins/colors.rb", "lib/cabin/mixins/dragons.rb", "lib/cabin/mixins/logger.rb", "lib/cabin/mixins/pipe.rb", "lib/cabin/mixins/terminal.rb", "lib/cabin/mixins/timer.rb", "lib/cabin/mixins/timestamp.rb", "lib/cabin/namespace.rb", "lib/cabin/outputs/em/stdlib-logger.rb", "lib/cabin/outputs/io.rb", "lib/cabin/outputs/stdlib-logger.rb", "lib/cabin/outputs/zeromq.rb", "lib/cabin/publisher.rb", "lib/cabin/subscriber.rb", "lib/cabin/timer.rb", "test/all.rb", "test/cabin/test_logging.rb", "test/cabin/test_metrics.rb", "test/cabin/test_pipe.rb", "test/cabin/test_zeromq.rb", "test/support/minitest-patch.rb", "test/test_helper.rb"] s.homepage = "https://github.com/jordansissel/ruby-cabin" s.licenses = ["Apache License (2.0)"] s.require_paths = ["lib", "lib"] s.rubygems_version = "1.8.23" s.summary = "Experiments in structured and contextual logging" if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_development_dependency(%q, ["~> 10.4.2"]) else s.add_dependency(%q, ["~> 10.4.2"]) end else s.add_dependency(%q, ["~> 10.4.2"]) end end