camayoc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2011 by Appozite, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,163 @@
1
+ Overview
2
+ ========
3
+ Camayoc is a flexible way to keep track of application stats. Inspired by
4
+ various logging frameworks (especially Log4r) it makes it easy to send stats
5
+ information to multiple destinations via the use of Handlers. Right out of the
6
+ box, it supports sending stats to the following:
7
+
8
+ * A [statsd daemon](https://github.com/etsy/statsd/) for ridiculously easy [Graphite](http://graphite.wikidot.com/) stats
9
+ * An in-memory hash for in-process stats
10
+ * A logger or IO stream
11
+ * Redis (EXPERIMENTAL - you'll need the [redis gem](https://github.com/ezmobius/redis-rb))
12
+
13
+ Philosophy
14
+ ----------
15
+ Application stats are critical. But even critical things get ignored if they're
16
+ hard (believe me, we know). Stats should be easy:
17
+
18
+ * Collecting stats should take just one line of code
19
+ * All stat collection should be fire-and-forget with no error handling required
20
+ * Organizing stats should be easy
21
+ * There should be essentially no performance cost to collecting stats
22
+
23
+ Examples
24
+ ==========
25
+ Here's all it takes to fire stats to statsd.
26
+
27
+ require 'camayoc'
28
+ # Grab a stats instance
29
+ stats = Camayoc["my_app"]
30
+ # Add a handler for statsd
31
+ stats.add(Camayoc::Handlers::Statsd.new(:host=>"localhost",:port=>1234))
32
+
33
+ # later in your app
34
+ def do_some_stuff
35
+ # Do some stuff
36
+ Camayoc["my_app"].increment("did_stuff")
37
+ end
38
+
39
+ Your stat will be sent to statsd with the name "my_app.did_stuff". See the
40
+ statsd docs for more information about how that gets translated into Graphite.
41
+
42
+ Namespaces
43
+ ----------
44
+ Many logging frameworks support the concept of namespaced logs that "extend"
45
+ other logs. This makes it easy to log messages from different areas of your
46
+ app and stay sane. Camayoc does this as well.
47
+
48
+ Let's say you have a service within your app where you want to store some timing
49
+ data.
50
+
51
+ stats = Camayoc["my_app"]
52
+ stats.add(Camayoc::Handlers::Statsd.new(:host=>"localhost",:port=>1234))
53
+
54
+ class AwesomeService
55
+ def be_awesome
56
+ start_time = Time.now.to_f
57
+ # Be, you know, awesome
58
+ ms = ((Time.now_to_f - start_time) * 1000).round
59
+ Camayoc["my_app:awesome_service"].timing("awesome_duration",ms)
60
+ end
61
+ end
62
+
63
+ This will automatically create a timing metric in graphite via statsd called
64
+ "my_app.awesome_service.awesome_duration". It does this by using the statsd
65
+ handler already configured for its "my_app" parent. Now, about handlers...
66
+
67
+ Handlers
68
+ --------
69
+ Just like loggers can have multiple outputters, you might want to send your
70
+ stats to different places.
71
+
72
+ app_stats = Camayoc["my_app"]
73
+ app_stats.add(Camayoc::Handlers::Statsd.new(:host=>"localhost",:port=>1234))
74
+
75
+ foo_stats = Camayoc["my_app:foo"]
76
+ foo_stats.add(Camayoc::Handlers::Redis.new(:host=>"localhost"))
77
+
78
+ app_stats.count("bar",1000) # Stats end up in statsd
79
+
80
+ foo_stats.count("baz",150) # Stats go to redis *and* statsd
81
+
82
+ Filters
83
+ -------
84
+ Sometimes you may want to send only certain stats to certain places.
85
+
86
+ app_stats = Camayoc["my_app"]
87
+ app_stats.add(Camayoc::Handlers::Statsd.new(:host=>"localhost",:port=>1234))
88
+
89
+ foo_stats = Camayoc["my_app:foo"]
90
+ foo_stats.add(Camayoc::Handlers::Redis.new(:host=>"localhost"),:when=>/baz/)
91
+
92
+ foo_stats.increment("baz",1000) #Stats go to redis and statsd
93
+ foo_stats.increment("bar",5) #Stats only go to statsd, not redis
94
+
95
+ There are other options as well like :if and :unless that take Procs that can
96
+ be executed to determine if a metric should be sent to the specified handler.
97
+ See Camayoc::Handlers::Filter for more.
98
+
99
+ Available Handlers
100
+ ==================
101
+ Statsd
102
+ ------
103
+ Class: Camayoc::Handlers::Statsd
104
+
105
+ This handler sends data to the statd daemon for use in graphite. If you can get
106
+ graphite and statsd going, then you'll get pretty graphs.
107
+
108
+ Memory
109
+ ------
110
+ Class: Camayoc::Handlers::Memory
111
+
112
+ Stores counts and timing data in an in-memory hash. This might be handy for
113
+ storing some temporary in-process stats.
114
+
115
+ Logger
116
+ ------
117
+ Class: Camayoc::Handlers::Logger
118
+
119
+ Writes out stat values and a timestamp to a logger instance for later analysis.
120
+ The format and method called on the logger can be controlled by constructor
121
+ arguments. See the class for details.
122
+
123
+ IO
124
+ --
125
+ Class: Camayoc::Handlers::IO
126
+
127
+ Writes out stats values and a timestamp to some stream of your choice. See
128
+ class for configuration details.
129
+
130
+ Redis (Experimental)
131
+ --------------------
132
+ Class: Camayoc::Handlers::Redis (require "camayoc/handlers/redis" first)
133
+
134
+ The redis gem is required to use this handler. This is a very, very simple
135
+ implementation that stores some data in redis. It is in no way a full-fledged
136
+ time-based stats system. It does make it easy to collect some simple counts and
137
+ some timing data. You can easily retrieve the stored data from redis by using
138
+ the #get method.
139
+
140
+
141
+ Implementing a Handler
142
+ ======================
143
+ Let's say you want to implement your own handler, pehaps to MongoDB. Handlers
144
+ just need to respond to a simple interface. See Camayoc::Handlers::StatEvent
145
+ for info on the argument to the two methods.
146
+
147
+ class SomeHandler
148
+ def count(stat_event)
149
+ # Do something
150
+ end
151
+
152
+ def timing(stat_event)
153
+ # Do something
154
+ end
155
+ end
156
+
157
+ If you write a handler and would like it included in Camayoc, please fork
158
+ and send a pull request and we'll get it integrated in.
159
+
160
+ Acknowledgements
161
+ ================
162
+ * The basic structure of Camayoc owes a lot of [Log4r](http://log4r.rubyforge.org/)
163
+ * The socket code for the Statsd handler is a modification of [reinh](https://github.com/reinh)'s [statsd](https://github.com/reinh/statsd)
@@ -0,0 +1,43 @@
1
+ module Camayoc
2
+ module Handlers
3
+ class Filter
4
+
5
+ def initialize(dest,options={},&block)
6
+ @dest = dest
7
+ if block_given?
8
+ @filter = block
9
+ elsif options[:with]
10
+ pattern = options[:with]
11
+ @filter = Proc.new {|type,event| event.ns_stat =~ pattern }
12
+ elsif options[:if]
13
+ @filter = options[:if]
14
+ elsif options[:unless]
15
+ proc = options[:unless]
16
+ @filter = Proc.new do |type,event|
17
+ !proc.call(type,event)
18
+ end
19
+ else
20
+ @filter = Proc.new {|args| true }
21
+ end
22
+ end
23
+
24
+ def count(event)
25
+ if allowed?(:count,event)
26
+ @dest.count(event)
27
+ end
28
+ end
29
+
30
+ def timing(event)
31
+ if allowed?(:timing,event)
32
+ @dest.timing(event)
33
+ end
34
+ end
35
+
36
+ private
37
+ def allowed?(type,event)
38
+ @filter.call(type,event)
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ module Camayoc
2
+ module Handlers
3
+
4
+ # Write to a raw IO stream with optional thread-safe locking before writing
5
+ # to the stream. By default, each stat message is written on a single line
6
+ # using puts. See the options in Camayoc::Handlers:Logger for options.
7
+ class IO < Logger
8
+
9
+ include ThreadSafety
10
+
11
+ def initialize(io=$stdout,options={})
12
+ super(io,{:method=>:puts}.merge(options))
13
+ self.thread_safe = Camayoc.thread_safe?
14
+ end
15
+
16
+ protected
17
+ def write(type,event)
18
+ synchronize do
19
+ super(type,event)
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ module Camayoc
2
+ module Handlers
3
+
4
+ # Write stats to a logger. Specify the method to call on the logger instance
5
+ # with :method (usually something like :info). If not :method is specified
6
+ # :debug will be called on the logger. You can control the format of the
7
+ # message passed to the logger method using the :formatter Proc.
8
+ class Logger
9
+
10
+ attr_accessor :logger, :method, :formatter
11
+
12
+ def initialize(logger, options={})
13
+ self.logger = logger
14
+ self.method = options[:method]
15
+ self.formatter = (options[:formatter] || default_formatter)
16
+ end
17
+
18
+ def count(event)
19
+ write(:count,event)
20
+ end
21
+
22
+ def timing(event)
23
+ write(:timing,event)
24
+ end
25
+
26
+ def default_formatter
27
+ Proc.new do |type,event|
28
+ "#{type} #{event.ns_stat} #{event.value} #{Time.now.utc.to_i}"
29
+ end
30
+ end
31
+
32
+ protected
33
+ def write(type,event)
34
+ msg = formatter.call(type,event)
35
+ if @method
36
+ @logger.send(@method,msg)
37
+ else
38
+ @logger.debug(msg)
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,35 @@
1
+ module Camayoc
2
+ module Handlers
3
+ class Memory
4
+
5
+ include ThreadSafety
6
+
7
+ def initialize(options={})
8
+ @data = {}
9
+ self.thread_safe = Camayoc.thread_safe?
10
+ end
11
+
12
+ def count(event)
13
+ stat = event.ns_stat
14
+ synchronize do
15
+ @data[stat] ||= 0
16
+ @data[stat] += event.value
17
+ end
18
+ end
19
+
20
+ def timing(event)
21
+ synchronize do
22
+ (@data[event.ns_stat] ||= Timing.new) << event.value
23
+ end
24
+ end
25
+
26
+ def get(stat)
27
+ synchronize do
28
+ @data[stat]
29
+ end
30
+ end
31
+ alias_method :[], :get
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,49 @@
1
+ require 'redis'
2
+
3
+ module Camayoc
4
+ module Handlers
5
+
6
+ # This class is experimental.
7
+ class Redis
8
+
9
+ def initialize(options={})
10
+ @namespace = options.delete(:namespace)
11
+ @redis = (options[:redis] || ::Redis.new(options))
12
+ end
13
+
14
+ def count(event)
15
+ @redis.incrby(key(event.ns_stat),event.value)
16
+ end
17
+
18
+ def timing(event)
19
+ stat = event.ns_stat
20
+ ms = event.value
21
+ @redis.set(key(stat),"timing")
22
+ @redis.incrby(key("#{stat}:_count"),1)
23
+ @redis.incrby(key("#{stat}:_total"),ms)
24
+ end
25
+
26
+ def get(stat)
27
+ value = @redis.get(key(stat))
28
+ if value == "timing"
29
+ Timing.new(
30
+ @redis.get(key("#{stat}:_total")).to_i,
31
+ @redis.get(key("#{stat}:_count")).to_i,
32
+ nil,nil # Don't support min and max right now in redis
33
+ )
34
+ elsif value
35
+ value.to_i
36
+ else
37
+ nil
38
+ end
39
+ end
40
+ alias_method :[], :get
41
+
42
+ private
43
+ def key(stat)
44
+ @namespace ? "#{@namespace}:#{stat}" : stat
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,47 @@
1
+ require 'socket'
2
+
3
+ # This class is a port of the code in https://github.com/reinh/statsd to fit
4
+ # with the Camayoc handler interface.
5
+ module Camayoc
6
+ module Handlers
7
+ class Statsd
8
+
9
+ include ThreadSafety
10
+
11
+ attr_accessor :namespace
12
+
13
+ def initialize(options={})
14
+ self.namespace = options[:namespace]
15
+ @host = options[:host]
16
+ @port = options[:port]
17
+ @socket = UDPSocket.new
18
+ self.thread_safe = Camayoc.thread_safe?
19
+ end
20
+
21
+ def count(event)
22
+ send(event.ns_stat,event.value,'c',event.options[:sample_rate]||1)
23
+ end
24
+
25
+ def timing(event)
26
+ send(event.ns_stat, event.value, 'ms', event.options[:sample_rate]||1)
27
+ end
28
+
29
+ private
30
+ def sampled(sample_rate)
31
+ yield unless sample_rate < 1 and rand > sample_rate
32
+ end
33
+
34
+ def send(stat, delta, type, sample_rate)
35
+ prefix = "#{@namespace}." unless @namespace.nil?
36
+ sampled(sample_rate) do
37
+ stat = stat.gsub(Camayoc::DELIMITER,'.')
38
+ msg = "#{prefix}#{stat}:#{delta}|#{type}#{'|@' << sample_rate.to_s if sample_rate < 1}"
39
+ synchronize do
40
+ @socket.send(msg, 0, @host, @port)
41
+ end
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,26 @@
1
+ module Camayoc
2
+ module Handlers
3
+ class Timing
4
+ attr_accessor :total, :count, :min, :max
5
+
6
+ def initialize(total=0,count=0,min=nil,max=nil)
7
+ self.total = total
8
+ self.count = count
9
+ self.max = max
10
+ self.min = min
11
+ end
12
+
13
+ def <<(ms)
14
+ @total += ms
15
+ @count += 1
16
+ @max = ms if @max.nil? || ms > @max
17
+ @min = ms if @min.nil? || ms < @min
18
+ end
19
+
20
+ def average
21
+ return nil if @count == 0
22
+ @total/@count
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ module Camayoc
2
+ class StatEvent
3
+
4
+ attr_accessor :source, :stat, :value, :options
5
+
6
+ def initialize(source,stat,value,options={})
7
+ self.source = source
8
+ self.stat = stat
9
+ self.value = value
10
+ self.options = options
11
+ end
12
+
13
+ def ns_stat
14
+ "#{source}#{Camayoc::DELIMITER}#{stat}"
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,77 @@
1
+ module Camayoc
2
+ class Stats
3
+
4
+ attr_accessor :name, :parent, :handlers
5
+
6
+ def initialize(name,parent=nil)
7
+ self.name = name
8
+ self.parent = parent
9
+ self.handlers = []
10
+ end
11
+
12
+ def add(handler,filter_opts={})
13
+ if !filter_opts.empty?
14
+ handler = Handlers::Filter.new(handler,filter_opts)
15
+ end
16
+ self.handlers << handler
17
+ self
18
+ end
19
+
20
+ def <<(handler)
21
+ self.add(handler)
22
+ end
23
+
24
+ def increment(stat,options={})
25
+ count(stat,1,options)
26
+ end
27
+
28
+ def decrement(stat,options={})
29
+ count(stat,-1,options)
30
+ end
31
+
32
+ def count(stat,value,options={})
33
+ count_event(StatEvent.new(name,stat,value,options))
34
+ end
35
+
36
+ def timing(stat,ms,options={})
37
+ timing_event(StatEvent.new(name,stat,ms,options))
38
+ end
39
+
40
+ protected
41
+ def count_event(event)
42
+ each_handler do |handler|
43
+ handler.count(event)
44
+ end
45
+ if parent
46
+ parent.count_event(event)
47
+ end
48
+ self
49
+ end
50
+
51
+ def timing_event(event)
52
+ each_handler do |handler|
53
+ handler.timing(event)
54
+ end
55
+ if parent
56
+ parent.timing_event(event)
57
+ end
58
+ self
59
+ end
60
+
61
+ private
62
+ def each_handler
63
+ handlers.each do |handler|
64
+ begin
65
+ yield(handler)
66
+ rescue
67
+ # Swallow up errors
68
+ end
69
+ end
70
+ end
71
+
72
+ def ns_stat(stat,ns=nil)
73
+ "#{ns || name}#{Camayoc::NAME_DELIMITER}#{stat}"
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,31 @@
1
+ module Camayoc
2
+ module ThreadSafety
3
+
4
+ class PlaceboLock
5
+ def synchronize
6
+ yield
7
+ end
8
+ end
9
+
10
+ # Ideally this should be called from your constructor
11
+ def thread_safe=(value)
12
+ @lock = value ? Mutex.new : PlaceboLock.new
13
+ end
14
+
15
+ def thread_safe?
16
+ lock.is_a?(Mutex)
17
+ end
18
+
19
+ def synchronize
20
+ lock.synchronize { yield }
21
+ end
22
+
23
+ private
24
+ def lock
25
+ # If we get here and have to init the lock because it's nil, obviously
26
+ # it's not thread safe
27
+ @lock ||= PlaceboLock.new
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ module Camayoc
2
+ VERSION = "0.1.0"
3
+ end
data/lib/camayoc.rb ADDED
@@ -0,0 +1,67 @@
1
+ require 'rubygems'
2
+ require 'camayoc/version'
3
+ require 'camayoc/thread_safety'
4
+ require 'camayoc/stat_event'
5
+ require 'camayoc/handlers/timing'
6
+ require 'camayoc/handlers/filter'
7
+ require 'camayoc/handlers/logger'
8
+ require 'camayoc/handlers/io'
9
+ require 'camayoc/handlers/memory'
10
+ require 'camayoc/handlers/statsd'
11
+ require 'camayoc/stats'
12
+
13
+ module Camayoc
14
+
15
+ DELIMITER = ":"
16
+
17
+ @registry = {}
18
+ @lock = Mutex.new
19
+
20
+ class << self
21
+ def [](name)
22
+ @lock.synchronize { @registry[name] ||= _new(name) }
23
+ end
24
+
25
+ def new(name)
26
+ @lock.synchronize { _new(name) }
27
+ end
28
+
29
+ def all
30
+ @lock.synchronize { @registry.values.dup }
31
+ end
32
+
33
+ def thread_safe=(value)
34
+ @thread_safe = value
35
+ end
36
+
37
+ def thread_safe?
38
+ @thread_safe == true
39
+ end
40
+
41
+ private
42
+ def _new(name)
43
+ stats = Stats.new(name,ancestor(name))
44
+ @registry[name] = stats
45
+ reassign_children(stats)
46
+ stats
47
+ end
48
+
49
+ def reassign_children(node)
50
+ @registry.values.each do |other_node|
51
+ if other_node != node && other_node.name.index(node.name) == 0
52
+ other_node.parent = node
53
+ end
54
+ end
55
+ end
56
+
57
+ def ancestor(name)
58
+ ancestor = nil
59
+ path = name.split(DELIMITER)
60
+ while path.pop && !ancestor
61
+ ancestor = @registry[path.join(DELIMITER)]
62
+ end
63
+ ancestor
64
+ end
65
+ end
66
+
67
+ end
@@ -0,0 +1,53 @@
1
+ class FilterTest < Test::Unit::TestCase
2
+
3
+ def setup
4
+ @handler = mock("handler")
5
+ end
6
+
7
+ def test_constructor_block_used_as_filter
8
+ filter = Camayoc::Handlers::Filter.new(@handler) do |type,event|
9
+ event.stat.length > 5
10
+ end
11
+ @handler.expects(:count).once
12
+ @handler.expects(:timing).once
13
+ filter.count(Camayoc::StatEvent.new("foo:bar","short",500))
14
+ filter.count(Camayoc::StatEvent.new("foo:bar","very_long",500))
15
+ filter.timing(Camayoc::StatEvent.new("foo:bar","short",500))
16
+ filter.timing(Camayoc::StatEvent.new("foo:bar","very_long",500))
17
+ end
18
+
19
+ def test_with_filters_on_namespaced_stat
20
+ filter = Camayoc::Handlers::Filter.new(@handler,:with=>/a{2}/)
21
+ @handler.expects(:count).once
22
+ @handler.expects(:timing).once
23
+ filter.count(Camayoc::StatEvent.new("foo:bar","aa_blah",500))
24
+ filter.count(Camayoc::StatEvent.new("foo:bar","bb_blah",500))
25
+ filter.timing(Camayoc::StatEvent.new("foo:bar","aa_blah",500))
26
+ filter.timing(Camayoc::StatEvent.new("foo:bar","bb_blah",500))
27
+ end
28
+
29
+ def test_constructor_block_used_as_filter
30
+ filter = Camayoc::Handlers::Filter.new(@handler,
31
+ :if=>Proc.new{|type,event| type == :timing && event.value > 1000 })
32
+
33
+ @handler.expects(:count).never
34
+ @handler.expects(:timing).once
35
+ filter.count(Camayoc::StatEvent.new("foo:bar","short",500))
36
+ filter.timing(Camayoc::StatEvent.new("foo:bar","short",500))
37
+ filter.timing(Camayoc::StatEvent.new("foo:bar","very_long",1500))
38
+ end
39
+
40
+ def test_constructor_block_used_as_filter
41
+ filter = Camayoc::Handlers::Filter.new(@handler,
42
+ :unless=>Proc.new{|type,event| type == :count })
43
+
44
+ @handler.expects(:count).never
45
+ @handler.expects(:timing).twice
46
+ filter.count(Camayoc::StatEvent.new("foo:bar","short",500))
47
+ filter.count(Camayoc::StatEvent.new("foo:bar","very_long",500))
48
+ filter.timing(Camayoc::StatEvent.new("foo:bar","short",500))
49
+ filter.timing(Camayoc::StatEvent.new("foo:bar","very_long",1500))
50
+ end
51
+
52
+
53
+ end
@@ -0,0 +1,33 @@
1
+ class IOTest < Test::Unit::TestCase
2
+
3
+ def setup
4
+ # Hack to get around weird Mocha issue with expecting :puts
5
+ @io = []
6
+ def @io.puts(msg)
7
+ self << msg
8
+ end
9
+ @handler = Camayoc::Handlers::IO.new(@io)
10
+ end
11
+
12
+ def test_count_sends_correct_log_message
13
+ expect_message(/count foo:bar:baz 500/)
14
+ @handler.count(Camayoc::StatEvent.new("foo:bar","baz",500))
15
+ end
16
+
17
+ def test_timing_sends_correct_log_message
18
+ expect_message(/timing foo:bar:time 100/)
19
+ @handler.timing(Camayoc::StatEvent.new("foo:bar","time",100))
20
+ end
21
+
22
+ def test_formatter_changes_format_of_message
23
+ @handler.formatter = Proc.new{|type,event| "#{type}: #{event.ns_stat}"}
24
+ expect_message(/timing: foo:bar:time/)
25
+ @handler.timing(Camayoc::StatEvent.new("foo:bar","time",100))
26
+ end
27
+
28
+ private
29
+ def expect_message(*messages)
30
+ assert(@io.zip(messages).all?{ |actual,patt| actual =~ patt })
31
+ end
32
+
33
+ end
@@ -0,0 +1,37 @@
1
+ class LoggerTest < Test::Unit::TestCase
2
+
3
+ def setup
4
+ @dest_logger = mock("logger")
5
+ @handler = Camayoc::Handlers::Logger.new(@dest_logger)
6
+ end
7
+
8
+ def test_count_sends_correct_log_message
9
+ expect_message(:debug, /count foo:bar:baz 500/)
10
+ @handler.count(Camayoc::StatEvent.new("foo:bar","baz",500))
11
+ end
12
+
13
+ def test_timing_sends_correct_log_message
14
+ expect_message(:debug,/timing foo:bar:time 100/)
15
+ @handler.timing(Camayoc::StatEvent.new("foo:bar","time",100))
16
+ end
17
+
18
+ def test_method_option_changes_method_called_on_logger
19
+ @handler.method = :info
20
+ expect_message(:info,/timing foo:bar:time 100/)
21
+ @handler.timing(Camayoc::StatEvent.new("foo:bar","time",100))
22
+ expect_message(:info, /count foo:bar:baz 5000/)
23
+ @handler.count(Camayoc::StatEvent.new("foo:bar","baz",5000))
24
+ end
25
+
26
+ def test_formatter_changes_format_of_message
27
+ @handler.formatter = Proc.new{|type,event| "#{type}: #{event.ns_stat}"}
28
+ expect_message(:debug,/timing: foo:bar:time/)
29
+ @handler.timing(Camayoc::StatEvent.new("foo:bar","time",100))
30
+ end
31
+
32
+ private
33
+ def expect_message(method,message)
34
+ @dest_logger.expects(method).with(regexp_matches(message))
35
+ end
36
+
37
+ end
@@ -0,0 +1,38 @@
1
+ class MemoryTest < Test::Unit::TestCase
2
+
3
+ def setup
4
+ @handler = Camayoc::Handlers::Memory.new
5
+ end
6
+
7
+ def test_count_initializes_to_0_and_increments_value
8
+ @handler.count(Camayoc::StatEvent.new("foo:bar","count",100))
9
+ assert_equal(100,@handler["foo:bar:count"])
10
+ end
11
+
12
+ def test_multiple_counts_increments_value
13
+ @handler.count(Camayoc::StatEvent.new("foo:bar","count",100))
14
+ @handler.count(Camayoc::StatEvent.new("foo:bar","count",500))
15
+ @handler.count(Camayoc::StatEvent.new("foo:bar","count",400))
16
+ assert_equal(1000,@handler["foo:bar:count"])
17
+ end
18
+
19
+ def test_timing_initializes_timing_object_and_adds_data
20
+ @handler.timing(Camayoc::StatEvent.new("foo:bar","time",500))
21
+ timing = @handler["foo:bar:time"]
22
+ assert_not_nil(timing)
23
+ assert_equal(1,timing.count)
24
+ end
25
+
26
+ def test_timing_increments_timing_object
27
+ @handler.timing(Camayoc::StatEvent.new("foo:bar","time",100))
28
+ @handler.timing(Camayoc::StatEvent.new("foo:bar","time",600))
29
+ @handler.timing(Camayoc::StatEvent.new("foo:bar","time",1000))
30
+ @handler.timing(Camayoc::StatEvent.new("foo:bar","time",500))
31
+ timing = @handler["foo:bar:time"]
32
+ assert_equal(4,timing.count)
33
+ assert_equal(100,timing.min)
34
+ assert_equal(1000,timing.max)
35
+ assert_equal(2200,timing.total)
36
+ assert_equal(550,timing.average)
37
+ end
38
+ end
@@ -0,0 +1,28 @@
1
+ if (begin; require 'camayoc/handlers/redis'; true; rescue LoadError; false end)
2
+ class RedisTest < Test::Unit::TestCase
3
+ def setup
4
+ @redis = mock("redis")
5
+ @handler = Camayoc::Handlers::Redis.new(:redis=>@redis)
6
+ end
7
+
8
+ def test_count_increments_correct_key_in_redis
9
+ @redis.expects(:incrby).with("foo:bar:baz",500)
10
+ @handler.count(Camayoc::StatEvent.new("foo:bar","baz",500))
11
+ end
12
+
13
+ def test_timing_updates_redis_keys_with_timing_info
14
+ seq = sequence("increments")
15
+ @redis.expects(:set).with("foo:bar:time","timing").times(3)
16
+ @redis.expects(:incrby).with("foo:bar:time:_count",1).times(3)
17
+ @redis.expects(:incrby).with("foo:bar:time:_total",500).in_sequence(seq)
18
+ @redis.expects(:incrby).with("foo:bar:time:_total",400).in_sequence(seq)
19
+ @redis.expects(:incrby).with("foo:bar:time:_total",200).in_sequence(seq)
20
+ @handler.timing(Camayoc::StatEvent.new("foo:bar","time",500))
21
+ @handler.timing(Camayoc::StatEvent.new("foo:bar","time",400))
22
+ @handler.timing(Camayoc::StatEvent.new("foo:bar","time",200))
23
+ end
24
+
25
+ end
26
+ else
27
+ puts "Warn: Skipping Redis test because redis gem is not installed"
28
+ end
@@ -0,0 +1,22 @@
1
+ class StatsdTest < Test::Unit::TestCase
2
+
3
+ def setup
4
+ @statsd = Camayoc::Handlers::Statsd.new(:host=>"localhost",:port=>1234)
5
+ end
6
+
7
+ def test_count_sends_correct_statsd_message
8
+ expect_message("foo.bar.baz:500|c")
9
+ @statsd.count(Camayoc::StatEvent.new("foo:bar","baz",500,{}))
10
+ end
11
+
12
+ def test_timing_sends_correct_statsd_message
13
+ expect_message("foo.bar.time:100|ms")
14
+ @statsd.timing(Camayoc::StatEvent.new("foo:bar","time",100,{}))
15
+ end
16
+
17
+ private
18
+ def expect_message(message)
19
+ @statsd.instance_variable_get("@socket").expects(:send).with(message,0,"localhost",1234)
20
+ end
21
+
22
+ end
@@ -0,0 +1,57 @@
1
+ class RegistrationTest < Test::Unit::TestCase
2
+
3
+ def test_accessing_stats_creates_it
4
+ stats = Camayoc["foo:bar"]
5
+ assert_equal(Camayoc::Stats,stats.class)
6
+ end
7
+
8
+ def test_accessing_stats_returns_same_instance
9
+ stats1 = Camayoc["foo:bar"]
10
+ stats2 = Camayoc["foo:bar"]
11
+ assert_same(stats1,stats2)
12
+ end
13
+
14
+ def test_accessing_path_with_no_ancestor_leaves_parent_nil
15
+ stats1 = Camayoc["foo:bar"]
16
+ assert_nil(stats1.parent)
17
+ end
18
+
19
+ def test_accessing_path_with_already_existing_ancestor_sets_parent
20
+ parent = Camayoc["foo"]
21
+ child = Camayoc["foo:bar"]
22
+ assert_same(parent,child.parent)
23
+ end
24
+
25
+ def test_accessing_path_with_intermediate_steps_gets_closest_existing_ancestor_as_parent
26
+ parent = Camayoc["foo"]
27
+ child = Camayoc["foo:a:b:c:baz"]
28
+ assert_same(parent,child.parent)
29
+ end
30
+
31
+ def test_accessing_path_in_middle_of_existing_hierarchy_reassociates_children
32
+ root = Camayoc["foo"]
33
+ child1 = Camayoc["foo:a:b:c:1"]
34
+ child2 = Camayoc["foo:a:b:c:2"]
35
+ middle = Camayoc["foo:a:b"]
36
+
37
+ assert_same(root,middle.parent)
38
+ assert_same(middle,child1.parent)
39
+ assert_same(middle,child2.parent)
40
+ end
41
+
42
+ def test_all_lists_all_existing_instances
43
+ root = Camayoc["foo"]
44
+ child1 = Camayoc["foo:a:b:c:1"]
45
+ child2 = Camayoc["foo:a:b:c:2"]
46
+ middle = Camayoc["foo:a:b"]
47
+
48
+ all = Camayoc.all
49
+ all = all.sort_by{|stats| stats.name}
50
+
51
+ assert_equal([root,middle,child1,child2],all)
52
+ end
53
+
54
+ def teardown
55
+ Camayoc.instance_variable_get("@registry").clear
56
+ end
57
+ end
@@ -0,0 +1,8 @@
1
+ class StatsTest < Test::Unit::TestCase
2
+
3
+ def test_ns_stat_uses_source_for_namespace
4
+ event = Camayoc::StatEvent.new("foo:bar:baz","stat",10000,{})
5
+ assert_equal("foo:bar:baz:stat",event.ns_stat)
6
+ end
7
+
8
+ end
@@ -0,0 +1,94 @@
1
+ class StatsTest < Test::Unit::TestCase
2
+
3
+ def setup
4
+ @stats = Camayoc::Stats.new("foo:bar")
5
+ @handler = mock("handler1")
6
+ @stats.add(@handler)
7
+ end
8
+
9
+ def test_count_fires_to_all_handlers
10
+ h2 = mock("handler2")
11
+ @stats.add(h2)
12
+
13
+ @handler.expects(:count).with(kind_of(Camayoc::StatEvent))
14
+ h2.expects(:count).with(kind_of(Camayoc::StatEvent))
15
+ @stats.count("count",500)
16
+ end
17
+
18
+ def test_count_generates_correct_stat_event
19
+ @handler.expects(:count).with(
20
+ &stat_event_match("foo:bar","count",500,{:pass_through=>true}))
21
+ @stats.count("count",500,:pass_through=>true)
22
+ end
23
+
24
+ def test_count_propagates_event_to_parent_after_firing_to_handlers
25
+ @stats.parent = Camayoc::Stats.new("foo")
26
+
27
+ seq = sequence("firing")
28
+ evt = stat_event_match("foo:bar","count",100,{:pass_through=>true})
29
+ @handler.expects(:count).with(&evt).in_sequence(seq)
30
+ @stats.parent.expects(:count_event).with(&evt).in_sequence(seq)
31
+
32
+ @stats.count("count",100,:pass_through=>true)
33
+ end
34
+
35
+ def test_increment_delegates_to_count
36
+ @stats.expects(:count).with("count",1,{})
37
+ @stats.increment("count")
38
+ end
39
+
40
+ def test_decrement_delegates_to_count
41
+ @stats.expects(:count).with("count",-1,{})
42
+ @stats.decrement("count")
43
+ end
44
+
45
+ def test_timing_fires_to_all_handlers
46
+ h2 = mock("handler2")
47
+ @stats.add(h2)
48
+
49
+ @handler.expects(:timing).with(kind_of(Camayoc::StatEvent))
50
+ h2.expects(:timing).with(kind_of(Camayoc::StatEvent))
51
+ @stats.timing("time",500)
52
+ end
53
+
54
+ def test_timing_generates_correct_stat_event
55
+ @handler.expects(:timing).with(
56
+ &stat_event_match("foo:bar","time",1,{:pass_through=>true}))
57
+ @stats.timing("time",1,:pass_through=>true)
58
+ end
59
+
60
+ def test_timing_propagates_event_to_parent_after_firing_to_handlers
61
+ @stats.parent = Camayoc::Stats.new("foo")
62
+
63
+ seq = sequence("firing")
64
+ evt = stat_event_match("foo:bar","time",100,{:pass_through=>true})
65
+ @handler.expects(:timing).with(&evt).in_sequence(seq)
66
+ @stats.parent.expects(:timing_event).with(&evt).in_sequence(seq)
67
+
68
+ @stats.timing("time",100,:pass_through=>true)
69
+ end
70
+
71
+ def test_handler_errors_are_swallowed_and_firing_continues
72
+ h2 = mock("handler2")
73
+ @stats.add(h2)
74
+
75
+ seq = sequence("firing")
76
+ @handler.expects(:count).raises("FAIL").in_sequence(seq)
77
+ h2.expects(:count).in_sequence(seq)
78
+
79
+ assert_nothing_raised do
80
+ @stats.count("baz",100)
81
+ end
82
+ end
83
+
84
+ private
85
+ def stat_event_match(*args)
86
+ template = Camayoc::StatEvent.new(*args)
87
+ Proc.new do |event|
88
+ event.source == template.source &&
89
+ event.stat == template.stat &&
90
+ event.value == template.value &&
91
+ event.options == template.options
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'mocha'
4
+
5
+ $: << File.join(File.dirname(__FILE__),"..","lib")
6
+ require 'camayoc'
@@ -0,0 +1,33 @@
1
+ class ThreadSafetyTest < Test::Unit::TestCase
2
+
3
+ class SafeThing
4
+ include Camayoc::ThreadSafety
5
+ end
6
+
7
+ def setup
8
+ @thing = SafeThing.new
9
+ end
10
+
11
+ def test_thread_safe_turns_on_lock
12
+ @thing.thread_safe = true
13
+ assert_equal(Mutex,@thing.__send__(:lock).class)
14
+ end
15
+
16
+ def test_lock_is_placebo_by_default
17
+ assert_equal(Camayoc::ThreadSafety::PlaceboLock,
18
+ @thing.__send__(:lock).class)
19
+ end
20
+
21
+ def test_synchronize_yields_whether_safe_or_not
22
+ yielded = false
23
+ @thing.thread_safe = true
24
+ @thing.synchronize { yielded = true }
25
+ assert(yielded)
26
+
27
+ yielded = false
28
+ @thing.thread_safe = false
29
+ @thing.synchronize {yielded = true}
30
+ assert(yielded)
31
+ end
32
+
33
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: camayoc
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Hayes Davis
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-06-02 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: mocha
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id001
35
+ description: " Inspired by logging frameworks, Camayoc makes it easy send stats and metrics \n from your application to collectors like Graphite (via statsd) or Redis. \n Extensible handlers mean you can use whatever storage system you like \n with the same interface.\n"
36
+ email: hayes@appozite.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.md
44
+ files:
45
+ - README.md
46
+ - LICENSE
47
+ - lib/camayoc/handlers/filter.rb
48
+ - lib/camayoc/handlers/io.rb
49
+ - lib/camayoc/handlers/logger.rb
50
+ - lib/camayoc/handlers/memory.rb
51
+ - lib/camayoc/handlers/redis.rb
52
+ - lib/camayoc/handlers/statsd.rb
53
+ - lib/camayoc/handlers/timing.rb
54
+ - lib/camayoc/stat_event.rb
55
+ - lib/camayoc/stats.rb
56
+ - lib/camayoc/thread_safety.rb
57
+ - lib/camayoc/version.rb
58
+ - lib/camayoc.rb
59
+ - test/handlers/filter_test.rb
60
+ - test/handlers/io_test.rb
61
+ - test/handlers/logger_test.rb
62
+ - test/handlers/memory_test.rb
63
+ - test/handlers/redis_test.rb
64
+ - test/handlers/statsd_test.rb
65
+ - test/registration_test.rb
66
+ - test/stat_event_test.rb
67
+ - test/stats_test.rb
68
+ - test/test_helper.rb
69
+ - test/thread_safety_test.rb
70
+ has_rdoc: true
71
+ homepage: http://github.com/appozite/camayoc
72
+ licenses: []
73
+
74
+ post_install_message:
75
+ rdoc_options: []
76
+
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ hash: 3
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ hash: 3
94
+ segments:
95
+ - 0
96
+ version: "0"
97
+ requirements: []
98
+
99
+ rubyforge_project:
100
+ rubygems_version: 1.3.7
101
+ signing_key:
102
+ specification_version: 3
103
+ summary: Camayoc makes it easy to send stats anywhere.
104
+ test_files: []
105
+