lookout-statsd 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -0
- data/Gemfile +16 -0
- data/README.md +126 -0
- data/Rakefile +10 -0
- data/bin/statsd +45 -0
- data/config.yml +10 -0
- data/lib/statsd.rb +130 -0
- data/lib/statsd/echos.rb +21 -0
- data/lib/statsd/graphite.rb +70 -0
- data/lib/statsd/server.rb +82 -0
- data/lib/statsd/test.rb +3 -0
- data/netcat-example.sh +5 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/statsd/server_spec.rb +16 -0
- data/spec/statsd_spec.rb +130 -0
- data/stats.rb +28 -0
- data/statsd.gemspec +24 -0
- metadata +117 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# StatsD
|
2
|
+
|
3
|
+
A network daemon for aggregating statistics (counters and timers), rolling them up, then sending them to [graphite][graphite].
|
4
|
+
|
5
|
+
|
6
|
+
### Installation
|
7
|
+
|
8
|
+
gem install statsd
|
9
|
+
|
10
|
+
### Configuration
|
11
|
+
|
12
|
+
Create config.yml to your liking.
|
13
|
+
|
14
|
+
Example config.yml
|
15
|
+
---
|
16
|
+
bind: 127.0.0.1
|
17
|
+
port: 8125
|
18
|
+
|
19
|
+
# Flush interval should be your finest retention in seconds
|
20
|
+
flush_interval: 10
|
21
|
+
|
22
|
+
# Graphite
|
23
|
+
graphite_host: localhost
|
24
|
+
graphite_port: 2003
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
### Server
|
29
|
+
Run the server:
|
30
|
+
|
31
|
+
Flush to Graphite (default):
|
32
|
+
statsd -c config.yml
|
33
|
+
|
34
|
+
### Client
|
35
|
+
In your client code:
|
36
|
+
|
37
|
+
require 'rubygems'
|
38
|
+
require 'statsd'
|
39
|
+
STATSD = Statsd::Client.new('localhost',8125)
|
40
|
+
|
41
|
+
STATSD.increment('some_counter') # basic incrementing
|
42
|
+
STATSD.increment('system.nested_counter', 0.1) # incrementing with sampling (10%)
|
43
|
+
|
44
|
+
STATSD.decrement(:some_other_counter) # basic decrememting using a symbol
|
45
|
+
STATSD.decrement('system.nested_counter', 0.1) # decrementing with sampling (10%)
|
46
|
+
|
47
|
+
STATSD.timing('some_job_time', 20) # reporting job that took 20ms
|
48
|
+
STATSD.timing('some_job_time', 20, 0.05) # reporting job that took 20ms with sampling (5% sampling)
|
49
|
+
|
50
|
+
Concepts
|
51
|
+
--------
|
52
|
+
|
53
|
+
* *buckets*
|
54
|
+
Each stat is in it's own "bucket". They are not predefined anywhere. Buckets can be named anything that will translate to Graphite (periods make folders, etc)
|
55
|
+
|
56
|
+
* *values*
|
57
|
+
Each stat will have a value. How it is interpreted depends on modifiers
|
58
|
+
|
59
|
+
* *flush*
|
60
|
+
After the flush interval timeout (default 10 seconds), stats are munged and sent over to Graphite.
|
61
|
+
|
62
|
+
Counting
|
63
|
+
--------
|
64
|
+
|
65
|
+
gorets:1|c
|
66
|
+
|
67
|
+
This is a simple counter. Add 1 to the "gorets" bucket. It stays in memory until the flush interval.
|
68
|
+
|
69
|
+
|
70
|
+
Timing
|
71
|
+
------
|
72
|
+
|
73
|
+
glork:320|ms
|
74
|
+
|
75
|
+
The glork took 320ms to complete this time. StatsD figures out 90th percentile, average (mean), lower and upper bounds for the flush interval.
|
76
|
+
|
77
|
+
Sampling
|
78
|
+
--------
|
79
|
+
|
80
|
+
gorets:1|c|@0.1
|
81
|
+
|
82
|
+
Tells StatsD that this counter is being sent sampled ever 1/10th of the time.
|
83
|
+
|
84
|
+
|
85
|
+
Guts
|
86
|
+
----
|
87
|
+
|
88
|
+
* [UDP][udp]
|
89
|
+
Client libraries use UDP to send information to the StatsD daemon.
|
90
|
+
|
91
|
+
* [EventMachine][eventmachine]
|
92
|
+
* [Graphite][graphite]
|
93
|
+
|
94
|
+
|
95
|
+
Graphite
|
96
|
+
--------
|
97
|
+
|
98
|
+
Graphite uses "schemas" to define the different round robin datasets it houses (analogous to RRAs in rrdtool):
|
99
|
+
|
100
|
+
[stats]
|
101
|
+
priority = 110
|
102
|
+
pattern = ^stats\..*
|
103
|
+
retentions = 10:2160,60:10080,600:262974
|
104
|
+
|
105
|
+
That translates to:
|
106
|
+
|
107
|
+
* 6 hours of 10 second data (what we consider "near-realtime")
|
108
|
+
* 1 week of 1 minute data
|
109
|
+
* 5 years of 10 minute data
|
110
|
+
|
111
|
+
This has been a good tradeoff so far between size-of-file (round robin databases are fixed size) and data we care about. Each "stats" database is about 3.2 megs with these retentions.
|
112
|
+
|
113
|
+
|
114
|
+
Inspiration
|
115
|
+
-----------
|
116
|
+
[Etsy's][etsy] [blog post][blog post].
|
117
|
+
|
118
|
+
StatsD was inspired (heavily) by the project (of the same name) at Flickr. Here's a post where Cal Henderson described it in depth:
|
119
|
+
[Counting and timing](http://code.flickr.com/blog/2008/10/27/counting-timing/). Cal re-released the code recently: [Perl StatsD](https://github.com/iamcal/Flickr-StatsD)
|
120
|
+
|
121
|
+
|
122
|
+
[graphite]: http://graphite.wikidot.com
|
123
|
+
[etsy]: http://www.etsy.com
|
124
|
+
[blog post]: http://codeascraft.etsy.com/2011/02/15/measure-anything-measure-everything/
|
125
|
+
[udp]: http://enwp.org/udp
|
126
|
+
[eventmachine]: http://rubyeventmachine.com/
|
data/Rakefile
ADDED
data/bin/statsd
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
require 'yaml'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
begin
|
8
|
+
ORIGINAL_ARGV = ARGV.dup
|
9
|
+
options = {}
|
10
|
+
|
11
|
+
parser = OptionParser.new do |opts|
|
12
|
+
opts.banner = "Usage: statsd [options]"
|
13
|
+
|
14
|
+
opts.separator ""
|
15
|
+
opts.separator "Options:"
|
16
|
+
|
17
|
+
opts.on("-cCONFIG", "--config-file CONFIG", "Configuration file") do |x|
|
18
|
+
options[:config] = x
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on("-h", "--help", "Show this message") do
|
22
|
+
puts opts
|
23
|
+
exit
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
parser.parse!
|
28
|
+
|
29
|
+
# dispatch
|
30
|
+
if !options[:config]
|
31
|
+
puts parser.help
|
32
|
+
else
|
33
|
+
require 'statsd'
|
34
|
+
require 'statsd/server'
|
35
|
+
Statsd::Server::Daemon.new.run(options)
|
36
|
+
end
|
37
|
+
rescue Exception => e
|
38
|
+
if e.instance_of?(SystemExit)
|
39
|
+
raise
|
40
|
+
else
|
41
|
+
puts 'Uncaught exception'
|
42
|
+
puts e.message
|
43
|
+
puts e.backtrace.join("\n")
|
44
|
+
end
|
45
|
+
end
|
data/config.yml
ADDED
data/lib/statsd.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'resolv'
|
3
|
+
|
4
|
+
module Statsd
|
5
|
+
# initialize singleton instance in an initializer
|
6
|
+
def self.create_instance(opts={})
|
7
|
+
raise "Already initialized Statsd" if defined? @@instance
|
8
|
+
@@instance ||= Client.new(opts)
|
9
|
+
end
|
10
|
+
|
11
|
+
# access singleton instance, which must have been initialized with #create_instance
|
12
|
+
def self.instance
|
13
|
+
raise "Statsd has not been initialized" unless @@instance
|
14
|
+
@@instance
|
15
|
+
end
|
16
|
+
|
17
|
+
class Client
|
18
|
+
attr_accessor :host, :port, :prefix
|
19
|
+
|
20
|
+
def initialize(opts={})
|
21
|
+
@host = opts[:host] || 'localhost'
|
22
|
+
@port = opts[:port] || 8125
|
23
|
+
@prefix = opts[:prefix]
|
24
|
+
end
|
25
|
+
|
26
|
+
def host_ip_addr
|
27
|
+
@host_ip_addr ||= Resolv.getaddress(host)
|
28
|
+
end
|
29
|
+
|
30
|
+
def host=(h)
|
31
|
+
@host_ip_addr = nil
|
32
|
+
@host = h
|
33
|
+
end
|
34
|
+
|
35
|
+
# +stat+ to log timing for
|
36
|
+
# +time+ is the time to log in ms
|
37
|
+
def timing(stat, time = nil, sample_rate = 1)
|
38
|
+
value = nil
|
39
|
+
if block_given?
|
40
|
+
start_time = Time.now.to_f
|
41
|
+
value = yield
|
42
|
+
time = ((Time.now.to_f - start_time) * 1000).floor
|
43
|
+
end
|
44
|
+
|
45
|
+
if @prefix
|
46
|
+
stat = "#{@prefix}.#{stat}"
|
47
|
+
end
|
48
|
+
|
49
|
+
send_stats("#{stat}:#{time}|ms", sample_rate)
|
50
|
+
value
|
51
|
+
end
|
52
|
+
|
53
|
+
# +stats+ can be a string or an array of strings
|
54
|
+
def increment(stats, sample_rate = 1)
|
55
|
+
if @prefix
|
56
|
+
stats = "#{@prefix}.#{stats}"
|
57
|
+
end
|
58
|
+
update_counter stats, 1, sample_rate
|
59
|
+
end
|
60
|
+
|
61
|
+
# +stats+ can be a string or an array of strings
|
62
|
+
def decrement(stats, sample_rate = 1)
|
63
|
+
if @prefix
|
64
|
+
stats = "#{@prefix}.#{stats}"
|
65
|
+
end
|
66
|
+
update_counter stats, -1, sample_rate
|
67
|
+
end
|
68
|
+
|
69
|
+
# +stats+ can be a string or array of strings
|
70
|
+
def update_counter(stats, delta = 1, sample_rate = 1)
|
71
|
+
stats = Array(stats)
|
72
|
+
send_stats(stats.map { |s| "#{s}:#{delta}|c" }, sample_rate)
|
73
|
+
end
|
74
|
+
|
75
|
+
# +stats+ is a hash
|
76
|
+
def gauge(stats)
|
77
|
+
send_stats(stats.map { |s,val|
|
78
|
+
if @prefix
|
79
|
+
s = "#{@prefix}.#{s}"
|
80
|
+
end
|
81
|
+
"#{s}:#{val}|g"
|
82
|
+
})
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def send_stats(data, sample_rate = 1)
|
88
|
+
data = Array(data)
|
89
|
+
sampled_data = []
|
90
|
+
|
91
|
+
# Apply sample rate if less than one
|
92
|
+
if sample_rate < 1
|
93
|
+
data.each do |d|
|
94
|
+
if rand <= sample_rate
|
95
|
+
sampled_data << "#{d}@#{sample_rate}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
data = sampled_data
|
99
|
+
end
|
100
|
+
|
101
|
+
return if data.empty?
|
102
|
+
|
103
|
+
raise "host and port must be set" unless host && port
|
104
|
+
|
105
|
+
begin
|
106
|
+
sock = UDPSocket.new
|
107
|
+
data.each do |d|
|
108
|
+
sock.send(d, 0, host, port)
|
109
|
+
end
|
110
|
+
rescue # silent but deadly
|
111
|
+
ensure
|
112
|
+
sock.close
|
113
|
+
end
|
114
|
+
true
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
module Rails
|
120
|
+
# to monitor all actions for this controller (and its descendents) with graphite,
|
121
|
+
# use "around_filter Statsd::Rails::ActionTimerFilter"
|
122
|
+
class ActionTimerFilter
|
123
|
+
def self.filter(controller, &block)
|
124
|
+
key = "requests.#{controller.controller_name}.#{controller.params[:action]}"
|
125
|
+
Statsd.instance.timing(key, &block)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
data/lib/statsd/echos.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'eventmachine'
|
6
|
+
|
7
|
+
module EchoServer
|
8
|
+
def post_init
|
9
|
+
puts "-- someone connected to the server!"
|
10
|
+
end
|
11
|
+
|
12
|
+
def receive_data data
|
13
|
+
puts data
|
14
|
+
send_data ">>> you sent: #{data}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
EventMachine::run {
|
19
|
+
EventMachine::start_server "127.0.0.1", 2003, EchoServer
|
20
|
+
puts 'running dummy graphite echo server on 2003'
|
21
|
+
}
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require 'eventmachine'
|
3
|
+
|
4
|
+
|
5
|
+
module Statsd
|
6
|
+
class Graphite < EM::Connection
|
7
|
+
attr_accessor :counters, :timers, :flush_interval
|
8
|
+
|
9
|
+
def flush_stats
|
10
|
+
puts "#{Time.now} Flushing #{counters.count} counters and #{timers.count} timers to Graphite."
|
11
|
+
|
12
|
+
stat_string = ''
|
13
|
+
|
14
|
+
ts = Time.now.to_i
|
15
|
+
num_stats = 0
|
16
|
+
|
17
|
+
# store counters
|
18
|
+
counters.each_pair do |key,value|
|
19
|
+
message = "#{key} #{value} #{ts}\n"
|
20
|
+
stat_string += message
|
21
|
+
counters[key] = 0
|
22
|
+
|
23
|
+
num_stats += 1
|
24
|
+
end
|
25
|
+
|
26
|
+
# store timers
|
27
|
+
timers.each_pair do |key, values|
|
28
|
+
if (values.length > 0)
|
29
|
+
pct_threshold = 90
|
30
|
+
values.sort!
|
31
|
+
count = values.count
|
32
|
+
min = values.first
|
33
|
+
max = values.last
|
34
|
+
|
35
|
+
mean = min
|
36
|
+
max_at_threshold = max
|
37
|
+
|
38
|
+
if (count > 1)
|
39
|
+
# average all the timing data
|
40
|
+
sum = values.inject( 0 ) { |s,x| s+x }
|
41
|
+
mean = sum / values.count
|
42
|
+
|
43
|
+
# strip off the top 100-threshold
|
44
|
+
threshold_index = (((100 - pct_threshold) / 100.0) * count).round
|
45
|
+
values = values[0..-threshold_index]
|
46
|
+
max_at_threshold = values.last
|
47
|
+
end
|
48
|
+
|
49
|
+
message = ""
|
50
|
+
message += "#{key}.mean #{mean} #{ts}\n"
|
51
|
+
message += "#{key}.upper #{max} #{ts}\n"
|
52
|
+
message += "#{key}.upper_#{pct_threshold} #{max_at_threshold} #{ts}\n"
|
53
|
+
message += "#{key}.lower #{min} #{ts}\n"
|
54
|
+
message += "#{key}.count #{count} #{ts}\n"
|
55
|
+
stat_string += message
|
56
|
+
|
57
|
+
timers[key] = []
|
58
|
+
|
59
|
+
num_stats += 1
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
stat_string += "statsd.numStats #{num_stats} #{ts}\n"
|
64
|
+
|
65
|
+
# send to graphite
|
66
|
+
send_data stat_string
|
67
|
+
close_connection_after_writing
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'yaml'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
require 'statsd/graphite'
|
6
|
+
|
7
|
+
module Statsd
|
8
|
+
module Server
|
9
|
+
Version = '0.5.5'
|
10
|
+
|
11
|
+
FLUSH_INTERVAL = 10
|
12
|
+
COUNTERS = {}
|
13
|
+
TIMERS = {}
|
14
|
+
GAUGES = {}
|
15
|
+
|
16
|
+
def post_init
|
17
|
+
puts "statsd server started!"
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.get_and_clear_stats!
|
21
|
+
counters = COUNTERS.dup
|
22
|
+
timers = TIMERS.dup
|
23
|
+
gauges = GAUGES.dup
|
24
|
+
COUNTERS.clear
|
25
|
+
TIMERS.clear
|
26
|
+
GAUGES.clear
|
27
|
+
[counters,timers,gauges]
|
28
|
+
end
|
29
|
+
|
30
|
+
def receive_data(msg)
|
31
|
+
msg.split("\n").each do |row|
|
32
|
+
bits = row.split(':')
|
33
|
+
key = bits.shift.gsub(/\s+/, '_').gsub(/\//, '-').gsub(/[^a-zA-Z_\-0-9\.]/, '')
|
34
|
+
bits.each do |record|
|
35
|
+
sample_rate = 1
|
36
|
+
fields = record.split("|")
|
37
|
+
if fields.nil? || fields.count < 2
|
38
|
+
next
|
39
|
+
end
|
40
|
+
if (fields[1].strip == "ms")
|
41
|
+
TIMERS[key] ||= []
|
42
|
+
TIMERS[key].push(fields[0].to_i)
|
43
|
+
elsif (fields[1].strip == "c")
|
44
|
+
if (fields[2] && fields[2].match(/^@([\d\.]+)/))
|
45
|
+
sample_rate = fields[2].match(/^@([\d\.]+)/)[1]
|
46
|
+
end
|
47
|
+
COUNTERS[key] ||= 0
|
48
|
+
COUNTERS[key] += (fields[0].to_i || 1) * (1.0 / sample_rate.to_f)
|
49
|
+
elsif (fields[1].strip == "g")
|
50
|
+
GAUGES[key] ||= (fields[0].to_i || 0)
|
51
|
+
else
|
52
|
+
puts "Invalid statistic #{fields.inspect} received; ignoring"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Daemon
|
59
|
+
def run(options)
|
60
|
+
config = YAML::load(ERB.new(IO.read(options[:config])).result)
|
61
|
+
|
62
|
+
EventMachine::run do
|
63
|
+
EventMachine::open_datagram_socket(config['bind'], config['port'], Statsd::Server)
|
64
|
+
puts "Listening on #{config['bind']}:#{config['port']}"
|
65
|
+
|
66
|
+
# Periodically Flush
|
67
|
+
EventMachine::add_periodic_timer(config['flush_interval']) do
|
68
|
+
counters,timers = Statsd::Server.get_and_clear_stats!
|
69
|
+
|
70
|
+
EventMachine.connect config['graphite_host'], config['graphite_port'], Statsd::Graphite do |conn|
|
71
|
+
conn.counters = counters
|
72
|
+
conn.timers = timers
|
73
|
+
conn.flush_interval = config['flush_interval']
|
74
|
+
conn.flush_stats
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
data/lib/statsd/test.rb
ADDED
data/netcat-example.sh
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
|
4
|
+
describe Statsd::Server do
|
5
|
+
include Statsd::Server
|
6
|
+
|
7
|
+
describe :receive_data do
|
8
|
+
it 'should not vomit on bad data' do
|
9
|
+
bad_data = "dev.rwygand.app.flexd.exception.no action responded to index. actions: authenticate, authentication_request, authorization, bubble_stacktrace?, decode_credentials, encode_credentials, not_found, and user_name_and_password:1|c"
|
10
|
+
|
11
|
+
expect {
|
12
|
+
receive_data(bad_data)
|
13
|
+
}.not_to raise_error
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/spec/statsd_spec.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Statsd do
|
4
|
+
describe '#create_instance' do
|
5
|
+
after(:each) do
|
6
|
+
Statsd.send(:remove_class_variable, :@@instance)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should create an instance' do
|
10
|
+
Statsd.create_instance
|
11
|
+
Statsd.instance.should_not be nil
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should raise if called twice' do
|
15
|
+
Statsd.create_instance
|
16
|
+
expect { Statsd.create_instance }.to raise_error
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#instance' do
|
21
|
+
it 'should raise if not created' do
|
22
|
+
expect { Statsd.instance }.to raise_error
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe Statsd::Client do
|
28
|
+
describe '#initialize' do
|
29
|
+
it 'should work without arguments' do
|
30
|
+
c = Statsd::Client.new
|
31
|
+
c.should_not be nil
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should accept a :host keyword argument' do
|
35
|
+
host = 'zombo.com'
|
36
|
+
c = Statsd::Client.new(:host => host)
|
37
|
+
c.host.should match(host)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should accept a :port keyword argument' do
|
41
|
+
port = 1337
|
42
|
+
c = Statsd::Client.new(:port => port)
|
43
|
+
c.port.should == port
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should accept a :prefix keyword argument' do
|
47
|
+
prefix = 'dev'
|
48
|
+
c = Statsd::Client.new(:prefix => prefix)
|
49
|
+
c.prefix.should match(prefix)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '#timing' do
|
54
|
+
let(:c) { Statsd::Client.new }
|
55
|
+
|
56
|
+
it 'should pass the sample rate along' do
|
57
|
+
sample = 10
|
58
|
+
c.should_receive(:send_stats).with(anything(), sample)
|
59
|
+
c.timing('foo', 1, sample)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should use the right stat name' do
|
63
|
+
c.should_receive(:send_stats).with('foo:1|ms', anything())
|
64
|
+
c.timing('foo', 1)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should prefix its stats if it has a prefix' do
|
68
|
+
c.should_receive(:send_stats).with('dev.foo:1|ms', anything())
|
69
|
+
c.prefix = 'dev'
|
70
|
+
c.timing('foo', 1)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should wrap a block correctly' do
|
74
|
+
# Pretend our block took one second
|
75
|
+
c.should_receive(:send_stats).with('foo:1000|ms', anything())
|
76
|
+
Time.stub_chain(:now, :to_f).and_return(1, 2)
|
77
|
+
|
78
|
+
c.timing('foo') do
|
79
|
+
true.should be true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should return the return value from the block' do
|
84
|
+
# Pretend our block took one second
|
85
|
+
c.should_receive(:send_stats).with('foo:1000|ms', anything())
|
86
|
+
Time.stub_chain(:now, :to_f).and_return(1, 2)
|
87
|
+
|
88
|
+
value = c.timing('foo') { 1337 }
|
89
|
+
value.should == 1337
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe '#increment' do
|
94
|
+
let(:c) { Statsd::Client.new }
|
95
|
+
|
96
|
+
it 'should prepend the prefix if it has one' do
|
97
|
+
c.prefix = 'dev'
|
98
|
+
c.should_receive(:update_counter).with('dev.foo', anything(), anything())
|
99
|
+
c.increment('foo')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe '#decrement' do
|
104
|
+
let(:c) { Statsd::Client.new }
|
105
|
+
|
106
|
+
it 'should prepend the prefix if it has one' do
|
107
|
+
c.prefix = 'dev'
|
108
|
+
c.should_receive(:update_counter).with('dev.foo', anything(), anything())
|
109
|
+
c.decrement('foo')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe '#gauge' do
|
114
|
+
let(:c) { Statsd::Client.new }
|
115
|
+
|
116
|
+
it 'should encode the values correctly' do
|
117
|
+
c.should_receive(:send_stats).with do |array|
|
118
|
+
array.should include('foo:1|g')
|
119
|
+
array.should include('bar:2|g')
|
120
|
+
end
|
121
|
+
c.gauge('foo' => 1, 'bar' => 2)
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'should prepend the prefix if it has one' do
|
125
|
+
c.prefix = 'dev'
|
126
|
+
c.should_receive(:send_stats).with(['dev.foo:1|g'])
|
127
|
+
c.gauge('foo' => 1)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/stats.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'statsd'
|
3
|
+
require 'statsd/server'
|
4
|
+
require 'statsd/graphite'
|
5
|
+
|
6
|
+
require 'yaml'
|
7
|
+
require 'erb'
|
8
|
+
|
9
|
+
ROOT = File.expand_path(File.dirname(__FILE__))
|
10
|
+
APP_CONFIG = YAML::load(ERB.new(IO.read(File.join(ROOT,'config.yml'))).result)
|
11
|
+
|
12
|
+
# Start the server
|
13
|
+
EventMachine::run do
|
14
|
+
EventMachine::open_datagram_socket('127.0.0.1', 8125, Statsd::Server)
|
15
|
+
EventMachine::add_periodic_timer(APP_CONFIG['flush_interval']) do
|
16
|
+
counters,timers = Statsd::Server.get_and_clear_stats!
|
17
|
+
|
18
|
+
# Graphite
|
19
|
+
EventMachine.connect APP_CONFIG['graphite_host'], APP_CONFIG['graphite_port'], Statsd::Graphite do |conn|
|
20
|
+
conn.counters = counters
|
21
|
+
conn.timers = timers
|
22
|
+
conn.flush_interval = 10
|
23
|
+
conn.flush_stats
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
end
|
data/statsd.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "lookout-statsd"
|
5
|
+
s.version = "0.7.#{ENV['BUILD_NUMBER'] || 'dev'}"
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
|
8
|
+
s.authors = ['R. Tyler Croy', 'Andrew Coldham', 'Ben VandenBos']
|
9
|
+
s.email = ['rtyler.croy@mylookout.com']
|
10
|
+
s.homepage = "https://github.com/lookout/statsd"
|
11
|
+
|
12
|
+
s.summary = "Ruby version of statsd."
|
13
|
+
s.description = "A network daemon for aggregating statistics (counters and timers), rolling them up, then sending them to graphite."
|
14
|
+
|
15
|
+
s.required_rubygems_version = ">= 1.3.6"
|
16
|
+
|
17
|
+
s.add_dependency "eventmachine", ">= 0.12.10"
|
18
|
+
s.add_dependency "erubis", ">= 2.6.6"
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
|
22
|
+
s.require_path = 'lib'
|
23
|
+
end
|
24
|
+
|
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lookout-statsd
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 3
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 7
|
9
|
+
- 0
|
10
|
+
version: 0.7.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- R. Tyler Croy
|
14
|
+
- Andrew Coldham
|
15
|
+
- Ben VandenBos
|
16
|
+
autorequire:
|
17
|
+
bindir: bin
|
18
|
+
cert_chain: []
|
19
|
+
|
20
|
+
date: 2013-04-08 00:00:00 Z
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 59
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
- 12
|
32
|
+
- 10
|
33
|
+
version: 0.12.10
|
34
|
+
prerelease: false
|
35
|
+
type: :runtime
|
36
|
+
requirement: *id001
|
37
|
+
name: eventmachine
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 27
|
45
|
+
segments:
|
46
|
+
- 2
|
47
|
+
- 6
|
48
|
+
- 6
|
49
|
+
version: 2.6.6
|
50
|
+
prerelease: false
|
51
|
+
type: :runtime
|
52
|
+
requirement: *id002
|
53
|
+
name: erubis
|
54
|
+
description: A network daemon for aggregating statistics (counters and timers), rolling them up, then sending them to graphite.
|
55
|
+
email:
|
56
|
+
- rtyler.croy@mylookout.com
|
57
|
+
executables:
|
58
|
+
- statsd
|
59
|
+
extensions: []
|
60
|
+
|
61
|
+
extra_rdoc_files: []
|
62
|
+
|
63
|
+
files:
|
64
|
+
- .gitignore
|
65
|
+
- Gemfile
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- bin/statsd
|
69
|
+
- config.yml
|
70
|
+
- lib/statsd.rb
|
71
|
+
- lib/statsd/echos.rb
|
72
|
+
- lib/statsd/graphite.rb
|
73
|
+
- lib/statsd/server.rb
|
74
|
+
- lib/statsd/test.rb
|
75
|
+
- netcat-example.sh
|
76
|
+
- spec/spec_helper.rb
|
77
|
+
- spec/statsd/server_spec.rb
|
78
|
+
- spec/statsd_spec.rb
|
79
|
+
- stats.rb
|
80
|
+
- statsd.gemspec
|
81
|
+
homepage: https://github.com/lookout/statsd
|
82
|
+
licenses: []
|
83
|
+
|
84
|
+
post_install_message:
|
85
|
+
rdoc_options: []
|
86
|
+
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
hash: 3
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
version: "0"
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
hash: 23
|
104
|
+
segments:
|
105
|
+
- 1
|
106
|
+
- 3
|
107
|
+
- 6
|
108
|
+
version: 1.3.6
|
109
|
+
requirements: []
|
110
|
+
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 1.8.25
|
113
|
+
signing_key:
|
114
|
+
specification_version: 3
|
115
|
+
summary: Ruby version of statsd.
|
116
|
+
test_files: []
|
117
|
+
|