├── .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 | --------------------------------------------------------------------------------