ruby-metrics 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +32 -0
- data/LICENSE +21 -0
- data/README.md +44 -0
- data/Rakefile +19 -0
- data/autotest/discover.rb +3 -0
- data/examples/counter.rb +11 -0
- data/examples/gauge.rb +26 -0
- data/examples/histogram.rb +28 -0
- data/examples/meter.rb +25 -0
- data/lib/ruby-metrics.rb +15 -0
- data/lib/ruby-metrics/agent.rb +55 -0
- data/lib/ruby-metrics/instruments.rb +64 -0
- data/lib/ruby-metrics/instruments/base.rb +19 -0
- data/lib/ruby-metrics/instruments/counter.rb +33 -0
- data/lib/ruby-metrics/instruments/gauge.rb +20 -0
- data/lib/ruby-metrics/instruments/histogram.rb +155 -0
- data/lib/ruby-metrics/instruments/meter.rb +99 -0
- data/lib/ruby-metrics/logging.rb +21 -0
- data/lib/ruby-metrics/statistics/sample.rb +21 -0
- data/lib/ruby-metrics/statistics/uniform_sample.rb +38 -0
- data/lib/ruby-metrics/version.rb +3 -0
- data/ruby-metrics.gemspec +27 -0
- data/spec/agent_spec.rb +42 -0
- data/spec/instruments/base_spec.rb +16 -0
- data/spec/instruments/counter_spec.rb +72 -0
- data/spec/instruments/gauge_spec.rb +44 -0
- data/spec/instruments/histogram_spec.rb +89 -0
- data/spec/instruments/meter_spec.rb +80 -0
- data/spec/instruments_spec.rb +47 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/statistics/sample_spec.rb +22 -0
- data/spec/statistics/uniform_sample_spec.rb +59 -0
- metadata +163 -0
@@ -0,0 +1,155 @@
|
|
1
|
+
module Metrics
|
2
|
+
module Instruments
|
3
|
+
class Histogram < Base
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@count = 0
|
7
|
+
@sample = Metrics::Statistics::UniformSample.new
|
8
|
+
@min = nil
|
9
|
+
@max = nil
|
10
|
+
@sum = 0
|
11
|
+
@variance_s = 0
|
12
|
+
@variance_m = -1
|
13
|
+
end
|
14
|
+
|
15
|
+
def update(value)
|
16
|
+
@count += 1
|
17
|
+
@sum += value
|
18
|
+
@sample.update(value)
|
19
|
+
update_max(value)
|
20
|
+
update_min(value)
|
21
|
+
update_variance(value);
|
22
|
+
end
|
23
|
+
|
24
|
+
def clear
|
25
|
+
@sample.clear
|
26
|
+
@min = nil
|
27
|
+
@max = nil
|
28
|
+
@sum = 0
|
29
|
+
@count = 0
|
30
|
+
@variance_m = -1
|
31
|
+
@variance_s = 0
|
32
|
+
end
|
33
|
+
|
34
|
+
def quantiles(percentiles)
|
35
|
+
# Calculated using the same logic as R and Ecxel use
|
36
|
+
# as outlined by the NIST here: http://www.itl.nist.gov/div898/handbook/prc/section2/prc252.htm
|
37
|
+
count = @count
|
38
|
+
scores = {}
|
39
|
+
values = @sample.values[0..count-1]
|
40
|
+
|
41
|
+
percentiles.each do |pct|
|
42
|
+
scores[pct] = 0.0
|
43
|
+
end
|
44
|
+
|
45
|
+
if count > 0
|
46
|
+
values.sort!
|
47
|
+
percentiles.each do |pct|
|
48
|
+
idx = pct * (values.length - 1) + 1.0
|
49
|
+
if idx <= 1
|
50
|
+
scores[pct] = values[0]
|
51
|
+
elsif idx >= values.length
|
52
|
+
scores[pct] = values[values.length-1]
|
53
|
+
else
|
54
|
+
lower = values[idx.to_i - 1]
|
55
|
+
upper = values[idx.to_i]
|
56
|
+
scores[pct] = lower + (idx - idx.floor) * (upper - lower)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
return scores
|
62
|
+
end
|
63
|
+
|
64
|
+
def update_min(value)
|
65
|
+
if (@min == nil || value < @min)
|
66
|
+
@min = value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def update_max(value)
|
71
|
+
if (@max == nil || value > @max)
|
72
|
+
@max = value
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def update_variance(value)
|
77
|
+
count = @count
|
78
|
+
old_m = @variance_m
|
79
|
+
new_m = @variance_m + ((value - old_m) / count)
|
80
|
+
new_s = @variance_s + ((value - old_m) * (value - new_m))
|
81
|
+
|
82
|
+
@variance_m = new_m
|
83
|
+
@variance_s = new_s
|
84
|
+
end
|
85
|
+
|
86
|
+
def variance
|
87
|
+
count = @count
|
88
|
+
variance_s = @variance_s
|
89
|
+
|
90
|
+
if count <= 1
|
91
|
+
return 0.0
|
92
|
+
else
|
93
|
+
return variance_s.to_f / (count - 1).to_i
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def count
|
98
|
+
count = @count
|
99
|
+
return count
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
def max
|
104
|
+
max = @max
|
105
|
+
if max != nil
|
106
|
+
return max
|
107
|
+
else
|
108
|
+
return 0.0
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def min
|
113
|
+
min = @min
|
114
|
+
if min != nil
|
115
|
+
return min
|
116
|
+
else
|
117
|
+
return 0.0
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def mean
|
122
|
+
count = @count
|
123
|
+
sum = @sum
|
124
|
+
|
125
|
+
if count > 0
|
126
|
+
return sum / count
|
127
|
+
else
|
128
|
+
return 0.0
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def std_dev
|
133
|
+
count = @count
|
134
|
+
variance = self.variance()
|
135
|
+
|
136
|
+
if count > 0
|
137
|
+
return Math.sqrt(variance)
|
138
|
+
else
|
139
|
+
return 0.0
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def to_s
|
144
|
+
{
|
145
|
+
:min => self.min,
|
146
|
+
:max => self.max,
|
147
|
+
:mean => self.mean,
|
148
|
+
:variance => self.variance,
|
149
|
+
:percentiles => self.quantiles([0.25, 0.50, 0.75, 0.95, 0.97, 0.98, 0.99])
|
150
|
+
}.to_json
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'ruby-units'
|
2
|
+
|
3
|
+
module Metrics
|
4
|
+
module Instruments
|
5
|
+
class Meter < Base
|
6
|
+
# From http://www.teamquest.com/pdfs/whitepaper/ldavg2.pdf
|
7
|
+
INTERVAL = 5.0
|
8
|
+
INTERVAL_IN_NS = 5000000000.0
|
9
|
+
ONE_MINUTE_FACTOR = 1 - Math.exp(-INTERVAL / 60.0)
|
10
|
+
FIVE_MINUTE_FACTOR = 1 - Math.exp(-INTERVAL / (60.0 * 5.0))
|
11
|
+
FIFTEEN_MINUTE_FACTOR = 1 - Math.exp(-INTERVAL / (60.0 * 15.0))
|
12
|
+
|
13
|
+
attr_reader :counted, :uncounted
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
@one_minute_rate = @five_minute_rate = @fifteen_minute_rate = 0.0
|
17
|
+
@counted = @uncounted = 0
|
18
|
+
@initialized = false
|
19
|
+
|
20
|
+
if options[:interval]
|
21
|
+
interval = options[:interval]
|
22
|
+
else
|
23
|
+
interval = "5 seconds"
|
24
|
+
end
|
25
|
+
|
26
|
+
if options[:rateunit]
|
27
|
+
@rateunit = options[:rateunit]
|
28
|
+
else
|
29
|
+
@rateunit = interval
|
30
|
+
end
|
31
|
+
|
32
|
+
# HACK: this is here because ruby-units thinks 1s in ns is 999,999,999.9999999 not 1bn
|
33
|
+
# TODO: either fix ruby-units, or remove it?
|
34
|
+
@ratemultiplier = @rateunit.to("nanoseconds").scalar.ceil
|
35
|
+
|
36
|
+
@timer_thread = Thread.new do
|
37
|
+
sleep_time = interval.to("seconds").scalar
|
38
|
+
begin
|
39
|
+
loop do
|
40
|
+
self.tick
|
41
|
+
sleep(sleep_time)
|
42
|
+
end
|
43
|
+
rescue Exception => e
|
44
|
+
logger.error "Error in timer thread: #{e.class.name}: #{e}\n #{e.backtrace.join("\n ")}"
|
45
|
+
end # begin
|
46
|
+
end # thread new
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
def clear
|
51
|
+
end
|
52
|
+
|
53
|
+
def mark(count = 1)
|
54
|
+
@uncounted += count
|
55
|
+
@counted += count
|
56
|
+
end
|
57
|
+
|
58
|
+
def calc_rate(rate, factor, count)
|
59
|
+
rate = rate + (factor * (count - rate))
|
60
|
+
rate
|
61
|
+
end
|
62
|
+
|
63
|
+
def tick
|
64
|
+
count = @uncounted.to_f / INTERVAL_IN_NS.to_f
|
65
|
+
|
66
|
+
if (@initialized)
|
67
|
+
@one_minute_rate = calc_rate(@one_minute_rate, ONE_MINUTE_FACTOR, count)
|
68
|
+
@five_minute_rate = calc_rate(@five_minute_rate, FIVE_MINUTE_FACTOR, count)
|
69
|
+
@fifteen_minute_rate = calc_rate(@fifteen_minute_rate, FIFTEEN_MINUTE_FACTOR, count)
|
70
|
+
else
|
71
|
+
@one_minute_rate = @five_minute_rate = @fifteen_minute_rate = (count)
|
72
|
+
@initialized = true
|
73
|
+
end
|
74
|
+
|
75
|
+
@uncounted = 0
|
76
|
+
end
|
77
|
+
|
78
|
+
def one_minute_rate
|
79
|
+
@one_minute_rate * @ratemultiplier
|
80
|
+
end
|
81
|
+
|
82
|
+
def five_minute_rate
|
83
|
+
@five_minute_rate * @ratemultiplier
|
84
|
+
end
|
85
|
+
|
86
|
+
def fifteen_minute_rate
|
87
|
+
@fifteen_minute_rate * @ratemultiplier
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_s
|
91
|
+
{
|
92
|
+
:one_minute_rate => self.one_minute_rate,
|
93
|
+
:five_minute_rate => self.five_minute_rate,
|
94
|
+
:fifteen_minute_rate => self.fifteen_minute_rate
|
95
|
+
}.to_json
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Metrics
|
4
|
+
module Logging
|
5
|
+
|
6
|
+
def self.included(target)
|
7
|
+
target.extend ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
def logger
|
11
|
+
self.class.logger
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def logger
|
16
|
+
Metrics.logger
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Metrics
|
2
|
+
module Statistics
|
3
|
+
class Sample
|
4
|
+
def clear
|
5
|
+
raise NotImplementedError
|
6
|
+
end
|
7
|
+
|
8
|
+
def size
|
9
|
+
raise NotImplementedError
|
10
|
+
end
|
11
|
+
|
12
|
+
def update(value)
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
def values
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Metrics
|
2
|
+
module Statistics
|
3
|
+
class UniformSample < Sample
|
4
|
+
|
5
|
+
def initialize(size = 1028)
|
6
|
+
@values = Array.new(size)
|
7
|
+
@count = 0
|
8
|
+
@size = size
|
9
|
+
self.clear
|
10
|
+
end
|
11
|
+
|
12
|
+
def clear
|
13
|
+
(0..@values.length-1).each do |i|
|
14
|
+
@values[i] = 0
|
15
|
+
end
|
16
|
+
@count = 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def size
|
20
|
+
@values.length
|
21
|
+
end
|
22
|
+
|
23
|
+
def update(value)
|
24
|
+
if @count < @values.length
|
25
|
+
@values[@count] = value
|
26
|
+
@count += 1
|
27
|
+
else
|
28
|
+
index = rand(@size) % @count
|
29
|
+
@values[index] = value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def values
|
34
|
+
@values.dup
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
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 = "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 = "ruby-metrics"
|
16
|
+
|
17
|
+
s.add_dependency "json"
|
18
|
+
s.add_dependency "ruby-units"
|
19
|
+
|
20
|
+
s.add_development_dependency "rspec"
|
21
|
+
s.add_development_dependency "simplecov", [">= 0.3.8"] #, :require => false
|
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,42 @@
|
|
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 meter instrument correctly" do
|
24
|
+
@meter = Metrics::Instruments::Meter.new
|
25
|
+
Metrics::Instruments::Meter.stub!(:new).and_return @meter
|
26
|
+
|
27
|
+
@agent.meter(:test_meter).should == @meter
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should start the WEBrick daemon" do
|
31
|
+
Thread.stub!(:new).and_return do |block|
|
32
|
+
block.call
|
33
|
+
end
|
34
|
+
|
35
|
+
mock_server = mock(WEBrick::HTTPServer)
|
36
|
+
WEBrick::HTTPServer.should_receive(:new).and_return mock_server
|
37
|
+
mock_server.should_receive(:mount)
|
38
|
+
mock_server.should_receive(:start)
|
39
|
+
@agent.start
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Metrics::Instruments::Base do
|
4
|
+
before(:each) do
|
5
|
+
@base = Metrics::Instruments::Base.new
|
6
|
+
end
|
7
|
+
|
8
|
+
%w( to_i to_f to_s ).each do |method|
|
9
|
+
it "should raise a NotImplementedError for ##{method}" do
|
10
|
+
lambda do
|
11
|
+
@base.send(method)
|
12
|
+
end.should raise_exception(NotImplementedError)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Metrics::Instruments::Counter do
|
4
|
+
before(:each) do
|
5
|
+
@counter = Metrics::Instruments::Counter.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should create a new entity with zero as its value" do
|
9
|
+
@counter.to_i.should == 0
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should increment its counter by the value specified" do
|
13
|
+
value = 1
|
14
|
+
lambda do
|
15
|
+
@counter.inc(value)
|
16
|
+
end.should change{ @counter.to_i }.by(value)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should increment its counter by one by default" do
|
20
|
+
lambda do
|
21
|
+
@counter.inc
|
22
|
+
end.should change{ @counter.to_i }.by(1)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should decrement its counter by the value specified" do
|
26
|
+
value = 1
|
27
|
+
lambda do
|
28
|
+
@counter.dec(value)
|
29
|
+
end.should change{ @counter.to_i }.by(-value)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should decrement its counter by one by default" do
|
33
|
+
lambda do
|
34
|
+
@counter.dec
|
35
|
+
end.should change{ @counter.to_i }.by(-1)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should alias #incr to #inc" do
|
39
|
+
lambda do
|
40
|
+
@counter.incr
|
41
|
+
end.should change{ @counter.to_i }.by(1)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should alias #decr to #dec" do
|
45
|
+
lambda do
|
46
|
+
@counter.decr
|
47
|
+
end.should change{ @counter.to_i }.by(-1)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should clear the counter correctly" do
|
51
|
+
@counter.clear
|
52
|
+
@counter.to_i.should == 0
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should correctly represent the value as a string" do
|
56
|
+
@counter.clear
|
57
|
+
@counter.to_i.should == 0
|
58
|
+
@counter.to_s.should == "0"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should return the new count when incrementing" do
|
62
|
+
count = @counter.to_i
|
63
|
+
@counter.inc(value = 1).should == count + value
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should return the new count when decrementing" do
|
67
|
+
lambda do
|
68
|
+
@counter.dec(1)
|
69
|
+
end.should change{ @counter.to_i }.by(-1)
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|