lookout-statsd 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|