statsd-ruby 1.1.1 → 1.5.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
+ 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