justinaiken-statsd 1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
@@ -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
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - jruby-19mode
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -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.
@@ -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.
@@ -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
+
@@ -0,0 +1 @@
1
+ require 'statsd'
@@ -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
@@ -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
+
@@ -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: