pax_global_header00006660000000000000000000000064132533003500014504gustar00rootroot0000000000000052 comment=7f44e2baadaae64d9e2cba279c54ba34260495f9 ruby-statsd-1.4.0/000077500000000000000000000000001325330035000137675ustar00rootroot00000000000000ruby-statsd-1.4.0/.document000066400000000000000000000000671325330035000156110ustar00rootroot00000000000000lib/**/*.rb bin/* - features/**/*.feature LICENSE.txt ruby-statsd-1.4.0/.gitignore000066400000000000000000000014131325330035000157560ustar00rootroot00000000000000# simplecov generated coverage # rdoc generated rdoc # yard generated doc .yardoc # bundler .bundle Gemfile.lock # jeweler generated pkg # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: # # * Create a file at ~/.gitignore # * Include files you want ignored # * Run: git config --global core.excludesfile ~/.gitignore # # After doing this, these files will be ignored in all your git projects, # saving you from having to 'pollute' every project you touch with them # # 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) # # For MacOS: # #.DS_Store # # For TextMate #*.tmproj #tmtags # # For emacs: #*~ #\#* #.\#* # # For vim: #*.swp ruby-statsd-1.4.0/.travis.yml000066400000000000000000000003361325330035000161020ustar00rootroot00000000000000--- language: ruby rvm: - 2.0.0 - 2.1 - 2.2 - 2.3.0 - 2.4.0 - rbx-2 - jruby - ruby-head sudo: false matrix: allow_failures: - rvm: rbx-2 - rvm: ruby-head - rvm: jruby - rvm: jruby-head ruby-statsd-1.4.0/Gemfile000066400000000000000000000000461325330035000152620ustar00rootroot00000000000000source 'https://rubygems.org' gemspec ruby-statsd-1.4.0/LICENSE.txt000066400000000000000000000020551325330035000156140ustar00rootroot00000000000000Copyright (c) 2011, 2012, 2013 Rein Henrichs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ruby-statsd-1.4.0/README.rdoc000066400000000000000000000044701325330035000156020ustar00rootroot00000000000000= statsd-ruby {}[http://travis-ci.org/reinh/statsd] A Ruby client for {StatsD}[https://github.com/etsy/statsd] = Installing Bundler: gem "statsd-ruby" = Basic Usage # Set up a global Statsd client for a server on localhost:9125 $statsd = Statsd.new 'localhost', 9125 # Set up a global Statsd client for a server on IPv6 port 9125 $statsd = Statsd.new '::1', 9125 # Send some stats $statsd.increment 'garets' $statsd.timing 'glork', 320 $statsd.gauge 'bork', 100 # Use {#time} to time the execution of a block $statsd.time('account.activate') { @account.activate! } # Create a namespaced statsd client and increment 'account.activate' statsd = Statsd.new('localhost').tap{|sd| sd.namespace = 'account'} statsd.increment 'activate' = Testing Run the specs with rake spec = Performance * 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). = Extensions / Libraries / Extra Docs * See the wiki[https://github.com/reinh/statsd/wiki] == Contributing to statsd * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it * Fork the project * Start a feature/bugfix branch * Commit and push until you are happy with your contribution * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. * 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. == Contributors * Rein Henrichs * Alex Williams * Andrew Meyer * Chris Gaffney * Cody Cutrer * Corey Donohoe * Dotan Nahum * Erez Rabih * Eric Chapweske * Gabriel Burt * Hannes Georg * James Tucker * Jeremy Kemper * John Nunemaker * Lann Martin * Mahesh Murthy * Manu J * Matt Sanford * Nate Bird * Noah Lorang * Oscar Del Ben * Peter Mounce * Ray Krueger * Reed Lipman * rick * Ryan Tomayko * Schuyler Erle * Thomas Whaples * Trae Robrock == Copyright Copyright (c) 2011, 2012, 2013 Rein Henrichs. See LICENSE.txt for further details. ruby-statsd-1.4.0/Rakefile000066400000000000000000000004501325330035000154330ustar00rootroot00000000000000require 'bundler/setup' require 'bundler/gem_tasks' task :default => :spec require 'rake/testtask' Rake::TestTask.new(:spec) do |spec| spec.libs << 'lib' << 'spec' spec.pattern = 'spec/**/*_spec.rb' spec.verbose = true spec.warning = true end require 'yard' YARD::Rake::YardocTask.new ruby-statsd-1.4.0/lib/000077500000000000000000000000001325330035000145355ustar00rootroot00000000000000ruby-statsd-1.4.0/lib/statsd-ruby.rb000066400000000000000000000000211325330035000173340ustar00rootroot00000000000000require 'statsd' ruby-statsd-1.4.0/lib/statsd.rb000066400000000000000000000352001325330035000163640ustar00rootroot00000000000000require 'socket' require 'forwardable' require 'json' # = Statsd: A Statsd client (https://github.com/etsy/statsd) # # @example Set up a global Statsd client for a server on localhost:8125 # $statsd = Statsd.new 'localhost', 8125 # @example Set up a global Statsd client for a server on IPv6 port 8125 # $statsd = Statsd.new '::1', 8125 # @example Send some stats # $statsd.increment 'garets' # $statsd.timing 'glork', 320 # $statsd.gauge 'bork', 100 # @example Use {#time} to time the execution of a block # $statsd.time('account.activate') { @account.activate! } # @example Create a namespaced statsd client and increment 'account.activate' # statsd = Statsd.new('localhost').tap{|sd| sd.namespace = 'account'} # statsd.increment 'activate' # # Statsd instances are thread safe for general usage, by utilizing the thread # safe nature of UDP sends. The attributes are stateful, and are not # mutexed, it is expected that users will not change these at runtime in # threaded environments. If users require such use cases, it is recommend that # users either mutex around their Statsd object, or create separate objects for # each namespace / host+port combination. class Statsd # = Batch: A batching statsd proxy # # @example Batch a set of instruments using Batch and manual flush: # $statsd = Statsd.new 'localhost', 8125 # batch = Statsd::Batch.new($statsd) # batch.increment 'garets' # batch.timing 'glork', 320 # batch.gauge 'bork', 100 # batch.flush # # Batch is a subclass of Statsd, but with a constructor that proxies to a # normal Statsd instance. It has it's own batch_size and namespace parameters # (that inherit defaults from the supplied Statsd instance). It is recommended # that some care is taken if setting very large batch sizes. If the batch size # exceeds the allowed packet size for UDP on your network, communication # troubles may occur and data will be lost. class Batch < Statsd extend Forwardable def_delegators :@statsd, :namespace, :namespace=, :host, :host=, :port, :port=, :prefix, :postfix, :delimiter, :delimiter= attr_accessor :batch_size, :batch_byte_size # @param [Statsd] requires a configured Statsd instance def initialize(statsd) @statsd = statsd @batch_size = statsd.batch_size @batch_byte_size = statsd.batch_byte_size @backlog = [] @backlog_bytesize = 0 end # @yields [Batch] yields itself # # A convenience method to ensure that data is not lost in the event of an # exception being thrown. Batches will be transmitted on the parent socket # as soon as the batch is full, and when the block finishes. def easy yield self ensure flush end def flush unless @backlog.empty? @statsd.send_to_socket @backlog.join("\n") @backlog.clear @backlog_bytesize = 0 end end protected def send_to_socket(message) # this message wouldn't fit; flush the queue. note that we don't have # to do this for message based flushing, because we're incrementing by # one, so the post-queue check will always catch it if (@batch_byte_size && @backlog_bytesize + message.bytesize + 1 > @batch_byte_size) flush end @backlog << message @backlog_bytesize += message.bytesize # skip the interleaved newline for the first item @backlog_bytesize += 1 if @backlog.length != 1 # if we're precisely full now, flush if (@batch_size && @backlog.size == @batch_size) || (@batch_byte_size && @backlog_bytesize == @batch_byte_size) flush end end end class Admin # StatsD host. Defaults to 127.0.0.1. attr_reader :host # StatsD admin port. Defaults to 8126. attr_reader :port class << self # Set to a standard logger instance to enable debug logging. attr_accessor :logger end # @attribute [w] host. # Users should call connect after changing this. def host=(host) @host = host || '127.0.0.1' end # @attribute [w] port. # Users should call connect after changing this. def port=(port) @port = port || 8126 end # @param [String] host your statsd host # @param [Integer] port your statsd port def initialize(host = '127.0.0.1', port = 8126) @host = host || '127.0.0.1' @port = port || 8126 # protects @socket transactions @socket = nil @s_mu = Mutex.new connect end # Reads all gauges from StatsD. def gauges read_metric :gauges end # Reads all timers from StatsD. def timers read_metric :timers end # Reads all counters from StatsD. def counters read_metric :counters end # @param[String] item # Deletes one or more gauges. Wildcards are allowed. def delgauges item delete_metric :gauges, item end # @param[String] item # Deletes one or more timers. Wildcards are allowed. def deltimers item delete_metric :timers, item end # @param[String] item # Deletes one or more counters. Wildcards are allowed. def delcounters item delete_metric :counters, item end def stats result = @s_mu.synchronize do # the format of "stats" isn't JSON, who knows why send_to_socket "stats" read_from_socket end items = {} result.split("\n").each do |line| key, val = line.chomp.split(": ") items[key] = val.to_i end items end # Reconnects the socket, for when the statsd address may have changed. Users # do not normally need to call this, but calling it may be appropriate when # reconfiguring a process (e.g. from HUP) def connect @s_mu.synchronize do begin @socket.flush rescue nil @socket.close if @socket rescue # Ignore socket errors on close. end @socket = TCPSocket.new(host, port) end end private def read_metric name result = @s_mu.synchronize do send_to_socket name read_from_socket end # for some reason, the reply looks like JSON, but isn't, quite JSON.parse result.gsub("'", "\"") end def delete_metric name, item result = @s_mu.synchronize do send_to_socket "del#{name} #{item}" read_from_socket end deleted = [] result.split("\n").each do |line| deleted << line.chomp.split(": ")[-1] end deleted end def send_to_socket(message) self.class.logger.debug { "Statsd: #{message}" } if self.class.logger @socket.write(message.to_s + "\n") rescue => boom self.class.logger.error { "Statsd: #{boom.class} #{boom}" } if self.class.logger nil end def read_from_socket buffer = "" loop do line = @socket.readline break if line == "END\n" buffer += line end @socket.readline # clear the closing newline out of the socket buffer end end # A namespace to prepend to all statsd calls. attr_reader :namespace # StatsD host. Defaults to 127.0.0.1. attr_reader :host # StatsD port. Defaults to 8125. attr_reader :port # StatsD namespace prefix, generated from #namespace attr_reader :prefix # The default batch size for new batches. Set to nil to use batch_byte_size (default: 10) attr_accessor :batch_size # The default batch size, in bytes, for new batches (default: default nil; use batch_size) attr_accessor :batch_byte_size # a postfix to append to all metrics attr_reader :postfix # The replacement of :: on ruby module names when transformed to statsd metric names attr_reader :delimiter class << self # Set to a standard logger instance to enable debug logging. attr_accessor :logger end # @param [String] host your statsd host # @param [Integer] port your statsd port # @param [Symbol] :tcp for TCP, :udp or any other value for UDP def initialize(host = '127.0.0.1', port = 8125, protocol = :udp) @host = host || '127.0.0.1' @port = port || 8125 self.delimiter = "." @prefix = nil @batch_size = 10 @batch_byte_size = nil @postfix = nil @socket = nil @protocol = protocol || :udp @s_mu = Mutex.new connect end # @attribute [w] namespace # Writes are not thread safe. def namespace=(namespace) @namespace = namespace @prefix = "#{namespace}." end # @attribute [w] postfix # A value to be appended to the stat name after a '.'. If the value is # blank then the postfix will be reset to nil (rather than to '.'). def postfix=(pf) case pf when nil, false, '' then @postfix = nil else @postfix = ".#{pf}" end end # @attribute [w] host # Writes are not thread safe. # Users should call hup after making changes. def host=(host) @host = host || '127.0.0.1' end # @attribute [w] port # Writes are not thread safe. # Users should call hup after making changes. def port=(port) @port = port || 8125 end # @attribute [w] stat_delimiter # Allows for custom delimiter replacement for :: when Ruby modules are transformed to statsd metric name def delimiter=(delimiter) @delimiter = delimiter || "." end # Sends an increment (count = 1) for the given stat to the statsd server. # # @param [String] stat stat name # @param [Numeric] sample_rate sample rate, 1 for always # @see #count def increment(stat, sample_rate=1) count stat, 1, sample_rate end # Sends a decrement (count = -1) for the given stat to the statsd server. # # @param [String] stat stat name # @param [Numeric] sample_rate sample rate, 1 for always # @see #count def decrement(stat, sample_rate=1) count stat, -1, sample_rate end # Sends an arbitrary count for the given stat to the statsd server. # # @param [String] stat stat name # @param [Integer] count count # @param [Numeric] sample_rate sample rate, 1 for always def count(stat, count, sample_rate=1) send_stats stat, count, :c, sample_rate end # Sends an arbitary gauge value for the given stat to the statsd server. # # This is useful for recording things like available disk space, # memory usage, and the like, which have different semantics than # counters. # # @param [String] stat stat name. # @param [Numeric] value gauge value. # @param [Numeric] sample_rate sample rate, 1 for always # @example Report the current user count: # $statsd.gauge('user.count', User.count) def gauge(stat, value, sample_rate=1) send_stats stat, value, :g, sample_rate end # Sends an arbitary set value for the given stat to the statsd server. # # This is for recording counts of unique events, which are useful to # see on graphs to correlate to other values. For example, a deployment # might get recorded as a set, and be drawn as annotations on a CPU history # graph. # # @param [String] stat stat name. # @param [Numeric] value event value. # @param [Numeric] sample_rate sample rate, 1 for always # @example Report a deployment happening: # $statsd.set('deployment', DEPLOYMENT_EVENT_CODE) def set(stat, value, sample_rate=1) send_stats stat, value, :s, sample_rate end # Sends a timing (in ms) for the given stat to the statsd server. The # sample_rate determines what percentage of the time this report is sent. The # statsd server then uses the sample_rate to correctly track the average # timing for the stat. # # @param [String] stat stat name # @param [Integer] ms timing in milliseconds # @param [Numeric] sample_rate sample rate, 1 for always def timing(stat, ms, sample_rate=1) send_stats stat, ms, :ms, sample_rate end # Reports execution time of the provided block using {#timing}. # # @param [String] stat stat name # @param [Numeric] sample_rate sample rate, 1 for always # @yield The operation to be timed # @see #timing # @example Report the time (in ms) taken to activate an account # $statsd.time('account.activate') { @account.activate! } def time(stat, sample_rate=1) start = Time.now result = yield ensure timing(stat, ((Time.now - start) * 1000).round, sample_rate) result end # Creates and yields a Batch that can be used to batch instrument reports into # larger packets. Batches are sent either when the packet is "full" (defined # by batch_size), or when the block completes, whichever is the sooner. # # @yield [Batch] a statsd subclass that collects and batches instruments # @example Batch two instument operations: # $statsd.batch do |batch| # batch.increment 'sys.requests' # batch.gauge('user.count', User.count) # end def batch(&block) Batch.new(self).easy(&block) end # Reconnects the socket, useful if the address of the statsd has changed. This # method is not thread safe from a perspective of stat submission. It is safe # from resource leaks. Users do not normally need to call this, but calling it # may be appropriate when reconfiguring a process (e.g. from HUP). def connect @s_mu.synchronize do begin @socket.close if @socket rescue # Errors are ignored on reconnects. end case @protocol when :tcp @socket = TCPSocket.new @host, @port else @socket = UDPSocket.new Addrinfo.ip(@host).afamily @socket.connect host, port end end end protected def send_to_socket(message) self.class.logger.debug { "Statsd: #{message}" } if self.class.logger retries = 0 n = 0 while true # send(2) is atomic, however, in stream cases (TCP) the socket is left # in an inconsistent state if a partial message is written. If that case # occurs, the socket is closed down and we retry on a new socket. n = socket.write(message) if n == message.length break end connect retries += 1 raise "statsd: Failed to send after #{retries} attempts" if retries >= 5 end n rescue => boom self.class.logger.error { "Statsd: #{boom.class} #{boom}" } if self.class.logger nil end private def send_stats(stat, delta, type, sample_rate=1) if sample_rate == 1 or rand < sample_rate # Replace Ruby module scoping with '.' and reserved chars (: | @) with underscores. stat = stat.to_s.gsub('::', delimiter).tr(':|@', '_') rate = "|@#{sample_rate}" unless sample_rate == 1 send_to_socket "#{prefix}#{stat}#{postfix}:#{delta}|#{type}#{rate}" end end def socket # Subtle: If the socket is half-way through initialization in connect, it # cannot be used yet. @s_mu.synchronize { @socket } || raise(ThreadError, "socket missing") end end ruby-statsd-1.4.0/spec/000077500000000000000000000000001325330035000147215ustar00rootroot00000000000000ruby-statsd-1.4.0/spec/helper.rb000066400000000000000000000010771325330035000165320ustar00rootroot00000000000000require 'bundler/setup' require 'simplecov' SimpleCov.start require 'minitest/autorun' require 'statsd' require 'logger' require 'timeout' class FakeUDPSocket def initialize @buffer = [] end def write(message) @buffer.push [message] message.length end def recv @buffer.shift end def clear @buffer = [] end def to_s inspect end def inspect "<#{self.class.name}: #{@buffer.inspect}>" end end class FakeTCPSocket < FakeUDPSocket alias_method :readline, :recv def write(message) @buffer.push message end end ruby-statsd-1.4.0/spec/statsd_admin_spec.rb000066400000000000000000000054651325330035000207440ustar00rootroot00000000000000require 'helper' describe Statsd::Admin do before do class Statsd::Admin o, $VERBOSE = $VERBOSE, nil alias connect_old connect def connect $connect_count ||= 0 $connect_count += 1 end $VERBOSE = o end @admin = Statsd::Admin.new('localhost', 1234) @socket = @admin.instance_variable_set(:@socket, FakeTCPSocket.new) end after do class Statsd::Admin o, $VERBOSE = $VERBOSE, nil alias connect connect_old $VERBOSE = o end end describe "#initialize" do it "should set the host and port" do @admin.host.must_equal 'localhost' @admin.port.must_equal 1234 end it "should default the host to 127.0.0.1 and port to 8126" do statsd = Statsd::Admin.new statsd.host.must_equal '127.0.0.1' statsd.port.must_equal 8126 end end describe "#host and #port" do it "should set host and port" do @admin.host = '1.2.3.4' @admin.port = 5678 @admin.host.must_equal '1.2.3.4' @admin.port.must_equal 5678 end it "should not resolve hostnames to IPs" do @admin.host = 'localhost' @admin.host.must_equal 'localhost' end it "should set nil host to default" do @admin.host = nil @admin.host.must_equal '127.0.0.1' end it "should set nil port to default" do @admin.port = nil @admin.port.must_equal 8126 end end %w(gauges counters timers).each do |action| describe "##{action}" do it "should send a command and return a Hash" do ["{'foo.bar': 0,\n", "'foo.baz': 1,\n", "'foo.quux': 2 }\n", "END\n","\n"].each do |line| @socket.write line end result = @admin.send action.to_sym result.must_be_kind_of Hash result.size.must_equal 3 @socket.readline.must_equal "#{action}\n" end end describe "#del#{action}" do it "should send a command and return an Array" do ["deleted: foo.bar\n", "deleted: foo.baz\n", "deleted: foo.quux\n", "END\n", "\n"].each do |line| @socket.write line end result = @admin.send "del#{action}", "foo.*" result.must_be_kind_of Array result.size.must_equal 3 @socket.readline.must_equal "del#{action} foo.*\n" end end end describe "#stats" do it "should send a command and return a Hash" do ["whatever: 0\n", "END\n", "\n"].each do |line| @socket.write line end result = @admin.stats result.must_be_kind_of Hash result["whatever"].must_equal 0 @socket.readline.must_equal "stats\n" end end describe "#connect" do it "should reconnect" do c = $connect_count @admin.connect ($connect_count - c).must_equal 1 end end end ruby-statsd-1.4.0/spec/statsd_spec.rb000066400000000000000000000362351325330035000175730ustar00rootroot00000000000000require 'helper' describe Statsd do before do class Statsd o, $VERBOSE = $VERBOSE, nil alias connect_old connect def connect $connect_count ||= 1 $connect_count += 1 end $VERBOSE = o end @statsd = Statsd.new('localhost', 1234) @socket = @statsd.instance_variable_set(:@socket, FakeUDPSocket.new) end after do class Statsd o, $VERBOSE = $VERBOSE, nil alias connect connect_old $VERBOSE = o end end describe "#initialize" do it "should set the host and port" do @statsd.host.must_equal 'localhost' @statsd.port.must_equal 1234 end it "should default the host to 127.0.0.1 and port to 8125" do statsd = Statsd.new statsd.host.must_equal '127.0.0.1' statsd.port.must_equal 8125 end it "should set delimiter to period by default" do @statsd.delimiter.must_equal "." end end describe "#host and #port" do it "should set host and port" do @statsd.host = '1.2.3.4' @statsd.port = 5678 @statsd.host.must_equal '1.2.3.4' @statsd.port.must_equal 5678 end it "should not resolve hostnames to IPs" do @statsd.host = 'localhost' @statsd.host.must_equal 'localhost' end it "should set nil host to default" do @statsd.host = nil @statsd.host.must_equal '127.0.0.1' end it "should set nil port to default" do @statsd.port = nil @statsd.port.must_equal 8125 end it "should allow an IPv6 address" do @statsd.host = '::1' @statsd.host.must_equal '::1' end end describe "#delimiter" do it "should set delimiter" do @statsd.delimiter = "-" @statsd.delimiter.must_equal "-" end it "should set default to period if not given a value" do @statsd.delimiter = nil @statsd.delimiter.must_equal "." end end describe "#increment" do it "should format the message according to the statsd spec" do @statsd.increment('foobar') @socket.recv.must_equal ['foobar:1|c'] end describe "with a sample rate" do before { class << @statsd; def rand; 0; end; end } # ensure delivery it "should format the message according to the statsd spec" do @statsd.increment('foobar', 0.5) @socket.recv.must_equal ['foobar:1|c|@0.5'] end end end describe "#decrement" do it "should format the message according to the statsd spec" do @statsd.decrement('foobar') @socket.recv.must_equal ['foobar:-1|c'] end describe "with a sample rate" do before { class << @statsd; def rand; 0; end; end } # ensure delivery it "should format the message according to the statsd spec" do @statsd.decrement('foobar', 0.5) @socket.recv.must_equal ['foobar:-1|c|@0.5'] end end end describe "#gauge" do it "should send a message with a 'g' type, per the nearbuy fork" do @statsd.gauge('begrutten-suffusion', 536) @socket.recv.must_equal ['begrutten-suffusion:536|g'] @statsd.gauge('begrutten-suffusion', -107.3) @socket.recv.must_equal ['begrutten-suffusion:-107.3|g'] end describe "with a sample rate" do before { class << @statsd; def rand; 0; end; end } # ensure delivery it "should format the message according to the statsd spec" do @statsd.gauge('begrutten-suffusion', 536, 0.1) @socket.recv.must_equal ['begrutten-suffusion:536|g|@0.1'] end end end describe "#timing" do it "should format the message according to the statsd spec" do @statsd.timing('foobar', 500) @socket.recv.must_equal ['foobar:500|ms'] end describe "with a sample rate" do before { class << @statsd; def rand; 0; end; end } # ensure delivery it "should format the message according to the statsd spec" do @statsd.timing('foobar', 500, 0.5) @socket.recv.must_equal ['foobar:500|ms|@0.5'] end end end describe "#set" do it "should format the message according to the statsd spec" do @statsd.set('foobar', 765) @socket.recv.must_equal ['foobar:765|s'] end describe "with a sample rate" do before { class << @statsd; def rand; 0; end; end } # ensure delivery it "should format the message according to the statsd spec" do @statsd.set('foobar', 500, 0.5) @socket.recv.must_equal ['foobar:500|s|@0.5'] end end end describe "#time" do it "should format the message according to the statsd spec" do @statsd.time('foobar') { 'test' } @socket.recv.must_equal ['foobar:0|ms'] end it "should return the result of the block" do result = @statsd.time('foobar') { 'test' } result.must_equal 'test' end describe "when given a block with an explicit return" do it "should format the message according to the statsd spec" do lambda { @statsd.time('foobar') { return 'test' } }.call @socket.recv.must_equal ['foobar:0|ms'] end it "should return the result of the block" do result = lambda { @statsd.time('foobar') { return 'test' } }.call result.must_equal 'test' end end describe "with a sample rate" do before { class << @statsd; def rand; 0; end; end } # ensure delivery it "should format the message according to the statsd spec" do @statsd.time('foobar', 0.5) { 'test' } @socket.recv.must_equal ['foobar:0|ms|@0.5'] end end end describe "#sampled" do describe "when the sample rate is 1" do before { class << @statsd; def rand; raise end; end } it "should send" do @statsd.timing('foobar', 500, 1) @socket.recv.must_equal ['foobar:500|ms'] end end describe "when the sample rate is greater than a random value [0,1]" do before { class << @statsd; def rand; 0; end; end } # ensure delivery it "should send" do @statsd.timing('foobar', 500, 0.5) @socket.recv.must_equal ['foobar:500|ms|@0.5'] end end describe "when the sample rate is less than a random value [0,1]" do before { class << @statsd; def rand; 1; end; end } # ensure no delivery it "should not send" do assert_nil @statsd.timing('foobar', 500, 0.5) end end describe "when the sample rate is equal to a random value [0,1]" do before { class << @statsd; def rand; 0; end; end } # ensure delivery it "should send" do @statsd.timing('foobar', 500, 0.5) @socket.recv.must_equal ['foobar:500|ms|@0.5'] end end end describe "with namespace" do before { @statsd.namespace = 'service' } it "should add namespace to increment" do @statsd.increment('foobar') @socket.recv.must_equal ['service.foobar:1|c'] end it "should add namespace to decrement" do @statsd.decrement('foobar') @socket.recv.must_equal ['service.foobar:-1|c'] end it "should add namespace to timing" do @statsd.timing('foobar', 500) @socket.recv.must_equal ['service.foobar:500|ms'] end it "should add namespace to gauge" do @statsd.gauge('foobar', 500) @socket.recv.must_equal ['service.foobar:500|g'] end end describe "with postfix" do before { @statsd.postfix = 'ip-23-45-56-78' } it "should add postfix to increment" do @statsd.increment('foobar') @socket.recv.must_equal ['foobar.ip-23-45-56-78:1|c'] end it "should add postfix to decrement" do @statsd.decrement('foobar') @socket.recv.must_equal ['foobar.ip-23-45-56-78:-1|c'] end it "should add namespace to timing" do @statsd.timing('foobar', 500) @socket.recv.must_equal ['foobar.ip-23-45-56-78:500|ms'] end it "should add namespace to gauge" do @statsd.gauge('foobar', 500) @socket.recv.must_equal ['foobar.ip-23-45-56-78:500|g'] end end describe '#postfix=' do describe "when nil, false, or empty" do it "should set postfix to nil" do [nil, false, ''].each do |value| @statsd.postfix = 'a postfix' @statsd.postfix = value assert_nil @statsd.postfix end end end end describe "with logging" do require 'stringio' before { Statsd.logger = Logger.new(@log = StringIO.new)} it "should write to the log in debug" do Statsd.logger.level = Logger::DEBUG @statsd.increment('foobar') @log.string.must_match "Statsd: foobar:1|c" end it "should not write to the log unless debug" do Statsd.logger.level = Logger::INFO @statsd.increment('foobar') @log.string.must_be_empty end end describe "stat names" do it "should accept anything as stat" do @statsd.increment(Object, 1) end it "should replace ruby constant delimeter with graphite package name" do class Statsd::SomeClass; end @statsd.increment(Statsd::SomeClass, 1) @socket.recv.must_equal ['Statsd.SomeClass:1|c'] end describe "custom delimiter" do before do @statsd.delimiter = "-" end it "should replace ruby constant delimiter with custom delimiter" do class Statsd::SomeOtherClass; end @statsd.increment(Statsd::SomeOtherClass, 1) @socket.recv.must_equal ['Statsd-SomeOtherClass:1|c'] end end it "should replace statsd reserved chars in the stat name" do @statsd.increment('ray@hostname.blah|blah.blah:blah', 1) @socket.recv.must_equal ['ray_hostname.blah_blah.blah_blah:1|c'] end end describe "handling socket errors" do before do require 'stringio' Statsd.logger = Logger.new(@log = StringIO.new) @socket.instance_eval { def write(*) raise SocketError end } end it "should ignore socket errors" do assert_nil @statsd.increment('foobar') end it "should log socket errors" do @statsd.increment('foobar') @log.string.must_match 'Statsd: SocketError' end end describe "batching" do it "should have a default batch size of 10" do @statsd.batch_size.must_equal 10 end it "should have a default batch byte size of nil" do assert_nil @statsd.batch_byte_size end it "should have a modifiable batch size" do @statsd.batch_size = 7 @statsd.batch_size.must_equal 7 @statsd.batch do |b| b.batch_size.must_equal 7 end @statsd.batch_size = nil @statsd.batch_byte_size = 1472 @statsd.batch do |b| assert_nil b.batch_size b.batch_byte_size.must_equal 1472 end end it "should flush the batch at the batch size or at the end of the block" do @statsd.batch do |b| b.batch_size = 3 # The first three should flush, the next two will be flushed when the # block is done. 5.times { b.increment('foobar') } @socket.recv.must_equal [(["foobar:1|c"] * 3).join("\n")] end @socket.recv.must_equal [(["foobar:1|c"] * 2).join("\n")] end it "should flush based on batch byte size" do @statsd.batch do |b| b.batch_size = nil b.batch_byte_size = 22 # The first two should flush, the last will be flushed when the # block is done. 3.times { b.increment('foobar') } @socket.recv.must_equal [(["foobar:1|c"] * 2).join("\n")] end @socket.recv.must_equal ["foobar:1|c"] end it "should flush immediately when the queue is exactly a batch size" do @statsd.batch do |b| b.batch_size = nil b.batch_byte_size = 21 # The first two should flush, the last will be flushed when the # block is done. 2.times { b.increment('foobar') } @socket.recv.must_equal [(["foobar:1|c"] * 2).join("\n")] end end it "should not flush to the socket if the backlog is empty" do batch = Statsd::Batch.new(@statsd) batch.flush @socket.recv.must_be :nil? batch.increment 'foobar' batch.flush @socket.recv.must_equal %w[foobar:1|c] end it "should support setting namespace for the underlying instance" do batch = Statsd::Batch.new(@statsd) batch.namespace = 'ns' @statsd.namespace.must_equal 'ns' end it "should support setting host for the underlying instance" do batch = Statsd::Batch.new(@statsd) batch.host = '1.2.3.4' @statsd.host.must_equal '1.2.3.4' end it "should support setting port for the underlying instance" do batch = Statsd::Batch.new(@statsd) batch.port = 42 @statsd.port.must_equal 42 end end describe "#connect" do it "should reconnect" do c = $connect_count @statsd.connect ($connect_count - c).must_equal 1 end end end describe Statsd do describe "with a real UDP socket" do it "should actually send stuff over the socket" do family = Addrinfo.udp(UDPSocket.getaddress('localhost'), 0).afamily begin socket = UDPSocket.new family host, port = 'localhost', 0 socket.bind(host, port) port = socket.addr[1] statsd = Statsd.new(host, port) statsd.increment('foobar') message = socket.recvfrom(16).first message.must_equal 'foobar:1|c' ensure socket.close end end it "should send stuff over an IPv4 socket" do begin socket = UDPSocket.new Socket::AF_INET host, port = '127.0.0.1', 0 socket.bind(host, port) port = socket.addr[1] statsd = Statsd.new(host, port) statsd.increment('foobar') message = socket.recvfrom(16).first message.must_equal 'foobar:1|c' ensure socket.close end end it "should send stuff over an IPv6 socket" do begin socket = UDPSocket.new Socket::AF_INET6 host, port = '::1', 0 socket.bind(host, port) port = socket.addr[1] statsd = Statsd.new(host, port) statsd.increment('foobar') message = socket.recvfrom(16).first message.must_equal 'foobar:1|c' ensure socket.close end end end describe "supports TCP sockets" do it "should connect to and send stats over TCPv4" do begin host, port = '127.0.0.1', 0 server = TCPServer.new host, port port = server.addr[1] socket = nil Thread.new { socket = server.accept } statsd = Statsd.new(host, port, :tcp) statsd.increment('foobar') Timeout.timeout(5) do Thread.pass while socket == nil end message = socket.recvfrom(16).first message.must_equal 'foobar:1|c' ensure socket.close if socket server.close end end it "should connect to and send stats over TCPv6" do begin host, port = '::1', 0 server = TCPServer.new host, port port = server.addr[1] socket = nil Thread.new { socket = server.accept } statsd = Statsd.new(host, port, :tcp) statsd.increment('foobar') Timeout.timeout(5) do Thread.pass while socket == nil end message = socket.recvfrom(16).first message.must_equal 'foobar:1|c' ensure socket.close if socket server.close end end end end ruby-statsd-1.4.0/statsd-ruby.gemspec000066400000000000000000000014351325330035000176200ustar00rootroot00000000000000# -*- encoding: utf-8 -*- Gem::Specification.new("statsd-ruby", "1.4.0") do |s| s.authors = `git log --format='%aN' | sort -u`.split("\n") s.email = "reinh@reinh.com" s.summary = "A Ruby StatsD client" s.description = "A Ruby StatsD client (https://github.com/etsy/statsd)" s.homepage = "https://github.com/reinh/statsd" s.licenses = %w[MIT] s.extra_rdoc_files = %w[LICENSE.txt README.rdoc] if $0 =~ /gem/ # If running under rubygems (building), otherwise, just leave s.files = `git ls-files`.split($\) s.test_files = s.files.grep(%r{^(test|spec|features)/}) end s.add_development_dependency "minitest", ">= 3.2.0" s.add_development_dependency "yard" s.add_development_dependency "simplecov", ">= 0.6.4" s.add_development_dependency "rake" end