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