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
data/lib/ruby-metrics.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'ruby-metrics/logging'
|
2
|
+
|
3
|
+
require 'ruby-metrics/time_units'
|
4
|
+
|
5
|
+
require 'ruby-metrics/instruments/counter'
|
6
|
+
require 'ruby-metrics/instruments/meter'
|
7
|
+
require 'ruby-metrics/instruments/gauge'
|
8
|
+
require 'ruby-metrics/instruments/histogram'
|
9
|
+
require 'ruby-metrics/instruments/timer'
|
10
|
+
|
11
|
+
require 'ruby-metrics/integration'
|
12
|
+
|
13
|
+
require 'json'
|
14
|
+
|
15
|
+
module Metrics
|
16
|
+
class Agent
|
17
|
+
include Logging
|
18
|
+
|
19
|
+
attr_reader :instruments
|
20
|
+
|
21
|
+
def initialize(options = {})
|
22
|
+
@instruments = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
alias_method :registered, :instruments
|
26
|
+
|
27
|
+
def counter(name)
|
28
|
+
@instruments[name] ||= Instruments::Counter.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def meter(name)
|
32
|
+
@instruments[name] ||= Instruments::Meter.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def gauge(name, &block)
|
36
|
+
@instruments[name] ||= Instruments::Gauge.new(&block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def timer(name, options = {})
|
40
|
+
@instruments[name] ||= Instruments::Timer.new(options)
|
41
|
+
end
|
42
|
+
|
43
|
+
def uniform_histogram(name)
|
44
|
+
@instruments[name] ||= Instruments::UniformHistogram.new
|
45
|
+
end
|
46
|
+
|
47
|
+
# For backwards compatibility
|
48
|
+
alias_method :histogram, :uniform_histogram
|
49
|
+
|
50
|
+
def exponential_histogram(name)
|
51
|
+
@instruments[name] ||= Instruments::ExponentialHistogram.new
|
52
|
+
end
|
53
|
+
|
54
|
+
def as_json(*_)
|
55
|
+
@instruments
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_json(*_)
|
59
|
+
as_json.to_json
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Metrics
|
2
|
+
module Instruments
|
3
|
+
class Counter
|
4
|
+
def initialize
|
5
|
+
@value = 0
|
6
|
+
end
|
7
|
+
|
8
|
+
def inc(value = 1)
|
9
|
+
@value += value
|
10
|
+
end
|
11
|
+
alias_method :incr, :inc
|
12
|
+
|
13
|
+
def dec(value = 1)
|
14
|
+
@value -= value
|
15
|
+
end
|
16
|
+
alias_method :decr, :dec
|
17
|
+
|
18
|
+
def clear
|
19
|
+
@value = 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_i
|
23
|
+
@value.to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
@value.to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
def as_json(*_)
|
31
|
+
@value
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_json(*_)
|
35
|
+
as_json.to_json
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Metrics
|
2
|
+
module Instruments
|
3
|
+
class Gauge
|
4
|
+
def initialize(&block)
|
5
|
+
raise ArgumentError, "a block is required" unless block_given?
|
6
|
+
@block = block
|
7
|
+
end
|
8
|
+
|
9
|
+
def get
|
10
|
+
instance_exec(&@block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def as_json(*_)
|
14
|
+
value = get
|
15
|
+
value.respond_to?(:as_json) ? value.as_json : value
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_json(*_)
|
19
|
+
as_json.to_json
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'ruby-metrics/statistics/uniform_sample'
|
2
|
+
require 'ruby-metrics/statistics/exponential_sample'
|
3
|
+
|
4
|
+
module Metrics
|
5
|
+
module Instruments
|
6
|
+
class Histogram
|
7
|
+
|
8
|
+
def initialize(type = :uniform)
|
9
|
+
@count = 0
|
10
|
+
case type
|
11
|
+
when :uniform
|
12
|
+
@sample = Metrics::Statistics::UniformSample.new
|
13
|
+
when :exponential
|
14
|
+
@sample = Metrics::Statistics::ExponentialSample.new
|
15
|
+
end
|
16
|
+
@min = nil
|
17
|
+
@max = nil
|
18
|
+
@sum = 0
|
19
|
+
@variance_s = 0
|
20
|
+
@variance_m = -1
|
21
|
+
end
|
22
|
+
|
23
|
+
def update(value)
|
24
|
+
@count += 1
|
25
|
+
@sum += value
|
26
|
+
@sample.update(value)
|
27
|
+
update_max(value)
|
28
|
+
update_min(value)
|
29
|
+
update_variance(value);
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear
|
33
|
+
@sample.clear
|
34
|
+
@min = nil
|
35
|
+
@max = nil
|
36
|
+
@sum = 0
|
37
|
+
@count = 0
|
38
|
+
@variance_m = -1
|
39
|
+
@variance_s = 0
|
40
|
+
end
|
41
|
+
|
42
|
+
def quantiles(percentiles)
|
43
|
+
# Calculated using the same logic as R and Excel use
|
44
|
+
# as outlined by the NIST here: http://www.itl.nist.gov/div898/handbook/prc/section2/prc252.htm
|
45
|
+
count = @count
|
46
|
+
scores = {}
|
47
|
+
values = @sample.values[0..count-1]
|
48
|
+
|
49
|
+
percentiles.each do |pct|
|
50
|
+
scores[pct] = 0.0
|
51
|
+
end
|
52
|
+
|
53
|
+
if count > 0
|
54
|
+
values.sort!
|
55
|
+
percentiles.each do |pct|
|
56
|
+
idx = pct * (values.length - 1) + 1.0
|
57
|
+
if idx <= 1
|
58
|
+
scores[pct] = values[0]
|
59
|
+
elsif idx >= values.length
|
60
|
+
scores[pct] = values[values.length-1]
|
61
|
+
else
|
62
|
+
lower = values[idx.to_i - 1]
|
63
|
+
upper = values[idx.to_i]
|
64
|
+
scores[pct] = lower + (idx - idx.floor) * (upper - lower)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
return scores
|
70
|
+
end
|
71
|
+
|
72
|
+
def update_min(value)
|
73
|
+
if (@min == nil || value < @min)
|
74
|
+
@min = value
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def update_max(value)
|
79
|
+
if (@max == nil || value > @max)
|
80
|
+
@max = value
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def update_variance(value)
|
85
|
+
count = @count
|
86
|
+
old_m = @variance_m
|
87
|
+
new_m = @variance_m + ((value - old_m) / count)
|
88
|
+
new_s = @variance_s + ((value - old_m) * (value - new_m))
|
89
|
+
|
90
|
+
@variance_m = new_m
|
91
|
+
@variance_s = new_s
|
92
|
+
end
|
93
|
+
|
94
|
+
def variance
|
95
|
+
count = @count
|
96
|
+
variance_s = @variance_s
|
97
|
+
|
98
|
+
if count <= 1
|
99
|
+
return 0.0
|
100
|
+
else
|
101
|
+
return variance_s.to_f / (count - 1).to_i
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def count
|
106
|
+
count = @count
|
107
|
+
return count
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
def max
|
112
|
+
max = @max
|
113
|
+
if max != nil
|
114
|
+
return max
|
115
|
+
else
|
116
|
+
return 0.0
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def min
|
121
|
+
min = @min
|
122
|
+
if min != nil
|
123
|
+
return min
|
124
|
+
else
|
125
|
+
return 0.0
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def mean
|
130
|
+
count = @count
|
131
|
+
sum = @sum
|
132
|
+
|
133
|
+
if count > 0
|
134
|
+
return sum / count
|
135
|
+
else
|
136
|
+
return 0.0
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def std_dev
|
141
|
+
count = @count
|
142
|
+
variance = self.variance()
|
143
|
+
|
144
|
+
if count > 0
|
145
|
+
return Math.sqrt(variance)
|
146
|
+
else
|
147
|
+
return 0.0
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def values
|
152
|
+
@sample.values
|
153
|
+
end
|
154
|
+
|
155
|
+
def as_json(*_)
|
156
|
+
percentiles = self.quantiles([0.5, 0.75, 0.95, 0.98, 0.99, 0.999])
|
157
|
+
{
|
158
|
+
:min => self.min,
|
159
|
+
:max => self.max,
|
160
|
+
:mean => self.mean,
|
161
|
+
:median => percentiles['0.5'],
|
162
|
+
:std_dev => self.std_dev,
|
163
|
+
:p75 => percentiles['0.75'],
|
164
|
+
:p95 => percentiles['0.95'],
|
165
|
+
:p98 => percentiles['0.98'],
|
166
|
+
:p99 => percentiles['0.99'],
|
167
|
+
:p999 => percentiles['0.999']
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
def to_json(*_)
|
172
|
+
as_json.to_json
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
class ExponentialHistogram < Histogram
|
177
|
+
def initialize
|
178
|
+
super(:exponential)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class UniformHistogram < Histogram
|
183
|
+
def initialize
|
184
|
+
super(:uniform)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'ruby-metrics/time_units'
|
2
|
+
|
3
|
+
module Metrics
|
4
|
+
module Instruments
|
5
|
+
class Meter
|
6
|
+
include Metrics::TimeConversion
|
7
|
+
|
8
|
+
# From http://www.teamquest.com/pdfs/whitepaper/ldavg2.pdf
|
9
|
+
INTERVAL = 5.0
|
10
|
+
INTERVAL_IN_NS = 5000000000.0
|
11
|
+
ONE_MINUTE_FACTOR = 1 - Math.exp(-INTERVAL / 60.0)
|
12
|
+
FIVE_MINUTE_FACTOR = 1 - Math.exp(-INTERVAL / (60.0 * 5.0))
|
13
|
+
FIFTEEN_MINUTE_FACTOR = 1 - Math.exp(-INTERVAL / (60.0 * 15.0))
|
14
|
+
|
15
|
+
attr_reader :count
|
16
|
+
alias_method :counted, :count
|
17
|
+
|
18
|
+
def initialize(options = {})
|
19
|
+
@one_minute_rate = @five_minute_rate = @fifteen_minute_rate = 0.0
|
20
|
+
@count = 0
|
21
|
+
@initialized = false
|
22
|
+
@start_time = Time.now.to_f
|
23
|
+
|
24
|
+
@timer_thread = Thread.new do
|
25
|
+
begin
|
26
|
+
loop do
|
27
|
+
self.tick
|
28
|
+
sleep(INTERVAL)
|
29
|
+
end
|
30
|
+
rescue Exception => e
|
31
|
+
logger.error "Error in timer thread: #{e.class.name}: #{e}\n #{e.backtrace.join("\n ")}"
|
32
|
+
end # begin
|
33
|
+
end # thread new
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
def clear
|
38
|
+
end
|
39
|
+
|
40
|
+
def mark(count = 1)
|
41
|
+
@count += count
|
42
|
+
end
|
43
|
+
|
44
|
+
def calc_rate(rate, factor, count)
|
45
|
+
rate = rate.to_f + (factor.to_f * (count.to_f - rate.to_f))
|
46
|
+
rate.to_f
|
47
|
+
end
|
48
|
+
|
49
|
+
def tick
|
50
|
+
count = @count.to_f / Seconds.to_nsec(INTERVAL).to_f
|
51
|
+
|
52
|
+
if (@initialized)
|
53
|
+
@one_minute_rate = calc_rate(@one_minute_rate, ONE_MINUTE_FACTOR, count)
|
54
|
+
@five_minute_rate = calc_rate(@five_minute_rate, FIVE_MINUTE_FACTOR, count)
|
55
|
+
@fifteen_minute_rate = calc_rate(@fifteen_minute_rate, FIFTEEN_MINUTE_FACTOR, count)
|
56
|
+
else
|
57
|
+
@one_minute_rate = @five_minute_rate = @fifteen_minute_rate = (count)
|
58
|
+
@initialized = true
|
59
|
+
end
|
60
|
+
|
61
|
+
@count = 0
|
62
|
+
end
|
63
|
+
|
64
|
+
def one_minute_rate(rate_unit = :seconds)
|
65
|
+
convert_to_ns @one_minute_rate, rate_unit
|
66
|
+
end
|
67
|
+
|
68
|
+
def five_minute_rate(rate_unit = :seconds)
|
69
|
+
convert_to_ns @five_minute_rate, rate_unit
|
70
|
+
end
|
71
|
+
|
72
|
+
def fifteen_minute_rate(rate_unit = :seconds)
|
73
|
+
convert_to_ns @fifteen_minute_rate, rate_unit
|
74
|
+
end
|
75
|
+
|
76
|
+
def mean_rate(rate_unit = :seconds)
|
77
|
+
count = @count
|
78
|
+
if count == 0
|
79
|
+
return 0.0;
|
80
|
+
else
|
81
|
+
elapsed = Time.now.to_f - @start_time.to_f
|
82
|
+
convert_to_ns (count.to_f / elapsed.to_f), rate_unit
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def as_json(*_)
|
87
|
+
{
|
88
|
+
:one_minute_rate => self.one_minute_rate,
|
89
|
+
:five_minute_rate => self.five_minute_rate,
|
90
|
+
:fifteen_minute_rate => self.fifteen_minute_rate
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_json(*_)
|
95
|
+
as_json.to_json
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'time_units')
|
2
|
+
|
3
|
+
module Metrics
|
4
|
+
module Instruments
|
5
|
+
class Timer
|
6
|
+
include Metrics::TimeConversion
|
7
|
+
|
8
|
+
attr_reader :duration_unit, :rate_unit
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@meter = Meter.new
|
12
|
+
@histogram = ExponentialHistogram.new
|
13
|
+
|
14
|
+
@duration_unit = options[:duration_unit] || :seconds
|
15
|
+
@rate_unit = options[:rate_unit] || :seconds
|
16
|
+
|
17
|
+
clear
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear
|
21
|
+
@histogram.clear
|
22
|
+
end
|
23
|
+
|
24
|
+
def update(duration, unit)
|
25
|
+
mult = convert_to_ns(1, unit)
|
26
|
+
self.update_timer(duration * mult)
|
27
|
+
end
|
28
|
+
|
29
|
+
def time(&block)
|
30
|
+
start_time = Time.now.to_f
|
31
|
+
result = block.call
|
32
|
+
time_diff = Time.now.to_f - start_time
|
33
|
+
time_in_ns = convert_to_ns time_diff, :seconds
|
34
|
+
update_timer(time_in_ns)
|
35
|
+
result
|
36
|
+
end
|
37
|
+
|
38
|
+
def count
|
39
|
+
@histogram.count
|
40
|
+
end
|
41
|
+
|
42
|
+
def fifteen_minute_rate
|
43
|
+
@meter.fifteen_minute_rate(@rate_unit)
|
44
|
+
end
|
45
|
+
|
46
|
+
def five_minute_rate
|
47
|
+
@meter.five_minute_rate(@rate_unit)
|
48
|
+
end
|
49
|
+
|
50
|
+
def one_minute_rate
|
51
|
+
@meter.one_minute_rate(@rate_unit)
|
52
|
+
end
|
53
|
+
|
54
|
+
def mean_rate
|
55
|
+
@meter.mean_rate(@rate_unit)
|
56
|
+
end
|
57
|
+
|
58
|
+
def max
|
59
|
+
scale_duration_to_ns @histogram.max, @duration_unit
|
60
|
+
end
|
61
|
+
|
62
|
+
def min
|
63
|
+
scale_duration_to_ns @histogram.min, @duration_unit
|
64
|
+
end
|
65
|
+
|
66
|
+
def mean
|
67
|
+
scale_duration_to_ns @histogram.mean, @duration_unit
|
68
|
+
end
|
69
|
+
|
70
|
+
def std_dev
|
71
|
+
scale_duration_to_ns @histogram.std_dev, @duration_unit
|
72
|
+
end
|
73
|
+
|
74
|
+
def quantiles(percentiles = [0.99,0.97,0.95,0.75,0.5,0.25])
|
75
|
+
result = {}
|
76
|
+
|
77
|
+
@histogram.quantiles(percentiles).each do |k,v|
|
78
|
+
result[k] = scale_duration_to_ns v, @duration_unit
|
79
|
+
end
|
80
|
+
|
81
|
+
result
|
82
|
+
end
|
83
|
+
|
84
|
+
def values
|
85
|
+
result = []
|
86
|
+
|
87
|
+
@histogram.values.each do |value|
|
88
|
+
result << (scale_duration_to_ns value, @duration_unit)
|
89
|
+
end
|
90
|
+
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
def update_timer(duration)
|
95
|
+
if duration >= 0
|
96
|
+
@histogram.update(duration)
|
97
|
+
@meter.mark
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def as_json(*_)
|
102
|
+
percentiles = self.quantiles([0.5, 0.75, 0.95, 0.98, 0.99, 0.999])
|
103
|
+
{
|
104
|
+
:rates => {
|
105
|
+
:count => count,
|
106
|
+
:m1 => one_minute_rate,
|
107
|
+
:m5 => five_minute_rate,
|
108
|
+
:m15 => fifteen_minute_rate,
|
109
|
+
:mean => mean_rate,
|
110
|
+
:unit => @rate_unit
|
111
|
+
},
|
112
|
+
:duration => {
|
113
|
+
:min => self.min,
|
114
|
+
:max => self.max,
|
115
|
+
:mean => self.mean,
|
116
|
+
:median => percentiles['0.5'],
|
117
|
+
:std_dev => self.std_dev,
|
118
|
+
:p75 => percentiles['0.75'],
|
119
|
+
:p95 => percentiles['0.95'],
|
120
|
+
:p98 => percentiles['0.98'],
|
121
|
+
:p99 => percentiles['0.99'],
|
122
|
+
:p999 => percentiles['0.999'],
|
123
|
+
:unit => @duration_unit
|
124
|
+
}
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_json(*_)
|
129
|
+
as_json.to_json
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
def scale_duration_to_ns(value, unit)
|
134
|
+
value.to_f / convert_to_ns(1, unit).to_f
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|