statsd-ruby 1.1.1 → 1.5.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: 0edd467226454eea7a63a7ed08e3468ac48a2d7438fdf8d291ab266a781c128b
4
+ data.tar.gz: 82f88de7bf7738e974a8d9f2d70f57f039d31d7f69f9b98c359b7d5c7970f93c
5
+ SHA512:
6
+ metadata.gz: 3520cc824d180d7304fa883fefa945d3f785d560f313c328fed8f28e4d799728c6ab61cd6b9a855d8023f644f2d328f7f6626e0af7770d8e83d99b2bb0e7a2f8
7
+ data.tar.gz: f0bc2ade747bee51cca11128a6d6ce21491b13354f519f582f90c94cd09ce1ee26372b52945c8d369dfc5160194cfd967644254350b0dd1567ff28cd64de8206
@@ -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
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 Rein Henrichs
1
+ Copyright (c) 2011, 2012, 2013 Rein Henrichs
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -1,17 +1,20 @@
1
- = statsd-ruby {<img src="https://secure.travis-ci.org/reinh/statsd.png" />}[http://travis-ci.org/reinh/statsd]
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
2
 
3
3
  A Ruby client for {StatsD}[https://github.com/etsy/statsd]
4
4
 
5
5
  = Installing
6
6
 
7
7
  Bundler:
8
- gem "statsd-ruby", :require => "statsd"
8
+ gem "statsd-ruby"
9
9
 
10
10
  = Basic Usage
11
11
 
12
12
  # Set up a global Statsd client for a server on localhost:9125
13
13
  $statsd = Statsd.new 'localhost', 9125
14
14
 
15
+ # Set up a global Statsd client for a server on IPv6 port 9125
16
+ $statsd = Statsd.new '::1', 9125
17
+
15
18
  # Send some stats
16
19
  $statsd.increment 'garets'
17
20
  $statsd.timing 'glork', 320
@@ -28,14 +31,16 @@ Bundler:
28
31
 
29
32
  Run the specs with <tt>rake spec</tt>
30
33
 
31
- Run the specs and include live integration specs with <tt>LIVE=true rake spec</tt>. Note: This will test over a real UDP socket.
32
-
33
34
  = Performance
34
35
 
35
- * 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).
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]
36
41
 
37
42
  == Contributing to statsd
38
-
43
+
39
44
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
40
45
  * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
41
46
  * Fork the project
@@ -47,15 +52,35 @@ Run the specs and include live integration specs with <tt>LIVE=true rake spec</t
47
52
  == Contributors
48
53
 
49
54
  * Rein Henrichs
50
- * Ray Krueger
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
51
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
52
79
  * Ryan Tomayko
53
- * Gabriel Burt
54
- * Rick Olson
80
+ * Schuyler Erle
81
+ * Thomas Whaples
55
82
  * Trae Robrock
56
- * Corey Donohoe
57
- * James Tucker
58
83
 
59
84
  == Copyright
60
85
 
61
- Copyright (c) 2011 Rein Henrichs. See LICENSE.txt for further details.
86
+ Copyright (c) 2011, 2012, 2013 Rein Henrichs. See LICENSE.txt for further details.
@@ -0,0 +1 @@
1
+ require 'statsd'
@@ -1,10 +1,15 @@
1
1
  require 'socket'
2
2
  require 'forwardable'
3
+ require 'json'
4
+
5
+ require 'statsd/monotonic_time'
3
6
 
4
7
  # = Statsd: A Statsd client (https://github.com/etsy/statsd)
5
8
  #
6
- # @example Set up a global Statsd client for a server on localhost:9125
9
+ # @example Set up a global Statsd client for a server on localhost:8125
7
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
8
13
  # @example Send some stats
9
14
  # $statsd.increment 'garets'
10
15
  # $statsd.timing 'glork', 320
@@ -15,8 +20,8 @@ require 'forwardable'
15
20
  # statsd = Statsd.new('localhost').tap{|sd| sd.namespace = 'account'}
16
21
  # statsd.increment 'activate'
17
22
  #
18
- # Statsd instances are thread safe for general usage, by using a thread local
19
- # UDPSocket and carrying no state. The attributes are stateful, and are not
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
20
25
  # mutexed, it is expected that users will not change these at runtime in
21
26
  # threaded environments. If users require such use cases, it is recommend that
22
27
  # users either mutex around their Statsd object, or create separate objects for
@@ -43,18 +48,27 @@ class Statsd
43
48
 
44
49
  extend Forwardable
45
50
  def_delegators :@statsd,
46
- :namespace, :namespace=, :host, :port, :prefix, :postfix
51
+ :namespace, :namespace=,
52
+ :host, :host=,
53
+ :port, :port=,
54
+ :prefix,
55
+ :postfix,
56
+ :delimiter, :delimiter=
47
57
 
48
- attr_accessor :batch_size
58
+ attr_accessor :batch_size, :batch_byte_size, :flush_interval
49
59
 
50
- # @param [Statsd] requires a configured Statsd instance
60
+ # @param [Statsd] statsd requires a configured Statsd instance
51
61
  def initialize(statsd)
52
62
  @statsd = statsd
53
63
  @batch_size = statsd.batch_size
64
+ @batch_byte_size = statsd.batch_byte_size
65
+ @flush_interval = statsd.flush_interval
54
66
  @backlog = []
67
+ @backlog_bytesize = 0
68
+ @last_flush = Time.now
55
69
  end
56
70
 
57
- # @yields [Batch] yields itself
71
+ # @yield [Batch] yields itself
58
72
  #
59
73
  # A convenience method to ensure that data is not lost in the event of an
60
74
  # exception being thrown. Batches will be transmitted on the parent socket
@@ -69,20 +83,178 @@ class Statsd
69
83
  unless @backlog.empty?
70
84
  @statsd.send_to_socket @backlog.join("\n")
71
85
  @backlog.clear
86
+ @backlog_bytesize = 0
87
+ @last_flush = Time.now
72
88
  end
73
89
  end
74
90
 
75
91
  protected
76
92
 
77
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
78
101
  @backlog << message
79
- if @backlog.size >= @batch_size
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)
80
108
  flush
81
109
  end
82
110
  end
83
111
 
112
+ def last_flush_seconds_ago
113
+ Time.now - @last_flush
114
+ end
115
+
84
116
  end
85
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
86
258
 
87
259
  # A namespace to prepend to all statsd calls.
88
260
  attr_reader :namespace
@@ -96,12 +268,21 @@ class Statsd
96
268
  # StatsD namespace prefix, generated from #namespace
97
269
  attr_reader :prefix
98
270
 
99
- # The default batch size for new batches (default: 10)
271
+ # The default batch size for new batches. Set to nil to use batch_byte_size (default: 10)
100
272
  attr_accessor :batch_size
101
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
+
102
280
  # a postfix to append to all metrics
103
281
  attr_reader :postfix
104
282
 
283
+ # The replacement of :: on ruby module names when transformed to statsd metric names
284
+ attr_reader :delimiter
285
+
105
286
  class << self
106
287
  # Set to a standard logger instance to enable debug logging.
107
288
  attr_accessor :logger
@@ -109,11 +290,20 @@ class Statsd
109
290
 
110
291
  # @param [String] host your statsd host
111
292
  # @param [Integer] port your statsd port
112
- def initialize(host = '127.0.0.1', port = 8125)
113
- self.host, self.port = host, 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 = "."
114
298
  @prefix = nil
115
299
  @batch_size = 10
300
+ @batch_byte_size = nil
301
+ @flush_interval = nil
116
302
  @postfix = nil
303
+ @socket = nil
304
+ @protocol = protocol || :udp
305
+ @s_mu = Mutex.new
306
+ connect
117
307
  end
118
308
 
119
309
  # @attribute [w] namespace
@@ -135,16 +325,24 @@ class Statsd
135
325
 
136
326
  # @attribute [w] host
137
327
  # Writes are not thread safe.
328
+ # Users should call hup after making changes.
138
329
  def host=(host)
139
330
  @host = host || '127.0.0.1'
140
331
  end
141
332
 
142
333
  # @attribute [w] port
143
334
  # Writes are not thread safe.
335
+ # Users should call hup after making changes.
144
336
  def port=(port)
145
337
  @port = port || 8125
146
338
  end
147
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
+
148
346
  # Sends an increment (count = 1) for the given stat to the statsd server.
149
347
  #
150
348
  # @param [String] stat stat name
@@ -187,6 +385,22 @@ class Statsd
187
385
  send_stats stat, value, :g, sample_rate
188
386
  end
189
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
+
190
404
  # Sends a timing (in ms) for the given stat to the statsd server. The
191
405
  # sample_rate determines what percentage of the time this report is sent. The
192
406
  # statsd server then uses the sample_rate to correctly track the average
@@ -208,9 +422,10 @@ class Statsd
208
422
  # @example Report the time (in ms) taken to activate an account
209
423
  # $statsd.time('account.activate') { @account.activate! }
210
424
  def time(stat, sample_rate=1)
211
- start = Time.now
425
+ start = MonotonicTime.time_in_ms
212
426
  result = yield
213
- timing(stat, ((Time.now - start) * 1000).round, sample_rate)
427
+ ensure
428
+ timing(stat, (MonotonicTime.time_in_ms - start).round, sample_rate)
214
429
  result
215
430
  end
216
431
 
@@ -225,14 +440,53 @@ class Statsd
225
440
  # batch.gauge('user.count', User.count)
226
441
  # end
227
442
  def batch(&block)
228
- Batch.new(self).easy &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
229
466
  end
230
467
 
231
468
  protected
232
469
 
233
470
  def send_to_socket(message)
234
471
  self.class.logger.debug { "Statsd: #{message}" } if self.class.logger
235
- socket.send(message, 0, @host, @port)
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
+ message = @protocol == :tcp ? message + "\n" : message
480
+ n = socket.write(message) rescue (err = $!; 0)
481
+ if n == message.length
482
+ break
483
+ end
484
+
485
+ connect
486
+ retries += 1
487
+ raise (err || "statsd: Failed to send after #{retries} attempts") if retries >= 5
488
+ end
489
+ n
236
490
  rescue => boom
237
491
  self.class.logger.error { "Statsd: #{boom.class} #{boom}" } if self.class.logger
238
492
  nil
@@ -243,13 +497,15 @@ class Statsd
243
497
  def send_stats(stat, delta, type, sample_rate=1)
244
498
  if sample_rate == 1 or rand < sample_rate
245
499
  # Replace Ruby module scoping with '.' and reserved chars (: | @) with underscores.
246
- stat = stat.to_s.gsub('::', '.').tr(':|@', '_')
500
+ stat = stat.to_s.gsub('::', delimiter).tr(':|@', '_')
247
501
  rate = "|@#{sample_rate}" unless sample_rate == 1
248
502
  send_to_socket "#{prefix}#{stat}#{postfix}:#{delta}|#{type}#{rate}"
249
503
  end
250
504
  end
251
505
 
252
506
  def socket
253
- Thread.current[:statsd_socket] ||= UDPSocket.new
507
+ # Subtle: If the socket is half-way through initialization in connect, it
508
+ # cannot be used yet.
509
+ @s_mu.synchronize { @socket } || raise(ThreadError, "socket missing")
254
510
  end
255
511
  end