librato-statsd-ruby 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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