├── .document
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── Gemfile
├── LICENSE.txt
├── README.rdoc
├── Rakefile
├── lib
├── statsd-ruby.rb
├── statsd.rb
└── statsd
│ └── monotonic_time.rb
├── spec
├── helper.rb
├── statsd_admin_spec.rb
└── statsd_spec.rb
└── statsd-ruby.gemspec
/.document:
--------------------------------------------------------------------------------
1 | lib/**/*.rb
2 | bin/*
3 | -
4 | features/**/*.feature
5 | LICENSE.txt
6 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
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 | - 2.7
17 | - '3.0'
18 | - 3.1
19 | - 3.2
20 | # TODO: jruby, rbx
21 |
22 | runs-on: ${{ matrix.os }}-latest
23 |
24 | steps:
25 | - uses: actions/checkout@v3
26 |
27 | - name: Set up Ruby
28 | uses: ruby/setup-ruby@v1
29 | with:
30 | ruby-version: ${{ matrix.ruby }}
31 | bundler: 2.3.26
32 |
33 | - name: Build and test with Rake
34 | run: |
35 | bundle install
36 | bundle exec rake
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
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
44 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | gemspec
3 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
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.
21 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = statsd-ruby CI: {
}[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 rake spec
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.
87 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
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
16 |
--------------------------------------------------------------------------------
/lib/statsd-ruby.rb:
--------------------------------------------------------------------------------
1 | require 'statsd'
2 |
--------------------------------------------------------------------------------
/lib/statsd.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'socket'
4 | require 'forwardable'
5 | require 'json'
6 |
7 | require 'statsd/monotonic_time'
8 |
9 | # = Statsd: A Statsd client (https://github.com/etsy/statsd)
10 | #
11 | # @example Set up a global Statsd client for a server on localhost:8125
12 | # $statsd = Statsd.new 'localhost', 8125
13 | # @example Set up a global Statsd client for a server on IPv6 port 8125
14 | # $statsd = Statsd.new '::1', 8125
15 | # @example Send some stats
16 | # $statsd.increment 'garets'
17 | # $statsd.timing 'glork', 320
18 | # $statsd.gauge 'bork', 100
19 | # @example Use {#time} to time the execution of a block
20 | # $statsd.time('account.activate') { @account.activate! }
21 | # @example Create a namespaced statsd client and increment 'account.activate'
22 | # statsd = Statsd.new('localhost').tap{|sd| sd.namespace = 'account'}
23 | # statsd.increment 'activate'
24 | #
25 | # Statsd instances are thread safe for general usage, by utilizing the thread
26 | # safe nature of UDP sends. The attributes are stateful, and are not
27 | # mutexed, it is expected that users will not change these at runtime in
28 | # threaded environments. If users require such use cases, it is recommend that
29 | # users either mutex around their Statsd object, or create separate objects for
30 | # each namespace / host+port combination.
31 | class Statsd
32 |
33 | # = Batch: A batching statsd proxy
34 | #
35 | # @example Batch a set of instruments using Batch and manual flush:
36 | # $statsd = Statsd.new 'localhost', 8125
37 | # batch = Statsd::Batch.new($statsd)
38 | # batch.increment 'garets'
39 | # batch.timing 'glork', 320
40 | # batch.gauge 'bork', 100
41 | # batch.flush
42 | #
43 | # Batch is a subclass of Statsd, but with a constructor that proxies to a
44 | # normal Statsd instance. It has it's own batch_size and namespace parameters
45 | # (that inherit defaults from the supplied Statsd instance). It is recommended
46 | # that some care is taken if setting very large batch sizes. If the batch size
47 | # exceeds the allowed packet size for UDP on your network, communication
48 | # troubles may occur and data will be lost.
49 | class Batch < Statsd
50 |
51 | extend Forwardable
52 | def_delegators :@statsd,
53 | :namespace, :namespace=,
54 | :host, :host=,
55 | :port, :port=,
56 | :prefix,
57 | :postfix,
58 | :delimiter, :delimiter=
59 |
60 | attr_accessor :batch_size, :batch_byte_size, :flush_interval
61 |
62 | # @param [Statsd] statsd requires a configured Statsd instance
63 | def initialize(statsd)
64 | @statsd = statsd
65 | @batch_size = statsd.batch_size
66 | @batch_byte_size = statsd.batch_byte_size
67 | @flush_interval = statsd.flush_interval
68 | @backlog = []
69 | @backlog_bytesize = 0
70 | @last_flush = Time.now
71 | end
72 |
73 | # @yield [Batch] yields itself
74 | #
75 | # A convenience method to ensure that data is not lost in the event of an
76 | # exception being thrown. Batches will be transmitted on the parent socket
77 | # as soon as the batch is full, and when the block finishes.
78 | def easy
79 | yield self
80 | ensure
81 | flush
82 | end
83 |
84 | def flush
85 | unless @backlog.empty?
86 | @statsd.send_to_socket @backlog.join("\n")
87 | @backlog.clear
88 | @backlog_bytesize = 0
89 | @last_flush = Time.now
90 | end
91 | end
92 |
93 | protected
94 |
95 | def send_to_socket(message)
96 | # this message wouldn't fit; flush the queue. note that we don't have
97 | # to do this for message based flushing, because we're incrementing by
98 | # one, so the post-queue check will always catch it
99 | if (@batch_byte_size && @backlog_bytesize + message.bytesize + 1 > @batch_byte_size) ||
100 | (@flush_interval && last_flush_seconds_ago >= @flush_interval)
101 | flush
102 | end
103 | @backlog << message
104 | @backlog_bytesize += message.bytesize
105 | # skip the interleaved newline for the first item
106 | @backlog_bytesize += 1 if @backlog.length != 1
107 | # if we're precisely full now, flush
108 | if (@batch_size && @backlog.size == @batch_size) ||
109 | (@batch_byte_size && @backlog_bytesize == @batch_byte_size)
110 | flush
111 | end
112 | end
113 |
114 | def last_flush_seconds_ago
115 | Time.now - @last_flush
116 | end
117 |
118 | end
119 |
120 | class Admin
121 | # StatsD host. Defaults to 127.0.0.1.
122 | attr_reader :host
123 |
124 | # StatsD admin port. Defaults to 8126.
125 | attr_reader :port
126 |
127 | class << self
128 | # Set to a standard logger instance to enable debug logging.
129 | attr_accessor :logger
130 | end
131 |
132 | # @attribute [w] host.
133 | # Users should call connect after changing this.
134 | def host=(host)
135 | @host = host || '127.0.0.1'
136 | end
137 |
138 | # @attribute [w] port.
139 | # Users should call connect after changing this.
140 | def port=(port)
141 | @port = port || 8126
142 | end
143 |
144 | # @param [String] host your statsd host
145 | # @param [Integer] port your statsd port
146 | def initialize(host = '127.0.0.1', port = 8126)
147 | @host = host || '127.0.0.1'
148 | @port = port || 8126
149 | # protects @socket transactions
150 | @socket = nil
151 | @s_mu = Mutex.new
152 | connect
153 | end
154 |
155 | # Reads all gauges from StatsD.
156 | def gauges
157 | read_metric :gauges
158 | end
159 |
160 | # Reads all timers from StatsD.
161 | def timers
162 | read_metric :timers
163 | end
164 |
165 | # Reads all counters from StatsD.
166 | def counters
167 | read_metric :counters
168 | end
169 |
170 | # @param[String] item
171 | # Deletes one or more gauges. Wildcards are allowed.
172 | def delgauges item
173 | delete_metric :gauges, item
174 | end
175 |
176 | # @param[String] item
177 | # Deletes one or more timers. Wildcards are allowed.
178 | def deltimers item
179 | delete_metric :timers, item
180 | end
181 |
182 | # @param[String] item
183 | # Deletes one or more counters. Wildcards are allowed.
184 | def delcounters item
185 | delete_metric :counters, item
186 | end
187 |
188 | def stats
189 | result = @s_mu.synchronize do
190 | # the format of "stats" isn't JSON, who knows why
191 | send_to_socket "stats"
192 | read_from_socket
193 | end
194 | items = {}
195 | result.split("\n").each do |line|
196 | key, val = line.chomp.split(": ")
197 | items[key] = val.to_i
198 | end
199 | items
200 | end
201 |
202 | # Reconnects the socket, for when the statsd address may have changed. Users
203 | # do not normally need to call this, but calling it may be appropriate when
204 | # reconfiguring a process (e.g. from HUP)
205 | def connect
206 | @s_mu.synchronize do
207 | begin
208 | @socket.flush rescue nil
209 | @socket.close if @socket
210 | rescue
211 | # Ignore socket errors on close.
212 | end
213 | @socket = TCPSocket.new(host, port)
214 | end
215 | end
216 |
217 | private
218 |
219 | def read_metric name
220 | result = @s_mu.synchronize do
221 | send_to_socket name
222 | read_from_socket
223 | end
224 | # for some reason, the reply looks like JSON, but isn't, quite
225 | JSON.parse result.gsub("'", "\"")
226 | end
227 |
228 | def delete_metric name, item
229 | result = @s_mu.synchronize do
230 | send_to_socket "del#{name} #{item}"
231 | read_from_socket
232 | end
233 | deleted = []
234 | result.split("\n").each do |line|
235 | deleted << line.chomp.split(": ")[-1]
236 | end
237 | deleted
238 | end
239 |
240 | def send_to_socket(message)
241 | self.class.logger.debug { "Statsd: #{message}" } if self.class.logger
242 | @socket.write(message.to_s + "\n")
243 | rescue => boom
244 | self.class.logger.error { "Statsd: #{boom.class} #{boom}" } if self.class.logger
245 | nil
246 | end
247 |
248 |
249 | def read_from_socket
250 | buffer = ""
251 | loop do
252 | line = @socket.readline
253 | break if line == "END\n"
254 | buffer += line
255 | end
256 | @socket.readline # clear the closing newline out of the socket
257 | buffer
258 | end
259 | end
260 |
261 | # A namespace to prepend to all statsd calls.
262 | attr_reader :namespace
263 |
264 | # StatsD host. Defaults to 127.0.0.1.
265 | attr_reader :host
266 |
267 | # StatsD port. Defaults to 8125.
268 | attr_reader :port
269 |
270 | # StatsD namespace prefix, generated from #namespace
271 | attr_reader :prefix
272 |
273 | # The default batch size for new batches. Set to nil to use batch_byte_size (default: 10)
274 | attr_accessor :batch_size
275 |
276 | # The default batch size, in bytes, for new batches (default: default nil; use batch_size)
277 | attr_accessor :batch_byte_size
278 |
279 | # The flush interval, in seconds, for new batches (default: nil)
280 | attr_accessor :flush_interval
281 |
282 | # a postfix to append to all metrics
283 | attr_reader :postfix
284 |
285 | # The replacement of :: on ruby module names when transformed to statsd metric names
286 | attr_reader :delimiter
287 |
288 | class << self
289 | # Set to a standard logger instance to enable debug logging.
290 | attr_accessor :logger
291 | end
292 |
293 | # @param [String] host your statsd host
294 | # @param [Integer] port your statsd port
295 | # @param [Symbol] protocol :tcp for TCP, :udp or any other value for UDP
296 | def initialize(host = '127.0.0.1', port = 8125, protocol = :udp)
297 | @host = host || '127.0.0.1'
298 | @port = port || 8125
299 | self.delimiter = "."
300 | @prefix = nil
301 | @batch_size = 10
302 | @batch_byte_size = nil
303 | @flush_interval = nil
304 | @postfix = nil
305 | @socket = nil
306 | @protocol = protocol || :udp
307 | @s_mu = Mutex.new
308 | connect
309 | end
310 |
311 | # @attribute [w] namespace
312 | # Writes are not thread safe.
313 | def namespace=(namespace)
314 | @namespace = namespace
315 | @prefix = "#{namespace}."
316 | end
317 |
318 | # @attribute [w] postfix
319 | # A value to be appended to the stat name after a '.'. If the value is
320 | # blank then the postfix will be reset to nil (rather than to '.').
321 | def postfix=(pf)
322 | case pf
323 | when nil, false, '' then @postfix = nil
324 | else @postfix = ".#{pf}"
325 | end
326 | end
327 |
328 | # @attribute [w] host
329 | # Writes are not thread safe.
330 | # Users should call hup after making changes.
331 | def host=(host)
332 | @host = host || '127.0.0.1'
333 | end
334 |
335 | # @attribute [w] port
336 | # Writes are not thread safe.
337 | # Users should call hup after making changes.
338 | def port=(port)
339 | @port = port || 8125
340 | end
341 |
342 | # @attribute [w] stat_delimiter
343 | # Allows for custom delimiter replacement for :: when Ruby modules are transformed to statsd metric name
344 | def delimiter=(delimiter)
345 | @delimiter = delimiter || "."
346 | end
347 |
348 | # Sends an increment (count = 1) for the given stat to the statsd server.
349 | #
350 | # @param [String] stat stat name
351 | # @param [Numeric] sample_rate sample rate, 1 for always
352 | # @see #count
353 | def increment(stat, sample_rate=1)
354 | count stat, 1, sample_rate
355 | end
356 |
357 | # Sends a decrement (count = -1) for the given stat to the statsd server.
358 | #
359 | # @param [String] stat stat name
360 | # @param [Numeric] sample_rate sample rate, 1 for always
361 | # @see #count
362 | def decrement(stat, sample_rate=1)
363 | count stat, -1, sample_rate
364 | end
365 |
366 | # Sends an arbitrary count for the given stat to the statsd server.
367 | #
368 | # @param [String] stat stat name
369 | # @param [Integer] count count
370 | # @param [Numeric] sample_rate sample rate, 1 for always
371 | def count(stat, count, sample_rate=1)
372 | send_stats stat, count, :c, sample_rate
373 | end
374 |
375 | # Sends an arbitary gauge value for the given stat to the statsd server.
376 | #
377 | # This is useful for recording things like available disk space,
378 | # memory usage, and the like, which have different semantics than
379 | # counters.
380 | #
381 | # @param [String] stat stat name.
382 | # @param [Numeric] value gauge value.
383 | # @param [Numeric] sample_rate sample rate, 1 for always
384 | # @example Report the current user count:
385 | # $statsd.gauge('user.count', User.count)
386 | def gauge(stat, value, sample_rate=1)
387 | send_stats stat, value, :g, sample_rate
388 | end
389 |
390 | # Sends an arbitary set value for the given stat to the statsd server.
391 | #
392 | # This is for recording counts of unique events, which are useful to
393 | # see on graphs to correlate to other values. For example, a deployment
394 | # might get recorded as a set, and be drawn as annotations on a CPU history
395 | # graph.
396 | #
397 | # @param [String] stat stat name.
398 | # @param [Numeric] value event value.
399 | # @param [Numeric] sample_rate sample rate, 1 for always
400 | # @example Report a deployment happening:
401 | # $statsd.set('deployment', DEPLOYMENT_EVENT_CODE)
402 | def set(stat, value, sample_rate=1)
403 | send_stats stat, value, :s, sample_rate
404 | end
405 |
406 | # Sends a timing (in ms) for the given stat to the statsd server. The
407 | # sample_rate determines what percentage of the time this report is sent. The
408 | # statsd server then uses the sample_rate to correctly track the average
409 | # timing for the stat.
410 | #
411 | # @param [String] stat stat name
412 | # @param [Integer] ms timing in milliseconds
413 | # @param [Numeric] sample_rate sample rate, 1 for always
414 | def timing(stat, ms, sample_rate=1)
415 | send_stats stat, ms, :ms, sample_rate
416 | end
417 |
418 | # Reports execution time of the provided block using {#timing}.
419 | #
420 | # @param [String] stat stat name
421 | # @param [Numeric] sample_rate sample rate, 1 for always
422 | # @yield The operation to be timed
423 | # @see #timing
424 | # @example Report the time (in ms) taken to activate an account
425 | # $statsd.time('account.activate') { @account.activate! }
426 | def time(stat, sample_rate=1)
427 | start = MonotonicTime.time_in_ms
428 | result = yield
429 | ensure
430 | timing(stat, (MonotonicTime.time_in_ms - start).round, sample_rate)
431 | result
432 | end
433 |
434 | # Creates and yields a Batch that can be used to batch instrument reports into
435 | # larger packets. Batches are sent either when the packet is "full" (defined
436 | # by batch_size), or when the block completes, whichever is the sooner.
437 | #
438 | # @yield [Batch] a statsd subclass that collects and batches instruments
439 | # @example Batch two instument operations:
440 | # $statsd.batch do |batch|
441 | # batch.increment 'sys.requests'
442 | # batch.gauge('user.count', User.count)
443 | # end
444 | def batch(&block)
445 | Batch.new(self).easy(&block)
446 | end
447 |
448 | # Reconnects the socket, useful if the address of the statsd has changed. This
449 | # method is not thread safe from a perspective of stat submission. It is safe
450 | # from resource leaks. Users do not normally need to call this, but calling it
451 | # may be appropriate when reconfiguring a process (e.g. from HUP).
452 | def connect
453 | @s_mu.synchronize do
454 | begin
455 | @socket.close if @socket
456 | rescue
457 | # Errors are ignored on reconnects.
458 | end
459 |
460 | case @protocol
461 | when :tcp
462 | @socket = TCPSocket.new @host, @port
463 | else
464 | @socket = UDPSocket.new Addrinfo.ip(@host).afamily
465 | @socket.connect host, port
466 | end
467 | end
468 | end
469 |
470 | protected
471 |
472 | def send_to_socket(message)
473 | self.class.logger.debug { "Statsd: #{message}" } if self.class.logger
474 |
475 | retries = 0
476 | n = 0
477 | while true
478 | # send(2) is atomic, however, in stream cases (TCP) the socket is left
479 | # in an inconsistent state if a partial message is written. If that case
480 | # occurs, the socket is closed down and we retry on a new socket.
481 | message = @protocol == :tcp ? message + "\n" : message
482 | n = socket.write(message) rescue (err = $!; 0)
483 | if n == message.length
484 | break
485 | end
486 |
487 | connect
488 | retries += 1
489 | raise (err || "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
514 |
--------------------------------------------------------------------------------
/lib/statsd/monotonic_time.rb:
--------------------------------------------------------------------------------
1 | class Statsd
2 | # = MonotonicTime: a helper for getting monotonic time
3 | #
4 | # @example
5 | # MonotonicTime.time_in_ms #=> 287138801.144576
6 |
7 | # MonotonicTime guarantees that the time is strictly linearly
8 | # increasing (unlike realtime).
9 | # @see http://pubs.opengroup.org/onlinepubs/9699919799/functions/clock_getres.html
10 | module MonotonicTime
11 | class << self
12 | # @return [Integer] current monotonic time in milliseconds
13 | def time_in_ms
14 | time_in_nanoseconds / (10.0 ** 6)
15 | end
16 |
17 | private
18 |
19 | if defined?(Process::CLOCK_MONOTONIC)
20 | def time_in_nanoseconds
21 | Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
22 | end
23 | elsif RUBY_ENGINE == 'jruby'
24 | def time_in_nanoseconds
25 | java.lang.System.nanoTime
26 | end
27 | else
28 | def time_in_nanoseconds
29 | t = Time.now
30 | t.to_i * (10 ** 9) + t.nsec
31 | end
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/helper.rb:
--------------------------------------------------------------------------------
1 | require 'bundler/setup'
2 |
3 | require 'simplecov'
4 | SimpleCov.start
5 |
6 | require 'minitest/autorun'
7 | require 'statsd'
8 | require 'logger'
9 | require 'timeout'
10 |
11 | class FakeUDPSocket
12 | def initialize
13 | @buffer = []
14 | end
15 |
16 | def write(message)
17 | @buffer.push [message]
18 | message.length
19 | end
20 |
21 | def recv
22 | @buffer.shift
23 | end
24 |
25 | def clear
26 | @buffer = []
27 | end
28 |
29 | def to_s
30 | inspect
31 | end
32 |
33 | def inspect
34 | "<#{self.class.name}: #{@buffer.inspect}>"
35 | end
36 | end
37 |
38 | class FakeTCPSocket < FakeUDPSocket
39 | alias_method :readline, :recv
40 | def write(message)
41 | @buffer.push message
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/spec/statsd_admin_spec.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 |
3 | describe Statsd::Admin do
4 |
5 | before do
6 | class Statsd::Admin
7 | o, $VERBOSE = $VERBOSE, nil
8 | alias connect_old connect
9 | def connect
10 | $connect_count ||= 0
11 | $connect_count += 1
12 | end
13 | $VERBOSE = o
14 | end
15 | @admin = Statsd::Admin.new('localhost', 1234)
16 | @socket = @admin.instance_variable_set(:@socket, FakeTCPSocket.new)
17 | end
18 |
19 | after do
20 | class Statsd::Admin
21 | o, $VERBOSE = $VERBOSE, nil
22 | alias connect connect_old
23 | $VERBOSE = o
24 | end
25 | end
26 |
27 | describe "#initialize" do
28 | it "should set the host and port" do
29 | _(@admin.host).must_equal 'localhost'
30 | _(@admin.port).must_equal 1234
31 | end
32 |
33 | it "should default the host to 127.0.0.1 and port to 8126" do
34 | statsd = Statsd::Admin.new
35 | _(statsd.host).must_equal '127.0.0.1'
36 | _(statsd.port).must_equal 8126
37 | end
38 | end
39 |
40 | describe "#host and #port" do
41 | it "should set host and port" do
42 | @admin.host = '1.2.3.4'
43 | @admin.port = 5678
44 | _(@admin.host).must_equal '1.2.3.4'
45 | _(@admin.port).must_equal 5678
46 | end
47 |
48 | it "should not resolve hostnames to IPs" do
49 | @admin.host = 'localhost'
50 | _(@admin.host).must_equal 'localhost'
51 | end
52 |
53 | it "should set nil host to default" do
54 | @admin.host = nil
55 | _(@admin.host).must_equal '127.0.0.1'
56 | end
57 |
58 | it "should set nil port to default" do
59 | @admin.port = nil
60 | _(@admin.port).must_equal 8126
61 | end
62 | end
63 |
64 | %w(gauges counters timers).each do |action|
65 | describe "##{action}" do
66 | it "should send a command and return a Hash" do
67 | ["{'foo.bar': 0,\n",
68 | "'foo.baz': 1,\n",
69 | "'foo.quux': 2 }\n",
70 | "END\n","\n"].each do |line|
71 | @socket.write line
72 | end
73 | result = @admin.send action.to_sym
74 | _(result).must_be_kind_of Hash
75 | _(result.size).must_equal 3
76 | _(@socket.readline).must_equal "#{action}\n"
77 | end
78 | end
79 |
80 | describe "#del#{action}" do
81 | it "should send a command and return an Array" do
82 | ["deleted: foo.bar\n",
83 | "deleted: foo.baz\n",
84 | "deleted: foo.quux\n",
85 | "END\n", "\n"].each do |line|
86 | @socket.write line
87 | end
88 | result = @admin.send "del#{action}", "foo.*"
89 | _(result).must_be_kind_of Array
90 | _(result.size).must_equal 3
91 | _(@socket.readline).must_equal "del#{action} foo.*\n"
92 | end
93 | end
94 | end
95 |
96 | describe "#stats" do
97 | it "should send a command and return a Hash" do
98 | ["whatever: 0\n", "END\n", "\n"].each do |line|
99 | @socket.write line
100 | end
101 | result = @admin.stats
102 | _(result).must_be_kind_of Hash
103 | _(result["whatever"]).must_equal 0
104 | _(@socket.readline).must_equal "stats\n"
105 | end
106 | end
107 |
108 | describe "#connect" do
109 | it "should reconnect" do
110 | c = $connect_count
111 | @admin.connect
112 | _(($connect_count - c)).must_equal 1
113 | end
114 | end
115 | end
116 |
117 |
118 |
--------------------------------------------------------------------------------
/spec/statsd_spec.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 |
3 | describe Statsd do
4 | before do
5 | class Statsd
6 | o, $VERBOSE = $VERBOSE, nil
7 | alias connect_old connect
8 | def connect
9 | $connect_count ||= 1
10 | $connect_count += 1
11 | end
12 | $VERBOSE = o
13 | end
14 |
15 | @statsd = Statsd.new('localhost', 1234)
16 | @socket = @statsd.instance_variable_set(:@socket, FakeUDPSocket.new)
17 | end
18 |
19 | after do
20 | class Statsd
21 | o, $VERBOSE = $VERBOSE, nil
22 | alias connect connect_old
23 | $VERBOSE = o
24 | end
25 | end
26 |
27 | describe "#initialize" do
28 | it "should set the host and port" do
29 | _(@statsd.host).must_equal 'localhost'
30 | _(@statsd.port).must_equal 1234
31 | end
32 |
33 | it "should default the host to 127.0.0.1 and port to 8125" do
34 | statsd = Statsd.new
35 | _(statsd.host).must_equal '127.0.0.1'
36 | _(statsd.port).must_equal 8125
37 | end
38 |
39 | it "should set delimiter to period by default" do
40 | _(@statsd.delimiter).must_equal "."
41 | end
42 | end
43 |
44 | describe "#host and #port" do
45 | it "should set host and port" do
46 | @statsd.host = '1.2.3.4'
47 | @statsd.port = 5678
48 | _(@statsd.host).must_equal '1.2.3.4'
49 | _(@statsd.port).must_equal 5678
50 | end
51 |
52 | it "should not resolve hostnames to IPs" do
53 | @statsd.host = 'localhost'
54 | _(@statsd.host).must_equal 'localhost'
55 | end
56 |
57 | it "should set nil host to default" do
58 | @statsd.host = nil
59 | _(@statsd.host).must_equal '127.0.0.1'
60 | end
61 |
62 | it "should set nil port to default" do
63 | @statsd.port = nil
64 | _(@statsd.port).must_equal 8125
65 | end
66 |
67 | it "should allow an IPv6 address" do
68 | @statsd.host = '::1'
69 | _(@statsd.host).must_equal '::1'
70 | end
71 | end
72 |
73 | describe "#delimiter" do
74 | it "should set delimiter" do
75 | @statsd.delimiter = "-"
76 | _(@statsd.delimiter).must_equal "-"
77 | end
78 |
79 | it "should set default to period if not given a value" do
80 | @statsd.delimiter = nil
81 | _(@statsd.delimiter).must_equal "."
82 | end
83 | end
84 |
85 | describe "#increment" do
86 | it "should format the message according to the statsd spec" do
87 | @statsd.increment('foobar')
88 | _(@socket.recv).must_equal ['foobar:1|c']
89 | end
90 |
91 | describe "with a sample rate" do
92 | before { class << @statsd; def rand; 0; end; end } # ensure delivery
93 | it "should format the message according to the statsd spec" do
94 | @statsd.increment('foobar', 0.5)
95 | _(@socket.recv).must_equal ['foobar:1|c|@0.5']
96 | end
97 | end
98 | end
99 |
100 | describe "#decrement" do
101 | it "should format the message according to the statsd spec" do
102 | @statsd.decrement('foobar')
103 | _(@socket.recv).must_equal ['foobar:-1|c']
104 | end
105 |
106 | describe "with a sample rate" do
107 | before { class << @statsd; def rand; 0; end; end } # ensure delivery
108 | it "should format the message according to the statsd spec" do
109 | @statsd.decrement('foobar', 0.5)
110 | _(@socket.recv).must_equal ['foobar:-1|c|@0.5']
111 | end
112 | end
113 | end
114 |
115 | describe "#gauge" do
116 | it "should send a message with a 'g' type, per the nearbuy fork" do
117 | @statsd.gauge('begrutten-suffusion', 536)
118 | _(@socket.recv).must_equal ['begrutten-suffusion:536|g']
119 | @statsd.gauge('begrutten-suffusion', -107.3)
120 | _(@socket.recv).must_equal ['begrutten-suffusion:-107.3|g']
121 | end
122 |
123 | describe "with a sample rate" do
124 | before { class << @statsd; def rand; 0; end; end } # ensure delivery
125 | it "should format the message according to the statsd spec" do
126 | @statsd.gauge('begrutten-suffusion', 536, 0.1)
127 | _(@socket.recv).must_equal ['begrutten-suffusion:536|g|@0.1']
128 | end
129 | end
130 | end
131 |
132 | describe "#timing" do
133 | it "should format the message according to the statsd spec" do
134 | @statsd.timing('foobar', 500)
135 | _(@socket.recv).must_equal ['foobar:500|ms']
136 | end
137 |
138 | describe "with a sample rate" do
139 | before { class << @statsd; def rand; 0; end; end } # ensure delivery
140 | it "should format the message according to the statsd spec" do
141 | @statsd.timing('foobar', 500, 0.5)
142 | _(@socket.recv).must_equal ['foobar:500|ms|@0.5']
143 | end
144 | end
145 | end
146 |
147 | describe "#set" do
148 | it "should format the message according to the statsd spec" do
149 | @statsd.set('foobar', 765)
150 | _(@socket.recv).must_equal ['foobar:765|s']
151 | end
152 |
153 | describe "with a sample rate" do
154 | before { class << @statsd; def rand; 0; end; end } # ensure delivery
155 | it "should format the message according to the statsd spec" do
156 | @statsd.set('foobar', 500, 0.5)
157 | _(@socket.recv).must_equal ['foobar:500|s|@0.5']
158 | end
159 | end
160 | end
161 |
162 | describe "#time" do
163 | it "should format the message according to the statsd spec" do
164 | @statsd.time('foobar') { 'test' }
165 | _(@socket.recv).must_equal ['foobar:0|ms']
166 | end
167 |
168 | it "should return the result of the block" do
169 | result = @statsd.time('foobar') { 'test' }
170 | _(result).must_equal 'test'
171 | end
172 |
173 | describe "when given a block with an explicit return" do
174 | it "should format the message according to the statsd spec" do
175 | lambda { @statsd.time('foobar') { return 'test' } }.call
176 | _(@socket.recv).must_equal ['foobar:0|ms']
177 | end
178 |
179 | it "should return the result of the block" do
180 | result = lambda { @statsd.time('foobar') { return 'test' } }.call
181 | _(result).must_equal 'test'
182 | end
183 | end
184 |
185 | describe "with a sample rate" do
186 | before { class << @statsd; def rand; 0; end; end } # ensure delivery
187 |
188 | it "should format the message according to the statsd spec" do
189 | @statsd.time('foobar', 0.5) { 'test' }
190 | _(@socket.recv).must_equal ['foobar:0|ms|@0.5']
191 | end
192 | end
193 | end
194 |
195 | describe "#sampled" do
196 | describe "when the sample rate is 1" do
197 | before { class << @statsd; def rand; raise end; end }
198 | it "should send" do
199 | @statsd.timing('foobar', 500, 1)
200 | _(@socket.recv).must_equal ['foobar:500|ms']
201 | end
202 | end
203 |
204 | describe "when the sample rate is greater than a random value [0,1]" do
205 | before { class << @statsd; def rand; 0; end; end } # ensure delivery
206 | it "should send" do
207 | @statsd.timing('foobar', 500, 0.5)
208 | _(@socket.recv).must_equal ['foobar:500|ms|@0.5']
209 | end
210 | end
211 |
212 | describe "when the sample rate is less than a random value [0,1]" do
213 | before { class << @statsd; def rand; 1; end; end } # ensure no delivery
214 | it "should not send" do
215 | assert_nil @statsd.timing('foobar', 500, 0.5)
216 | end
217 | end
218 |
219 | describe "when the sample rate is equal to a random value [0,1]" do
220 | before { class << @statsd; def rand; 0; end; end } # ensure delivery
221 | it "should send" do
222 | @statsd.timing('foobar', 500, 0.5)
223 | _(@socket.recv).must_equal ['foobar:500|ms|@0.5']
224 | end
225 | end
226 | end
227 |
228 | describe "with namespace" do
229 | before { @statsd.namespace = 'service' }
230 |
231 | it "should add namespace to increment" do
232 | @statsd.increment('foobar')
233 | _(@socket.recv).must_equal ['service.foobar:1|c']
234 | end
235 |
236 | it "should add namespace to decrement" do
237 | @statsd.decrement('foobar')
238 | _(@socket.recv).must_equal ['service.foobar:-1|c']
239 | end
240 |
241 | it "should add namespace to timing" do
242 | @statsd.timing('foobar', 500)
243 | _(@socket.recv).must_equal ['service.foobar:500|ms']
244 | end
245 |
246 | it "should add namespace to gauge" do
247 | @statsd.gauge('foobar', 500)
248 | _(@socket.recv).must_equal ['service.foobar:500|g']
249 | end
250 | end
251 |
252 | describe "with postfix" do
253 | before { @statsd.postfix = 'ip-23-45-56-78' }
254 |
255 | it "should add postfix to increment" do
256 | @statsd.increment('foobar')
257 | _(@socket.recv).must_equal ['foobar.ip-23-45-56-78:1|c']
258 | end
259 |
260 | it "should add postfix to decrement" do
261 | @statsd.decrement('foobar')
262 | _(@socket.recv).must_equal ['foobar.ip-23-45-56-78:-1|c']
263 | end
264 |
265 | it "should add namespace to timing" do
266 | @statsd.timing('foobar', 500)
267 | _(@socket.recv).must_equal ['foobar.ip-23-45-56-78:500|ms']
268 | end
269 |
270 | it "should add namespace to gauge" do
271 | @statsd.gauge('foobar', 500)
272 | _(@socket.recv).must_equal ['foobar.ip-23-45-56-78:500|g']
273 | end
274 | end
275 |
276 | describe '#postfix=' do
277 | describe "when nil, false, or empty" do
278 | it "should set postfix to nil" do
279 | [nil, false, ''].each do |value|
280 | @statsd.postfix = 'a postfix'
281 | @statsd.postfix = value
282 | assert_nil @statsd.postfix
283 | end
284 | end
285 | end
286 | end
287 |
288 | describe "with logging" do
289 | require 'stringio'
290 | before { Statsd.logger = Logger.new(@log = StringIO.new)}
291 |
292 | it "should write to the log in debug" do
293 | Statsd.logger.level = Logger::DEBUG
294 |
295 | @statsd.increment('foobar')
296 |
297 | _(@log.string).must_match "Statsd: foobar:1|c"
298 | end
299 |
300 | it "should not write to the log unless debug" do
301 | Statsd.logger.level = Logger::INFO
302 |
303 | @statsd.increment('foobar')
304 |
305 | _(@log.string).must_be_empty
306 | end
307 | end
308 |
309 | describe "stat names" do
310 | it "should accept anything as stat" do
311 | @statsd.increment(Object, 1)
312 | end
313 |
314 | it "should replace ruby constant delimeter with graphite package name" do
315 | class Statsd::SomeClass; end
316 | @statsd.increment(Statsd::SomeClass, 1)
317 |
318 | _(@socket.recv).must_equal ['Statsd.SomeClass:1|c']
319 | end
320 |
321 | describe "custom delimiter" do
322 | before do
323 | @statsd.delimiter = "-"
324 | end
325 |
326 | it "should replace ruby constant delimiter with custom delimiter" do
327 | class Statsd::SomeOtherClass; end
328 | @statsd.increment(Statsd::SomeOtherClass, 1)
329 |
330 | _(@socket.recv).must_equal ['Statsd-SomeOtherClass:1|c']
331 | end
332 | end
333 |
334 | it "should replace statsd reserved chars in the stat name" do
335 | @statsd.increment('ray@hostname.blah|blah.blah:blah', 1)
336 | _(@socket.recv).must_equal ['ray_hostname.blah_blah.blah_blah:1|c']
337 | end
338 | end
339 |
340 | describe "handling socket errors" do
341 | before do
342 | require 'stringio'
343 | Statsd.logger = Logger.new(@log = StringIO.new)
344 | @socket.instance_variable_set(:@err_count, 0)
345 | @socket.instance_eval { def write(*) @err_count+=1; raise SocketError end }
346 | end
347 |
348 | it "should ignore socket errors" do
349 | assert_nil @statsd.increment('foobar')
350 | end
351 |
352 | it "should log socket errors" do
353 | @statsd.increment('foobar')
354 | _(@log.string).must_match 'Statsd: SocketError'
355 | end
356 |
357 | it "should retry and reconnect on socket errors" do
358 | $connect_count = 0
359 | @statsd.increment('foobar')
360 | _(@socket.instance_variable_get(:@err_count)).must_equal 5
361 | _($connect_count).must_equal 5
362 | end
363 | end
364 |
365 | describe "batching" do
366 | it "should have a default batch size of 10" do
367 | _(@statsd.batch_size).must_equal 10
368 | end
369 |
370 | it "should have a default batch byte size of nil" do
371 | assert_nil @statsd.batch_byte_size
372 | end
373 |
374 | it "should have a default flush interval of nil" do
375 | assert_nil @statsd.flush_interval
376 | end
377 |
378 | it "should have a modifiable batch size" do
379 | @statsd.batch_size = 7
380 | _(@statsd.batch_size).must_equal 7
381 | @statsd.batch do |b|
382 | _(b.batch_size).must_equal 7
383 | end
384 |
385 | @statsd.batch_size = nil
386 | @statsd.batch_byte_size = 1472
387 | @statsd.batch do |b|
388 | assert_nil b.batch_size
389 | _(b.batch_byte_size).must_equal 1472
390 | end
391 |
392 | end
393 |
394 | it 'should have a modifiable flush interval' do
395 | @statsd.flush_interval = 1
396 | _(@statsd.flush_interval).must_equal 1
397 | @statsd.batch do |b|
398 | _(b.flush_interval).must_equal 1
399 | end
400 | end
401 |
402 | it "should flush the batch at the batch size or at the end of the block" do
403 | @statsd.batch do |b|
404 | b.batch_size = 3
405 |
406 | # The first three should flush, the next two will be flushed when the
407 | # block is done.
408 | 5.times { b.increment('foobar') }
409 |
410 | _(@socket.recv).must_equal [(["foobar:1|c"] * 3).join("\n")]
411 | end
412 |
413 | _(@socket.recv).must_equal [(["foobar:1|c"] * 2).join("\n")]
414 | end
415 |
416 | it "should flush based on batch byte size" do
417 | @statsd.batch do |b|
418 | b.batch_size = nil
419 | b.batch_byte_size = 22
420 |
421 | # The first two should flush, the last will be flushed when the
422 | # block is done.
423 | 3.times { b.increment('foobar') }
424 |
425 | _(@socket.recv).must_equal [(["foobar:1|c"] * 2).join("\n")]
426 | end
427 |
428 | _(@socket.recv).must_equal ["foobar:1|c"]
429 | end
430 |
431 | it "should flush immediately when the queue is exactly a batch size" do
432 | @statsd.batch do |b|
433 | b.batch_size = nil
434 | b.batch_byte_size = 21
435 |
436 | # The first two should flush together
437 | 2.times { b.increment('foobar') }
438 |
439 | _(@socket.recv).must_equal [(["foobar:1|c"] * 2).join("\n")]
440 | end
441 | end
442 |
443 | it "should flush when the interval has passed" do
444 | @statsd.batch do |b|
445 | b.batch_size = nil
446 | b.flush_interval = 0.01
447 |
448 | # The first two should flush, the last will be flushed when the
449 | # block is done.
450 | 2.times { b.increment('foobar') }
451 | sleep(0.03)
452 | b.increment('foobar')
453 |
454 | _(@socket.recv).must_equal [(["foobar:1|c"] * 2).join("\n")]
455 | end
456 |
457 | _(@socket.recv).must_equal ["foobar:1|c"]
458 | end
459 |
460 | it "should not flush to the socket if the backlog is empty" do
461 | batch = Statsd::Batch.new(@statsd)
462 | batch.flush
463 | _(@socket.recv).must_be :nil?
464 |
465 | batch.increment 'foobar'
466 | batch.flush
467 | _(@socket.recv).must_equal %w[foobar:1|c]
468 | end
469 |
470 | it "should support setting namespace for the underlying instance" do
471 | batch = Statsd::Batch.new(@statsd)
472 | batch.namespace = 'ns'
473 | _(@statsd.namespace).must_equal 'ns'
474 | end
475 |
476 | it "should support setting host for the underlying instance" do
477 | batch = Statsd::Batch.new(@statsd)
478 | batch.host = '1.2.3.4'
479 | _(@statsd.host).must_equal '1.2.3.4'
480 | end
481 |
482 | it "should support setting port for the underlying instance" do
483 | batch = Statsd::Batch.new(@statsd)
484 | batch.port = 42
485 | _(@statsd.port).must_equal 42
486 | end
487 |
488 | end
489 |
490 | describe "#connect" do
491 | it "should reconnect" do
492 | c = $connect_count
493 | @statsd.connect
494 | _(($connect_count - c)).must_equal 1
495 | end
496 | end
497 |
498 | end
499 |
500 | describe Statsd do
501 | describe "with a real UDP socket" do
502 | it "should actually send stuff over the socket" do
503 | family = Addrinfo.udp(UDPSocket.getaddress('localhost'), 0).afamily
504 | begin
505 | socket = UDPSocket.new family
506 | host, port = 'localhost', 0
507 | socket.bind(host, port)
508 | port = socket.addr[1]
509 |
510 | statsd = Statsd.new(host, port)
511 | statsd.increment('foobar')
512 | message = socket.recvfrom(16).first
513 | _(message).must_equal 'foobar:1|c'
514 | ensure
515 | socket.close
516 | end
517 | end
518 |
519 | it "should send stuff over an IPv4 socket" do
520 | begin
521 | socket = UDPSocket.new Socket::AF_INET
522 | host, port = '127.0.0.1', 0
523 | socket.bind(host, port)
524 | port = socket.addr[1]
525 |
526 | statsd = Statsd.new(host, port)
527 | statsd.increment('foobar')
528 | message = socket.recvfrom(16).first
529 | _(message).must_equal 'foobar:1|c'
530 | ensure
531 | socket.close
532 | end
533 | end
534 |
535 | it "should send stuff over an IPv6 socket" do
536 | begin
537 | socket = UDPSocket.new Socket::AF_INET6
538 | host, port = '::1', 0
539 | socket.bind(host, port)
540 | port = socket.addr[1]
541 |
542 | statsd = Statsd.new(host, port)
543 | statsd.increment('foobar')
544 | message = socket.recvfrom(16).first
545 | _(message).must_equal 'foobar:1|c'
546 | ensure
547 | socket.close
548 | end
549 | end
550 | end
551 |
552 | describe "supports TCP sockets" do
553 | it "should connect to and send stats over TCPv4" do
554 | begin
555 | host, port = '127.0.0.1', 0
556 | server = TCPServer.new host, port
557 | port = server.addr[1]
558 |
559 | socket = nil
560 | Thread.new { socket = server.accept }
561 |
562 | statsd = Statsd.new(host, port, :tcp)
563 | statsd.increment('foobar')
564 |
565 | Timeout.timeout(5) do
566 | Thread.pass while socket == nil
567 | end
568 |
569 | message = socket.recvfrom(16).first
570 | _(message).must_equal "foobar:1|c\n"
571 | ensure
572 | socket.close if socket
573 | server.close
574 | end
575 | end
576 |
577 | it "should connect to and send stats over TCPv6" do
578 | begin
579 | host, port = '::1', 0
580 | server = TCPServer.new host, port
581 | port = server.addr[1]
582 |
583 | socket = nil
584 | Thread.new { socket = server.accept }
585 |
586 | statsd = Statsd.new(host, port, :tcp)
587 | statsd.increment('foobar')
588 |
589 | Timeout.timeout(5) do
590 | Thread.pass while socket == nil
591 | end
592 |
593 | message = socket.recvfrom(16).first
594 | _(message).must_equal "foobar:1|c\n"
595 | ensure
596 | socket.close if socket
597 | server.close
598 | end
599 | end
600 | end
601 | end
602 |
--------------------------------------------------------------------------------
/statsd-ruby.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | Gem::Specification.new("statsd-ruby", "1.5.0") do |s|
4 | s.authors = `git log --format='%aN' | sort -u`.split("\n")
5 | s.email = "reinh@reinh.com"
6 |
7 | s.summary = "A Ruby StatsD client"
8 | s.description = "A Ruby StatsD client (https://github.com/etsy/statsd)"
9 |
10 | s.homepage = "https://github.com/reinh/statsd"
11 | s.licenses = %w[MIT]
12 |
13 | s.extra_rdoc_files = %w[LICENSE.txt README.rdoc]
14 |
15 | if $0 =~ /gem/ # If running under rubygems (building), otherwise, just leave
16 | s.files = `git ls-files`.split($\)
17 | s.test_files = s.files.grep(%r{^(test|spec|features)/})
18 | end
19 |
20 | s.add_development_dependency "minitest", ">= 5.6.0"
21 | s.add_development_dependency "yard"
22 | s.add_development_dependency "simplecov", ">= 0.6.4"
23 | s.add_development_dependency "rake"
24 | end
25 |
26 |
--------------------------------------------------------------------------------