justinaiken-statsd 1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.gitignore +43 -0
- data/.travis.yml +6 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +88 -0
- data/Rakefile +15 -0
- data/justinaiken-statsd.gemspec +25 -0
- data/lib/statsd-ruby.rb +1 -0
- data/lib/statsd.rb +402 -0
- data/spec/helper.rb +41 -0
- data/spec/statsd_admin_spec.rb +97 -0
- data/spec/statsd_spec.rb +425 -0
- metadata +118 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b6b8132592957c3e992e9302fd0b454721e6418a
|
4
|
+
data.tar.gz: b8632d053418d4eaca9f62930e5f7bce5c2908a3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 49d8768b3bee014dcd58111e19e1a8ab5ee78e69a7dbcdd6b54489320eda99bcb229042c887f67cb45d4e40ba47bf4a45587a815cde49b56daa01eacbe6a9775
|
7
|
+
data.tar.gz: a01b02b8bae3c7d3b887d9694bc3bb545c96ff5dbd0cd612946d0db9cf834b40f09fe6a96cee6bf54cef04d3d514e97261ad19fb9c3826f78dc72237098c9108
|
data/.document
ADDED
data/.gitignore
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# simplecov generated
|
2
|
+
coverage
|
3
|
+
|
4
|
+
# rdoc generated
|
5
|
+
rdoc
|
6
|
+
|
7
|
+
# yard generated
|
8
|
+
doc
|
9
|
+
.yardoc
|
10
|
+
|
11
|
+
# bundler
|
12
|
+
.bundle
|
13
|
+
Gemfile.lock
|
14
|
+
|
15
|
+
# jeweler generated
|
16
|
+
pkg
|
17
|
+
|
18
|
+
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
|
19
|
+
#
|
20
|
+
# * Create a file at ~/.gitignore
|
21
|
+
# * Include files you want ignored
|
22
|
+
# * Run: git config --global core.excludesfile ~/.gitignore
|
23
|
+
#
|
24
|
+
# After doing this, these files will be ignored in all your git projects,
|
25
|
+
# saving you from having to 'pollute' every project you touch with them
|
26
|
+
#
|
27
|
+
# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
|
28
|
+
#
|
29
|
+
# For MacOS:
|
30
|
+
#
|
31
|
+
#.DS_Store
|
32
|
+
#
|
33
|
+
# For TextMate
|
34
|
+
#*.tmproj
|
35
|
+
#tmtags
|
36
|
+
#
|
37
|
+
# For emacs:
|
38
|
+
#*~
|
39
|
+
#\#*
|
40
|
+
#.\#*
|
41
|
+
#
|
42
|
+
# For vim:
|
43
|
+
#*.swp
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011, 2012, 2013 Rein Henrichs
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
= statsd-ruby {<img src="https://secure.travis-ci.org/JustinAiken/statsd.png" />}[http://travis-ci.org/JustinAiken/statsd]
|
2
|
+
|
3
|
+
A Ruby client for {StatsD}[https://github.com/etsy/statsd]
|
4
|
+
|
5
|
+
Forked from {reinh's StatsD}[https://github.com/reinh/statsd]
|
6
|
+
|
7
|
+
= Installing
|
8
|
+
|
9
|
+
Bundler:
|
10
|
+
gem "justinaiken-statsd"
|
11
|
+
|
12
|
+
= Basic Usage
|
13
|
+
|
14
|
+
# Set up a global Statsd client for a server on localhost:9125
|
15
|
+
$statsd = Statsd.new 'localhost', 9125
|
16
|
+
|
17
|
+
# Set up a global Statsd client for a server on IPv6 port 9125
|
18
|
+
$statsd = Statsd.new '::1', 9125
|
19
|
+
|
20
|
+
# Send some stats
|
21
|
+
$statsd.increment 'garets'
|
22
|
+
$statsd.timing 'glork', 320
|
23
|
+
$statsd.gauge 'bork', 100
|
24
|
+
|
25
|
+
# Use {#time} to time the execution of a block
|
26
|
+
$statsd.time('account.activate') { @account.activate! }
|
27
|
+
|
28
|
+
# Create a namespaced statsd client and increment 'account.activate'
|
29
|
+
statsd = Statsd.new('localhost').tap{|sd| sd.namespace = 'account'}
|
30
|
+
statsd.increment 'activate'
|
31
|
+
|
32
|
+
# Create a Statsd client that uses a single socket
|
33
|
+
statsd = Statsd.new 'localhost', 9125, UDPSocket.new
|
34
|
+
|
35
|
+
= Testing
|
36
|
+
|
37
|
+
Run the specs with <tt>rake spec</tt>
|
38
|
+
|
39
|
+
Run the specs and include live integration specs with <tt>LIVE=true rake spec</tt>. Note: This will test over a real UDP socket.
|
40
|
+
|
41
|
+
= Performance
|
42
|
+
|
43
|
+
* A short note about DNS: If you use a dns name for the host option, then you will want to use a local caching dns service for optimial performance (e.g. nscd).
|
44
|
+
|
45
|
+
= Extensions / Libraries / Extra Docs
|
46
|
+
|
47
|
+
* See the wiki[https://github.com/reinh/statsd/wiki]
|
48
|
+
|
49
|
+
== Contributing to statsd
|
50
|
+
|
51
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
52
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
53
|
+
* Fork the project
|
54
|
+
* Start a feature/bugfix branch
|
55
|
+
* Commit and push until you are happy with your contribution
|
56
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
57
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
58
|
+
|
59
|
+
== Contributors
|
60
|
+
|
61
|
+
* Rein Henrichs
|
62
|
+
* Ray Krueger
|
63
|
+
* Jeremy Kemper
|
64
|
+
* Ryan Tomayko
|
65
|
+
* Gabriel Burt
|
66
|
+
* Rick Olson
|
67
|
+
* Trae Robrock
|
68
|
+
* Corey Donohoe
|
69
|
+
* James Tucker
|
70
|
+
* Dotan Nahum
|
71
|
+
* Eric Chapweske
|
72
|
+
* Hannes Georg
|
73
|
+
* John Nunemaker
|
74
|
+
* Mahesh Murthy
|
75
|
+
* Manu J
|
76
|
+
* Matt Sanford
|
77
|
+
* Nate Bird
|
78
|
+
* Noah Lorang
|
79
|
+
* Oscar Del Ben
|
80
|
+
* Peter Mounce
|
81
|
+
* Ray Krueger
|
82
|
+
* Reed Lipman
|
83
|
+
* Thomas Whaples
|
84
|
+
* Justin Aiken
|
85
|
+
|
86
|
+
== Copyright
|
87
|
+
|
88
|
+
Copyright (c) 2011, 2012, 2013 Rein Henrichs. See LICENSE.txt for further details.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
task :default => :spec
|
5
|
+
|
6
|
+
require 'rake/testtask'
|
7
|
+
Rake::TestTask.new(:spec) do |spec|
|
8
|
+
spec.libs << 'lib' << 'spec'
|
9
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
10
|
+
spec.verbose = true
|
11
|
+
spec.warning = true
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'yard'
|
15
|
+
YARD::Rake::YardocTask.new
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new("justinaiken-statsd", "1.3") do |s|
|
4
|
+
s.authors = ["Justin Aiken"]
|
5
|
+
s.email = "60tonangel@gmail.com"
|
6
|
+
|
7
|
+
s.summary = "A Ruby StatsD client, with a tweak"
|
8
|
+
s.description = "A Ruby StatsD client, with a tweak "
|
9
|
+
|
10
|
+
s.homepage = "https://github.com/JustinAiken/statsd"
|
11
|
+
s.licenses = %w[MIT]
|
12
|
+
|
13
|
+
s.extra_rdoc_files = %w[LICENSE.txt README.rdoc]
|
14
|
+
|
15
|
+
if $0 =~ /gem/ # If running under rubygems (building), otherwise, just leave
|
16
|
+
s.files = `git ls-files`.split($\)
|
17
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
|
20
|
+
s.add_development_dependency "minitest", ">= 3.2.0"
|
21
|
+
s.add_development_dependency "yard"
|
22
|
+
s.add_development_dependency "simplecov", ">= 0.6.4"
|
23
|
+
s.add_development_dependency "rake"
|
24
|
+
end
|
25
|
+
|
data/lib/statsd-ruby.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'statsd'
|
data/lib/statsd.rb
ADDED
@@ -0,0 +1,402 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
# = Statsd: A Statsd client (https://github.com/etsy/statsd)
|
6
|
+
#
|
7
|
+
# @example Set up a global Statsd client for a server on localhost:8125
|
8
|
+
# $statsd = Statsd.new 'localhost', 8125
|
9
|
+
# @example Set up a global Statsd client for a server on IPv6 port 8125
|
10
|
+
# $statsd = Statsd.new '::1', 8125
|
11
|
+
# @example Send some stats
|
12
|
+
# $statsd.increment 'garets'
|
13
|
+
# $statsd.timing 'glork', 320
|
14
|
+
# $statsd.gauge 'bork', 100
|
15
|
+
# @example Use {#time} to time the execution of a block
|
16
|
+
# $statsd.time('account.activate') { @account.activate! }
|
17
|
+
# @example Create a namespaced statsd client and increment 'account.activate'
|
18
|
+
# statsd = Statsd.new('localhost').tap{|sd| sd.namespace = 'account'}
|
19
|
+
# statsd.increment 'activate'
|
20
|
+
#
|
21
|
+
# Statsd instances are thread safe for general usage, by using a thread local
|
22
|
+
# UDPSocket and carrying no state. The attributes are stateful, and are not
|
23
|
+
# mutexed, it is expected that users will not change these at runtime in
|
24
|
+
# threaded environments. If users require such use cases, it is recommend that
|
25
|
+
# users either mutex around their Statsd object, or create separate objects for
|
26
|
+
# each namespace / host+port combination.
|
27
|
+
class Statsd
|
28
|
+
|
29
|
+
# = Batch: A batching statsd proxy
|
30
|
+
#
|
31
|
+
# @example Batch a set of instruments using Batch and manual flush:
|
32
|
+
# $statsd = Statsd.new 'localhost', 8125
|
33
|
+
# batch = Statsd::Batch.new($statsd)
|
34
|
+
# batch.increment 'garets'
|
35
|
+
# batch.timing 'glork', 320
|
36
|
+
# batch.gauge 'bork', 100
|
37
|
+
# batch.flush
|
38
|
+
#
|
39
|
+
# Batch is a subclass of Statsd, but with a constructor that proxies to a
|
40
|
+
# normal Statsd instance. It has it's own batch_size and namespace parameters
|
41
|
+
# (that inherit defaults from the supplied Statsd instance). It is recommended
|
42
|
+
# that some care is taken if setting very large batch sizes. If the batch size
|
43
|
+
# exceeds the allowed packet size for UDP on your network, communication
|
44
|
+
# troubles may occur and data will be lost.
|
45
|
+
class Batch < Statsd
|
46
|
+
|
47
|
+
extend Forwardable
|
48
|
+
def_delegators :@statsd,
|
49
|
+
:namespace, :namespace=,
|
50
|
+
:host, :host=,
|
51
|
+
:port, :port=,
|
52
|
+
:prefix,
|
53
|
+
:postfix
|
54
|
+
|
55
|
+
attr_accessor :batch_size
|
56
|
+
|
57
|
+
# @param [Statsd] requires a configured Statsd instance
|
58
|
+
def initialize(statsd)
|
59
|
+
@statsd = statsd
|
60
|
+
@batch_size = statsd.batch_size
|
61
|
+
@backlog = []
|
62
|
+
end
|
63
|
+
|
64
|
+
# @yields [Batch] yields itself
|
65
|
+
#
|
66
|
+
# A convenience method to ensure that data is not lost in the event of an
|
67
|
+
# exception being thrown. Batches will be transmitted on the parent socket
|
68
|
+
# as soon as the batch is full, and when the block finishes.
|
69
|
+
def easy
|
70
|
+
yield self
|
71
|
+
ensure
|
72
|
+
flush
|
73
|
+
end
|
74
|
+
|
75
|
+
def flush
|
76
|
+
unless @backlog.empty?
|
77
|
+
@statsd.send_to_socket @backlog.join("\n")
|
78
|
+
@backlog.clear
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
|
84
|
+
def send_to_socket(message)
|
85
|
+
@backlog << message
|
86
|
+
if @backlog.size >= @batch_size
|
87
|
+
flush
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
class Admin
|
94
|
+
# StatsD host. Defaults to 127.0.0.1.
|
95
|
+
attr_reader :host
|
96
|
+
|
97
|
+
# StatsD admin port. Defaults to 8126.
|
98
|
+
attr_reader :port
|
99
|
+
|
100
|
+
class << self
|
101
|
+
# Set to a standard logger instance to enable debug logging.
|
102
|
+
attr_accessor :logger
|
103
|
+
end
|
104
|
+
|
105
|
+
# @attribute [w] host
|
106
|
+
# Writes are not thread safe.
|
107
|
+
def host=(host)
|
108
|
+
@host = host || '127.0.0.1'
|
109
|
+
end
|
110
|
+
|
111
|
+
# @attribute [w] port
|
112
|
+
# Writes are not thread safe.
|
113
|
+
def port=(port)
|
114
|
+
@port = port || 8126
|
115
|
+
end
|
116
|
+
|
117
|
+
# @param [String] host your statsd host
|
118
|
+
# @param [Integer] port your statsd port
|
119
|
+
def initialize(host = '127.0.0.1', port = 8126)
|
120
|
+
self.host, self.port = host, port
|
121
|
+
end
|
122
|
+
|
123
|
+
# Reads all gauges from StatsD.
|
124
|
+
def gauges
|
125
|
+
read_metric :gauges
|
126
|
+
end
|
127
|
+
|
128
|
+
# Reads all timers from StatsD.
|
129
|
+
def timers
|
130
|
+
read_metric :timers
|
131
|
+
end
|
132
|
+
|
133
|
+
# Reads all counters from StatsD.
|
134
|
+
def counters
|
135
|
+
read_metric :counters
|
136
|
+
end
|
137
|
+
|
138
|
+
# @param[String] item
|
139
|
+
# Deletes one or more gauges. Wildcards are allowed.
|
140
|
+
def delgauges item
|
141
|
+
delete_metric :gauges, item
|
142
|
+
end
|
143
|
+
|
144
|
+
# @param[String] item
|
145
|
+
# Deletes one or more timers. Wildcards are allowed.
|
146
|
+
def deltimers item
|
147
|
+
delete_metric :timers, item
|
148
|
+
end
|
149
|
+
|
150
|
+
# @param[String] item
|
151
|
+
# Deletes one or more counters. Wildcards are allowed.
|
152
|
+
def delcounters item
|
153
|
+
delete_metric :counters, item
|
154
|
+
end
|
155
|
+
|
156
|
+
def stats
|
157
|
+
# the format of "stats" isn't JSON, who knows why
|
158
|
+
send_to_socket "stats"
|
159
|
+
result = read_from_socket
|
160
|
+
items = {}
|
161
|
+
result.split("\n").each do |line|
|
162
|
+
key, val = line.chomp.split(": ")
|
163
|
+
items[key] = val.to_i
|
164
|
+
end
|
165
|
+
items
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
def read_metric name
|
171
|
+
send_to_socket name
|
172
|
+
result = read_from_socket
|
173
|
+
# for some reason, the reply looks like JSON, but isn't, quite
|
174
|
+
JSON.parse result.gsub("'", "\"")
|
175
|
+
end
|
176
|
+
|
177
|
+
def delete_metric name, item
|
178
|
+
send_to_socket "del#{name} #{item}"
|
179
|
+
result = read_from_socket
|
180
|
+
deleted = []
|
181
|
+
result.split("\n").each do |line|
|
182
|
+
deleted << line.chomp.split(": ")[-1]
|
183
|
+
end
|
184
|
+
deleted
|
185
|
+
end
|
186
|
+
|
187
|
+
def send_to_socket(message)
|
188
|
+
self.class.logger.debug { "Statsd: #{message}" } if self.class.logger
|
189
|
+
socket.write(message.to_s + "\n")
|
190
|
+
rescue => boom
|
191
|
+
self.class.logger.error { "Statsd: #{boom.class} #{boom}" } if self.class.logger
|
192
|
+
nil
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
def read_from_socket
|
197
|
+
buffer = ""
|
198
|
+
loop do
|
199
|
+
line = socket.readline
|
200
|
+
break if line == "END\n"
|
201
|
+
buffer += line
|
202
|
+
end
|
203
|
+
socket.readline # clear the closing newline out of the socket
|
204
|
+
buffer
|
205
|
+
end
|
206
|
+
|
207
|
+
def socket
|
208
|
+
Thread.current[:statsd_admin_socket] ||= TCPSocket.new(host, port)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# A namespace to prepend to all statsd calls.
|
213
|
+
attr_reader :namespace
|
214
|
+
|
215
|
+
# StatsD host. Defaults to 127.0.0.1.
|
216
|
+
attr_reader :host
|
217
|
+
|
218
|
+
# StatsD port. Defaults to 8125.
|
219
|
+
attr_reader :port
|
220
|
+
|
221
|
+
# StatsD namespace prefix, generated from #namespace
|
222
|
+
attr_reader :prefix
|
223
|
+
|
224
|
+
# The default batch size for new batches (default: 10)
|
225
|
+
attr_accessor :batch_size
|
226
|
+
|
227
|
+
# a postfix to append to all metrics
|
228
|
+
attr_reader :postfix
|
229
|
+
|
230
|
+
class << self
|
231
|
+
# Set to a standard logger instance to enable debug logging.
|
232
|
+
attr_accessor :logger
|
233
|
+
end
|
234
|
+
|
235
|
+
# @param [String] host your statsd host
|
236
|
+
# @param [Integer] port your statsd port
|
237
|
+
# @param [Object] A socket object (probably UDPSocket) to reuse
|
238
|
+
def initialize(host = '127.0.0.1', port = 8125, socket = nil)
|
239
|
+
self.host, self.port = host, port
|
240
|
+
@prefix = nil
|
241
|
+
@batch_size = 10
|
242
|
+
@postfix = nil
|
243
|
+
@socket = socket
|
244
|
+
end
|
245
|
+
|
246
|
+
# @attribute [w] namespace
|
247
|
+
# Writes are not thread safe.
|
248
|
+
def namespace=(namespace)
|
249
|
+
@namespace = namespace
|
250
|
+
@prefix = "#{namespace}."
|
251
|
+
end
|
252
|
+
|
253
|
+
# @attribute [w] postfix
|
254
|
+
# A value to be appended to the stat name after a '.'. If the value is
|
255
|
+
# blank then the postfix will be reset to nil (rather than to '.').
|
256
|
+
def postfix=(pf)
|
257
|
+
case pf
|
258
|
+
when nil, false, '' then @postfix = nil
|
259
|
+
else @postfix = ".#{pf}"
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# @attribute [w] host
|
264
|
+
# Writes are not thread safe.
|
265
|
+
def host=(host)
|
266
|
+
@host = host || '127.0.0.1'
|
267
|
+
end
|
268
|
+
|
269
|
+
# @attribute [w] port
|
270
|
+
# Writes are not thread safe.
|
271
|
+
def port=(port)
|
272
|
+
@port = port || 8125
|
273
|
+
end
|
274
|
+
|
275
|
+
# Sends an increment (count = 1) for the given stat to the statsd server.
|
276
|
+
#
|
277
|
+
# @param [String] stat stat name
|
278
|
+
# @param [Numeric] sample_rate sample rate, 1 for always
|
279
|
+
# @see #count
|
280
|
+
def increment(stat, sample_rate=1)
|
281
|
+
count stat, 1, sample_rate
|
282
|
+
end
|
283
|
+
|
284
|
+
# Sends a decrement (count = -1) for the given stat to the statsd server.
|
285
|
+
#
|
286
|
+
# @param [String] stat stat name
|
287
|
+
# @param [Numeric] sample_rate sample rate, 1 for always
|
288
|
+
# @see #count
|
289
|
+
def decrement(stat, sample_rate=1)
|
290
|
+
count stat, -1, sample_rate
|
291
|
+
end
|
292
|
+
|
293
|
+
# Sends an arbitrary count for the given stat to the statsd server.
|
294
|
+
#
|
295
|
+
# @param [String] stat stat name
|
296
|
+
# @param [Integer] count count
|
297
|
+
# @param [Numeric] sample_rate sample rate, 1 for always
|
298
|
+
def count(stat, count, sample_rate=1)
|
299
|
+
send_stats stat, count, :c, sample_rate
|
300
|
+
end
|
301
|
+
|
302
|
+
# Sends an arbitary gauge value for the given stat to the statsd server.
|
303
|
+
#
|
304
|
+
# This is useful for recording things like available disk space,
|
305
|
+
# memory usage, and the like, which have different semantics than
|
306
|
+
# counters.
|
307
|
+
#
|
308
|
+
# @param [String] stat stat name.
|
309
|
+
# @param [Numeric] value gauge value.
|
310
|
+
# @param [Numeric] sample_rate sample rate, 1 for always
|
311
|
+
# @example Report the current user count:
|
312
|
+
# $statsd.gauge('user.count', User.count)
|
313
|
+
def gauge(stat, value, sample_rate=1)
|
314
|
+
send_stats stat, value, :g, sample_rate
|
315
|
+
end
|
316
|
+
|
317
|
+
# Sends an arbitary set value for the given stat to the statsd server.
|
318
|
+
#
|
319
|
+
# This is for recording counts of unique events, which are useful to
|
320
|
+
# see on graphs to correlate to other values. For example, a deployment
|
321
|
+
# might get recorded as a set, and be drawn as annotations on a CPU history
|
322
|
+
# graph.
|
323
|
+
#
|
324
|
+
# @param [String] stat stat name.
|
325
|
+
# @param [Numeric] value event value.
|
326
|
+
# @param [Numeric] sample_rate sample rate, 1 for always
|
327
|
+
# @example Report a deployment happening:
|
328
|
+
# $statsd.set('deployment', DEPLOYMENT_EVENT_CODE)
|
329
|
+
def set(stat, value, sample_rate=1)
|
330
|
+
send_stats stat, value, :s, sample_rate
|
331
|
+
end
|
332
|
+
|
333
|
+
# Sends a timing (in ms) for the given stat to the statsd server. The
|
334
|
+
# sample_rate determines what percentage of the time this report is sent. The
|
335
|
+
# statsd server then uses the sample_rate to correctly track the average
|
336
|
+
# timing for the stat.
|
337
|
+
#
|
338
|
+
# @param [String] stat stat name
|
339
|
+
# @param [Integer] ms timing in milliseconds
|
340
|
+
# @param [Numeric] sample_rate sample rate, 1 for always
|
341
|
+
def timing(stat, ms, sample_rate=1)
|
342
|
+
send_stats stat, ms, :ms, sample_rate
|
343
|
+
end
|
344
|
+
|
345
|
+
# Reports execution time of the provided block using {#timing}.
|
346
|
+
#
|
347
|
+
# @param [String] stat stat name
|
348
|
+
# @param [Numeric] sample_rate sample rate, 1 for always
|
349
|
+
# @yield The operation to be timed
|
350
|
+
# @see #timing
|
351
|
+
# @example Report the time (in ms) taken to activate an account
|
352
|
+
# $statsd.time('account.activate') { @account.activate! }
|
353
|
+
def time(stat, sample_rate=1)
|
354
|
+
start = Time.now
|
355
|
+
result = yield
|
356
|
+
timing(stat, ((Time.now - start) * 1000).round, sample_rate)
|
357
|
+
result
|
358
|
+
end
|
359
|
+
|
360
|
+
# Creates and yields a Batch that can be used to batch instrument reports into
|
361
|
+
# larger packets. Batches are sent either when the packet is "full" (defined
|
362
|
+
# by batch_size), or when the block completes, whichever is the sooner.
|
363
|
+
#
|
364
|
+
# @yield [Batch] a statsd subclass that collects and batches instruments
|
365
|
+
# @example Batch two instument operations:
|
366
|
+
# $statsd.batch do |batch|
|
367
|
+
# batch.increment 'sys.requests'
|
368
|
+
# batch.gauge('user.count', User.count)
|
369
|
+
# end
|
370
|
+
def batch(&block)
|
371
|
+
Batch.new(self).easy &block
|
372
|
+
end
|
373
|
+
|
374
|
+
protected
|
375
|
+
|
376
|
+
def send_to_socket(message)
|
377
|
+
self.class.logger.debug { "Statsd: #{message}" } if self.class.logger
|
378
|
+
socket.send(message, 0, @host, @port)
|
379
|
+
rescue => boom
|
380
|
+
self.class.logger.error { "Statsd: #{boom.class} #{boom}" } if self.class.logger
|
381
|
+
nil
|
382
|
+
end
|
383
|
+
|
384
|
+
private
|
385
|
+
|
386
|
+
def send_stats(stat, delta, type, sample_rate=1)
|
387
|
+
if sample_rate == 1 or rand < sample_rate
|
388
|
+
# Replace Ruby module scoping with '.' and reserved chars (: | @) with underscores.
|
389
|
+
stat = stat.to_s.gsub('::', '.').tr(':|@', '_')
|
390
|
+
rate = "|@#{sample_rate}" unless sample_rate == 1
|
391
|
+
send_to_socket "#{prefix}#{stat}#{postfix}:#{delta}|#{type}#{rate}"
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
def socket
|
396
|
+
@socket || Thread.current[:statsd_socket] ||= UDPSocket.new(addr_family)
|
397
|
+
end
|
398
|
+
|
399
|
+
def addr_family
|
400
|
+
Addrinfo.udp(@host, @port).ipv6? ? Socket::AF_INET6 : Socket::AF_INET
|
401
|
+
end
|
402
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
|
4
|
+
require 'simplecov'
|
5
|
+
SimpleCov.start
|
6
|
+
|
7
|
+
require 'statsd'
|
8
|
+
require 'logger'
|
9
|
+
|
10
|
+
class FakeUDPSocket
|
11
|
+
def initialize
|
12
|
+
@buffer = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def send(message, *rest)
|
16
|
+
@buffer.push [message]
|
17
|
+
end
|
18
|
+
|
19
|
+
def recv
|
20
|
+
@buffer.shift
|
21
|
+
end
|
22
|
+
|
23
|
+
def clear
|
24
|
+
@buffer = []
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
inspect
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"<#{self.class.name}: #{@buffer.inspect}>"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class FakeTCPSocket < FakeUDPSocket
|
37
|
+
alias_method :readline, :recv
|
38
|
+
def write(message)
|
39
|
+
@buffer.push message
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Statsd::Admin do
|
4
|
+
class Statsd::Admin
|
5
|
+
public :socket
|
6
|
+
end
|
7
|
+
|
8
|
+
before do
|
9
|
+
@admin = Statsd::Admin.new('localhost', 1234)
|
10
|
+
@socket = Thread.current[:statsd_admin_socket] = FakeTCPSocket.new
|
11
|
+
end
|
12
|
+
|
13
|
+
after { Thread.current[:statsd_socket] = nil }
|
14
|
+
|
15
|
+
describe "#initialize" do
|
16
|
+
it "should set the host and port" do
|
17
|
+
@admin.host.must_equal 'localhost'
|
18
|
+
@admin.port.must_equal 1234
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should default the host to 127.0.0.1 and port to 8126" do
|
22
|
+
statsd = Statsd::Admin.new
|
23
|
+
statsd.host.must_equal '127.0.0.1'
|
24
|
+
statsd.port.must_equal 8126
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#host and #port" do
|
29
|
+
it "should set host and port" do
|
30
|
+
@admin.host = '1.2.3.4'
|
31
|
+
@admin.port = 5678
|
32
|
+
@admin.host.must_equal '1.2.3.4'
|
33
|
+
@admin.port.must_equal 5678
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should not resolve hostnames to IPs" do
|
37
|
+
@admin.host = 'localhost'
|
38
|
+
@admin.host.must_equal 'localhost'
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should set nil host to default" do
|
42
|
+
@admin.host = nil
|
43
|
+
@admin.host.must_equal '127.0.0.1'
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should set nil port to default" do
|
47
|
+
@admin.port = nil
|
48
|
+
@admin.port.must_equal 8126
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
%w(gauges counters timers).each do |action|
|
53
|
+
describe "##{action}" do
|
54
|
+
it "should send a command and return a Hash" do
|
55
|
+
["{'foo.bar': 0,\n",
|
56
|
+
"'foo.baz': 1,\n",
|
57
|
+
"'foo.quux': 2 }\n",
|
58
|
+
"END\n","\n"].each do |line|
|
59
|
+
@socket.write line
|
60
|
+
end
|
61
|
+
result = @admin.send action.to_sym
|
62
|
+
result.must_be_kind_of Hash
|
63
|
+
result.size.must_equal 3
|
64
|
+
@socket.readline.must_equal "#{action}\n"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#del#{action}" do
|
69
|
+
it "should send a command and return an Array" do
|
70
|
+
["deleted: foo.bar\n",
|
71
|
+
"deleted: foo.baz\n",
|
72
|
+
"deleted: foo.quux\n",
|
73
|
+
"END\n", "\n"].each do |line|
|
74
|
+
@socket.write line
|
75
|
+
end
|
76
|
+
result = @admin.send "del#{action}", "foo.*"
|
77
|
+
result.must_be_kind_of Array
|
78
|
+
result.size.must_equal 3
|
79
|
+
@socket.readline.must_equal "del#{action} foo.*\n"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "#stats" do
|
85
|
+
it "should send a command and return a Hash" do
|
86
|
+
["whatever: 0\n", "END\n", "\n"].each do |line|
|
87
|
+
@socket.write line
|
88
|
+
end
|
89
|
+
result = @admin.stats
|
90
|
+
result.must_be_kind_of Hash
|
91
|
+
result["whatever"].must_equal 0
|
92
|
+
@socket.readline.must_equal "stats\n"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
data/spec/statsd_spec.rb
ADDED
@@ -0,0 +1,425 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Statsd do
|
4
|
+
class Statsd
|
5
|
+
public :socket
|
6
|
+
end
|
7
|
+
|
8
|
+
before do
|
9
|
+
@statsd = Statsd.new('localhost', 1234)
|
10
|
+
@socket = Thread.current[:statsd_socket] = FakeUDPSocket.new
|
11
|
+
end
|
12
|
+
|
13
|
+
after { Thread.current[:statsd_socket] = nil }
|
14
|
+
|
15
|
+
describe "#initialize" do
|
16
|
+
it "should set the host and port" do
|
17
|
+
@statsd.host.must_equal 'localhost'
|
18
|
+
@statsd.port.must_equal 1234
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should default the host to 127.0.0.1 and port to 8125" do
|
22
|
+
statsd = Statsd.new
|
23
|
+
statsd.host.must_equal '127.0.0.1'
|
24
|
+
statsd.port.must_equal 8125
|
25
|
+
end
|
26
|
+
|
27
|
+
it "uses the thread's socket by default" do
|
28
|
+
@statsd.socket.must_equal @socket
|
29
|
+
end
|
30
|
+
|
31
|
+
it "can use a single socket passed in" do
|
32
|
+
statsd_with_socket = Statsd.new('foo', 1234, :socket)
|
33
|
+
statsd_with_socket.socket.must_equal :socket
|
34
|
+
Thread.current[:statsd_socket] = FakeUDPSocket.new
|
35
|
+
statsd_with_socket.socket.must_equal :socket
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#host and #port" do
|
40
|
+
it "should set host and port" do
|
41
|
+
@statsd.host = '1.2.3.4'
|
42
|
+
@statsd.port = 5678
|
43
|
+
@statsd.host.must_equal '1.2.3.4'
|
44
|
+
@statsd.port.must_equal 5678
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should not resolve hostnames to IPs" do
|
48
|
+
@statsd.host = 'localhost'
|
49
|
+
@statsd.host.must_equal 'localhost'
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should set nil host to default" do
|
53
|
+
@statsd.host = nil
|
54
|
+
@statsd.host.must_equal '127.0.0.1'
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should set nil port to default" do
|
58
|
+
@statsd.port = nil
|
59
|
+
@statsd.port.must_equal 8125
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should allow an IPv6 address" do
|
63
|
+
@statsd.host = '::1'
|
64
|
+
@statsd.host.must_equal '::1'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#increment" do
|
69
|
+
it "should format the message according to the statsd spec" do
|
70
|
+
@statsd.increment('foobar')
|
71
|
+
@socket.recv.must_equal ['foobar:1|c']
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "with a sample rate" do
|
75
|
+
before { class << @statsd; def rand; 0; end; end } # ensure delivery
|
76
|
+
it "should format the message according to the statsd spec" do
|
77
|
+
@statsd.increment('foobar', 0.5)
|
78
|
+
@socket.recv.must_equal ['foobar:1|c|@0.5']
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "#decrement" do
|
84
|
+
it "should format the message according to the statsd spec" do
|
85
|
+
@statsd.decrement('foobar')
|
86
|
+
@socket.recv.must_equal ['foobar:-1|c']
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "with a sample rate" do
|
90
|
+
before { class << @statsd; def rand; 0; end; end } # ensure delivery
|
91
|
+
it "should format the message according to the statsd spec" do
|
92
|
+
@statsd.decrement('foobar', 0.5)
|
93
|
+
@socket.recv.must_equal ['foobar:-1|c|@0.5']
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "#gauge" do
|
99
|
+
it "should send a message with a 'g' type, per the nearbuy fork" do
|
100
|
+
@statsd.gauge('begrutten-suffusion', 536)
|
101
|
+
@socket.recv.must_equal ['begrutten-suffusion:536|g']
|
102
|
+
@statsd.gauge('begrutten-suffusion', -107.3)
|
103
|
+
@socket.recv.must_equal ['begrutten-suffusion:-107.3|g']
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "with a sample rate" do
|
107
|
+
before { class << @statsd; def rand; 0; end; end } # ensure delivery
|
108
|
+
it "should format the message according to the statsd spec" do
|
109
|
+
@statsd.gauge('begrutten-suffusion', 536, 0.1)
|
110
|
+
@socket.recv.must_equal ['begrutten-suffusion:536|g|@0.1']
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "#timing" do
|
116
|
+
it "should format the message according to the statsd spec" do
|
117
|
+
@statsd.timing('foobar', 500)
|
118
|
+
@socket.recv.must_equal ['foobar:500|ms']
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "with a sample rate" do
|
122
|
+
before { class << @statsd; def rand; 0; end; end } # ensure delivery
|
123
|
+
it "should format the message according to the statsd spec" do
|
124
|
+
@statsd.timing('foobar', 500, 0.5)
|
125
|
+
@socket.recv.must_equal ['foobar:500|ms|@0.5']
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe "#set" do
|
131
|
+
it "should format the message according to the statsd spec" do
|
132
|
+
@statsd.set('foobar', 765)
|
133
|
+
@socket.recv.must_equal ['foobar:765|s']
|
134
|
+
end
|
135
|
+
|
136
|
+
describe "with a sample rate" do
|
137
|
+
before { class << @statsd; def rand; 0; end; end } # ensure delivery
|
138
|
+
it "should format the message according to the statsd spec" do
|
139
|
+
@statsd.set('foobar', 500, 0.5)
|
140
|
+
@socket.recv.must_equal ['foobar:500|s|@0.5']
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe "#time" do
|
146
|
+
it "should format the message according to the statsd spec" do
|
147
|
+
@statsd.time('foobar') { 'test' }
|
148
|
+
@socket.recv.must_equal ['foobar:0|ms']
|
149
|
+
end
|
150
|
+
|
151
|
+
it "should return the result of the block" do
|
152
|
+
result = @statsd.time('foobar') { 'test' }
|
153
|
+
result.must_equal 'test'
|
154
|
+
end
|
155
|
+
|
156
|
+
describe "with a sample rate" do
|
157
|
+
before { class << @statsd; def rand; 0; end; end } # ensure delivery
|
158
|
+
|
159
|
+
it "should format the message according to the statsd spec" do
|
160
|
+
@statsd.time('foobar', 0.5) { 'test' }
|
161
|
+
@socket.recv.must_equal ['foobar:0|ms|@0.5']
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
describe "#sampled" do
|
167
|
+
describe "when the sample rate is 1" do
|
168
|
+
before { class << @statsd; def rand; raise end; end }
|
169
|
+
it "should send" do
|
170
|
+
@statsd.timing('foobar', 500, 1)
|
171
|
+
@socket.recv.must_equal ['foobar:500|ms']
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe "when the sample rate is greater than a random value [0,1]" do
|
176
|
+
before { class << @statsd; def rand; 0; end; end } # ensure delivery
|
177
|
+
it "should send" do
|
178
|
+
@statsd.timing('foobar', 500, 0.5)
|
179
|
+
@socket.recv.must_equal ['foobar:500|ms|@0.5']
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe "when the sample rate is less than a random value [0,1]" do
|
184
|
+
before { class << @statsd; def rand; 1; end; end } # ensure no delivery
|
185
|
+
it "should not send" do
|
186
|
+
@statsd.timing('foobar', 500, 0.5).must_equal nil
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
describe "when the sample rate is equal to a random value [0,1]" do
|
191
|
+
before { class << @statsd; def rand; 0; end; end } # ensure delivery
|
192
|
+
it "should send" do
|
193
|
+
@statsd.timing('foobar', 500, 0.5)
|
194
|
+
@socket.recv.must_equal ['foobar:500|ms|@0.5']
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
describe "with namespace" do
|
200
|
+
before { @statsd.namespace = 'service' }
|
201
|
+
|
202
|
+
it "should add namespace to increment" do
|
203
|
+
@statsd.increment('foobar')
|
204
|
+
@socket.recv.must_equal ['service.foobar:1|c']
|
205
|
+
end
|
206
|
+
|
207
|
+
it "should add namespace to decrement" do
|
208
|
+
@statsd.decrement('foobar')
|
209
|
+
@socket.recv.must_equal ['service.foobar:-1|c']
|
210
|
+
end
|
211
|
+
|
212
|
+
it "should add namespace to timing" do
|
213
|
+
@statsd.timing('foobar', 500)
|
214
|
+
@socket.recv.must_equal ['service.foobar:500|ms']
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should add namespace to gauge" do
|
218
|
+
@statsd.gauge('foobar', 500)
|
219
|
+
@socket.recv.must_equal ['service.foobar:500|g']
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
describe "with postfix" do
|
224
|
+
before { @statsd.postfix = 'ip-23-45-56-78' }
|
225
|
+
|
226
|
+
it "should add postfix to increment" do
|
227
|
+
@statsd.increment('foobar')
|
228
|
+
@socket.recv.must_equal ['foobar.ip-23-45-56-78:1|c']
|
229
|
+
end
|
230
|
+
|
231
|
+
it "should add postfix to decrement" do
|
232
|
+
@statsd.decrement('foobar')
|
233
|
+
@socket.recv.must_equal ['foobar.ip-23-45-56-78:-1|c']
|
234
|
+
end
|
235
|
+
|
236
|
+
it "should add namespace to timing" do
|
237
|
+
@statsd.timing('foobar', 500)
|
238
|
+
@socket.recv.must_equal ['foobar.ip-23-45-56-78:500|ms']
|
239
|
+
end
|
240
|
+
|
241
|
+
it "should add namespace to gauge" do
|
242
|
+
@statsd.gauge('foobar', 500)
|
243
|
+
@socket.recv.must_equal ['foobar.ip-23-45-56-78:500|g']
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
describe '#postfix=' do
|
248
|
+
describe "when nil, false, or empty" do
|
249
|
+
it "should set postfix to nil" do
|
250
|
+
[nil, false, ''].each do |value|
|
251
|
+
@statsd.postfix = 'a postfix'
|
252
|
+
@statsd.postfix = value
|
253
|
+
@statsd.postfix.must_equal nil
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
describe "with logging" do
|
260
|
+
require 'stringio'
|
261
|
+
before { Statsd.logger = Logger.new(@log = StringIO.new)}
|
262
|
+
|
263
|
+
it "should write to the log in debug" do
|
264
|
+
Statsd.logger.level = Logger::DEBUG
|
265
|
+
|
266
|
+
@statsd.increment('foobar')
|
267
|
+
|
268
|
+
@log.string.must_match "Statsd: foobar:1|c"
|
269
|
+
end
|
270
|
+
|
271
|
+
it "should not write to the log unless debug" do
|
272
|
+
Statsd.logger.level = Logger::INFO
|
273
|
+
|
274
|
+
@statsd.increment('foobar')
|
275
|
+
|
276
|
+
@log.string.must_be_empty
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
describe "stat names" do
|
281
|
+
it "should accept anything as stat" do
|
282
|
+
@statsd.increment(Object, 1)
|
283
|
+
end
|
284
|
+
|
285
|
+
it "should replace ruby constant delimeter with graphite package name" do
|
286
|
+
class Statsd::SomeClass; end
|
287
|
+
@statsd.increment(Statsd::SomeClass, 1)
|
288
|
+
|
289
|
+
@socket.recv.must_equal ['Statsd.SomeClass:1|c']
|
290
|
+
end
|
291
|
+
|
292
|
+
it "should replace statsd reserved chars in the stat name" do
|
293
|
+
@statsd.increment('ray@hostname.blah|blah.blah:blah', 1)
|
294
|
+
@socket.recv.must_equal ['ray_hostname.blah_blah.blah_blah:1|c']
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
describe "handling socket errors" do
|
299
|
+
before do
|
300
|
+
require 'stringio'
|
301
|
+
Statsd.logger = Logger.new(@log = StringIO.new)
|
302
|
+
@socket.instance_eval { def send(*) raise SocketError end }
|
303
|
+
end
|
304
|
+
|
305
|
+
it "should ignore socket errors" do
|
306
|
+
@statsd.increment('foobar').must_equal nil
|
307
|
+
end
|
308
|
+
|
309
|
+
it "should log socket errors" do
|
310
|
+
@statsd.increment('foobar')
|
311
|
+
@log.string.must_match 'Statsd: SocketError'
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
describe "batching" do
|
316
|
+
it "should have a default batch size of 10" do
|
317
|
+
@statsd.batch_size.must_equal 10
|
318
|
+
end
|
319
|
+
|
320
|
+
it "should have a modifiable batch size" do
|
321
|
+
@statsd.batch_size = 7
|
322
|
+
@statsd.batch_size.must_equal 7
|
323
|
+
@statsd.batch do |b|
|
324
|
+
b.batch_size.must_equal 7
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
it "should flush the batch at the batch size or at the end of the block" do
|
329
|
+
@statsd.batch do |b|
|
330
|
+
b.batch_size = 3
|
331
|
+
|
332
|
+
# The first three should flush, the next two will be flushed when the
|
333
|
+
# block is done.
|
334
|
+
5.times { b.increment('foobar') }
|
335
|
+
|
336
|
+
@socket.recv.must_equal [(["foobar:1|c"] * 3).join("\n")]
|
337
|
+
end
|
338
|
+
|
339
|
+
@socket.recv.must_equal [(["foobar:1|c"] * 2).join("\n")]
|
340
|
+
end
|
341
|
+
|
342
|
+
it "should not flush to the socket if the backlog is empty" do
|
343
|
+
batch = Statsd::Batch.new(@statsd)
|
344
|
+
batch.flush
|
345
|
+
@socket.recv.must_be :nil?
|
346
|
+
|
347
|
+
batch.increment 'foobar'
|
348
|
+
batch.flush
|
349
|
+
@socket.recv.must_equal %w[foobar:1|c]
|
350
|
+
end
|
351
|
+
|
352
|
+
it "should support setting namespace for the underlying instance" do
|
353
|
+
batch = Statsd::Batch.new(@statsd)
|
354
|
+
batch.namespace = 'ns'
|
355
|
+
@statsd.namespace.must_equal 'ns'
|
356
|
+
end
|
357
|
+
|
358
|
+
it "should support setting host for the underlying instance" do
|
359
|
+
batch = Statsd::Batch.new(@statsd)
|
360
|
+
batch.host = '1.2.3.4'
|
361
|
+
@statsd.host.must_equal '1.2.3.4'
|
362
|
+
end
|
363
|
+
|
364
|
+
it "should support setting port for the underlying instance" do
|
365
|
+
batch = Statsd::Batch.new(@statsd)
|
366
|
+
batch.port = 42
|
367
|
+
@statsd.port.must_equal 42
|
368
|
+
end
|
369
|
+
|
370
|
+
end
|
371
|
+
|
372
|
+
describe "thread safety" do
|
373
|
+
|
374
|
+
it "should use a thread local socket" do
|
375
|
+
Thread.current[:statsd_socket].must_equal @socket
|
376
|
+
@statsd.send(:socket).must_equal @socket
|
377
|
+
end
|
378
|
+
|
379
|
+
it "should create a new socket when used in a new thread" do
|
380
|
+
sock = @statsd.send(:socket)
|
381
|
+
Thread.new { Thread.current[:statsd_socket] }.value.wont_equal sock
|
382
|
+
end
|
383
|
+
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
describe Statsd do
|
388
|
+
describe "with a real UDP socket" do
|
389
|
+
it "should actually send stuff over the socket" do
|
390
|
+
Thread.current[:statsd_socket] = nil
|
391
|
+
socket = UDPSocket.new
|
392
|
+
host, port = 'localhost', 12345
|
393
|
+
socket.bind(host, port)
|
394
|
+
|
395
|
+
statsd = Statsd.new(host, port)
|
396
|
+
statsd.increment('foobar')
|
397
|
+
message = socket.recvfrom(16).first
|
398
|
+
message.must_equal 'foobar:1|c'
|
399
|
+
end
|
400
|
+
|
401
|
+
it "should send stuff over an IPv4 socket" do
|
402
|
+
Thread.current[:statsd_socket] = nil
|
403
|
+
socket = UDPSocket.new Socket::AF_INET
|
404
|
+
host, port = '127.0.0.1', 12346
|
405
|
+
socket.bind(host, port)
|
406
|
+
|
407
|
+
statsd = Statsd.new(host, port)
|
408
|
+
statsd.increment('foobar')
|
409
|
+
message = socket.recvfrom(16).first
|
410
|
+
message.must_equal 'foobar:1|c'
|
411
|
+
end
|
412
|
+
|
413
|
+
it "should send stuff over an IPv6 socket" do
|
414
|
+
Thread.current[:statsd_socket] = nil
|
415
|
+
socket = UDPSocket.new Socket::AF_INET6
|
416
|
+
host, port = '::1', 12347
|
417
|
+
socket.bind(host, port)
|
418
|
+
|
419
|
+
statsd = Statsd.new(host, port)
|
420
|
+
statsd.increment('foobar')
|
421
|
+
message = socket.recvfrom(16).first
|
422
|
+
message.must_equal 'foobar:1|c'
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end if ENV['LIVE']
|
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: justinaiken-statsd
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1.3'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Justin Aiken
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-02-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.2.0
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: yard
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: simplecov
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.6.4
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.6.4
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: 'A Ruby StatsD client, with a tweak '
|
70
|
+
email: 60tonangel@gmail.com
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files:
|
74
|
+
- LICENSE.txt
|
75
|
+
- README.rdoc
|
76
|
+
files:
|
77
|
+
- .document
|
78
|
+
- .gitignore
|
79
|
+
- .travis.yml
|
80
|
+
- Gemfile
|
81
|
+
- LICENSE.txt
|
82
|
+
- README.rdoc
|
83
|
+
- Rakefile
|
84
|
+
- justinaiken-statsd.gemspec
|
85
|
+
- lib/statsd-ruby.rb
|
86
|
+
- lib/statsd.rb
|
87
|
+
- spec/helper.rb
|
88
|
+
- spec/statsd_admin_spec.rb
|
89
|
+
- spec/statsd_spec.rb
|
90
|
+
homepage: https://github.com/JustinAiken/statsd
|
91
|
+
licenses:
|
92
|
+
- MIT
|
93
|
+
metadata: {}
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubyforge_project:
|
110
|
+
rubygems_version: 2.0.3
|
111
|
+
signing_key:
|
112
|
+
specification_version: 4
|
113
|
+
summary: A Ruby StatsD client, with a tweak
|
114
|
+
test_files:
|
115
|
+
- spec/helper.rb
|
116
|
+
- spec/statsd_admin_spec.rb
|
117
|
+
- spec/statsd_spec.rb
|
118
|
+
has_rdoc:
|