yam-ruby-metrics 0.8.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.bundle/config +2 -0
- data/.gitignore +4 -0
- data/.rvmrc +1 -0
- data/CHANGELOG.md +48 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +36 -0
- data/LICENSE +21 -0
- data/README.md +86 -0
- data/Rakefile +19 -0
- data/autotest/discover.rb +3 -0
- data/examples/counter.rb +10 -0
- data/examples/gauge.rb +25 -0
- data/examples/histogram.rb +32 -0
- data/examples/integration/rack_endpoint.ru +24 -0
- data/examples/integration/rack_middleware.ru +21 -0
- data/examples/integration/webrick.rb +17 -0
- data/examples/meter.rb +25 -0
- data/examples/timer.rb +31 -0
- data/lib/ruby-metrics.rb +15 -0
- data/lib/ruby-metrics/agent.rb +62 -0
- data/lib/ruby-metrics/instruments/counter.rb +39 -0
- data/lib/ruby-metrics/instruments/gauge.rb +23 -0
- data/lib/ruby-metrics/instruments/histogram.rb +188 -0
- data/lib/ruby-metrics/instruments/meter.rb +99 -0
- data/lib/ruby-metrics/instruments/timer.rb +138 -0
- data/lib/ruby-metrics/integration.rb +11 -0
- data/lib/ruby-metrics/integration/rack_endpoint.rb +33 -0
- data/lib/ruby-metrics/integration/rack_middleware.rb +82 -0
- data/lib/ruby-metrics/integration/webrick.rb +47 -0
- data/lib/ruby-metrics/logging.rb +19 -0
- data/lib/ruby-metrics/statistics/exponential_sample.rb +91 -0
- data/lib/ruby-metrics/statistics/uniform_sample.rb +37 -0
- data/lib/ruby-metrics/time_units.rb +61 -0
- data/lib/ruby-metrics/version.rb +3 -0
- data/ruby-metrics.gemspec +27 -0
- data/spec/agent_spec.rb +48 -0
- data/spec/instruments/counter_spec.rb +79 -0
- data/spec/instruments/gauge_spec.rb +42 -0
- data/spec/instruments/histogram_spec.rb +115 -0
- data/spec/instruments/meter_spec.rb +99 -0
- data/spec/instruments/timer_spec.rb +146 -0
- data/spec/integration/rack_endpoint_spec.rb +60 -0
- data/spec/integration/rack_middleware_spec.rb +129 -0
- data/spec/integration/webrick_spec.rb +18 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/statistics/exponential_sample_spec.rb +138 -0
- data/spec/statistics/uniform_sample_spec.rb +59 -0
- data/spec/time_units_spec.rb +13 -0
- 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,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,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
|
data/spec/agent_spec.rb
ADDED
@@ -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
|