ruby-metrics 0.5.0
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/.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
|