librato-statsd-ruby 1.0.0

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: acf5ea703ef7bd50d8c4008df12027120c24c34a
4
+ data.tar.gz: ac13712a6cda73d8825699b5fa3d8530d8e34cfb
5
+ SHA512:
6
+ metadata.gz: e612a6953d1ccb319fd80c26802b078feb3ae41cce19132889f7949915c53e18590f1527b362f4dd20b0724b7a652e84d53983208f2b19f06765e0c4d5f4334e
7
+ data.tar.gz: ca68faf8e7f67f40850eb224fdecf22c36f4a7f5face4e78e9ed9e6be05ca2b53682f6e3e2b81f6fe70bbb7b5383608d1aaf39429ecb0d43b4636df4369fae59
@@ -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 @@
1
+ 2.4.1
@@ -0,0 +1,21 @@
1
+ ---
2
+ language: ruby
3
+
4
+ rvm:
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - 2.1
8
+ - 2.2
9
+ - 2.3.0
10
+ - rbx-2
11
+ - jruby
12
+ - jruby-head
13
+ - ruby-head
14
+
15
+ sudo: false
16
+
17
+ matrix:
18
+ allow_failures:
19
+ - rvm: rbx-2
20
+ - rvm: ruby-head
21
+ - rvm: jruby
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,47 @@
1
+ # librato-statsd-ruby
2
+
3
+ Librato-based Ruby client for [StatsD](https://github.com/etsy/statsd).
4
+
5
+ # Installing
6
+
7
+ `gem "librato-statsd-ruby", '1.0.0'`
8
+
9
+ You will also need to be running statsd with the Librato backend pluggin. See this [repo](https://github.com/librato/statsd-librato-backend) for more details.
10
+
11
+ # Getting Started
12
+
13
+ ``` ruby
14
+ # Require librato-statsd-ruby
15
+ require 'librato-statsd-ruby'
16
+
17
+ # Initialize Statsd
18
+ $statsd = Statsd.new 'localhost', 8125
19
+
20
+ # Counters
21
+ $statsd.increment 'user.signups'
22
+
23
+ # Gauges
24
+ $statsd.gauge 'user.sessions', 10, sample_rate: 0.5
25
+
26
+ # Histogram
27
+ $statsd.histogram 'user.downloads', 5
28
+
29
+ # Time a block of code
30
+ $statsd.time 'user.create' do
31
+ User.create(...)
32
+ end
33
+ ```
34
+
35
+ # Tags
36
+
37
+ You can also submit tags with any metrics.
38
+
39
+ ```ruby
40
+ $statsd.increment 'user.signups', tags: { country: 'US', addon_user: false }
41
+ ```
42
+
43
+ # Credit
44
+
45
+ This is a fork of the orignal Ruby statsd client library written by [Rein Henrichs](https://github.com/reinh/statsd) that adds support for Librato-specific features.
46
+
47
+ 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 @@
1
+ require 'statsd'
@@ -0,0 +1,477 @@
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 utilizing the thread
22
+ # safe nature of UDP sends. 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
+ :delimiter, :delimiter=
55
+
56
+ attr_accessor :batch_size
57
+
58
+ # @param [Statsd] requires a configured Statsd instance
59
+ def initialize(statsd)
60
+ @statsd = statsd
61
+ @batch_size = statsd.batch_size
62
+ @backlog = []
63
+ end
64
+
65
+ # @yields [Batch] yields itself
66
+ #
67
+ # A convenience method to ensure that data is not lost in the event of an
68
+ # exception being thrown. Batches will be transmitted on the parent socket
69
+ # as soon as the batch is full, and when the block finishes.
70
+ def easy
71
+ yield self
72
+ ensure
73
+ flush
74
+ end
75
+
76
+ def flush
77
+ unless @backlog.empty?
78
+ @statsd.send_to_socket @backlog.join("\n")
79
+ @backlog.clear
80
+ end
81
+ end
82
+
83
+ protected
84
+
85
+ def send_to_socket(message)
86
+ @backlog << message
87
+ if @backlog.size >= @batch_size
88
+ flush
89
+ end
90
+ end
91
+
92
+ end
93
+
94
+ class Admin
95
+ # StatsD host. Defaults to 127.0.0.1.
96
+ attr_reader :host
97
+
98
+ # StatsD admin port. Defaults to 8126.
99
+ attr_reader :port
100
+
101
+ class << self
102
+ # Set to a standard logger instance to enable debug logging.
103
+ attr_accessor :logger
104
+ end
105
+
106
+ # @attribute [w] host.
107
+ # Users should call connect after changing this.
108
+ def host=(host)
109
+ @host = host || '127.0.0.1'
110
+ end
111
+
112
+ # @attribute [w] port.
113
+ # Users should call connect after changing this.
114
+ def port=(port)
115
+ @port = port || 8126
116
+ end
117
+
118
+ # @param [String] host your statsd host
119
+ # @param [Integer] port your statsd port
120
+ def initialize(host = '127.0.0.1', port = 8126)
121
+ @host = host || '127.0.0.1'
122
+ @port = port || 8126
123
+ # protects @socket transactions
124
+ @socket = nil
125
+ @s_mu = Mutex.new
126
+ connect
127
+ end
128
+
129
+ # Reads all gauges from StatsD.
130
+ def gauges
131
+ read_metric :gauges
132
+ end
133
+
134
+ # Reads all timers from StatsD.
135
+ def timers
136
+ read_metric :timers
137
+ end
138
+
139
+ # Reads all counters from StatsD.
140
+ def counters
141
+ read_metric :counters
142
+ end
143
+
144
+ # @param[String] item
145
+ # Deletes one or more gauges. Wildcards are allowed.
146
+ def delgauges item
147
+ delete_metric :gauges, item
148
+ end
149
+
150
+ # @param[String] item
151
+ # Deletes one or more timers. Wildcards are allowed.
152
+ def deltimers item
153
+ delete_metric :timers, item
154
+ end
155
+
156
+ # @param[String] item
157
+ # Deletes one or more counters. Wildcards are allowed.
158
+ def delcounters item
159
+ delete_metric :counters, item
160
+ end
161
+
162
+ def stats
163
+ result = @s_mu.synchronize do
164
+ # the format of "stats" isn't JSON, who knows why
165
+ send_to_socket "stats"
166
+ read_from_socket
167
+ end
168
+ items = {}
169
+ result.split("\n").each do |line|
170
+ key, val = line.chomp.split(": ")
171
+ items[key] = val.to_i
172
+ end
173
+ items
174
+ end
175
+
176
+ # Reconnects the socket, for when the statsd address may have changed. Users
177
+ # do not normally need to call this, but calling it may be appropriate when
178
+ # reconfiguring a process (e.g. from HUP)
179
+ def connect
180
+ @s_mu.synchronize do
181
+ begin
182
+ @socket.flush rescue nil
183
+ @socket.close if @socket
184
+ rescue
185
+ # Ignore socket errors on close.
186
+ end
187
+ @socket = TCPSocket.new(host, port)
188
+ end
189
+ end
190
+
191
+ private
192
+
193
+ def read_metric name
194
+ result = @s_mu.synchronize do
195
+ send_to_socket name
196
+ read_from_socket
197
+ end
198
+ # for some reason, the reply looks like JSON, but isn't, quite
199
+ JSON.parse result.gsub("'", "\"")
200
+ end
201
+
202
+ def delete_metric name, item
203
+ result = @s_mu.synchronize do
204
+ send_to_socket "del#{name} #{item}"
205
+ read_from_socket
206
+ end
207
+ deleted = []
208
+ result.split("\n").each do |line|
209
+ deleted << line.chomp.split(": ")[-1]
210
+ end
211
+ deleted
212
+ end
213
+
214
+ def send_to_socket(message)
215
+ self.class.logger.debug { "Statsd: #{message}" } if self.class.logger
216
+ @socket.write(message.to_s + "\n")
217
+ rescue => boom
218
+ self.class.logger.error { "Statsd: #{boom.class} #{boom}" } if self.class.logger
219
+ nil
220
+ end
221
+
222
+
223
+ def read_from_socket
224
+ buffer = ""
225
+ loop do
226
+ line = @socket.readline
227
+ break if line == "END\n"
228
+ buffer += line
229
+ end
230
+ @socket.readline # clear the closing newline out of the socket
231
+ buffer
232
+ end
233
+ end
234
+
235
+ # A namespace to prepend to all statsd calls.
236
+ attr_reader :namespace
237
+
238
+ # StatsD host. Defaults to 127.0.0.1.
239
+ attr_reader :host
240
+
241
+ # StatsD port. Defaults to 8125.
242
+ attr_reader :port
243
+
244
+ # StatsD namespace prefix, generated from #namespace
245
+ attr_reader :prefix
246
+
247
+ # The default batch size for new batches (default: 10)
248
+ attr_accessor :batch_size
249
+
250
+ # a postfix to append to all metrics
251
+ attr_reader :postfix
252
+
253
+ # The replacement of :: on ruby module names when transformed to statsd metric names
254
+ attr_reader :delimiter
255
+
256
+ class << self
257
+ # Set to a standard logger instance to enable debug logging.
258
+ attr_accessor :logger
259
+ end
260
+
261
+ # @param [String] host your statsd host
262
+ # @param [Integer] port your statsd port
263
+ # @param [Symbol] :tcp for TCP, :udp or any other value for UDP
264
+ def initialize(host = '127.0.0.1', port = 8125, protocol = :udp)
265
+ @host = host || '127.0.0.1'
266
+ @port = port || 8125
267
+ self.delimiter = "."
268
+ @prefix = nil
269
+ @batch_size = 10
270
+ @postfix = nil
271
+ @socket = nil
272
+ @protocol = protocol || :udp
273
+ @s_mu = Mutex.new
274
+ connect
275
+ end
276
+
277
+ # @attribute [w] namespace
278
+ # Writes are not thread safe.
279
+ def namespace=(namespace)
280
+ @namespace = namespace
281
+ @prefix = "#{namespace}."
282
+ end
283
+
284
+ # @attribute [w] postfix
285
+ # A value to be appended to the stat name after a '.'. If the value is
286
+ # blank then the postfix will be reset to nil (rather than to '.').
287
+ def postfix=(pf)
288
+ case pf
289
+ when nil, false, '' then @postfix = nil
290
+ else @postfix = ".#{pf}"
291
+ end
292
+ end
293
+
294
+ # @attribute [w] host
295
+ # Writes are not thread safe.
296
+ # Users should call hup after making changes.
297
+ def host=(host)
298
+ @host = host || '127.0.0.1'
299
+ end
300
+
301
+ # @attribute [w] port
302
+ # Writes are not thread safe.
303
+ # Users should call hup after making changes.
304
+ def port=(port)
305
+ @port = port || 8125
306
+ end
307
+
308
+ # @attribute [w] stat_delimiter
309
+ # Allows for custom delimiter replacement for :: when Ruby modules are transformed to statsd metric name
310
+ def delimiter=(delimiter)
311
+ @delimiter = delimiter || "."
312
+ end
313
+
314
+ # Sends an increment (count = 1) for the given stat to the statsd server.
315
+ #
316
+ # @param [String] stat stat name
317
+ # @see #count
318
+ def increment(stat, opts={})
319
+ count stat, 1, opts
320
+ end
321
+
322
+ # Sends a decrement (count = -1) for the given stat to the statsd server.
323
+ #
324
+ # @param [String] stat stat name
325
+ # @see #count
326
+ def decrement(stat, opts={})
327
+ count stat, -1, opts
328
+ end
329
+
330
+ # Sends an arbitrary count for the given stat to the statsd server.
331
+ #
332
+ # @param [String] stat stat name
333
+ # @param [Integer] count count
334
+ def count(stat, count, opts={})
335
+ send_stats stat, count, :c, opts
336
+ end
337
+
338
+ # Sends an arbitary gauge value for the given stat to the statsd server.
339
+ #
340
+ # This is useful for recording things like available disk space,
341
+ # memory usage, and the like, which have different semantics than
342
+ # counters.
343
+ #
344
+ # @param [String] stat stat name.
345
+ # @param [Numeric] value gauge value.
346
+ # @example Report the current user count:
347
+ # $statsd.gauge('user.count', User.count)
348
+ def gauge(stat, value, opts={})
349
+ send_stats stat, value, :g, opts
350
+ end
351
+
352
+ # Sends an arbitary set value for the given stat to the statsd server.
353
+ #
354
+ # This is for recording counts of unique events, which are useful to
355
+ # see on graphs to correlate to other values. For example, a deployment
356
+ # might get recorded as a set, and be drawn as annotations on a CPU history
357
+ # graph.
358
+ #
359
+ # @param [String] stat stat name.
360
+ # @param [Numeric] value event value.
361
+ # @example Report a deployment happening:
362
+ # $statsd.set('deployment', DEPLOYMENT_EVENT_CODE)
363
+ def set(stat, value, opts={})
364
+ send_stats stat, value, :s, opts
365
+ end
366
+
367
+ # Sends a timing (in ms) for the given stat to the statsd server. The
368
+ # sample_rate determines what percentage of the time this report is sent. The
369
+ # statsd server then uses the sample_rate to correctly track the average
370
+ # timing for the stat.
371
+ #
372
+ # @param [String] stat stat name
373
+ # @param [Integer] ms timing in milliseconds
374
+ def timing(stat, ms, opts={})
375
+ send_stats stat, ms, :ms, opts
376
+ end
377
+
378
+ # Reports execution time of the provided block using {#timing}.
379
+ #
380
+ # @param [String] stat stat name
381
+ # @yield The operation to be timed
382
+ # @see #timing
383
+ # @example Report the time (in ms) taken to activate an account
384
+ # $statsd.time('account.activate') { @account.activate! }
385
+ def time(stat, opts={})
386
+ start = Time.now
387
+ result = yield
388
+ ensure
389
+ timing(stat, ((Time.now - start) * 1000).round, opts)
390
+ result
391
+ end
392
+
393
+ # Creates and yields a Batch that can be used to batch instrument reports into
394
+ # larger packets. Batches are sent either when the packet is "full" (defined
395
+ # by batch_size), or when the block completes, whichever is the sooner.
396
+ #
397
+ # @yield [Batch] a statsd subclass that collects and batches instruments
398
+ # @example Batch two instument operations:
399
+ # $statsd.batch do |batch|
400
+ # batch.increment 'sys.requests'
401
+ # batch.gauge('user.count', User.count)
402
+ # end
403
+ def batch(&block)
404
+ Batch.new(self).easy(&block)
405
+ end
406
+
407
+ # Reconnects the socket, useful if the address of the statsd has changed. This
408
+ # method is not thread safe from a perspective of stat submission. It is safe
409
+ # from resource leaks. Users do not normally need to call this, but calling it
410
+ # may be appropriate when reconfiguring a process (e.g. from HUP).
411
+ def connect
412
+ @s_mu.synchronize do
413
+ begin
414
+ @socket.close if @socket
415
+ rescue
416
+ # Errors are ignored on reconnects.
417
+ end
418
+
419
+ case @protocol
420
+ when :tcp
421
+ @socket = TCPSocket.new @host, @port
422
+ else
423
+ @socket = UDPSocket.new Addrinfo.ip(@host).afamily
424
+ @socket.connect host, port
425
+ end
426
+ end
427
+ end
428
+
429
+ protected
430
+
431
+ def send_to_socket(message)
432
+ self.class.logger.debug { "Statsd: #{message}" } if self.class.logger
433
+
434
+ retries = 0
435
+ n = 0
436
+ while true
437
+ # send(2) is atomic, however, in stream cases (TCP) the socket is left
438
+ # in an inconsistent state if a partial message is written. If that case
439
+ # occurs, the socket is closed down and we retry on a new socket.
440
+ n = socket.write(message)
441
+
442
+ if n == message.length
443
+ break
444
+ end
445
+
446
+ connect
447
+ retries += 1
448
+ raise "statsd: Failed to send after #{retries} attempts" if retries >= 5
449
+ end
450
+ n
451
+ rescue => boom
452
+ self.class.logger.error { "Statsd: #{boom.class} #{boom}" } if self.class.logger
453
+ nil
454
+ end
455
+
456
+ private
457
+
458
+ def send_stats(stat, delta, type, opts = {})
459
+ sample_rate = opts[:sample_rate] || 1
460
+ if sample_rate == 1 or rand < sample_rate
461
+ # Replace Ruby module scoping with '.' and reserved chars (: | @) with underscores.
462
+ stat = stat.to_s.gsub('::', delimiter).tr(':|@', '_')
463
+ if opts[:tags]
464
+ tags = opts[:tags].map { |k,v| "#{k}=#{v}"}.join(',')
465
+ formatted_tags = "##{tags}"
466
+ end
467
+ rate = "|@#{sample_rate}" unless sample_rate == 1
468
+ send_to_socket "#{prefix}#{stat}#{postfix}#{formatted_tags}:#{delta}|#{type}#{rate}"
469
+ end
470
+ end
471
+
472
+ def socket
473
+ # Subtle: If the socket is half-way through initialization in connect, it
474
+ # cannot be used yet.
475
+ @s_mu.synchronize { @socket } || raise(ThreadError, "socket missing")
476
+ end
477
+ end