statsd-ruby-tcp 0.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
+ SHA256:
3
+ metadata.gz: 4ff155c12281bc745e968ebfbde358cdbc24c870c6d1c10977b1bc1bab44878b
4
+ data.tar.gz: 529543111937436fae9df8c5306e1e05d7b2fade976d6dc2642908ad552e60a3
5
+ SHA512:
6
+ metadata.gz: 0a66d75d540e01cb9e4217fc3f371419f2a1e17d7633cffea22153c03cf0b295d3713175e71cb8125113e33b866ae7fe74f5787ca87d4ec4a683d3574f094471
7
+ data.tar.gz: 1115aabe2841a5fbff7ca22fb2fd5a9ac4ff812a6cb630744c39f7db26661392f701a2cd424ac7c106673e3a484918aa7e9ca35313dff9450e83057efba7d88f
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
@@ -0,0 +1,34 @@
1
+ name: Ruby
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ strategy:
8
+ matrix:
9
+ os:
10
+ - ubuntu
11
+ - macos
12
+ ruby:
13
+ - 2.4
14
+ - 2.5
15
+ - 2.6
16
+ # TODO: - 2.7
17
+ # TODO: jruby, rbx
18
+
19
+ runs-on: ${{ matrix.os }}-latest
20
+
21
+ steps:
22
+ - uses: actions/checkout@v1
23
+
24
+ - name: Set up Ruby
25
+ uses: actions/setup-ruby@v1
26
+ with:
27
+ ruby-version: ${{ matrix.ruby }}
28
+ architecture: x64
29
+
30
+ - name: Build and test with Rake
31
+ run: |
32
+ gem install bundler
33
+ bundle install
34
+ bundle exec rake
@@ -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,19 @@
1
+ ---
2
+ language: ruby
3
+
4
+ rvm:
5
+ - 2.1
6
+ - 2.2
7
+ - 2.3
8
+ - 2.4
9
+ - 2.5
10
+ - rbx-3
11
+ - jruby
12
+ - ruby-head
13
+
14
+ matrix:
15
+ allow_failures:
16
+ - rvm: rbx-3
17
+ - rvm: ruby-head
18
+ - rvm: jruby
19
+ - rvm: jruby-head
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,86 @@
1
+ = statsd-ruby Travis: {<img src="https://secure.travis-ci.org/reinh/statsd.svg" />}[http://travis-ci.org/reinh/statsd] CI: {<img src="https://github.com/reinh/statsd/workflows/Ruby/badge.svg" />}[https://github.com/reinh/statsd/actions?query=workflow%3ARuby]
2
+
3
+ A Ruby client for {StatsD}[https://github.com/etsy/statsd]
4
+
5
+ = Installing
6
+
7
+ Bundler:
8
+ gem "statsd-ruby"
9
+
10
+ = Basic Usage
11
+
12
+ # Set up a global Statsd client for a server on localhost:9125
13
+ $statsd = Statsd.new 'localhost', 9125
14
+
15
+ # Set up a global Statsd client for a server on IPv6 port 9125
16
+ $statsd = Statsd.new '::1', 9125
17
+
18
+ # Send some stats
19
+ $statsd.increment 'garets'
20
+ $statsd.timing 'glork', 320
21
+ $statsd.gauge 'bork', 100
22
+
23
+ # Use {#time} to time the execution of a block
24
+ $statsd.time('account.activate') { @account.activate! }
25
+
26
+ # Create a namespaced statsd client and increment 'account.activate'
27
+ statsd = Statsd.new('localhost').tap{|sd| sd.namespace = 'account'}
28
+ statsd.increment 'activate'
29
+
30
+ = Testing
31
+
32
+ Run the specs with <tt>rake spec</tt>
33
+
34
+ = Performance
35
+
36
+ * 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 optimal performance (e.g. nscd).
37
+
38
+ = Extensions / Libraries / Extra Docs
39
+
40
+ * See the wiki[https://github.com/reinh/statsd/wiki]
41
+
42
+ == Contributing to statsd
43
+
44
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
45
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
46
+ * Fork the project
47
+ * Start a feature/bugfix branch
48
+ * Commit and push until you are happy with your contribution
49
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
50
+ * 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.
51
+
52
+ == Contributors
53
+
54
+ * Rein Henrichs
55
+ * Alex Williams
56
+ * Andrew Meyer
57
+ * Chris Gaffney
58
+ * Cody Cutrer
59
+ * Corey Donohoe
60
+ * Dotan Nahum
61
+ * Erez Rabih
62
+ * Eric Chapweske
63
+ * Gabriel Burt
64
+ * Hannes Georg
65
+ * James Tucker
66
+ * Jeremy Kemper
67
+ * John Nunemaker
68
+ * Lann Martin
69
+ * Mahesh Murthy
70
+ * Manu J
71
+ * Matt Sanford
72
+ * Nate Bird
73
+ * Noah Lorang
74
+ * Oscar Del Ben
75
+ * Peter Mounce
76
+ * Ray Krueger
77
+ * Reed Lipman
78
+ * rick
79
+ * Ryan Tomayko
80
+ * Schuyler Erle
81
+ * Thomas Whaples
82
+ * Trae Robrock
83
+
84
+ == Copyright
85
+
86
+ 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,513 @@
1
+ require 'socket'
2
+ require 'forwardable'
3
+ require 'json'
4
+
5
+ require 'statsd/monotonic_time'
6
+
7
+ # = Statsd: A Statsd client (https://github.com/etsy/statsd)
8
+ #
9
+ # @example Set up a global Statsd client for a server on localhost:8125
10
+ # $statsd = Statsd.new 'localhost', 8125
11
+ # @example Set up a global Statsd client for a server on IPv6 port 8125
12
+ # $statsd = Statsd.new '::1', 8125
13
+ # @example Send some stats
14
+ # $statsd.increment 'garets'
15
+ # $statsd.timing 'glork', 320
16
+ # $statsd.gauge 'bork', 100
17
+ # @example Use {#time} to time the execution of a block
18
+ # $statsd.time('account.activate') { @account.activate! }
19
+ # @example Create a namespaced statsd client and increment 'account.activate'
20
+ # statsd = Statsd.new('localhost').tap{|sd| sd.namespace = 'account'}
21
+ # statsd.increment 'activate'
22
+ #
23
+ # Statsd instances are thread safe for general usage, by utilizing the thread
24
+ # safe nature of UDP sends. The attributes are stateful, and are not
25
+ # mutexed, it is expected that users will not change these at runtime in
26
+ # threaded environments. If users require such use cases, it is recommend that
27
+ # users either mutex around their Statsd object, or create separate objects for
28
+ # each namespace / host+port combination.
29
+ class Statsd
30
+
31
+ # = Batch: A batching statsd proxy
32
+ #
33
+ # @example Batch a set of instruments using Batch and manual flush:
34
+ # $statsd = Statsd.new 'localhost', 8125
35
+ # batch = Statsd::Batch.new($statsd)
36
+ # batch.increment 'garets'
37
+ # batch.timing 'glork', 320
38
+ # batch.gauge 'bork', 100
39
+ # batch.flush
40
+ #
41
+ # Batch is a subclass of Statsd, but with a constructor that proxies to a
42
+ # normal Statsd instance. It has it's own batch_size and namespace parameters
43
+ # (that inherit defaults from the supplied Statsd instance). It is recommended
44
+ # that some care is taken if setting very large batch sizes. If the batch size
45
+ # exceeds the allowed packet size for UDP on your network, communication
46
+ # troubles may occur and data will be lost.
47
+ class Batch < Statsd
48
+
49
+ extend Forwardable
50
+ def_delegators :@statsd,
51
+ :namespace, :namespace=,
52
+ :host, :host=,
53
+ :port, :port=,
54
+ :prefix,
55
+ :postfix,
56
+ :delimiter, :delimiter=
57
+
58
+ attr_accessor :batch_size, :batch_byte_size, :flush_interval
59
+
60
+ # @param [Statsd] statsd requires a configured Statsd instance
61
+ def initialize(statsd)
62
+ @statsd = statsd
63
+ @batch_size = statsd.batch_size
64
+ @batch_byte_size = statsd.batch_byte_size
65
+ @flush_interval = statsd.flush_interval
66
+ @backlog = []
67
+ @backlog_bytesize = 0
68
+ @last_flush = Time.now
69
+ end
70
+
71
+ # @yield [Batch] yields itself
72
+ #
73
+ # A convenience method to ensure that data is not lost in the event of an
74
+ # exception being thrown. Batches will be transmitted on the parent socket
75
+ # as soon as the batch is full, and when the block finishes.
76
+ def easy
77
+ yield self
78
+ ensure
79
+ flush
80
+ end
81
+
82
+ def flush
83
+ unless @backlog.empty?
84
+ @statsd.send_to_socket @backlog.join("\n")
85
+ @backlog.clear
86
+ @backlog_bytesize = 0
87
+ @last_flush = Time.now
88
+ end
89
+ end
90
+
91
+ protected
92
+
93
+ def send_to_socket(message)
94
+ # this message wouldn't fit; flush the queue. note that we don't have
95
+ # to do this for message based flushing, because we're incrementing by
96
+ # one, so the post-queue check will always catch it
97
+ if (@batch_byte_size && @backlog_bytesize + message.bytesize + 1 > @batch_byte_size) ||
98
+ (@flush_interval && last_flush_seconds_ago >= @flush_interval)
99
+ flush
100
+ end
101
+ @backlog << message
102
+ @backlog_bytesize += message.bytesize
103
+ # skip the interleaved newline for the first item
104
+ @backlog_bytesize += 1 if @backlog.length != 1
105
+ # if we're precisely full now, flush
106
+ if (@batch_size && @backlog.size == @batch_size) ||
107
+ (@batch_byte_size && @backlog_bytesize == @batch_byte_size)
108
+ flush
109
+ end
110
+ end
111
+
112
+ def last_flush_seconds_ago
113
+ Time.now - @last_flush
114
+ end
115
+
116
+ end
117
+
118
+ class Admin
119
+ # StatsD host. Defaults to 127.0.0.1.
120
+ attr_reader :host
121
+
122
+ # StatsD admin port. Defaults to 8126.
123
+ attr_reader :port
124
+
125
+ class << self
126
+ # Set to a standard logger instance to enable debug logging.
127
+ attr_accessor :logger
128
+ end
129
+
130
+ # @attribute [w] host.
131
+ # Users should call connect after changing this.
132
+ def host=(host)
133
+ @host = host || '127.0.0.1'
134
+ end
135
+
136
+ # @attribute [w] port.
137
+ # Users should call connect after changing this.
138
+ def port=(port)
139
+ @port = port || 8126
140
+ end
141
+
142
+ # @param [String] host your statsd host
143
+ # @param [Integer] port your statsd port
144
+ def initialize(host = '127.0.0.1', port = 8126)
145
+ @host = host || '127.0.0.1'
146
+ @port = port || 8126
147
+ # protects @socket transactions
148
+ @socket = nil
149
+ @s_mu = Mutex.new
150
+ connect
151
+ end
152
+
153
+ # Reads all gauges from StatsD.
154
+ def gauges
155
+ read_metric :gauges
156
+ end
157
+
158
+ # Reads all timers from StatsD.
159
+ def timers
160
+ read_metric :timers
161
+ end
162
+
163
+ # Reads all counters from StatsD.
164
+ def counters
165
+ read_metric :counters
166
+ end
167
+
168
+ # @param[String] item
169
+ # Deletes one or more gauges. Wildcards are allowed.
170
+ def delgauges item
171
+ delete_metric :gauges, item
172
+ end
173
+
174
+ # @param[String] item
175
+ # Deletes one or more timers. Wildcards are allowed.
176
+ def deltimers item
177
+ delete_metric :timers, item
178
+ end
179
+
180
+ # @param[String] item
181
+ # Deletes one or more counters. Wildcards are allowed.
182
+ def delcounters item
183
+ delete_metric :counters, item
184
+ end
185
+
186
+ def stats
187
+ result = @s_mu.synchronize do
188
+ # the format of "stats" isn't JSON, who knows why
189
+ send_to_socket "stats"
190
+ read_from_socket
191
+ end
192
+ items = {}
193
+ result.split("\n").each do |line|
194
+ key, val = line.chomp.split(": ")
195
+ items[key] = val.to_i
196
+ end
197
+ items
198
+ end
199
+
200
+ # Reconnects the socket, for when the statsd address may have changed. Users
201
+ # do not normally need to call this, but calling it may be appropriate when
202
+ # reconfiguring a process (e.g. from HUP)
203
+ def connect
204
+ @s_mu.synchronize do
205
+ begin
206
+ @socket.flush rescue nil
207
+ @socket.close if @socket
208
+ rescue
209
+ # Ignore socket errors on close.
210
+ end
211
+ @socket = TCPSocket.new(host, port)
212
+ end
213
+ end
214
+
215
+ private
216
+
217
+ def read_metric name
218
+ result = @s_mu.synchronize do
219
+ send_to_socket name
220
+ read_from_socket
221
+ end
222
+ # for some reason, the reply looks like JSON, but isn't, quite
223
+ JSON.parse result.gsub("'", "\"")
224
+ end
225
+
226
+ def delete_metric name, item
227
+ result = @s_mu.synchronize do
228
+ send_to_socket "del#{name} #{item}"
229
+ read_from_socket
230
+ end
231
+ deleted = []
232
+ result.split("\n").each do |line|
233
+ deleted << line.chomp.split(": ")[-1]
234
+ end
235
+ deleted
236
+ end
237
+
238
+ def send_to_socket(message)
239
+ self.class.logger.debug { "Statsd: #{message}" } if self.class.logger
240
+ @socket.write(message.to_s + "\n")
241
+ rescue => boom
242
+ self.class.logger.error { "Statsd: #{boom.class} #{boom}" } if self.class.logger
243
+ nil
244
+ end
245
+
246
+
247
+ def read_from_socket
248
+ buffer = ""
249
+ loop do
250
+ line = @socket.readline
251
+ break if line == "END\n"
252
+ buffer += line
253
+ end
254
+ @socket.readline # clear the closing newline out of the socket
255
+ buffer
256
+ end
257
+ end
258
+
259
+ # A namespace to prepend to all statsd calls.
260
+ attr_reader :namespace
261
+
262
+ # StatsD host. Defaults to 127.0.0.1.
263
+ attr_reader :host
264
+
265
+ # StatsD port. Defaults to 8125.
266
+ attr_reader :port
267
+
268
+ # StatsD namespace prefix, generated from #namespace
269
+ attr_reader :prefix
270
+
271
+ # The default batch size for new batches. Set to nil to use batch_byte_size (default: 10)
272
+ attr_accessor :batch_size
273
+
274
+ # The default batch size, in bytes, for new batches (default: default nil; use batch_size)
275
+ attr_accessor :batch_byte_size
276
+
277
+ # The flush interval, in seconds, for new batches (default: nil)
278
+ attr_accessor :flush_interval
279
+
280
+ # a postfix to append to all metrics
281
+ attr_reader :postfix
282
+
283
+ # The replacement of :: on ruby module names when transformed to statsd metric names
284
+ attr_reader :delimiter
285
+
286
+ class << self
287
+ # Set to a standard logger instance to enable debug logging.
288
+ attr_accessor :logger
289
+ end
290
+
291
+ # @param [String] host your statsd host
292
+ # @param [Integer] port your statsd port
293
+ # @param [Symbol] protocol :tcp for TCP, :udp or any other value for UDP
294
+ def initialize(host = '127.0.0.1', port = 8125, protocol = :udp)
295
+ @host = host || '127.0.0.1'
296
+ @port = port || 8125
297
+ self.delimiter = "."
298
+ @prefix = nil
299
+ @batch_size = 10
300
+ @batch_byte_size = nil
301
+ @flush_interval = nil
302
+ @postfix = nil
303
+ @socket = nil
304
+ @protocol = protocol || :udp
305
+ @s_mu = Mutex.new
306
+ connect
307
+ end
308
+
309
+ # @attribute [w] namespace
310
+ # Writes are not thread safe.
311
+ def namespace=(namespace)
312
+ @namespace = namespace
313
+ @prefix = "#{namespace}."
314
+ end
315
+
316
+ # @attribute [w] postfix
317
+ # A value to be appended to the stat name after a '.'. If the value is
318
+ # blank then the postfix will be reset to nil (rather than to '.').
319
+ def postfix=(pf)
320
+ case pf
321
+ when nil, false, '' then @postfix = nil
322
+ else @postfix = ".#{pf}"
323
+ end
324
+ end
325
+
326
+ # @attribute [w] host
327
+ # Writes are not thread safe.
328
+ # Users should call hup after making changes.
329
+ def host=(host)
330
+ @host = host || '127.0.0.1'
331
+ end
332
+
333
+ # @attribute [w] port
334
+ # Writes are not thread safe.
335
+ # Users should call hup after making changes.
336
+ def port=(port)
337
+ @port = port || 8125
338
+ end
339
+
340
+ # @attribute [w] stat_delimiter
341
+ # Allows for custom delimiter replacement for :: when Ruby modules are transformed to statsd metric name
342
+ def delimiter=(delimiter)
343
+ @delimiter = delimiter || "."
344
+ end
345
+
346
+ # Sends an increment (count = 1) for the given stat to the statsd server.
347
+ #
348
+ # @param [String] stat stat name
349
+ # @param [Numeric] sample_rate sample rate, 1 for always
350
+ # @see #count
351
+ def increment(stat, sample_rate=1)
352
+ count stat, 1, sample_rate
353
+ end
354
+
355
+ # Sends a decrement (count = -1) for the given stat to the statsd server.
356
+ #
357
+ # @param [String] stat stat name
358
+ # @param [Numeric] sample_rate sample rate, 1 for always
359
+ # @see #count
360
+ def decrement(stat, sample_rate=1)
361
+ count stat, -1, sample_rate
362
+ end
363
+
364
+ # Sends an arbitrary count for the given stat to the statsd server.
365
+ #
366
+ # @param [String] stat stat name
367
+ # @param [Integer] count count
368
+ # @param [Numeric] sample_rate sample rate, 1 for always
369
+ def count(stat, count, sample_rate=1)
370
+ send_stats stat, count, :c, sample_rate
371
+ end
372
+
373
+ # Sends an arbitary gauge value for the given stat to the statsd server.
374
+ #
375
+ # This is useful for recording things like available disk space,
376
+ # memory usage, and the like, which have different semantics than
377
+ # counters.
378
+ #
379
+ # @param [String] stat stat name.
380
+ # @param [Numeric] value gauge value.
381
+ # @param [Numeric] sample_rate sample rate, 1 for always
382
+ # @example Report the current user count:
383
+ # $statsd.gauge('user.count', User.count)
384
+ def gauge(stat, value, sample_rate=1)
385
+ send_stats stat, value, :g, sample_rate
386
+ end
387
+
388
+ # Sends an arbitary set value for the given stat to the statsd server.
389
+ #
390
+ # This is for recording counts of unique events, which are useful to
391
+ # see on graphs to correlate to other values. For example, a deployment
392
+ # might get recorded as a set, and be drawn as annotations on a CPU history
393
+ # graph.
394
+ #
395
+ # @param [String] stat stat name.
396
+ # @param [Numeric] value event value.
397
+ # @param [Numeric] sample_rate sample rate, 1 for always
398
+ # @example Report a deployment happening:
399
+ # $statsd.set('deployment', DEPLOYMENT_EVENT_CODE)
400
+ def set(stat, value, sample_rate=1)
401
+ send_stats stat, value, :s, sample_rate
402
+ end
403
+
404
+ # Sends a timing (in ms) for the given stat to the statsd server. The
405
+ # sample_rate determines what percentage of the time this report is sent. The
406
+ # statsd server then uses the sample_rate to correctly track the average
407
+ # timing for the stat.
408
+ #
409
+ # @param [String] stat stat name
410
+ # @param [Integer] ms timing in milliseconds
411
+ # @param [Numeric] sample_rate sample rate, 1 for always
412
+ def timing(stat, ms, sample_rate=1)
413
+ send_stats stat, ms, :ms, sample_rate
414
+ end
415
+
416
+ # Reports execution time of the provided block using {#timing}.
417
+ #
418
+ # @param [String] stat stat name
419
+ # @param [Numeric] sample_rate sample rate, 1 for always
420
+ # @yield The operation to be timed
421
+ # @see #timing
422
+ # @example Report the time (in ms) taken to activate an account
423
+ # $statsd.time('account.activate') { @account.activate! }
424
+ def time(stat, sample_rate=1)
425
+ start = MonotonicTime.time_in_ms
426
+ result = yield
427
+ ensure
428
+ timing(stat, (MonotonicTime.time_in_ms - start).round, sample_rate)
429
+ result
430
+ end
431
+
432
+ # Creates and yields a Batch that can be used to batch instrument reports into
433
+ # larger packets. Batches are sent either when the packet is "full" (defined
434
+ # by batch_size), or when the block completes, whichever is the sooner.
435
+ #
436
+ # @yield [Batch] a statsd subclass that collects and batches instruments
437
+ # @example Batch two instument operations:
438
+ # $statsd.batch do |batch|
439
+ # batch.increment 'sys.requests'
440
+ # batch.gauge('user.count', User.count)
441
+ # end
442
+ def batch(&block)
443
+ Batch.new(self).easy(&block)
444
+ end
445
+
446
+ # Reconnects the socket, useful if the address of the statsd has changed. This
447
+ # method is not thread safe from a perspective of stat submission. It is safe
448
+ # from resource leaks. Users do not normally need to call this, but calling it
449
+ # may be appropriate when reconfiguring a process (e.g. from HUP).
450
+ def connect
451
+ @s_mu.synchronize do
452
+ begin
453
+ @socket.close if @socket
454
+ rescue
455
+ # Errors are ignored on reconnects.
456
+ end
457
+
458
+ case @protocol
459
+ when :tcp
460
+ @socket = TCPSocket.new @host, @port
461
+ else
462
+ @socket = UDPSocket.new Addrinfo.ip(@host).afamily
463
+ @socket.connect host, port
464
+ end
465
+ end
466
+ end
467
+
468
+ protected
469
+
470
+ def send_to_socket(message)
471
+ self.class.logger.debug { "Statsd: #{message}" } if self.class.logger
472
+
473
+ retries = 0
474
+ n = 0
475
+ while true
476
+ # send(2) is atomic, however, in stream cases (TCP) the socket is left
477
+ # in an inconsistent state if a partial message is written. If that case
478
+ # occurs, the socket is closed down and we retry on a new socket.
479
+ l = @protocol == :tcp ? message.length + 1 : message.length
480
+ br = @protocol == :tcp ? "\n" : ""
481
+ n = socket.write(message + br)
482
+
483
+ if n == l
484
+ break
485
+ end
486
+
487
+ connect
488
+ retries += 1
489
+ raise "statsd: Failed to send after #{retries} attempts" if retries >= 5
490
+ end
491
+ n
492
+ rescue => boom
493
+ self.class.logger.error { "Statsd: #{boom.class} #{boom}" } if self.class.logger
494
+ nil
495
+ end
496
+
497
+ private
498
+
499
+ def send_stats(stat, delta, type, sample_rate=1)
500
+ if sample_rate == 1 or rand < sample_rate
501
+ # Replace Ruby module scoping with '.' and reserved chars (: | @) with underscores.
502
+ stat = stat.to_s.gsub('::', delimiter).tr(':|@', '_')
503
+ rate = "|@#{sample_rate}" unless sample_rate == 1
504
+ send_to_socket "#{prefix}#{stat}#{postfix}:#{delta}|#{type}#{rate}"
505
+ end
506
+ end
507
+
508
+ def socket
509
+ # Subtle: If the socket is half-way through initialization in connect, it
510
+ # cannot be used yet.
511
+ @s_mu.synchronize { @socket } || raise(ThreadError, "socket missing")
512
+ end
513
+ end