├── .bundle └── config ├── .gitignore ├── .rvmrc ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── autotest └── discover.rb ├── examples ├── counter.rb ├── gauge.rb ├── gmontest.rb ├── histogram.rb ├── integration │ ├── rack_endpoint.ru │ ├── rack_middleware.ru │ └── webrick.rb ├── meter.rb ├── opentsdb_reporter.rb └── timer.rb ├── lib ├── ruby-metrics.rb └── ruby-metrics │ ├── agent.rb │ ├── instruments │ ├── counter.rb │ ├── gauge.rb │ ├── histogram.rb │ ├── instrument.rb │ ├── meter.rb │ └── timer.rb │ ├── integration.rb │ ├── integration │ ├── rack_endpoint.rb │ ├── rack_middleware.rb │ └── webrick.rb │ ├── logging.rb │ ├── reporter.rb │ ├── reporters │ ├── ganglia.rb │ ├── librato.rb │ └── opentsdb.rb │ ├── statistics │ ├── exponential_sample.rb │ └── uniform_sample.rb │ ├── time_units.rb │ └── version.rb ├── ruby-metrics-opentsdb.gemspec ├── ruby-metrics.gemspec └── spec ├── agent_spec.rb ├── instruments ├── counter_spec.rb ├── gauge_spec.rb ├── histogram_spec.rb ├── meter_spec.rb └── timer_spec.rb ├── integration ├── rack_endpoint_spec.rb └── rack_middleware_spec.rb ├── reporter_spec.rb ├── reporters └── opentsdb_spec.rb ├── spec_helper.rb └── statistics ├── exponential_sample_spec.rb └── uniform_sample_spec.rb /.bundle/config: -------------------------------------------------------------------------------- 1 | --- {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | *.gem 3 | pkg/* 4 | .DS_Store 5 | doc 6 | .yardoc 7 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm ruby-1.9.3@metrics 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0 5 | - 2.1 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v0.8.6 -- June 2, 2011 2 | ====================== 3 | 4 | * Updated tests for JSON serialization of instruments 5 | * Fixed JSON serialization of some instruments 6 | 7 | v0.8.5 -- May 11, 2011 8 | ====================== 9 | 10 | * Removed dependency on quantity gem -- conflicted with ActiveSupport 11 | * Updated code to compute some unit conversions manually instead of depending on quantity 12 | 13 | 14 | v0.8.0 -- April 26, 2011 15 | ======================== 16 | 17 | * Added timer example 18 | * Changes to timer initialization 19 | * Added register_with_options to support timers with different units or options 20 | * Integrated Matt's changes for integration to modularize webrick and rack export options 21 | 22 | v0.7.0 -- April 18, 2011 23 | ======================== 24 | 25 | * Replaced ruby-units with quantity 26 | * Added time unit conversion internally 27 | * Initial implementation of Timer instrument 28 | * More spec tests to increase coverage 29 | 30 | 31 | v0.6.0 -- April 15, 2011 32 | ======================== 33 | 34 | * Exponentially decaying samples for histograms 35 | * Updates to Meter to use exponentially decaying samples 36 | * Ability to override WEBrick port in constructor 37 | * Updates to README 38 | 39 | v0.5.0 -- April 13, 2011 40 | ======================== 41 | 42 | * Initial gem packaging (courtesy of @richardiux) 43 | * Histograms 44 | * Sampling classes for histograms 45 | * Updates to weighted average calculations 46 | * Tests, tests, tests! 47 | * Mumblety-peg.... 48 | 49 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in ruby-metrics.gemspec 4 | gemspec :name => 'ruby-metrics' 5 | gemspec :name => 'ruby-metrics-opentsdb' 6 | 7 | group :test do 8 | gem 'rake' 9 | gem 'timecop' 10 | end 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 John Ewart 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 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/johnewart/ruby-metrics.svg?branch=master)](https://travis-ci.org/johnewart/ruby-metrics) 2 | 3 | ## What is this? 4 | 5 | This is a Ruby version of performance metrics inspired by [metrics][metrics] developed by Coda Hale at Yammer. Currently this is under *heavy* development -- it needs Gem packaging, more features, validation of metrics, more functional testing, and a little better test coverage. Pull requests happily accepted, please include docs and tests where possible! 6 | 7 | ## What needs to be done? 8 | 9 | Among other important things, this needs to be made more thread-safe. I'm currently looking at Mr. Nutter's ruby-atomic gem for making this less tedious but any suggestions are welcome! 10 | 11 | ## What's in this? 12 | 13 | Right now, I have: 14 | 15 | * Counters 16 | * Meters 17 | * Gauges 18 | * Histograms w/ uniform sampling 19 | * Histograms w/ exponentially decaying sampling 20 | * Timers 21 | 22 | ## Getting Started 23 | 24 | The goal of ruby-metrics is to get up and running quickly. You start an agent, register some instruments, and they're exported over HTTP via JSON. For example, getting started with a counter would look like this: 25 | 26 | @metrics = Metrics::Agent.new 27 | 28 | counter = @metrics.counter :my_counter 29 | counter.incr 30 | counter.incr 31 | 32 | puts @metrics.to_json 33 | #=> {"my_counter":"2"} 34 | 35 | 36 | ## Integration 37 | 38 | Integrating ruby-metrics into existing applications is entirely up to your needs. Provided options include: 39 | 40 | * Embedded WEBrick listener: 41 | 42 | This runs a background thread and enables HTTP access to a local port (8001 by default) for a JSON representation of the current metrics. 43 | 44 | ``` ruby 45 | require 'ruby-metrics/integration/webrick' 46 | @agent = Metrics::Agent.new 47 | @agent.start(:port => 8081) 48 | ``` 49 | 50 | * Rack Middleware: 51 | 52 | This will add metrics such as `requests` (a timer) as well as counters for each class of HTTP status code (1xx, 2xx, etc). Also counts uncaught exceptions before reraising. 53 | Provides a configurable path option (`:show`) to trigger the return of the metrics (as JSON) when the request path matches exactly (a string), as a regular expression, or as any object that responds to `call` for custom logic (passed the whole `env`). 54 | 55 | ``` ruby 56 | require 'ruby-metrics' 57 | @agent = Metrics::Agent.new 58 | 59 | use Metrics::Integration::Rack::Middleware, :agent => @agent, :show => '/stats' 60 | 61 | run app 62 | ``` 63 | 64 | * Rack Endpoint: 65 | 66 | Use this to expose an endpoint for external consumption for your metrics. 67 | Works best when used with a URLMap or mounted in addition to other routes, like Rails' `mount` route matcher. 68 | 69 | ``` ruby 70 | require 'ruby-metrics' 71 | @agent = Metrics::Agent.new 72 | 73 | run Metrics::Integration::Rack::Endpoint.new(:agent => @agent) 74 | ``` 75 | 76 | or 77 | 78 | ``` ruby 79 | # in config/router.rb 80 | mount Metrics::Integration::Rack::Endpoint.new(:agent => @agent) 81 | ``` 82 | 83 | [metrics]: https://github.com/codahale/metrics 84 | 85 | ## License 86 | 87 | Copyright 2011-2014 John Ewart . Released under the MIT license. See the file LICENSE for further details. 88 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__) + '/lib') 2 | require "rubygems" 3 | require "bundler" 4 | require "rspec/core/rake_task" 5 | require 'ruby-metrics/version' 6 | 7 | desc 'Build gem into the pkg directory' 8 | task :build do 9 | FileUtils.rm_rf('pkg') 10 | Dir['*.gemspec'].each do |gemspec| 11 | puts "Building #{gemspec}" 12 | system "gem build #{gemspec}" 13 | end 14 | FileUtils.mkdir_p('pkg') 15 | FileUtils.mv(Dir['*.gem'], 'pkg') 16 | end 17 | 18 | desc 'Tags version, pushes to remote, and pushes gem' 19 | task :release => :build do 20 | puts "Releasing v#{Metrics::VERSION}" 21 | sh 'git', 'tag', '-m', "Version #{Metrics::VERSION}", "v#{Metrics::VERSION}" 22 | sh "git push origin master" 23 | sh "git push origin v#{Metrics::VERSION}" 24 | sh "ls pkg/*.gem | xargs -n 1 gem push" 25 | end 26 | 27 | RSpec::Core::RakeTask.new do |t| 28 | t.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"] 29 | t.pattern = 'spec/**/*_spec.rb' 30 | end 31 | 32 | task :default => :spec 33 | -------------------------------------------------------------------------------- /autotest/discover.rb: -------------------------------------------------------------------------------- 1 | Autotest.add_discovery do 2 | "rspec2" 3 | end 4 | -------------------------------------------------------------------------------- /examples/counter.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require '../lib/ruby-metrics' 3 | 4 | @metrics = Metrics::Agent.new 5 | 6 | counter = @metrics.counter :my_counter 7 | counter.incr 8 | counter.incr 9 | 10 | puts "Counter: #{counter.to_i}" 11 | -------------------------------------------------------------------------------- /examples/gauge.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require '../lib/ruby-metrics' 3 | 4 | @metrics = Metrics::Agent.new 5 | 6 | hit_count = 42 7 | http_requests = 53 8 | 9 | gauge = @metrics.gauge :my_gauge do 10 | { 11 | :hit_count => hit_count, 12 | :http_requests => http_requests 13 | } 14 | end 15 | 16 | puts "Gauge: #{gauge.to_s}" 17 | 18 | hit_count = 65 19 | http_requests = 99 20 | 21 | puts "Gauge: #{gauge.to_s}" 22 | 23 | result = gauge.get 24 | 25 | puts "Result: #{result}" 26 | -------------------------------------------------------------------------------- /examples/gmontest.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gem 'gmetric' 3 | require 'gmetric' 4 | 5 | result = Ganglia::GMetric.send("172.16.32.130", 8670, { 6 | :hostname => 'ook.unixninjas.org', 7 | :spoof => 0, 8 | :name => 'narf', 9 | :units => 'reqs/sec', 10 | :type => 'uint8', 11 | :value => 7000, 12 | :tmax => 60, 13 | :dmax => 300, 14 | }) 15 | 16 | puts "Result: #{result.inspect}" 17 | 18 | -------------------------------------------------------------------------------- /examples/histogram.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require '../lib/ruby-metrics/integration/webrick' 3 | 4 | @metrics = Metrics::Agent.new 5 | @metrics.start 6 | 7 | exponential_histogram = @metrics.exponential_histogram :exponential_histogram 8 | uniform_histogram = @metrics.uniform_histogram :uniform_histogram 9 | 10 | data = %w(3.600 1.800 3.333 2.283 4.533 2.883 4.700 3.600 1.950 4.350 1.833 3.917 4.200 1.750 4.700 2.167 1.750 4.800 1.600 4.250 1.800 1.750 3.450 3.067 4.533 3.600 1.967 4.083 3.850 4.433 4.300 4.467 3.367 4.033 3.833 2.017 1.867 4.833 1.833 4.783 4.350 1.883 4.567 1.750 4.533 3.317 3.833 2.100 4.633 2.000 4.800 4.716 1.833 4.833 1.733 4.883 3.717 1.667 4.567 4.317 2.233 4.500 1.750 4.800 1.817 4.400 4.167 4.700 2.067 4.700 4.033 1.967 4.500 4.000 1.983 5.067 2.017 4.567 3.883 3.600 4.133 4.333 4.100 2.633 4.067 4.933 3.950 4.517 2.167 4.000 2.200 4.333 1.867 4.817 1.833 4.300 4.667 3.750 1.867 4.900 2.483 4.367 2.100 4.500 4.050 1.867 4.700 1.783 4.850 3.683 4.733 2.300 4.900 4.417 1.700 4.633 2.317 4.600 1.817 4.417 2.617 4.067 4.250 1.967 4.600 3.767 1.917 4.500 2.267 4.650 1.867 4.167 2.800 4.333 1.833 4.383 1.883 4.933 2.033 3.733 4.233 2.233 4.533 4.817 4.333 1.983 4.633 2.017 5.100 1.800 5.033 4.000 2.400 4.600 3.567 4.000 4.500 4.083 1.800 3.967 2.200 4.150 2.000 3.833 3.500 4.583 2.367 5.000 1.933 4.617 1.917 2.083 4.583 3.333 4.167 4.333 4.500 2.417 4.000 4.167 1.883 4.583 4.250 3.767 2.033 4.433 4.083 1.833 4.417 2.183 4.800 1.833 4.800 4.100 3.966 4.233 3.500 4.366 2.250 4.667 2.100 4.350 4.133 1.867 4.600 1.783 4.367 3.850 1.933 4.500 2.383 4.700 1.867 3.833 3.417 4.233 2.400 4.800 2.000 4.150 1.867 4.267 1.750 4.483 4.000 4.117 4.083 4.267 3.917 4.550 4.083 2.417 4.183 2.217 4.450 1.883 1.850 4.283 3.950 2.333 4.150 2.350 4.933 2.900 4.583 3.833 2.083 4.367 2.133 4.350 2.200 4.450 3.567 4.500 4.150 3.817 3.917 4.450 2.000 4.283 4.767 4.533 1.850 4.250 1.983 2.250 4.750 4.117 2.150 4.417 1.817 4.467) 11 | data.each do |point| 12 | exponential_histogram.update(point.to_f) 13 | uniform_histogram.update(point.to_f) 14 | end 15 | 16 | step = 0 17 | 18 | # This is here so that we will run indefinitely so you can hit the 19 | # status page on localhost:8001/stats 20 | loop do 21 | sleep 1 22 | 23 | modifier = rand(500).to_i / 100 24 | step += 1 25 | 26 | if (step % 2) 27 | modifier *= -1 28 | end 29 | 30 | uniform_histogram.update((2 + modifier).to_f) 31 | exponential_histogram.update((2 + modifier).to_f) 32 | end 33 | -------------------------------------------------------------------------------- /examples/integration/rack_endpoint.ru: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Ran with: 4 | # rackup -p 8000 ./examples/integration/rack_endpoint.ru 5 | # 6 | # Make requests to: http://localhost:8000/ 7 | # See stats at : http://localhost:8000/stats 8 | 9 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ruby-metrics') 10 | @agent = Metrics::Agent.new 11 | 12 | counter = @agent.counter(:my_counter) 13 | 14 | app = proc do |env| 15 | counter.incr 16 | [200, {'Content-Type' => 'text/plain'}, ["Counted!"]] 17 | end 18 | 19 | map = Rack::URLMap.new({ 20 | '/stats' => Metrics::Integration::Rack::Endpoint.new(:agent => @app), 21 | '/' => app 22 | }) 23 | 24 | run map 25 | -------------------------------------------------------------------------------- /examples/integration/rack_middleware.ru: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Ran with: 4 | # rackup -p 8000 ./examples/integration/rack_middleware.ru 5 | # 6 | # Make requests to: http://localhost:8000/ 7 | # See stats at : http://localhost:8000/stats 8 | 9 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ruby-metrics') 10 | @agent = Metrics::Agent.new 11 | 12 | counter = @agent.counter(:my_counter) 13 | 14 | use Metrics::Integration::Rack::Middleware, :agent => @agent 15 | 16 | app = proc do |env| 17 | counter.incr 18 | [200, {'Content-Type' => 'text/plain'}, ["Counted!"]] 19 | end 20 | 21 | run app 22 | -------------------------------------------------------------------------------- /examples/integration/webrick.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ruby-metrics')) 3 | 4 | # Run with: 5 | # ruby examples/integration/webrick.rb 6 | 7 | @agent = Metrics::Agent.new 8 | 9 | counter = @agent.counter(:my_counter) 10 | counter.incr 11 | counter.incr 12 | 13 | Metrics::Integration::WEBrick.start(:port => 8001, 14 | :agent => @agent) 15 | 16 | sleep 17 | # Now navigate to: http://localhost:8001/stats 18 | -------------------------------------------------------------------------------- /examples/meter.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require '../lib/ruby-metrics/integration/webrick' 3 | 4 | @metrics = Metrics::Agent.new 5 | @metrics.start :port => 8081 # optional 6 | 7 | timer = @metrics.meter :my_meter 8 | timer.mark(500) 9 | 10 | step = 0 11 | 12 | # This is here so that we will run indefinitely so you can hit the 13 | # status page on localhost:8081/stats 14 | loop do 15 | sleep 1 16 | 17 | modifier = rand(200).to_i 18 | step += 1 19 | 20 | if (step % 2) 21 | modifier *= -1 22 | end 23 | 24 | timer.mark(500 + modifier) 25 | end 26 | -------------------------------------------------------------------------------- /examples/opentsdb_reporter.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rubygems' 3 | $:.unshift(File.dirname(__FILE__) + '/../lib') 4 | 5 | require 'ruby-metrics' 6 | require 'ruby-metrics/reporters/opentsdb' 7 | 8 | @metrics = Metrics::Agent.new 9 | @hit_counter = 42 10 | @http_counter = 53 11 | 12 | def hit_count 13 | return 42 + rand(40) 14 | end 15 | 16 | def http_requests 17 | return 64 + rand(50) 18 | end 19 | 20 | gauge = @metrics.gauge :my_gauge do 21 | { 22 | :hit_count => hit_count, 23 | :http_requests => http_requests 24 | } 25 | end 26 | 27 | step = 0 28 | meter = @metrics.meter :faults, 'faults' 29 | 30 | puts "Gauge: #{gauge.to_s}" 31 | 32 | result = gauge.get 33 | counter = @metrics.counter :my_counter 34 | counter.incr 35 | counter.incr(50) 36 | 37 | counter_two = @metrics.counter :counter_two 38 | counter_two.incr(0) 39 | 40 | timer = @metrics.timer :request_timer, 'requests' 41 | puts "Result: #{result}" 42 | 43 | Thread.new { 44 | begin 45 | modifier = rand(200).to_i 46 | step += 1 47 | 48 | if (step % 2) 49 | modifier *= -1 50 | end 51 | 52 | meter.mark(500 + modifier) 53 | puts "Sleeping for 0.53s --> #{meter.count}" 54 | sleep 0.53 55 | end while(true) 56 | } 57 | 58 | Thread.new { 59 | while(true) 60 | begin 61 | puts "TIMER1: #{timer.inspect}" 62 | timer.time do 63 | sleeptime = rand(10) + 2 64 | puts "Sleeping for #{sleeptime}" 65 | sleep sleeptime 66 | true 67 | end 68 | puts "TIMER2: #{timer.inspect}" 69 | rescue => e 70 | puts "Error: #{e.inspect}" 71 | end 72 | end 73 | } 74 | 75 | opentsdb = Metrics::Reporters::OpenTSDBReporter.new({:hostname => 'localhost', :port => 4242, :agent => @metrics }) 76 | @metrics.report_to('opentsdb', opentsdb) 77 | @metrics.report_periodically(3.26) 78 | -------------------------------------------------------------------------------- /examples/timer.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require '../lib/ruby-metrics/integration/webrick' 3 | 4 | @metrics = Metrics::Agent.new 5 | @metrics.start :port => 8081 # optional 6 | 7 | timer = @metrics.timer :my_timer 8 | timer.update(500, :milliseconds) 9 | timer.update(5, :seconds) 10 | timer.update(4242, :nanoseconds) 11 | 12 | msec_timer = @metrics.timer :msec_timer, {:duration_unit => :microseconds, :rate_unit => :seconds} 13 | 14 | step = 0 15 | 16 | # This is here so that we will run indefinitely so you can hit the 17 | # status page on localhost:8081/stats 18 | loop do 19 | sleep 1 20 | 21 | modifier = rand(200).to_i 22 | step += 1 23 | 24 | if (step % 2) 25 | modifier *= -1 26 | end 27 | 28 | timer.update(500 + modifier, :microseconds) 29 | msec_timer.update(500 + modifier, :microseconds) 30 | 31 | end 32 | -------------------------------------------------------------------------------- /lib/ruby-metrics.rb: -------------------------------------------------------------------------------- 1 | # == Metrics Initialization 2 | # 3 | 4 | require 'logger' 5 | 6 | module Metrics 7 | class << self 8 | attr_writer :logger 9 | def logger 10 | @logger ||= Logger.new(STDOUT) 11 | end 12 | end 13 | end 14 | 15 | require 'ruby-metrics/agent' 16 | -------------------------------------------------------------------------------- /lib/ruby-metrics/agent.rb: -------------------------------------------------------------------------------- 1 | require 'ruby-metrics/logging' 2 | 3 | require 'ruby-metrics/time_units' 4 | 5 | require 'ruby-metrics/instruments/counter' 6 | require 'ruby-metrics/instruments/meter' 7 | require 'ruby-metrics/instruments/gauge' 8 | require 'ruby-metrics/instruments/histogram' 9 | require 'ruby-metrics/instruments/timer' 10 | 11 | require 'ruby-metrics/integration' 12 | require 'ruby-metrics/reporter' 13 | 14 | require 'json' 15 | 16 | module Metrics 17 | class Agent 18 | include Logging 19 | 20 | attr_reader :instruments, :reporters, :reporter 21 | 22 | def initialize(options = {}) 23 | @instruments = {} 24 | @reporters = {} 25 | end 26 | 27 | alias_method :registered, :instruments 28 | 29 | def counter(name, units = '') 30 | @instruments[name] ||= Instruments::Counter.new(:units => units) 31 | end 32 | 33 | def meter(name, units = '') 34 | @instruments[name] ||= Instruments::Meter.new(:units => units) 35 | end 36 | 37 | def gauge(name, units = '', &block) 38 | @instruments[name] ||= Instruments::Gauge.new(:units => units, &block) 39 | end 40 | 41 | def timer(name, units = '', options = {}) 42 | @instruments[name] ||= Instruments::Timer.new(options.merge(:units => units)) 43 | end 44 | 45 | def uniform_histogram(name) 46 | @instruments[name] ||= Instruments::UniformHistogram.new 47 | end 48 | 49 | # For backwards compatibility 50 | alias_method :histogram, :uniform_histogram 51 | 52 | def exponential_histogram(name) 53 | @instruments[name] ||= Instruments::ExponentialHistogram.new 54 | end 55 | 56 | def report_to(name, service) 57 | @reporters[name] ||= service 58 | end 59 | 60 | def send_metrics! 61 | @reporters.each do |name, service| 62 | service.report(self) 63 | end 64 | end 65 | 66 | def report_periodically(delay = nil) 67 | @reporter = Reporter.new({:agent => self, :delay => delay}) 68 | end 69 | 70 | def stop_reporting 71 | @reporter.stop 72 | end 73 | 74 | def as_json(*_) 75 | @instruments 76 | end 77 | 78 | def to_json(*_) 79 | as_json.to_json 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/ruby-metrics/instruments/counter.rb: -------------------------------------------------------------------------------- 1 | require_relative 'instrument' 2 | 3 | module Metrics 4 | module Instruments 5 | class Counter < Instrument 6 | 7 | attr_reader :units 8 | 9 | def initialize(options = {}) 10 | @value = 0 11 | @units = options[:units] 12 | end 13 | 14 | def inc(value = 1) 15 | @value += value 16 | end 17 | alias_method :incr, :inc 18 | 19 | def dec(value = 1) 20 | @value -= value 21 | end 22 | alias_method :decr, :dec 23 | 24 | def clear 25 | @value = 0 26 | end 27 | 28 | def to_i 29 | @value.to_i 30 | end 31 | 32 | def to_s 33 | @value.to_s 34 | end 35 | 36 | def as_json(*_) 37 | @value 38 | end 39 | 40 | def to_json(*_) 41 | as_json.to_json 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/ruby-metrics/instruments/gauge.rb: -------------------------------------------------------------------------------- 1 | require_relative 'instrument' 2 | 3 | module Metrics 4 | module Instruments 5 | class Gauge < Instrument 6 | attr_reader :units 7 | 8 | def initialize(options = {}, &block) 9 | raise ArgumentError, "a block is required" unless block_given? 10 | @block = block 11 | @units = options[:units] 12 | end 13 | 14 | def get 15 | instance_exec(&@block) 16 | end 17 | 18 | def as_json(*_) 19 | value = get 20 | value.respond_to?(:as_json) ? value.as_json : value 21 | end 22 | 23 | def to_json(*_) 24 | as_json.to_json 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ruby-metrics/instruments/histogram.rb: -------------------------------------------------------------------------------- 1 | require_relative 'instrument' 2 | require 'ruby-metrics/statistics/uniform_sample' 3 | require 'ruby-metrics/statistics/exponential_sample' 4 | 5 | module Metrics 6 | module Instruments 7 | class Histogram < Instrument 8 | attr_reader :count 9 | 10 | def initialize(type = :uniform) 11 | @sample = 12 | case type 13 | when :uniform 14 | Metrics::Statistics::UniformSample.new 15 | when :exponential 16 | Metrics::Statistics::ExponentialSample.new 17 | else 18 | raise ArgumentError, "Unknown type #{type.inspect}" 19 | end 20 | 21 | clear 22 | end 23 | 24 | def update(value) 25 | @count += 1 26 | @sum += value 27 | @sample.update(value) 28 | update_max(value) 29 | update_min(value) 30 | update_variance(value) 31 | end 32 | 33 | def clear 34 | @sample.clear 35 | @min = nil 36 | @max = nil 37 | @sum = 0 38 | @count = 0 39 | @variance_m = -1 40 | @variance_s = 0 41 | end 42 | 43 | def quantiles(percentiles) 44 | # Calculated using the same logic as R and Excel use 45 | # as outlined by the NIST here: http://www.itl.nist.gov/div898/handbook/prc/section2/prc252.htm 46 | 47 | sorted_values = @sample.values[0...@count].sort 48 | scores = { } 49 | percentiles.each do |pct| 50 | scores[pct] = 51 | if @count == 0 52 | 0.0 53 | else 54 | index = pct * (sorted_values.length - 1) + 1.0 55 | if index <= 1 56 | sorted_values.first 57 | elsif index >= sorted_values.length 58 | sorted_values.last 59 | else 60 | lower = sorted_values[index.to_i - 1] 61 | upper = sorted_values[index.to_i] 62 | lower + (index - index.floor) * (upper - lower) 63 | end 64 | end 65 | end 66 | scores 67 | end 68 | 69 | def update_min(value) 70 | if @min.nil? || value < @min 71 | @min = value 72 | end 73 | end 74 | 75 | def update_max(value) 76 | if @max.nil? || value > @max 77 | @max = value 78 | end 79 | end 80 | 81 | def update_variance(value) 82 | new_m = @variance_m + ((value - @variance_m) / @count) 83 | new_s = @variance_s + ((value - @variance_m) * (value - new_m)) 84 | 85 | @variance_m = new_m 86 | @variance_s = new_s 87 | end 88 | 89 | def variance 90 | if @count <= 1 91 | 0.0 92 | else 93 | @variance_s.to_f / (@count - 1) 94 | end 95 | end 96 | 97 | def max 98 | @max || 0.0 99 | end 100 | 101 | def min 102 | @min || 0.0 103 | end 104 | 105 | def mean 106 | if @count > 0 107 | @sum / @count 108 | else 109 | 0.0 110 | end 111 | end 112 | 113 | def std_dev 114 | if @count > 0 115 | Math.sqrt(variance) 116 | else 117 | 0.0 118 | end 119 | end 120 | 121 | def values 122 | @sample.values 123 | end 124 | 125 | def as_json(*_) 126 | { 127 | :min => min, 128 | :max => max, 129 | :mean => mean, 130 | :variance => variance, 131 | :percentiles => quantiles(Timer::DEFAULT_PERCENTILES) 132 | } 133 | end 134 | 135 | def to_json(*_) 136 | as_json.to_json 137 | end 138 | end 139 | 140 | class ExponentialHistogram < Histogram 141 | def initialize 142 | super(:exponential) 143 | end 144 | end 145 | 146 | class UniformHistogram < Histogram 147 | def initialize 148 | super(:uniform) 149 | end 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /lib/ruby-metrics/instruments/instrument.rb: -------------------------------------------------------------------------------- 1 | module Metrics 2 | module Instruments 3 | class Instrument 4 | def tags 5 | @tags ||= {} 6 | end 7 | 8 | def tag(key, value) 9 | tags[key] = value 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ruby-metrics/instruments/meter.rb: -------------------------------------------------------------------------------- 1 | require_relative 'instrument' 2 | require 'ruby-metrics/time_units' 3 | 4 | module Metrics 5 | module Instruments 6 | class Meter < Instrument 7 | include Metrics::TimeConversion 8 | 9 | # From http://www.teamquest.com/pdfs/whitepaper/ldavg2.pdf 10 | INTERVAL_SECONDS = 5.0 11 | ONE_MINUTE_FACTOR = 1 - Math.exp(-INTERVAL_SECONDS / 60.0) 12 | FIVE_MINUTE_FACTOR = 1 - Math.exp(-INTERVAL_SECONDS / (60.0 * 5.0)) 13 | FIFTEEN_MINUTE_FACTOR = 1 - Math.exp(-INTERVAL_SECONDS / (60.0 * 15.0)) 14 | 15 | attr_reader :count, :units 16 | alias_method :counted, :count 17 | 18 | def initialize(options = {}) 19 | @units = options[:units] 20 | clear 21 | 22 | @timer_thread = Thread.new do 23 | begin 24 | loop do 25 | tick 26 | sleep(INTERVAL_SECONDS) 27 | end 28 | rescue Exception => e 29 | logger.error "Error in timer thread: #{e.class.name}: #{e}\n #{e.backtrace.join("\n ")}" 30 | end 31 | end 32 | end 33 | 34 | def clear 35 | @one_minute_rate = @five_minute_rate = @fifteen_minute_rate = 0.0 36 | @count = 0 37 | @initialized = false 38 | @start_time = Time.now.to_f 39 | end 40 | 41 | def mark(count = 1) 42 | @count += count 43 | end 44 | 45 | def calc_rate(rate, factor, count) 46 | rate.to_f + factor.to_f * (count.to_f - rate.to_f) 47 | end 48 | 49 | def tick 50 | count = @count.to_f / Seconds.to_nsec(INTERVAL_SECONDS).to_f 51 | 52 | if @initialized 53 | @one_minute_rate = calc_rate(@one_minute_rate, ONE_MINUTE_FACTOR, count) 54 | @five_minute_rate = calc_rate(@five_minute_rate, FIVE_MINUTE_FACTOR, count) 55 | @fifteen_minute_rate = calc_rate(@fifteen_minute_rate, FIFTEEN_MINUTE_FACTOR, count) 56 | else 57 | @one_minute_rate = @five_minute_rate = @fifteen_minute_rate = count 58 | @initialized = true 59 | end 60 | 61 | @count = 0 62 | end 63 | 64 | def one_minute_rate(rate_unit = :seconds) 65 | convert_to_ns @one_minute_rate, rate_unit 66 | end 67 | 68 | def five_minute_rate(rate_unit = :seconds) 69 | convert_to_ns @five_minute_rate, rate_unit 70 | end 71 | 72 | def fifteen_minute_rate(rate_unit = :seconds) 73 | convert_to_ns @fifteen_minute_rate, rate_unit 74 | end 75 | 76 | def mean_rate(rate_unit = :seconds) 77 | if @count == 0 78 | 0.0 79 | else 80 | elapsed = Time.now.to_f - @start_time.to_f 81 | scale_factor = scale_time_units(:seconds, rate_unit) 82 | @count.to_f / (elapsed * scale_factor) 83 | end 84 | end 85 | 86 | def as_json(*_) 87 | { 88 | :one_minute_rate => one_minute_rate, 89 | :five_minute_rate => five_minute_rate, 90 | :fifteen_minute_rate => fifteen_minute_rate 91 | } 92 | end 93 | 94 | def to_json(*_) 95 | as_json.to_json 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/ruby-metrics/instruments/timer.rb: -------------------------------------------------------------------------------- 1 | require_relative 'instrument' 2 | require 'ruby-metrics/time_units' 3 | 4 | module Metrics 5 | module Instruments 6 | class Timer < Instrument 7 | include Metrics::TimeConversion 8 | 9 | attr_reader :duration_unit, :rate_unit, :units 10 | 11 | DEFAULT_PERCENTILES = [0.25, 0.50, 0.75, 0.95, 0.97, 0.98, 0.99] 12 | 13 | def initialize(options = {}) 14 | @meter = Meter.new 15 | @histogram = ExponentialHistogram.new 16 | 17 | @duration_unit = options[:duration_unit] || :seconds 18 | @rate_unit = options[:rate_unit] || :seconds 19 | @units = options[:units] 20 | 21 | clear 22 | end 23 | 24 | def clear 25 | @meter.clear 26 | @histogram.clear 27 | end 28 | 29 | def update(duration, unit) 30 | update_timer(convert_to_ns(duration, unit)) 31 | end 32 | 33 | def time 34 | start_time = Time.now.to_f 35 | result = yield 36 | time_diff = Time.now.to_f - start_time 37 | time_in_ns = convert_to_ns time_diff, :seconds 38 | update_timer(time_in_ns) 39 | result 40 | end 41 | 42 | def count 43 | @histogram.count 44 | end 45 | 46 | def fifteen_minute_rate 47 | @meter.fifteen_minute_rate(@rate_unit) 48 | end 49 | 50 | def five_minute_rate 51 | @meter.five_minute_rate(@rate_unit) 52 | end 53 | 54 | def one_minute_rate 55 | @meter.one_minute_rate(@rate_unit) 56 | end 57 | 58 | def mean_rate 59 | @meter.mean_rate(@rate_unit) 60 | end 61 | 62 | def max 63 | scale_duration_to_ns @histogram.max, @duration_unit 64 | end 65 | 66 | def min 67 | scale_duration_to_ns @histogram.min, @duration_unit 68 | end 69 | 70 | def mean 71 | scale_duration_to_ns @histogram.mean, @duration_unit 72 | end 73 | 74 | def std_dev 75 | scale_duration_to_ns @histogram.std_dev, @duration_unit 76 | end 77 | 78 | def quantiles(percentiles = DEFAULT_PERCENTILES) 79 | @histogram.quantiles(percentiles).inject({}) do |result, (k, v)| 80 | result[k] = scale_duration_to_ns v, @duration_unit 81 | result 82 | end 83 | end 84 | 85 | def values 86 | @histogram.values.map do |value| 87 | scale_duration_to_ns value, @duration_unit 88 | end 89 | end 90 | 91 | def update_timer(duration) 92 | if duration >= 0 93 | @histogram.update(duration) 94 | @meter.mark 95 | end 96 | end 97 | 98 | def as_json(*_) 99 | { 100 | :count => count, 101 | :rates => { 102 | :one_minute_rate => one_minute_rate, 103 | :five_minute_rate => five_minute_rate, 104 | :fifteen_minute_rate => fifteen_minute_rate, 105 | :unit => @rate_unit 106 | }, 107 | :durations => { 108 | :min => min, 109 | :max => max, 110 | :mean => mean, 111 | :percentiles => quantiles, 112 | :unit => @duration_unit 113 | } 114 | } 115 | end 116 | 117 | def to_json(*_) 118 | as_json.to_json 119 | end 120 | 121 | private 122 | def scale_duration_to_ns(value, unit) 123 | value.to_f / convert_to_ns(1.0, unit) 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/ruby-metrics/integration.rb: -------------------------------------------------------------------------------- 1 | module Metrics 2 | module Integration 3 | 4 | autoload :WEBrick, File.expand_path('../integration/webrick', __FILE__) 5 | 6 | module Rack 7 | autoload :Middleware, File.expand_path('../integration/rack_middleware', __FILE__) 8 | autoload :Endpoint, File.expand_path('../integration/rack_endpoint', __FILE__) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/ruby-metrics/integration/rack_endpoint.rb: -------------------------------------------------------------------------------- 1 | # Provides: 2 | # * configurable agent 3 | # * endpoint for accessing metrics JSON 4 | # 5 | module Metrics 6 | module Integration 7 | module Rack 8 | class Endpoint 9 | 10 | attr_accessor :app, :options, :agent, 11 | # integration metrics 12 | :requests, :uncaught_exceptions, 13 | :status_codes 14 | 15 | def initialize(options = {}) 16 | @options = options 17 | @agent = @options.delete(:agent) || Agent.new 18 | end 19 | 20 | def call(_) 21 | body = @agent.to_json 22 | 23 | [ 200, 24 | { 'Content-Type' => 'application/json', 25 | 'Content-Length' => body.size.to_s }, 26 | [body] 27 | ] 28 | end 29 | 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/ruby-metrics/integration/rack_middleware.rb: -------------------------------------------------------------------------------- 1 | # Provides: 2 | # * configurable agent 3 | # * configurable endpoint for current metrics 4 | # * strings == path_info 5 | # * regexp =~ path_info 6 | # * proc.call(env) #=> boolean 7 | # * env['metrics.agent'] upstream 8 | # * specific metrics by default 9 | # * requests (timer) 10 | # * uncaught_exceptions (counter) 11 | # * response_1xx through response_5xx (counter) 12 | # 13 | module Metrics 14 | module Integration 15 | module Rack 16 | class Middleware 17 | 18 | attr_accessor :app, :options, :agent 19 | 20 | def initialize(app, options ={}) 21 | @app = app 22 | @options = {:show => "/stats"}.merge(options) 23 | @agent = @options.delete(:agent) || Agent.new 24 | end 25 | 26 | def call(env) 27 | return show(env) if show?(env) 28 | 29 | env['metrics.agent'] = @agent 30 | 31 | status, headers, body = time_request { @app.call(env) } 32 | 33 | incr_status_code_counter(status / 100) 34 | 35 | [status, headers, body] 36 | rescue Exception 37 | # TODO: add "last_uncaught_exception" with string of error 38 | incr_uncaught_exceptions 39 | raise 40 | end 41 | 42 | private 43 | def show?(env, test = options[:show]) 44 | case 45 | when String === test 46 | env['PATH_INFO'] == test 47 | when Regexp === test 48 | env['PATH_INFO'] =~ test 49 | when test.respond_to?(:call) 50 | test.call(env) 51 | else 52 | test 53 | end 54 | end 55 | 56 | def show(_) 57 | body = @agent.to_json 58 | 59 | [ 200, 60 | { 'Content-Type' => 'application/json', 61 | 'Content-Length' => body.size.to_s }, 62 | [body] 63 | ] 64 | end 65 | 66 | def time_request(&block) 67 | @agent.timer(:_requests).time(&block) 68 | end 69 | 70 | def incr_uncaught_exceptions 71 | @agent.counter(:_uncaught_exceptions).incr 72 | end 73 | 74 | def incr_status_code_counter(hundred) 75 | @agent.counter(:"_status_#{hundred}xx").incr 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/ruby-metrics/integration/webrick.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', '..', 'ruby-metrics') 2 | 3 | require 'webrick' 4 | 5 | module Metrics 6 | 7 | class Agent 8 | def start(options = {}) 9 | Integration::WEBrick.start(options.merge(:agent => self)) 10 | end 11 | end 12 | 13 | module Integration 14 | class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet 15 | include Logging 16 | 17 | def self.start(options = {}) 18 | connection_options = {:Port => options.delete(:port) || options.delete(:Port) || 8001} 19 | agent = options.delete(:agent) || Agent.new 20 | 21 | logger.debug "Creating Metrics daemon thread." 22 | @thread = Thread.new do 23 | begin 24 | server = ::WEBrick::HTTPServer.new(connection_options) 25 | server.mount "/stats", self, agent 26 | server.start 27 | rescue Exception => e 28 | logger.error "Error in thread: %s: %s\n\t%s" % [e.class.to_s, 29 | e.message, 30 | e.backtrace.join("\n\t")] 31 | end 32 | end 33 | end 34 | 35 | def initialize(server, agent) 36 | @agent = agent 37 | end 38 | 39 | def do_GET(request, response) 40 | response.status = 200 41 | response['Content-Type'] = 'application/json' 42 | response.body = @agent.to_json 43 | end 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/ruby-metrics/logging.rb: -------------------------------------------------------------------------------- 1 | module Metrics 2 | module Logging 3 | 4 | def self.included(target) 5 | target.extend ClassMethods 6 | end 7 | 8 | def logger 9 | self.class.logger 10 | end 11 | 12 | module ClassMethods 13 | def logger 14 | Metrics.logger 15 | end 16 | end 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/ruby-metrics/reporter.rb: -------------------------------------------------------------------------------- 1 | module Metrics 2 | class Reporter 3 | # Default reporting delay is 60 seconds 4 | DEFAULT_REPORTING_DELAY = 60 5 | 6 | include Logging 7 | 8 | def stop 9 | @running = false 10 | end 11 | 12 | def initialize(options = {}) 13 | @running = true 14 | 15 | if options[:agent] == nil 16 | raise "Need an agent to report data from" 17 | end 18 | 19 | delay = options[:delay] || DEFAULT_REPORTING_DELAY 20 | agent = options[:agent] 21 | 22 | Thread.new { 23 | while @running 24 | agent.reporters.each do |name, service| 25 | service.report(agent) 26 | end 27 | sleep delay 28 | end 29 | } 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/ruby-metrics/reporters/ganglia.rb: -------------------------------------------------------------------------------- 1 | require 'ruby-metrics/version' 2 | require 'ruby-metrics' 3 | 4 | require 'gmetric' 5 | 6 | module Metrics 7 | module Reporters 8 | class GangliaReporter 9 | 10 | attr_reader :host_ip, :host_port 11 | 12 | def initialize(options = {}) 13 | @host_ip = options[:host_ip] 14 | @host_port = options[:host_port] 15 | end 16 | 17 | def send_data(data) 18 | puts "Sending data: #{data.inspect}" 19 | data_type = 20 | case data[:value] 21 | Fixnum 22 | "uint32" 23 | Float 24 | "float" 25 | String 26 | "string" 27 | else 28 | "unknown" 29 | end 30 | 31 | Ganglia::GMetric.send(@host_ip, @host_port.to_i, 32 | :spoof => 0, 33 | :name => data[:name], 34 | :units => data[:units], 35 | :type => data_type, 36 | :value => data[:value], 37 | :tmax => 60, 38 | :dmax => 300, 39 | ) 40 | end 41 | 42 | def report(agent) 43 | agent.instruments.each do |name, instrument| 44 | case instrument 45 | when Metrics::Instruments::Counter 46 | send_data :name => name, 47 | :value => instrument.to_i, 48 | :units => instrument.units 49 | when Metrics::Instruments::Gauge 50 | if instrument.get.is_a? Hash 51 | instrument.get.each do |key, value| 52 | send_data :name => "#{name}_#{key}", 53 | :value => value, 54 | :units => instrument.units 55 | end 56 | else 57 | send_data :name => name, 58 | :value => instrument.get, 59 | :units => instrument.units 60 | end 61 | when Metrics::Instruments::Timer 62 | [:count, :fifteen_minute_rate, :five_minute_rate, :one_minute_rate, :min, :max, :mean].each do |attribute| 63 | send_data :name => "#{name}_#{attribute}", 64 | :value => instrument.send(attribute), 65 | :units => instrument.units 66 | end 67 | when Metrics::Instruments::Meter 68 | [:count, :fifteen_minute_rate, :five_minute_rate, :one_minute_rate, :mean_rate].each do |attribute| 69 | send_data :name => "#{name_attribute}", 70 | :value => instrument.send(attribute), 71 | :units => instrument.units 72 | end 73 | else 74 | puts "Unhandled instrument" 75 | end 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/ruby-metrics/reporters/librato.rb: -------------------------------------------------------------------------------- 1 | require 'net/https' 2 | require 'ruby-metrics/version' 3 | require 'ruby-metrics' 4 | 5 | module Metrics 6 | module Reporters 7 | class Librato 8 | attr_reader :api_token 9 | attr_reader :user 10 | 11 | API_URL = "https://metrics-api.librato.com/v1" 12 | 13 | def initialize(options = {}) 14 | @api_token = options[:api_token] 15 | @user = options[:user] 16 | @headers = { 17 | 'User-Agent' => "ruby-metrics #{Metrics::VERSION}" 18 | } 19 | end 20 | 21 | def send_data(post_url, post_data) 22 | url = URI.parse(post_url) 23 | req = Net::HTTP::Post.new(url.path) 24 | req.basic_auth @user, @api_token 25 | @headers.each do |k,v| 26 | req.add_field(k, v) 27 | end 28 | req.set_form_data(post_data) 29 | https = Net::HTTP.new(url.host, url.port) 30 | https.use_ssl = true 31 | #https.set_debug_output($stdout) 32 | 33 | https.start do |http| 34 | result = http.request(req) 35 | case result 36 | when Net::HTTPCreated 37 | # OK 38 | puts "SENT!" 39 | else 40 | puts "FAILED TO SEND: #{https.inspect}" 41 | end 42 | end 43 | end 44 | 45 | def report(agent) 46 | agent.instruments.each do |name, instrument| 47 | measure_time = Time.now.to_i 48 | 49 | case instrument 50 | when Metrics::Instruments::Counter 51 | send_data "#{API_URL}/counters/#{name}.json", 52 | :measure_time => measure_time, 53 | :value => instrument.to_i 54 | 55 | when Metrics::Instruments::Gauge 56 | post_url = "#{API_URL}/gauges/#{name}.json" 57 | if instrument.get.is_a? Hash 58 | instrument.get.each do |key, value| 59 | send_data post_url, 60 | :measure_time => measure_time, 61 | :source => key, 62 | :value => value 63 | end 64 | else 65 | send_data post_url, 66 | :measure_time => measure_time, 67 | :value => instrument.get 68 | end 69 | 70 | when Metrics::Instruments::Timer 71 | [:count, :fifteen_minute_rate, :five_minute_rate, :one_minute_rate, :min, :max, :mean].each do |attribute| 72 | send_data "#{API_URL}/gauges/#{name}.json", 73 | :measure_time => measure_time, 74 | :source => attribute, 75 | :value => instrument.send(attribute) 76 | end 77 | 78 | else 79 | puts "Unhandled instrument" 80 | end 81 | end 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/ruby-metrics/reporters/opentsdb.rb: -------------------------------------------------------------------------------- 1 | require 'ruby-metrics' 2 | require 'ruby-metrics/logging' 3 | require 'opentsdb' 4 | 5 | module Metrics 6 | module Reporters 7 | class OpenTSDBReporter 8 | 9 | include Logging 10 | 11 | attr_reader :hostname 12 | attr_reader :port 13 | attr_reader :client 14 | attr_reader :tags 15 | 16 | def initialize(options = {}) 17 | @hostname = options[:hostname] 18 | @port = options[:port] 19 | @client = OpenTSDB::Client.new({:hostname => @hostname, :port => @port}) 20 | @tags = options[:tags] 21 | end 22 | 23 | def send_data(opts = {}) 24 | name = opts[:name] 25 | value = opts[:value] 26 | units = opts[:units] 27 | extra_tags = opts[:tags] 28 | 29 | all_tags = @tags.merge(:units => units || 'none').merge(extra_tags || {}) 30 | 31 | @client.put( 32 | :metric => name, 33 | :timestamp => Time.now.to_i, 34 | :value => value, 35 | :tags => all_tags 36 | ) 37 | end 38 | 39 | def report(agent) 40 | agent.instruments.clone.each do |name, instrument| 41 | case instrument 42 | when Metrics::Instruments::Counter 43 | send_data :name => "#{name}", 44 | :value => instrument.to_i, 45 | :tags => instrument.tags, 46 | :units => instrument.units 47 | 48 | when Metrics::Instruments::Gauge 49 | if instrument.get.is_a? Hash 50 | instrument.get.each do |key, value| 51 | send_data :name => "#{name}.#{key}", 52 | :value => value, 53 | :tags => instrument.tags, 54 | :units => instrument.units 55 | end 56 | else 57 | send_data :name => "#{name}", 58 | :value => instrument.get, 59 | :tags => instrument.tags, 60 | :units => instrument.units 61 | end 62 | 63 | when Metrics::Instruments::Timer 64 | rate_units = "#{instrument.units}/sec" 65 | time_units = "sec/#{instrument.units}" 66 | 67 | send_data :name => "#{name}.count", 68 | :value => instrument.count, 69 | :tags => instrument.tags, 70 | :units => instrument.units 71 | 72 | [:fifteen_minute_rate, :five_minute_rate, :one_minute_rate].each do |attribute| 73 | send_data :name => "#{name}.#{attribute}", 74 | :value => instrument.send(attribute), 75 | :tags => instrument.tags, 76 | :units => rate_units 77 | end 78 | 79 | [:min, :max, :mean].each do |attribute| 80 | send_data :name => "#{name}.#{attribute}", 81 | :value => instrument.send(attribute), 82 | :tags => instrument.tags, 83 | :units => time_units 84 | end 85 | 86 | when Metrics::Instruments::Meter 87 | tags = instrument.tags 88 | send_data :name => "#{name}.count", 89 | :value => instrument.counted, 90 | :tags => tags, 91 | :units => instrument.units 92 | 93 | rate_units = "#{instrument.units}/sec" 94 | [:fifteen_minute_rate, :five_minute_rate, :one_minute_rate, :mean_rate].each do |attribute| 95 | send_data :name => "#{name}.#{attribute}", 96 | :value => instrument.send(attribute), 97 | :tags => tags, 98 | :units => rate_units 99 | end 100 | 101 | else 102 | puts 'Unhandled instrument' 103 | end 104 | end 105 | end 106 | end 107 | end 108 | end 109 | 110 | -------------------------------------------------------------------------------- /lib/ruby-metrics/statistics/exponential_sample.rb: -------------------------------------------------------------------------------- 1 | module Metrics 2 | module Statistics 3 | class ExponentialSample 4 | RESCALE_WINDOW_SECONDS = 60 * 60 # 1 hour 5 | 6 | def initialize(size = 1028, alpha = 0.015) 7 | @size = size 8 | @alpha = alpha 9 | clear 10 | end 11 | 12 | def clear 13 | @values = { } 14 | @start_time = tick 15 | @next_scale_time = Time.now.to_f + RESCALE_WINDOW_SECONDS 16 | @count = 0 17 | end 18 | 19 | def size 20 | [@values.size, @count].min 21 | end 22 | 23 | def tick 24 | Time.now.to_f 25 | end 26 | 27 | def update(value) 28 | update_with_timestamp(value, tick) 29 | end 30 | 31 | def update_with_timestamp(value, timestamp) 32 | priority = weight(timestamp.to_f - @start_time.to_f) / rand 33 | @count += 1 34 | if @count <= @size 35 | @values[priority] = value 36 | else 37 | first_key = @values.keys.first 38 | if first_key && first_key < priority 39 | @values[priority] = value 40 | 41 | while values.any? && @values.delete(first_key).nil? 42 | first_key = @values.keys.first 43 | end 44 | end 45 | end 46 | 47 | now = Time.now.to_f 48 | 49 | if now >= @next_scale_time 50 | rescale(now + RESCALE_WINDOW_SECONDS) 51 | end 52 | end 53 | 54 | def values 55 | # read-lock? 56 | @values.keys.sort.map do |key| 57 | @values[key] 58 | end 59 | end 60 | 61 | private 62 | def rescale(next_scale_time) 63 | # writelock 64 | @next_scale_time = next_scale_time 65 | old_start_time = @start_time 66 | @start_time = tick 67 | time_delta = @start_time - old_start_time 68 | @values.keys.each do |key| 69 | value = @values.delete(key) 70 | new_key = key * Math.exp(-@alpha * time_delta) 71 | @values[new_key] = value 72 | end 73 | # unlock 74 | end 75 | 76 | def weight(factor) 77 | @alpha.to_f * factor.to_f 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/ruby-metrics/statistics/uniform_sample.rb: -------------------------------------------------------------------------------- 1 | module Metrics 2 | module Statistics 3 | class UniformSample 4 | def initialize(size = 1028) 5 | @values = Array.new(size) 6 | @size = size 7 | clear 8 | end 9 | 10 | def clear 11 | (0...@values.size).each do |i| 12 | @values[i] = 0 13 | end 14 | @count = 0 15 | end 16 | 17 | def size 18 | @values.size 19 | end 20 | 21 | def update(value) 22 | if @count < @values.length 23 | @values[@count] = value 24 | @count += 1 25 | else 26 | index = rand(@size) % @count 27 | @values[index] = value 28 | end 29 | end 30 | 31 | def values 32 | @values.dup 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/ruby-metrics/time_units.rb: -------------------------------------------------------------------------------- 1 | module Metrics 2 | 3 | module Nanoseconds 4 | def self.to_nsec(mult = 1) 5 | mult 6 | end 7 | end 8 | 9 | module Microseconds 10 | def self.to_nsec(mult = 1) 11 | 1000 * mult 12 | end 13 | end 14 | 15 | module Milliseconds 16 | def self.to_nsec(mult = 1) 17 | 1000000 * mult 18 | end 19 | end 20 | 21 | module Seconds 22 | def self.to_nsec(mult = 1) 23 | 1000000000 * mult 24 | end 25 | end 26 | 27 | module Minutes 28 | def self.to_nsec(mult = 1) 29 | 60000000000 * mult 30 | end 31 | end 32 | 33 | module Hours 34 | def self.to_nsec(mult = 1) 35 | 3600000000000 * mult 36 | end 37 | end 38 | 39 | module TimeConversion 40 | UNITS = { 41 | :nanoseconds => Nanoseconds, 42 | :microseconds => Microseconds, 43 | :milliseconds => Milliseconds, 44 | :seconds => Seconds, 45 | :minutes => Minutes, 46 | :hours => Hours 47 | } 48 | 49 | def convert_to_ns(value, unit) 50 | UNITS[unit].to_nsec.to_f * value.to_f 51 | end 52 | 53 | def scale_time_units(source, dest) 54 | UNITS[source].to_nsec.to_f / UNITS[dest].to_nsec.to_f 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/ruby-metrics/version.rb: -------------------------------------------------------------------------------- 1 | module Metrics 2 | VERSION = '0.9.3' 3 | end 4 | -------------------------------------------------------------------------------- /ruby-metrics-opentsdb.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "ruby-metrics/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "ruby-metrics-opentsdb" 7 | s.version = Metrics::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["John Ewart"] 10 | s.email = ["john@johnewart.net"] 11 | s.homepage = "https://github.com/johnewart/ruby-metrics-opentsdb" 12 | s.summary = %q{OpenTSDB reporter for ruby-metrics} 13 | s.description = %q{A reporter that uses OpenTSDB to stash metric data} 14 | s.license = 'MIT' 15 | 16 | s.files = ['lib/ruby-metrics/reporters/opentsdb.rb'] 17 | s.require_paths = ["lib"] 18 | 19 | s.add_dependency "opentsdb", '~> 0.1', '>= 0.1.0' 20 | s.add_dependency 'ruby-metrics', Metrics::VERSION 21 | end 22 | -------------------------------------------------------------------------------- /ruby-metrics.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path('../lib', __FILE__) 3 | require 'ruby-metrics/version' 4 | 5 | plugin_files = Dir['ruby-metrics-*.gemspec'].map { |gemspec| 6 | eval(File.read(gemspec)).files 7 | }.flatten.uniq 8 | 9 | Gem::Specification.new do |s| 10 | s.name = 'ruby-metrics' 11 | s.version = Metrics::VERSION 12 | s.platform = Gem::Platform::RUBY 13 | s.authors = ['John Ewart'] 14 | s.email = ['john@johnewart.net'] 15 | s.homepage = 'https://github.com/johnewart/ruby-metrics' 16 | s.summary = %q{Metrics for Ruby} 17 | s.description = %q{A Ruby implementation of metrics inspired by @coda's JVM metrics for those of us in Ruby land} 18 | s.license = 'MIT' 19 | 20 | s.add_dependency 'json', '~> 1.5', '>= 1.5.5' 21 | 22 | s.add_development_dependency 'rspec', '~> 2.14', '>= 2.14.0' 23 | s.add_development_dependency 'simplecov', '~> 0.3', '>= 0.3.8' 24 | s.add_development_dependency 'rack-test', '~> 0.5', '>= 0.5.7' 25 | 26 | s.files = `git ls-files`.split("\n") - plugin_files 27 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 28 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 29 | s.require_paths = ['lib'] 30 | end 31 | -------------------------------------------------------------------------------- /spec/agent_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper.rb' 2 | 3 | describe Metrics::Agent do 4 | before :each do 5 | @agent = Metrics::Agent.new 6 | end 7 | 8 | it "should create a new agent" do 9 | end 10 | 11 | it "should add a counter instrument correctly" do 12 | @counter = Metrics::Instruments::Counter.new(:units => "jobs") 13 | Metrics::Instruments::Counter.stub!(:new).and_return @counter 14 | @agent.counter(:test_counter, "jobs").should == @counter 15 | end 16 | 17 | it "should allow for creating a gauge with a block via #gauge" do 18 | @agent.gauge :test_gauge do 19 | "result" 20 | end 21 | end 22 | 23 | it "should add a Histogram instrument using uniform sampling" do 24 | histogram = Metrics::Instruments::UniformHistogram.new 25 | Metrics::Instruments::UniformHistogram.stub!(:new).and_return histogram 26 | @agent.uniform_histogram(:test_histogram).should == histogram 27 | end 28 | 29 | it "should allow for registering a Histogram instrument using exponentially decaying sampling" do 30 | histogram = Metrics::Instruments::ExponentialHistogram.new 31 | Metrics::Instruments::ExponentialHistogram.stub!(:new).and_return histogram 32 | @agent.exponential_histogram(:test_histogram).should == histogram 33 | end 34 | 35 | it "should set up a histogram using uniform distribution if just a histogram is registered" do 36 | histogram = Metrics::Instruments::UniformHistogram.new 37 | Metrics::Instruments::UniformHistogram.stub!(:new).and_return histogram 38 | @agent.histogram(:test_histogram).should == histogram 39 | end 40 | 41 | it "should add a meter instrument correctly" do 42 | @meter = Metrics::Instruments::Meter.new(:units => "hits/sec") 43 | Metrics::Instruments::Meter.stub!(:new).and_return @meter 44 | 45 | @agent.meter(:test_meter, "hits/sec").should == @meter 46 | end 47 | 48 | it "should add a timer instrument correctly" do 49 | @timer = Metrics::Instruments::Timer.new(:units => "reqs/sec") 50 | Metrics::Instruments::Timer.stub!(:new).and_return @timer 51 | 52 | timer = @agent.timer(:test_timer, "reqs/sec").should == @timer 53 | @timer.units.should == "reqs/sec" 54 | 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /spec/instruments/counter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Metrics::Instruments::Counter do 4 | let(:counter) { 5 | Metrics::Instruments::Counter.new 6 | } 7 | 8 | it 'can be tagged' do 9 | counter.tag(:foo, 'bar') 10 | expect(counter.tags.keys).to include(:foo) 11 | expect(counter.tags.values).to include('bar') 12 | end 13 | 14 | it 'does not need to be tagged' do 15 | expect(counter.tags.keys.size).to be 0 16 | end 17 | 18 | it 'should create a new entity with zero as its value' do 19 | counter.to_i.should == 0 20 | end 21 | 22 | it "should increment its counter by the value specified" do 23 | value = 1 24 | lambda do 25 | counter.inc(value) 26 | end.should change{ counter.to_i }.by(value) 27 | end 28 | 29 | it "should increment its counter by one by default" do 30 | lambda do 31 | counter.inc 32 | end.should change{ counter.to_i }.by(1) 33 | end 34 | 35 | it "should decrement its counter by the value specified" do 36 | value = 1 37 | lambda do 38 | counter.dec(value) 39 | end.should change{ counter.to_i }.by(-value) 40 | end 41 | 42 | it "should decrement its counter by one by default" do 43 | lambda do 44 | counter.dec 45 | end.should change{ counter.to_i }.by(-1) 46 | end 47 | 48 | it "should alias #incr to #inc" do 49 | lambda do 50 | counter.incr 51 | end.should change{ counter.to_i }.by(1) 52 | end 53 | 54 | it "should alias #decr to #dec" do 55 | lambda do 56 | counter.decr 57 | end.should change{ counter.to_i }.by(-1) 58 | end 59 | 60 | it "should clear the counter correctly" do 61 | counter.clear 62 | counter.to_i.should == 0 63 | end 64 | 65 | it "should correctly represent the value as a string" do 66 | counter.clear 67 | counter.to_i.should == 0 68 | counter.to_s.should == "0" 69 | end 70 | 71 | it "should return the new count when incrementing" do 72 | count = counter.to_i 73 | counter.inc(value = 1).should == count + value 74 | end 75 | 76 | it "should return the new count when decrementing" do 77 | lambda do 78 | counter.dec(1) 79 | end.should change{ counter.to_i }.by(-1) 80 | end 81 | 82 | context "to_json" do 83 | let(:json) { counter.to_json } 84 | it "should serialize to its current value" do 85 | json.should == counter.to_s 86 | end 87 | end 88 | 89 | end 90 | -------------------------------------------------------------------------------- /spec/instruments/gauge_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Metrics::Instruments::Gauge do 4 | before(:each) do 5 | end 6 | 7 | it "should create a new gauge" do 8 | callback = Proc.new {} 9 | @gauge = Metrics::Instruments::Gauge.new &callback 10 | end 11 | 12 | it "should correctly callback the block given when we call Gauge#get" do 13 | result = 42 14 | 15 | callback = Proc.new do 16 | { 17 | :result => result 18 | } 19 | end 20 | 21 | @gauge = Metrics::Instruments::Gauge.new &callback 22 | 23 | @gauge.get[:result].should == 42 24 | 25 | result += 1 26 | 27 | @gauge.get[:result].should == 43 28 | end 29 | 30 | context "to_json" do 31 | it "should serialize the current value" do 32 | result = 0 33 | gauge = Metrics::Instruments::Gauge.new{ result } 34 | 35 | gauge.to_json.should == result.to_s 36 | 37 | result = 2 38 | gauge.to_json.should == result.to_s 39 | end 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /spec/instruments/histogram_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # This is compared to results from R using the built-in old-faithful dataset 4 | 5 | describe Metrics::Instruments::Histogram do 6 | 7 | context "using a uniform sample" do 8 | before(:each) do 9 | @histogram = Metrics::Instruments::Histogram.new 10 | data = %w(3.600 1.800 3.333 2.283 4.533 2.883 4.700 3.600 1.950 4.350 1.833 3.917 4.200 1.750 4.700 2.167 1.750 4.800 1.600 4.250 1.800 1.750 3.450 3.067 4.533 3.600 1.967 4.083 3.850 4.433 4.300 4.467 3.367 4.033 3.833 2.017 1.867 4.833 1.833 4.783 4.350 1.883 4.567 1.750 4.533 3.317 3.833 2.100 4.633 2.000 4.800 4.716 1.833 4.833 1.733 4.883 3.717 1.667 4.567 4.317 2.233 4.500 1.750 4.800 1.817 4.400 4.167 4.700 2.067 4.700 4.033 1.967 4.500 4.000 1.983 5.067 2.017 4.567 3.883 3.600 4.133 4.333 4.100 2.633 4.067 4.933 3.950 4.517 2.167 4.000 2.200 4.333 1.867 4.817 1.833 4.300 4.667 3.750 1.867 4.900 2.483 4.367 2.100 4.500 4.050 1.867 4.700 1.783 4.850 3.683 4.733 2.300 4.900 4.417 1.700 4.633 2.317 4.600 1.817 4.417 2.617 4.067 4.250 1.967 4.600 3.767 1.917 4.500 2.267 4.650 1.867 4.167 2.800 4.333 1.833 4.383 1.883 4.933 2.033 3.733 4.233 2.233 4.533 4.817 4.333 1.983 4.633 2.017 5.100 1.800 5.033 4.000 2.400 4.600 3.567 4.000 4.500 4.083 1.800 3.967 2.200 4.150 2.000 3.833 3.500 4.583 2.367 5.000 1.933 4.617 1.917 2.083 4.583 3.333 4.167 4.333 4.500 2.417 4.000 4.167 1.883 4.583 4.250 3.767 2.033 4.433 4.083 1.833 4.417 2.183 4.800 1.833 4.800 4.100 3.966 4.233 3.500 4.366 2.250 4.667 2.100 4.350 4.133 1.867 4.600 1.783 4.367 3.850 1.933 4.500 2.383 4.700 1.867 3.833 3.417 4.233 2.400 4.800 2.000 4.150 1.867 4.267 1.750 4.483 4.000 4.117 4.083 4.267 3.917 4.550 4.083 2.417 4.183 2.217 4.450 1.883 1.850 4.283 3.950 2.333 4.150 2.350 4.933 2.900 4.583 3.833 2.083 4.367 2.133 4.350 2.200 4.450 3.567 4.500 4.150 3.817 3.917 4.450 2.000 4.283 4.767 4.533 1.850 4.250 1.983 2.250 4.750 4.117 2.150 4.417 1.817 4.467) 11 | data.each do |point| 12 | @histogram.update(point.to_f) 13 | end 14 | @histogram.count.should == 272 15 | end 16 | 17 | it "should update variance correctly" do 18 | @histogram.variance.should == 1.3027283328494685 19 | end 20 | 21 | it "should calculate standard deviations properly" do 22 | @histogram.std_dev.should == 1.1413712511052083 23 | end 24 | 25 | it "should accurately calculate quantiles" do 26 | quantiles = @histogram.quantiles([0.99, 0.98, 0.95, 0.80, 0.57, 0.32]) 27 | quantiles.should == 28 | { 29 | 0.99 => 5.009570000000001, 30 | 0.98 => 4.93300, 31 | 0.95 => 4.81700, 32 | 0.80 => 4.53300, 33 | 0.57 => 4.13300, 34 | 0.32 => 2.39524 35 | } 36 | end 37 | 38 | it "should accurately calculate the mean" do 39 | @histogram.mean.should == 3.4877830882352936 40 | end 41 | 42 | it "should return correct values for mean, std. deviation and variance when no elements are in the histogram" do 43 | histogram = Metrics::Instruments::Histogram.new 44 | histogram.variance.should == 0.0 45 | histogram.mean.should == 0.0 46 | histogram.std_dev.should == 0.0 47 | end 48 | 49 | it "should return the first value as the quantile if only one value" do 50 | histogram = Metrics::Instruments::Histogram.new 51 | histogram.update(42) 52 | histogram.quantiles([0.50]).should == {0.50 => 42} 53 | end 54 | 55 | it "should return the last value as the 100%" do 56 | histogram = Metrics::Instruments::Histogram.new 57 | histogram.update(42) 58 | histogram.update(64) 59 | histogram.quantiles([1.00]).should == {1.00 => 64} 60 | end 61 | 62 | 63 | it "should return correct values for min and max" do 64 | histogram = Metrics::Instruments::Histogram.new 65 | histogram.update(42) 66 | histogram.min.should == 42 67 | histogram.max.should == 42 68 | end 69 | 70 | context "resetting the histogram" do 71 | 72 | it "should clear data correctly" do 73 | sample = Metrics::Statistics::UniformSample.new 74 | sample.should_receive(:clear).twice 75 | Metrics::Statistics::UniformSample.should_receive(:new).and_return sample 76 | 77 | histogram = Metrics::Instruments::Histogram.new 78 | histogram.clear 79 | histogram.max.should == 0.0 80 | histogram.min.should == 0.0 81 | end 82 | 83 | end 84 | end 85 | 86 | context "using an exponentially weighted sample" do 87 | it "should return correct values for mean, std. deviation and variance when no elements are in the histogram" do 88 | histogram = Metrics::Instruments::Histogram.new(:exponential) 89 | histogram.variance.should == 0.0 90 | histogram.mean.should == 0.0 91 | histogram.std_dev.should == 0.0 92 | end 93 | end 94 | 95 | context "to_json" do 96 | before(:each) do 97 | @histogram = Metrics::Instruments::Histogram.new 98 | @hash = JSON.parse(@histogram.to_json) 99 | end 100 | 101 | %w( min max mean variance ).each do |attr| 102 | it "should serialize with the #{attr} value" do 103 | @hash[attr].should_not be_nil 104 | end 105 | end 106 | 107 | %w( 0.25 0.5 0.75 0.95 0.97 0.98 0.99 ).each do |percentile| 108 | it "should have the #{percentile} percentile" do 109 | @hash["percentiles"][percentile].should_not be_nil 110 | end 111 | end 112 | 113 | end 114 | 115 | end 116 | -------------------------------------------------------------------------------- /spec/instruments/meter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'timecop' 3 | 4 | describe Metrics::Instruments::Meter do 5 | before(:each) do 6 | Thread.stub!(:new).and_return do |block| 7 | # Do nothing 8 | end 9 | end 10 | 11 | it "should initialize averages to 0" do 12 | meter = Metrics::Instruments::Meter.new 13 | meter.one_minute_rate.should == 0.0 14 | meter.five_minute_rate.should == 0.0 15 | meter.fifteen_minute_rate.should == 0.0 16 | end 17 | 18 | it "should increment count" do 19 | meter = Metrics::Instruments::Meter.new 20 | meter.mark(500) 21 | meter.counted.should == 500 22 | end 23 | 24 | it "should accept options for the constructor" do 25 | meter = Metrics::Instruments::Meter.new 26 | end 27 | 28 | context "A timer with an initial mark of 3 at a 1 second rate unit on a 5 second interval" do 29 | 30 | before(:each) do 31 | @start_time = Time.local(2014, 1, 1, 12, 0, 0) 32 | Timecop.freeze(@start_time) 33 | @meter = Metrics::Instruments::Meter.new 34 | @meter.mark(3) 35 | @meter.tick() 36 | end 37 | 38 | after(:each) do 39 | Timecop.return 40 | end 41 | 42 | def tick_for(seconds) 43 | count = ((seconds) / 5).to_i 44 | (1..count).each do 45 | @meter.tick() 46 | end 47 | end 48 | 49 | context 'When computing mean rate' do 50 | it 'should have a mean rate of 3/sec one second in' do 51 | @meter.mark(3) 52 | Timecop.freeze(Time.now + 1) do 53 | @meter.mean_rate.should == 3 54 | end 55 | end 56 | 57 | it 'should have a mean rate of 0.003/msec one second in' do 58 | @meter.mark(3) 59 | Timecop.freeze(Time.now + 1) do 60 | @meter.mean_rate(:milliseconds).should == 0.003 61 | end 62 | end 63 | 64 | it 'should have a rate of 180/minute one second in' do 65 | @meter.mark(3) 66 | Timecop.freeze(Time.now + 1) do 67 | @meter.mean_rate(:minutes).should == 180 68 | end 69 | end 70 | 71 | it 'should have a rate of 10800/hour one second in' do 72 | @meter.mark(3) 73 | Timecop.freeze(Time.now + 1) do 74 | @meter.mean_rate(:hours).should == 10800 75 | end 76 | end 77 | end 78 | 79 | context "For a 1 minute window" do 80 | it "should have a rate of 0.6 events/sec after the first tick" do 81 | @meter.one_minute_rate.should == 0.6 82 | end 83 | 84 | it "should have a rate of 0.22072766470286553 events/sec after 1 minute" do 85 | tick_for(60) 86 | @meter.one_minute_rate.should == 0.22072766470286553 87 | end 88 | end 89 | 90 | context "For a 5 minute window" do 91 | it "should have a rate of 0.6 events/sec after the first tick" do 92 | @meter.five_minute_rate.should == 0.6 93 | end 94 | 95 | it "should have a rate of 0.49123845184678905 events/sec after 1 minute" do 96 | tick_for(60) 97 | @meter.five_minute_rate.should == 0.49123845184678905 98 | end 99 | end 100 | 101 | context "For a 15 minute window" do 102 | it "should have a rate of 0.6 events/sec after the first tick" do 103 | @meter.fifteen_minute_rate.should == 0.6 104 | end 105 | 106 | it "should have a rate of 36.0 events per minute after the first tick" do 107 | @meter.fifteen_minute_rate(:minutes).should == 36.0 108 | end 109 | 110 | it "should have a rate of 2160.0 events per hour after the first tick" do 111 | @meter.fifteen_minute_rate(:hours).should == 2160.0 112 | end 113 | 114 | it "should have a rate of 0.5613041910189706 events/sec after 1 minute" do 115 | tick_for(60) 116 | @meter.fifteen_minute_rate.should == 0.5613041910189706 117 | end 118 | end 119 | 120 | end 121 | 122 | context "to_json" do 123 | before(:each) do 124 | @meter = Metrics::Instruments::Meter.new 125 | @hash = JSON.parse(@meter.to_json) 126 | end 127 | 128 | %w( one_minute_rate five_minute_rate fifteen_minute_rate ).each do |attr| 129 | it "should serialize with the #{attr} value" do 130 | @hash[attr].should_not be_nil 131 | end 132 | end 133 | 134 | end 135 | 136 | end 137 | -------------------------------------------------------------------------------- /spec/instruments/timer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Metrics::Instruments::Timer do 4 | before(:each) do 5 | Thread.stub!(:new).and_return do |block| 6 | # Do nothing 7 | end 8 | end 9 | 10 | context "An empty timer" do 11 | before(:each) do 12 | @timer = Metrics::Instruments::Timer.new 13 | end 14 | 15 | it "should have a max of zero" do 16 | @timer.max.should == 0 17 | end 18 | 19 | it "should have a min of zero" do 20 | @timer.min.should == 0 21 | end 22 | 23 | it "should have a mean of zero" do 24 | @timer.mean.should == 0 25 | end 26 | 27 | it "should have a min of zero" do 28 | @timer.std_dev.should == 0 29 | end 30 | 31 | it "should have quantiles of zero" do 32 | @timer.quantiles.should == {0.25=>0.0, 0.50=>0.0, 0.75=>0.0, 0.95=>0.0, 0.97=>0.0, 0.98=>0.0, 0.99=>0.0 } 33 | end 34 | 35 | it "should have a mean rate of zero" do 36 | @timer.mean_rate.should == 0 37 | end 38 | 39 | it "should have a one-minute rate of zero" do 40 | @timer.one_minute_rate.should == 0 41 | end 42 | 43 | it "should have a five-minute rate of zero" do 44 | @timer.five_minute_rate.should == 0 45 | end 46 | 47 | it "should have a fifteen-minute rate of zero" do 48 | @timer.fifteen_minute_rate.should == 0 49 | end 50 | 51 | it "should have no values stored" do 52 | @timer.values.length.should == 0 53 | end 54 | 55 | end 56 | 57 | context "Timing some events" do 58 | before(:each) do 59 | @timer = Metrics::Instruments::Timer.new({:duration_unit => :milliseconds, :rate_unit => :seconds}) 60 | @timer.update(10, :milliseconds) 61 | @timer.update(20, :milliseconds) 62 | @timer.update(20, :milliseconds) 63 | @timer.update(30, :milliseconds) 64 | @timer.update(40, :milliseconds) 65 | end 66 | 67 | it "should have counted 5 events" do 68 | @timer.count.should == 5 69 | end 70 | 71 | it "should accurately calculate the minimum duration" do 72 | @timer.min.should == 10 73 | end 74 | 75 | it "should accurately calculate the maximum duration" do 76 | @timer.max.should == 40 77 | end 78 | 79 | it "should accurately calculate the mean duration" do 80 | @timer.mean.should == 24 81 | end 82 | 83 | it "should accurately calculate the standard deviation of the durations" do 84 | @timer.std_dev.should == 11.40175425099138 85 | end 86 | 87 | it "should accurately calculate percentiles" do 88 | @timer.quantiles.should == {0.25=>20.0, 0.5=>20.0, 0.75=>30.0, 0.95=>38.0, 0.97=>38.8, 0.98=>39.2, 0.99=>39.6} 89 | end 90 | 91 | it "should contain the series added" do 92 | @timer.values.sort.should == [10, 20, 20, 30, 40] 93 | end 94 | 95 | end 96 | 97 | context "Timing blocks of code" do 98 | before(:each) do 99 | @timer = Metrics::Instruments::Timer.new({:duration_unit => :nanoseconds, :rate_unit => :nanoseconds}) 100 | end 101 | 102 | it "should return the result of the block passed" do 103 | result = @timer.time do 104 | sleep(0.25) 105 | "narf" 106 | end 107 | 108 | result.should == "narf" 109 | 110 | @timer.max.should >= 250000000 111 | @timer.max.should <= 300000000 112 | 113 | end 114 | end 115 | 116 | context "to_json" do 117 | before(:each) do 118 | @timer = Metrics::Instruments::Timer.new 119 | @hash = JSON.parse(@timer.to_json) 120 | end 121 | 122 | it "should serialize with the count value" do 123 | @hash["count"].should_not be_nil 124 | end 125 | 126 | %w( one_minute_rate five_minute_rate fifteen_minute_rate ).each do |rate| 127 | it "should serialize with the #{rate} rate" do 128 | @hash["rates"][rate].should_not be_nil 129 | end 130 | end 131 | 132 | %w( min max mean ).each do |duration| 133 | it "should serialize with the #{duration} duration" do 134 | @hash["durations"][duration].should_not be_nil 135 | end 136 | end 137 | 138 | %w( 0.25 0.5 0.75 0.95 0.97 0.98 0.99 ).each do |percentile| 139 | it "should serialize with the #{percentile} duration percentile" do 140 | @hash["durations"]["percentiles"][percentile].should_not be_nil 141 | end 142 | end 143 | 144 | end 145 | 146 | end 147 | -------------------------------------------------------------------------------- /spec/integration/rack_endpoint_spec.rb: -------------------------------------------------------------------------------- 1 | require "rack/test" 2 | 3 | describe Metrics::Integration::Rack::Endpoint do 4 | include Rack::Test::Methods 5 | 6 | def app(options = {}) 7 | @app ||= Metrics::Integration::Rack::Endpoint.new(options) 8 | end 9 | def agent 10 | app.agent 11 | end 12 | 13 | it "should show stats" do 14 | get "/" 15 | last_response.should be_ok 16 | last_response.body.should == agent.to_json 17 | end 18 | 19 | context "configuring" do 20 | 21 | context "agent" do 22 | it "should create an agent by default" do 23 | app(:agent => nil) 24 | agent.should be_a(Metrics::Agent) 25 | end 26 | 27 | it "should use an agent if provided" do 28 | @agent = Metrics::Agent.new 29 | app(:agent => @agent) 30 | agent.should be_a(Metrics::Agent) 31 | agent.should == @agent 32 | end 33 | end 34 | 35 | context "show" do 36 | it "should match a string to the PATH_INFO exactly" do 37 | app(:show => "/foo") 38 | get '/foo' 39 | last_response.should be_ok 40 | last_response.body.should == agent.to_json 41 | end 42 | 43 | it "should match a regular expression to the PATH_INFO" do 44 | app(:show => /bar/) 45 | get '/foobarbaz' 46 | last_response.should be_ok 47 | last_response.body.should == agent.to_json 48 | end 49 | 50 | it "should call `call` on the show option if it responds to it" do 51 | app(:show => lambda{ |env| env['PATH_INFO'] == "/bing" }) 52 | get '/bing' 53 | last_response.should be_ok 54 | last_response.body.should == agent.to_json 55 | end 56 | end 57 | 58 | end 59 | 60 | end 61 | -------------------------------------------------------------------------------- /spec/integration/rack_middleware_spec.rb: -------------------------------------------------------------------------------- 1 | require "rack/test" 2 | 3 | describe Metrics::Integration::Rack::Middleware do 4 | include Rack::Test::Methods 5 | 6 | def app(status = 200, body = ["Hello, World!"], headers = {}) 7 | @app ||= lambda{ |env| [status, {'Content-Type' => 'text/plain', 'Content-Length' => body.to_s.size.to_s}.merge(headers), body] } 8 | end 9 | alias_method :original_app, :app 10 | 11 | describe "without integration" do 12 | it "should work normally" do 13 | get "/" 14 | 15 | last_response.should be_ok 16 | last_response.body.should == "Hello, World!" 17 | end 18 | end 19 | 20 | describe "with integration" do 21 | def app(options = {}) 22 | @integrated_app ||= Metrics::Integration::Rack::Middleware.new(original_app, options) 23 | end 24 | def agent 25 | app.agent 26 | end 27 | 28 | it "should work normally" do 29 | get "/" 30 | 31 | last_response.should be_ok 32 | last_response.body.should == "Hello, World!" 33 | end 34 | 35 | it "should show stats for default endpoint" do 36 | get "/stats" 37 | last_response.should be_ok 38 | last_response.body.should == agent.to_json 39 | end 40 | 41 | it "should make 'metrics.agent' available to the upstream environment" do 42 | @app = lambda do |env| 43 | env['metrics.agent'].should be_a(Metrics::Agent) 44 | env['metrics.agent'].should == agent 45 | [200, {}, []] 46 | end 47 | 48 | get '/' 49 | last_response.should be_ok 50 | end 51 | 52 | describe "integration metrics" do 53 | 54 | it "should count all requests" do 55 | lambda do 56 | get '/' 57 | end.should change{ app.agent.timer(:_requests).count }.by(1) 58 | end 59 | 60 | it "should count uncaught exceptions" do 61 | @app = lambda{ |env| raise } 62 | lambda do 63 | lambda do 64 | get '/' 65 | end.should raise_error 66 | end.should change{ app.agent.counter(:_uncaught_exceptions).to_i }.by(1) 67 | end 68 | 69 | it "should time request length" do 70 | length = 0.1 71 | @app = lambda{ |env| sleep(length); [200, {}, ['']] } 72 | get '/' 73 | app.agent.timer(:_requests).mean.should be_within(length / 10).of(length) 74 | end 75 | 76 | [200, 304, 404, 500].each do |status| 77 | it "should count #{status} HTTP status code as #{status / 100}xx" do 78 | @app = lambda{ |env| [status, {}, []] } 79 | lambda do 80 | get '/' 81 | end.should change{ app.agent.counter(:"_status_#{status / 100}xx").to_i }.by(1) 82 | end 83 | end 84 | 85 | end 86 | 87 | context "configuring" do 88 | 89 | context "agent" do 90 | it "should create an agent by default" do 91 | app(:agent => nil) 92 | agent.should be_a(Metrics::Agent) 93 | end 94 | 95 | it "should use an agent if provided" do 96 | @agent = Metrics::Agent.new 97 | app(:agent => @agent) 98 | agent.should be_a(Metrics::Agent) 99 | agent.should == @agent 100 | end 101 | end 102 | 103 | context "show" do 104 | it "should match a string to the PATH_INFO exactly" do 105 | app(:show => "/foo") 106 | get '/foo' 107 | last_response.should be_ok 108 | last_response.body.should == agent.to_json 109 | end 110 | 111 | it "should match a regular expression to the PATH_INFO" do 112 | app(:show => /bar/) 113 | get '/foobarbaz' 114 | last_response.should be_ok 115 | last_response.body.should == agent.to_json 116 | end 117 | 118 | it "should call `call` on the show option if it responds to it" do 119 | app(:show => lambda{ |env| env['PATH_INFO'] == "/bing" }) 120 | get '/bing' 121 | last_response.should be_ok 122 | last_response.body.should == agent.to_json 123 | end 124 | end 125 | 126 | end 127 | end 128 | 129 | end 130 | -------------------------------------------------------------------------------- /spec/reporter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ruby-metrics/agent' 2 | require 'ruby-metrics/reporter' 3 | require 'ruby-metrics/reporters/opentsdb' 4 | 5 | module Metrics 6 | describe 'Reporter' do 7 | let(:mock_reporter) { 8 | double(Metrics::Reporters::OpenTSDBReporter) 9 | } 10 | 11 | let(:agent) { 12 | agent = Metrics::Agent.new 13 | agent.report_to 'opentsdb', mock_reporter 14 | agent 15 | } 16 | 17 | it 'should report three times in 4 seconds with a 1 second interval' do 18 | expect(mock_reporter).to receive(:report).exactly(3).times 19 | agent.report_periodically(1) 20 | puts "Sleeping" 21 | sleep(4) 22 | puts "Stopping..." 23 | agent.stop_reporting 24 | end 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/reporters/opentsdb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'opentsdb/client' 3 | require 'ruby-metrics/reporters/opentsdb' 4 | require 'ruby-metrics' 5 | require 'timecop' 6 | 7 | module Metrics 8 | module Reporters 9 | describe 'OpenTSDBReporter' do 10 | 11 | let(:mock_tsdb_client) { 12 | mock_tsdb_client = double(OpenTSDB::Client) 13 | } 14 | 15 | let(:reporter) { 16 | expect(OpenTSDB::Client).to receive(:new).and_return mock_tsdb_client 17 | OpenTSDBReporter.new(:tags => {:foo => 'bar'}) 18 | } 19 | 20 | let(:agent) { 21 | Metrics::Agent.new 22 | } 23 | 24 | it 'should report a counter correctly' do 25 | counter = agent.counter :my_counter 26 | counter.incr 27 | counter.incr 28 | 29 | counter_data = { 30 | :value => 2, 31 | :timestamp => anything, 32 | :tags => { 33 | :units => '', 34 | :foo => 'bar' 35 | }, 36 | :metric => 'my_counter' 37 | } 38 | expect(mock_tsdb_client).to receive(:put).with(counter_data) 39 | 40 | reporter.report(agent) 41 | end 42 | 43 | it 'should report a tagged counter correctly' do 44 | counter = agent.counter :logins, 'logins' 45 | counter.incr 46 | counter.tag(:user, 'sam') 47 | 48 | counter_data = { 49 | :value => 1, 50 | :timestamp => anything, 51 | :tags => { 52 | :units => 'logins', 53 | :foo => 'bar', 54 | :user => 'sam' 55 | }, 56 | :metric => 'logins' 57 | } 58 | expect(mock_tsdb_client).to receive(:put).with(counter_data) 59 | 60 | reporter.report(agent) 61 | end 62 | 63 | it 'should report a gauge that returns a hash' do 64 | gauge = agent.gauge :my_gauge do 65 | { 66 | :hit_count => 42, 67 | :http_requests => 320 68 | } 69 | end 70 | gauge.tag(:mytag, 'somevalue') 71 | 72 | 73 | tags = { 74 | :units => '', 75 | :foo => 'bar', 76 | :mytag => 'somevalue' 77 | } 78 | 79 | gauge_hit_data = { 80 | :value => 42, 81 | :timestamp => anything, 82 | :tags => tags, 83 | :metric => 'my_gauge.hit_count' 84 | } 85 | gauge_requests_data = { 86 | :value => 320, 87 | :timestamp => anything, 88 | :tags => tags, 89 | :metric => 'my_gauge.http_requests' 90 | } 91 | expect(mock_tsdb_client).to receive(:put).with(gauge_hit_data) 92 | expect(mock_tsdb_client).to receive(:put).with(gauge_requests_data) 93 | 94 | reporter.report(agent) 95 | end 96 | 97 | 98 | it 'should report a gauge that returns a non-hash value' do 99 | agent.gauge :boring_gauge, 'units' do 100 | 42 101 | end 102 | 103 | gauge_data = { 104 | :value => 42, 105 | :timestamp => anything, 106 | :tags => { 107 | :units => 'units', 108 | :foo => 'bar', 109 | }, 110 | :metric => 'boring_gauge' 111 | } 112 | expect(mock_tsdb_client).to receive(:put).with(gauge_data) 113 | 114 | reporter.report(agent) 115 | end 116 | 117 | it 'should report a timer' do 118 | timer = agent.timer :some_timer, 'requests' 119 | timer.update(5, :seconds) 120 | 121 | timer_counter_data = { 122 | :value => 1, 123 | :timestamp => anything, 124 | :tags => { 125 | :units => 'requests', 126 | :foo => 'bar' 127 | }, 128 | :metric => 'some_timer.count' 129 | } 130 | expect(mock_tsdb_client).to receive(:put).with(timer_counter_data) 131 | 132 | timer_fifteen = { 133 | :value => anything, 134 | :timestamp => anything, 135 | :tags => { 136 | :units => 'requests/sec', 137 | :foo => 'bar' 138 | }, 139 | :metric => 'some_timer.fifteen_minute_rate' 140 | } 141 | expect(mock_tsdb_client).to receive(:put).with(timer_fifteen) 142 | 143 | timer_five = { 144 | :value => anything, 145 | :timestamp => anything, 146 | :tags => { 147 | :units => 'requests/sec', 148 | :foo => 'bar' 149 | }, 150 | :metric => 'some_timer.five_minute_rate' 151 | } 152 | expect(mock_tsdb_client).to receive(:put).with(timer_five) 153 | 154 | 155 | timer_one = { 156 | :value => anything, 157 | :timestamp => anything, 158 | :tags => { 159 | :units => 'requests/sec', 160 | :foo => 'bar' 161 | }, 162 | :metric => 'some_timer.one_minute_rate' 163 | } 164 | expect(mock_tsdb_client).to receive(:put).with(timer_one) 165 | 166 | 167 | timer_min = { 168 | :value => 5.0, 169 | :timestamp => anything, 170 | :tags => { 171 | :units => 'sec/requests', 172 | :foo => 'bar' 173 | }, 174 | :metric => 'some_timer.min' 175 | } 176 | expect(mock_tsdb_client).to receive(:put).with(timer_min) 177 | 178 | 179 | timer_max = { 180 | :value => 5.0, 181 | :timestamp => anything, 182 | :tags => { 183 | :units => 'sec/requests', 184 | :foo => 'bar' 185 | }, 186 | :metric => 'some_timer.max' 187 | } 188 | expect(mock_tsdb_client).to receive(:put).with(timer_max) 189 | 190 | timer_mean = { 191 | :value => 5.0, 192 | :timestamp => anything, 193 | :tags => { 194 | :units => 'sec/requests', 195 | :foo => 'bar' 196 | }, 197 | :metric => 'some_timer.mean' 198 | } 199 | expect(mock_tsdb_client).to receive(:put).with(timer_mean) 200 | 201 | reporter.report(agent) 202 | end 203 | 204 | it 'should report a gauge that returns a non-hash value' do 205 | agent.gauge :boring_gauge, 'units' do 206 | 42 207 | end 208 | 209 | gauge_data = { 210 | :value => 42, 211 | :timestamp => anything, 212 | :tags => { 213 | :units => 'units', 214 | :foo => 'bar', 215 | }, 216 | :metric => 'boring_gauge' 217 | } 218 | expect(mock_tsdb_client).to receive(:put).with(gauge_data) 219 | 220 | reporter.report(agent) 221 | end 222 | 223 | it 'should report a meter' do 224 | meter = agent.meter :http_requests, 'requests' 225 | meter.mark 226 | meter.mark 227 | meter.mark 228 | meter.tick 229 | meter.mark 230 | meter.tag :somekey, 'value' 231 | 232 | meter_counter_data = { 233 | :value => anything, 234 | :timestamp => anything, 235 | :tags => { 236 | :units => 'requests', 237 | :foo => 'bar', 238 | :somekey => 'value' 239 | }, 240 | :metric => 'http_requests.count' 241 | } 242 | expect(mock_tsdb_client).to receive(:put).with(meter_counter_data) 243 | 244 | meter_fifteen = { 245 | :value => anything, 246 | :timestamp => anything, 247 | :tags => { 248 | :units => 'requests/sec', 249 | :foo => 'bar', 250 | :somekey => 'value' 251 | }, 252 | :metric => 'http_requests.fifteen_minute_rate' 253 | } 254 | expect(mock_tsdb_client).to receive(:put).with(meter_fifteen) 255 | 256 | meter_five = { 257 | :value => anything, 258 | :timestamp => anything, 259 | :tags => { 260 | :units => 'requests/sec', 261 | :foo => 'bar', 262 | :somekey => 'value' 263 | }, 264 | :metric => 'http_requests.five_minute_rate' 265 | } 266 | expect(mock_tsdb_client).to receive(:put).with(meter_five) 267 | 268 | 269 | meter_one = { 270 | :value => anything, 271 | :timestamp => anything, 272 | :tags => { 273 | :units => 'requests/sec', 274 | :foo => 'bar', 275 | :somekey => 'value' 276 | }, 277 | :metric => 'http_requests.one_minute_rate' 278 | } 279 | expect(mock_tsdb_client).to receive(:put).with(meter_one) 280 | 281 | meter_mean = { 282 | :value => anything, 283 | :timestamp => anything, 284 | :tags => { 285 | :units => 'requests/sec', 286 | :foo => 'bar', 287 | :somekey => 'value' 288 | }, 289 | :metric => 'http_requests.mean_rate' 290 | } 291 | expect(mock_tsdb_client).to receive(:put).with(meter_mean) 292 | 293 | reporter.report(agent) 294 | end 295 | end 296 | end 297 | end 298 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | 3 | SimpleCov.start do 4 | add_filter "/spec/" 5 | add_group "Instruments", "metrics/instruments" 6 | add_group "Statistical Samples", "metrics/statistics" 7 | merge_timeout 3600 8 | end 9 | 10 | $:.unshift(File.expand_path('../../lib', __FILE__)) 11 | require 'ruby-metrics' 12 | 13 | Metrics.logger = Logger.new(STDERR) 14 | Metrics.logger.level = Logger::INFO 15 | 16 | RSpec.configure do |config| 17 | config.mock_with :rspec 18 | end 19 | -------------------------------------------------------------------------------- /spec/statistics/exponential_sample_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Metrics::Statistics::ExponentialSample do 4 | before(:each) do 5 | end 6 | 7 | it "should have a size equal 0 intially no matter to the initialization parameter" do 8 | sample = Metrics::Statistics::ExponentialSample.new(100) 9 | sample.size.should == 0 10 | 11 | end 12 | 13 | it "should have an empty backing hash initially" do 14 | sample = Metrics::Statistics::ExponentialSample.new(100) 15 | sample.values.length.should == 0 16 | end 17 | 18 | 19 | context "A sample of 100 out of 1000 elements" do 20 | before(:each) do 21 | @population = (0..99).to_a 22 | @sample = Metrics::Statistics::ExponentialSample.new(1000, 0.99) 23 | @population.each do |datum| 24 | @sample.update(datum) 25 | end 26 | end 27 | 28 | it "should have 100 elements" do 29 | @sample.size.should == 100 30 | @sample.values.length.should == 100 31 | end 32 | 33 | it "should only have elements from the population" do 34 | values = @sample.values 35 | @population.each do |datum| 36 | values.should include datum 37 | end 38 | end 39 | end 40 | 41 | context "A heavily-biased sample of 100 out of 1000 elements" do 42 | before(:each) do 43 | @population = (0..99).to_a 44 | @sample = Metrics::Statistics::ExponentialSample.new(1000, 0.01) 45 | @population.each do |datum| 46 | @sample.update(datum) 47 | end 48 | end 49 | 50 | it "should have 100 elements" do 51 | @sample.size.should == 100 52 | @sample.values.length.should == 100 53 | end 54 | 55 | it "should only have elements from the population" do 56 | values = @sample.values 57 | @population.each do |datum| 58 | values.should include datum 59 | end 60 | end 61 | 62 | it "should add another element when updating" do 63 | @sample.update(42) 64 | @sample.size.should == 101 65 | @sample.values.should include 42 66 | end 67 | 68 | it "should rescale when the clock is one hour in the future or more" do 69 | future_time = Time.now + (60 * 60) 70 | Time.stub(:now).and_return future_time 71 | @sample.update(42) 72 | @sample.size.should == 101 73 | 74 | values = @sample.values 75 | 76 | @population.each do |datum| 77 | values.should include datum 78 | end 79 | 80 | values.should include 42 81 | end 82 | end 83 | 84 | context "A heavily-biased sample of 1000 out of 1000 elements" do 85 | before(:each) do 86 | @population = (0..999).to_a 87 | @sample = Metrics::Statistics::ExponentialSample.new(1000, 0.01) 88 | @population.each do |datum| 89 | @sample.update(datum) 90 | end 91 | end 92 | 93 | it "should have 1000 elements" do 94 | @sample.size.should == 1000 95 | @sample.values.length.should == 1000 96 | end 97 | 98 | it "should only have elements from the population" do 99 | values = @sample.values 100 | @population.each do |datum| 101 | values.should include datum 102 | end 103 | end 104 | 105 | it "should replace an element when updating" do 106 | @sample.update(4242) 107 | @sample.size.should == 1000 108 | @sample.values.should include 4242 109 | end 110 | 111 | it "should rescale so that newer events are higher in priority in the hash" do 112 | future_time = Time.now + (60 * 60) 113 | Time.stub(:now).and_return future_time 114 | 115 | @sample.update(2121) 116 | 117 | 118 | @sample.size.should == 1000 119 | 120 | future_time = Time.now + (60 * 60 * 2) 121 | Time.stub(:now).and_return future_time 122 | 123 | @sample.update(4242) 124 | @sample.size.should == 1000 125 | 126 | values = @sample.values 127 | 128 | values.length.should == 1000 129 | values.should include 4242 130 | values.should include 2121 131 | 132 | # Most recently added values in time should be at the end with the highest priority 133 | values[999].should == 4242 134 | values[998].should == 2121 135 | end 136 | end 137 | 138 | end 139 | -------------------------------------------------------------------------------- /spec/statistics/uniform_sample_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Metrics::Statistics::UniformSample do 4 | before(:each) do 5 | end 6 | 7 | it "should have a size equal to the initialization parameter" do 8 | sample = Metrics::Statistics::UniformSample.new(100) 9 | sample.size.should == 100 10 | 11 | end 12 | 13 | it "should allocate an array of the requested size" do 14 | sample = Metrics::Statistics::UniformSample.new(100) 15 | sample.values.length.should == 100 16 | 17 | sample.values.each do |value| 18 | value.should == 0 19 | end 20 | end 21 | 22 | it "should update at the end of the list" do 23 | sample = Metrics::Statistics::UniformSample.new(100) 24 | (1..100).each do |i| 25 | sample.update(i) 26 | end 27 | 28 | values = sample.values 29 | 30 | (0..99).each do |index| 31 | values[index].should == (index + 1) 32 | end 33 | end 34 | 35 | it "should update a random entry in the list when it's full" do 36 | 37 | sample = Metrics::Statistics::UniformSample.new(100) 38 | sample.should_receive(:rand).with(any_args()).and_return(50) 39 | 40 | (1..101).each do |i| 41 | sample.update(i) 42 | end 43 | 44 | values = sample.values 45 | 46 | (0..49).each do |index| 47 | values[index].should == (index + 1) 48 | end 49 | 50 | values[50].should == 101 51 | 52 | (51..99).each do |index| 53 | values[index].should == (index + 1) 54 | end 55 | 56 | end 57 | 58 | 59 | end 60 | --------------------------------------------------------------------------------