yam-ruby-metrics 0.8.6

Sign up to get free protection for your applications and to get access to all the features.
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