yam-ruby-metrics 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. data/.bundle/config +2 -0
  2. data/.gitignore +4 -0
  3. data/.rvmrc +1 -0
  4. data/CHANGELOG.md +48 -0
  5. data/Gemfile +8 -0
  6. data/Gemfile.lock +36 -0
  7. data/LICENSE +21 -0
  8. data/README.md +86 -0
  9. data/Rakefile +19 -0
  10. data/autotest/discover.rb +3 -0
  11. data/examples/counter.rb +10 -0
  12. data/examples/gauge.rb +25 -0
  13. data/examples/histogram.rb +32 -0
  14. data/examples/integration/rack_endpoint.ru +24 -0
  15. data/examples/integration/rack_middleware.ru +21 -0
  16. data/examples/integration/webrick.rb +17 -0
  17. data/examples/meter.rb +25 -0
  18. data/examples/timer.rb +31 -0
  19. data/lib/ruby-metrics.rb +15 -0
  20. data/lib/ruby-metrics/agent.rb +62 -0
  21. data/lib/ruby-metrics/instruments/counter.rb +39 -0
  22. data/lib/ruby-metrics/instruments/gauge.rb +23 -0
  23. data/lib/ruby-metrics/instruments/histogram.rb +188 -0
  24. data/lib/ruby-metrics/instruments/meter.rb +99 -0
  25. data/lib/ruby-metrics/instruments/timer.rb +138 -0
  26. data/lib/ruby-metrics/integration.rb +11 -0
  27. data/lib/ruby-metrics/integration/rack_endpoint.rb +33 -0
  28. data/lib/ruby-metrics/integration/rack_middleware.rb +82 -0
  29. data/lib/ruby-metrics/integration/webrick.rb +47 -0
  30. data/lib/ruby-metrics/logging.rb +19 -0
  31. data/lib/ruby-metrics/statistics/exponential_sample.rb +91 -0
  32. data/lib/ruby-metrics/statistics/uniform_sample.rb +37 -0
  33. data/lib/ruby-metrics/time_units.rb +61 -0
  34. data/lib/ruby-metrics/version.rb +3 -0
  35. data/ruby-metrics.gemspec +27 -0
  36. data/spec/agent_spec.rb +48 -0
  37. data/spec/instruments/counter_spec.rb +79 -0
  38. data/spec/instruments/gauge_spec.rb +42 -0
  39. data/spec/instruments/histogram_spec.rb +115 -0
  40. data/spec/instruments/meter_spec.rb +99 -0
  41. data/spec/instruments/timer_spec.rb +146 -0
  42. data/spec/integration/rack_endpoint_spec.rb +60 -0
  43. data/spec/integration/rack_middleware_spec.rb +129 -0
  44. data/spec/integration/webrick_spec.rb +18 -0
  45. data/spec/spec_helper.rb +18 -0
  46. data/spec/statistics/exponential_sample_spec.rb +138 -0
  47. data/spec/statistics/uniform_sample_spec.rb +59 -0
  48. data/spec/time_units_spec.rb +13 -0
  49. metadata +184 -0
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,33 @@
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
@@ -0,0 +1,82 @@
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
+ # integration metrics
20
+ :requests, :uncaught_exceptions,
21
+ :status_codes
22
+
23
+ def initialize(app, options ={})
24
+ @app = app
25
+ @options = {:show => "/stats"}.merge(options)
26
+ @agent = @options.delete(:agent) || Agent.new
27
+
28
+ # Integration Metrics
29
+ @requests = @agent.timer(:_requests)
30
+ @uncaught_exceptions = @agent.counter(:_uncaught_exceptions)
31
+
32
+ # HTTP Status Codes
33
+ @status_codes = {
34
+ 1 => @agent.counter(:_status_1xx),
35
+ 2 => @agent.counter(:_status_2xx),
36
+ 3 => @agent.counter(:_status_3xx),
37
+ 4 => @agent.counter(:_status_4xx),
38
+ 5 => @agent.counter(:_status_5xx)
39
+ }
40
+ end
41
+
42
+ def call(env)
43
+ return show(env) if show?(env)
44
+
45
+ env['metrics.agent'] = @agent
46
+
47
+ status, headers, body = self.requests.time{ @app.call(env) }
48
+
49
+ if status_counter = self.status_codes[status / 100]
50
+ status_counter.incr
51
+ end
52
+
53
+ [status, headers, body]
54
+ rescue Exception
55
+ # TODO: add "last_uncaught_exception" with string of error
56
+ self.uncaught_exceptions.incr
57
+ raise
58
+ end
59
+
60
+ def show?(env, test = self.options[:show])
61
+ case
62
+ when String === test; env['PATH_INFO'] == test
63
+ when Regexp === test; env['PATH_INFO'] =~ test
64
+ when test.respond_to?(:call); test.call(env)
65
+ else test
66
+ end
67
+ end
68
+
69
+ def show(_)
70
+ body = @agent.to_json
71
+
72
+ [ 200,
73
+ { 'Content-Type' => 'application/json',
74
+ 'Content-Length' => body.size.to_s },
75
+ [body]
76
+ ]
77
+ end
78
+
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,47 @@
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
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,91 @@
1
+ module Metrics
2
+ module Statistics
3
+ class ExponentialSample
4
+ def initialize(size = 1028, alpha = 0.015)
5
+ @values = Hash.new
6
+ @count = 0
7
+ @size = size
8
+ @alpha = alpha
9
+ @rescale_window = 3600 #seconds -- 1 hour
10
+ self.clear
11
+ end
12
+
13
+ def clear
14
+ @values = Hash.new
15
+ @start_time = tick
16
+ @next_scale_time = Time.now.to_f + @rescale_window
17
+ @count = 0
18
+ end
19
+
20
+ def size
21
+ [@values.keys.length, @count].min
22
+ end
23
+
24
+ def tick
25
+ Time.now.to_f
26
+ end
27
+
28
+ def update(value)
29
+ update_with_timestamp(value, tick)
30
+ end
31
+
32
+ def update_with_timestamp(value, timestamp)
33
+ priority = weight(timestamp.to_f - @start_time.to_f) / rand
34
+ @count += 1
35
+ newcount = @count
36
+ if (newcount <= @size)
37
+ @values[priority] = value
38
+ else
39
+ firstkey = @values.keys[0]
40
+ if (firstkey < priority)
41
+ @values[priority] = value
42
+
43
+ while(@values.delete(firstkey) == nil)
44
+ firstkey = @values.keys[0]
45
+ end
46
+ end
47
+ end
48
+
49
+ now = Time.now.to_f
50
+ next_scale_time = @next_scale_time
51
+
52
+ if (now >= next_scale_time)
53
+ self.rescale(now, next_scale_time)
54
+ end
55
+ end
56
+
57
+
58
+ def rescale(now, next_scale_time)
59
+ if @next_scale_time == next_scale_time
60
+ # writelock
61
+ @next_scale_time = now + @rescale_window
62
+ old_start_time = @start_time
63
+ @start_time = tick
64
+ time_delta = @start_time - old_start_time
65
+ keys = @values.keys
66
+ keys.each do |key|
67
+ value = @values.delete(key)
68
+ new_key = (key * Math.exp(-@alpha * time_delta))
69
+ @values[new_key] = value
70
+ end
71
+ # unlock
72
+ end
73
+ end
74
+
75
+ def weight(factor)
76
+ return @alpha.to_f * factor.to_f
77
+ end
78
+
79
+ def values
80
+ # read-lock?
81
+ result = Array.new
82
+ keys = @values.keys.sort
83
+ keys.each do |key|
84
+ result << @values[key]
85
+ end
86
+
87
+ result
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,37 @@
1
+ module Metrics
2
+ module Statistics
3
+ class UniformSample
4
+ def initialize(size = 1028)
5
+ @values = Array.new(size)
6
+ @count = 0
7
+ @size = size
8
+ self.clear
9
+ end
10
+
11
+ def clear
12
+ (0..@values.length-1).each do |i|
13
+ @values[i] = 0
14
+ end
15
+ @count = 0
16
+ end
17
+
18
+ def size
19
+ @values.length
20
+ end
21
+
22
+ def update(value)
23
+ if @count < @values.length
24
+ @values[@count] = value
25
+ @count += 1
26
+ else
27
+ index = rand(@size) % @count
28
+ @values[index] = value
29
+ end
30
+ end
31
+
32
+ def values
33
+ @values.dup
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,61 @@
1
+ module Metrics
2
+
3
+ class TimeUnit
4
+ def self.to_nsec(mult = 1)
5
+ raise NotImplementedError
6
+ end
7
+ end
8
+
9
+ class Nanoseconds < TimeUnit
10
+ def self.to_nsec(mult = 1)
11
+ mult
12
+ end
13
+ end
14
+
15
+ class Microseconds < TimeUnit
16
+ def self.to_nsec(mult = 1)
17
+ 1000 * mult
18
+ end
19
+ end
20
+
21
+ class Milliseconds < TimeUnit
22
+ def self.to_nsec(mult = 1)
23
+ 1000000 * mult
24
+ end
25
+ end
26
+
27
+ class Seconds < TimeUnit
28
+ def self.to_nsec(mult = 1)
29
+ 1000000000 * mult
30
+ end
31
+ end
32
+
33
+ class Minutes < TimeUnit
34
+ def self.to_nsec(mult = 1)
35
+ 60000000000 * mult
36
+ end
37
+ end
38
+
39
+ class Hours < TimeUnit
40
+ def self.to_nsec(mult = 1)
41
+ 3600000000000 * mult
42
+ end
43
+ end
44
+
45
+ module TimeConversion
46
+
47
+
48
+ def convert_to_ns(value, unit)
49
+ units = {
50
+ :nanoseconds => Nanoseconds,
51
+ :microseconds => Microseconds,
52
+ :milliseconds => Milliseconds,
53
+ :seconds => Seconds,
54
+ :minutes => Minutes,
55
+ :hours => Hours
56
+ }
57
+
58
+ units[unit].to_nsec * value
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,3 @@
1
+ module Metrics
2
+ VERSION = "0.8.6"
3
+ end
@@ -0,0 +1,27 @@
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 = "yam-ruby-metrics"
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"
12
+ s.summary = %q{Metrics for Ruby}
13
+ s.description = %q{A Ruby implementation of metrics inspired by @coda's JVM metrics for those of us in Ruby land}
14
+
15
+ s.rubyforge_project = "yam-ruby-metrics"
16
+
17
+ s.add_dependency "json"
18
+
19
+ s.add_development_dependency "rspec"
20
+ s.add_development_dependency "simplecov", [">= 0.3.8"] #, :require => false
21
+ s.add_development_dependency "rack-test"
22
+
23
+ s.files = `git ls-files`.split("\n")
24
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
26
+ s.require_paths = ["lib"]
27
+ end
@@ -0,0 +1,48 @@
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
13
+ Metrics::Instruments::Counter.stub!(:new).and_return @counter
14
+ @agent.counter(:test_counter).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
43
+ Metrics::Instruments::Meter.stub!(:new).and_return @meter
44
+
45
+ @agent.meter(:test_meter).should == @meter
46
+ end
47
+
48
+ end