ruby-metrics 0.9.3 → 0.9.4
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.
- checksums.yaml +7 -0
- data/Rakefile +1 -1
- data/lib/ruby-metrics/instruments/histogram.rb +50 -81
- data/lib/ruby-metrics/instruments/instrument.rb +2 -3
- data/lib/ruby-metrics/instruments/meter.rb +26 -30
- data/lib/ruby-metrics/instruments/timer.rb +13 -18
- data/lib/ruby-metrics/integration/rack_endpoint.rb +1 -1
- data/lib/ruby-metrics/integration/rack_middleware.rb +31 -33
- data/lib/ruby-metrics/reporter.rb +1 -1
- data/lib/ruby-metrics/reporters/ganglia.rb +43 -42
- data/lib/ruby-metrics/reporters/librato.rb +34 -32
- data/lib/ruby-metrics/statistics/exponential_sample.rb +36 -46
- data/lib/ruby-metrics/statistics/uniform_sample.rb +8 -9
- data/lib/ruby-metrics/time_units.rb +9 -15
- data/lib/ruby-metrics/version.rb +1 -1
- data/ruby-metrics.gemspec +1 -1
- data/spec/instruments/histogram_spec.rb +1 -1
- data/spec/instruments/timer_spec.rb +2 -2
- data/spec/integration/rack_middleware_spec.rb +6 -6
- metadata +48 -47
- data/spec/time_units_spec.rb +0 -13
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '04971d78af7ca682008c7d66db2eac34111c089bb17778cc18ff434843707126'
|
4
|
+
data.tar.gz: e1ec094ef4a2082d12df838b211fac9cebaf207bf4fabb33fb3b30eaef1f97ff
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 60b859a4037eb7f22d2bbaa0965cc1ed55845817a613701a9a3f19bba088c6886feb68aad020e421506d330fb87eafa612b0c364773d850dbcd92d9e95798e56
|
7
|
+
data.tar.gz: 1b2f48bee677fadfffb53335058aa036985f64f9ce2f6f7901bae3fe8b996dc26c36b83d0303d1ea93065e5eef6cf298ff8c4328c8c13df0182592b694c2d02a
|
data/Rakefile
CHANGED
@@ -15,7 +15,7 @@ task :build do
|
|
15
15
|
FileUtils.mv(Dir['*.gem'], 'pkg')
|
16
16
|
end
|
17
17
|
|
18
|
-
desc
|
18
|
+
desc 'Tags version, pushes to remote, and pushes gem'
|
19
19
|
task :release => :build do
|
20
20
|
puts "Releasing v#{Metrics::VERSION}"
|
21
21
|
sh 'git', 'tag', '-m', "Version #{Metrics::VERSION}", "v#{Metrics::VERSION}"
|
@@ -5,20 +5,20 @@ require 'ruby-metrics/statistics/exponential_sample'
|
|
5
5
|
module Metrics
|
6
6
|
module Instruments
|
7
7
|
class Histogram < Instrument
|
8
|
+
attr_reader :count
|
8
9
|
|
9
10
|
def initialize(type = :uniform)
|
10
|
-
@
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
@variance_m = -1
|
11
|
+
@sample =
|
12
|
+
case type
|
13
|
+
when :uniform
|
14
|
+
Metrics::Statistics::UniformSample.new
|
15
|
+
when :exponential
|
16
|
+
Metrics::Statistics::ExponentialSample.new
|
17
|
+
else
|
18
|
+
raise ArgumentError, "Unknown type #{type.inspect}"
|
19
|
+
end
|
20
|
+
|
21
|
+
clear
|
22
22
|
end
|
23
23
|
|
24
24
|
def update(value)
|
@@ -27,7 +27,7 @@ module Metrics
|
|
27
27
|
@sample.update(value)
|
28
28
|
update_max(value)
|
29
29
|
update_min(value)
|
30
|
-
update_variance(value)
|
30
|
+
update_variance(value)
|
31
31
|
end
|
32
32
|
|
33
33
|
def clear
|
@@ -41,111 +41,80 @@ module Metrics
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def quantiles(percentiles)
|
44
|
-
# Calculated using the same logic as R and
|
44
|
+
# Calculated using the same logic as R and Excel use
|
45
45
|
# as outlined by the NIST here: http://www.itl.nist.gov/div898/handbook/prc/section2/prc252.htm
|
46
|
-
count = @count
|
47
|
-
scores = {}
|
48
|
-
values = @sample.values[0..count-1]
|
49
46
|
|
47
|
+
sorted_values = @sample.values[0...@count].sort
|
48
|
+
scores = { }
|
50
49
|
percentiles.each do |pct|
|
51
|
-
scores[pct] =
|
52
|
-
|
53
|
-
|
54
|
-
if count > 0
|
55
|
-
values.sort!
|
56
|
-
percentiles.each do |pct|
|
57
|
-
idx = pct * (values.length - 1) + 1.0
|
58
|
-
if idx <= 1
|
59
|
-
scores[pct] = values[0]
|
60
|
-
elsif idx >= values.length
|
61
|
-
scores[pct] = values[values.length-1]
|
50
|
+
scores[pct] =
|
51
|
+
if @count == 0
|
52
|
+
0.0
|
62
53
|
else
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
54
|
+
index = pct * (sorted_values.length - 1) + 1.0
|
55
|
+
if index <= 1
|
56
|
+
sorted_values.first
|
57
|
+
elsif index >= sorted_values.length
|
58
|
+
sorted_values.last
|
59
|
+
else
|
60
|
+
lower = sorted_values[index.to_i - 1]
|
61
|
+
upper = sorted_values[index.to_i]
|
62
|
+
lower + (index - index.floor) * (upper - lower)
|
63
|
+
end
|
67
64
|
end
|
68
65
|
end
|
69
|
-
|
70
|
-
return scores
|
66
|
+
scores
|
71
67
|
end
|
72
68
|
|
73
69
|
def update_min(value)
|
74
|
-
if
|
70
|
+
if @min.nil? || value < @min
|
75
71
|
@min = value
|
76
72
|
end
|
77
73
|
end
|
78
74
|
|
79
75
|
def update_max(value)
|
80
|
-
if
|
76
|
+
if @max.nil? || value > @max
|
81
77
|
@max = value
|
82
78
|
end
|
83
79
|
end
|
84
80
|
|
85
81
|
def update_variance(value)
|
86
|
-
|
87
|
-
|
88
|
-
new_m = @variance_m + ((value - old_m) / count)
|
89
|
-
new_s = @variance_s + ((value - old_m) * (value - new_m))
|
82
|
+
new_m = @variance_m + ((value - @variance_m) / @count)
|
83
|
+
new_s = @variance_s + ((value - @variance_m) * (value - new_m))
|
90
84
|
|
91
85
|
@variance_m = new_m
|
92
86
|
@variance_s = new_s
|
93
87
|
end
|
94
88
|
|
95
89
|
def variance
|
96
|
-
count
|
97
|
-
|
98
|
-
|
99
|
-
if count <= 1
|
100
|
-
return 0.0
|
90
|
+
if @count <= 1
|
91
|
+
0.0
|
101
92
|
else
|
102
|
-
|
93
|
+
@variance_s.to_f / (@count - 1)
|
103
94
|
end
|
104
95
|
end
|
105
96
|
|
106
|
-
def count
|
107
|
-
count = @count
|
108
|
-
return count
|
109
|
-
end
|
110
|
-
|
111
|
-
|
112
97
|
def max
|
113
|
-
max
|
114
|
-
if max != nil
|
115
|
-
return max
|
116
|
-
else
|
117
|
-
return 0.0
|
118
|
-
end
|
98
|
+
@max || 0.0
|
119
99
|
end
|
120
100
|
|
121
101
|
def min
|
122
|
-
min
|
123
|
-
if min != nil
|
124
|
-
return min
|
125
|
-
else
|
126
|
-
return 0.0
|
127
|
-
end
|
102
|
+
@min || 0.0
|
128
103
|
end
|
129
104
|
|
130
105
|
def mean
|
131
|
-
count
|
132
|
-
|
133
|
-
|
134
|
-
if count > 0
|
135
|
-
return sum / count
|
106
|
+
if @count > 0
|
107
|
+
@sum / @count
|
136
108
|
else
|
137
|
-
|
109
|
+
0.0
|
138
110
|
end
|
139
111
|
end
|
140
112
|
|
141
113
|
def std_dev
|
142
|
-
count
|
143
|
-
|
144
|
-
|
145
|
-
if count > 0
|
146
|
-
return Math.sqrt(variance)
|
114
|
+
if @count > 0
|
115
|
+
Math.sqrt(variance)
|
147
116
|
else
|
148
|
-
|
117
|
+
0.0
|
149
118
|
end
|
150
119
|
end
|
151
120
|
|
@@ -155,11 +124,11 @@ module Metrics
|
|
155
124
|
|
156
125
|
def as_json(*_)
|
157
126
|
{
|
158
|
-
:min
|
159
|
-
:max
|
160
|
-
:mean
|
161
|
-
:variance
|
162
|
-
:percentiles =>
|
127
|
+
:min => min,
|
128
|
+
:max => max,
|
129
|
+
:mean => mean,
|
130
|
+
:variance => variance,
|
131
|
+
:percentiles => quantiles(Timer::DEFAULT_PERCENTILES)
|
163
132
|
}
|
164
133
|
end
|
165
134
|
|
@@ -7,37 +7,35 @@ module Metrics
|
|
7
7
|
include Metrics::TimeConversion
|
8
8
|
|
9
9
|
# From http://www.teamquest.com/pdfs/whitepaper/ldavg2.pdf
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
attr_reader :count
|
17
|
-
attr_reader :units
|
10
|
+
INTERVAL_SECONDS = 5.0
|
11
|
+
ONE_MINUTE_FACTOR = 1 - Math.exp(-INTERVAL_SECONDS / 60.0)
|
12
|
+
FIVE_MINUTE_FACTOR = 1 - Math.exp(-INTERVAL_SECONDS / (60.0 * 5.0))
|
13
|
+
FIFTEEN_MINUTE_FACTOR = 1 - Math.exp(-INTERVAL_SECONDS / (60.0 * 15.0))
|
14
|
+
|
15
|
+
attr_reader :count, :units
|
18
16
|
alias_method :counted, :count
|
19
17
|
|
20
18
|
def initialize(options = {})
|
21
|
-
@one_minute_rate = @five_minute_rate = @fifteen_minute_rate = 0.0
|
22
|
-
@count = 0
|
23
|
-
@initialized = false
|
24
|
-
@start_time = Time.now.to_f
|
25
19
|
@units = options[:units]
|
20
|
+
clear
|
26
21
|
|
27
22
|
@timer_thread = Thread.new do
|
28
23
|
begin
|
29
24
|
loop do
|
30
|
-
|
31
|
-
sleep(
|
25
|
+
tick
|
26
|
+
sleep(INTERVAL_SECONDS)
|
32
27
|
end
|
33
28
|
rescue Exception => e
|
34
29
|
logger.error "Error in timer thread: #{e.class.name}: #{e}\n #{e.backtrace.join("\n ")}"
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
30
|
+
end
|
31
|
+
end
|
38
32
|
end
|
39
33
|
|
40
34
|
def clear
|
35
|
+
@one_minute_rate = @five_minute_rate = @fifteen_minute_rate = 0.0
|
36
|
+
@count = 0
|
37
|
+
@initialized = false
|
38
|
+
@start_time = Time.now.to_f
|
41
39
|
end
|
42
40
|
|
43
41
|
def mark(count = 1)
|
@@ -45,19 +43,18 @@ module Metrics
|
|
45
43
|
end
|
46
44
|
|
47
45
|
def calc_rate(rate, factor, count)
|
48
|
-
rate
|
49
|
-
rate.to_f
|
46
|
+
rate.to_f + factor.to_f * (count.to_f - rate.to_f)
|
50
47
|
end
|
51
48
|
|
52
49
|
def tick
|
53
|
-
count = @count.to_f
|
50
|
+
count = @count.to_f / Seconds.to_nsec(INTERVAL_SECONDS).to_f
|
54
51
|
|
55
|
-
if
|
52
|
+
if @initialized
|
56
53
|
@one_minute_rate = calc_rate(@one_minute_rate, ONE_MINUTE_FACTOR, count)
|
57
54
|
@five_minute_rate = calc_rate(@five_minute_rate, FIVE_MINUTE_FACTOR, count)
|
58
55
|
@fifteen_minute_rate = calc_rate(@fifteen_minute_rate, FIFTEEN_MINUTE_FACTOR, count)
|
59
56
|
else
|
60
|
-
@one_minute_rate = @five_minute_rate = @fifteen_minute_rate =
|
57
|
+
@one_minute_rate = @five_minute_rate = @fifteen_minute_rate = count
|
61
58
|
@initialized = true
|
62
59
|
end
|
63
60
|
|
@@ -77,21 +74,20 @@ module Metrics
|
|
77
74
|
end
|
78
75
|
|
79
76
|
def mean_rate(rate_unit = :seconds)
|
80
|
-
count
|
81
|
-
|
82
|
-
return 0.0
|
77
|
+
if @count == 0
|
78
|
+
0.0
|
83
79
|
else
|
84
80
|
elapsed = Time.now.to_f - @start_time.to_f
|
85
|
-
|
86
|
-
count.to_f / (
|
81
|
+
scale_factor = scale_time_units(:seconds, rate_unit)
|
82
|
+
@count.to_f / (elapsed * scale_factor)
|
87
83
|
end
|
88
84
|
end
|
89
85
|
|
90
86
|
def as_json(*_)
|
91
87
|
{
|
92
|
-
:one_minute_rate
|
93
|
-
:five_minute_rate
|
94
|
-
:fifteen_minute_rate
|
88
|
+
:one_minute_rate => one_minute_rate,
|
89
|
+
:five_minute_rate => five_minute_rate,
|
90
|
+
:fifteen_minute_rate => fifteen_minute_rate
|
95
91
|
}
|
96
92
|
end
|
97
93
|
|
@@ -8,6 +8,8 @@ module Metrics
|
|
8
8
|
|
9
9
|
attr_reader :duration_unit, :rate_unit, :units
|
10
10
|
|
11
|
+
DEFAULT_PERCENTILES = [0.25, 0.50, 0.75, 0.95, 0.97, 0.98, 0.99]
|
12
|
+
|
11
13
|
def initialize(options = {})
|
12
14
|
@meter = Meter.new
|
13
15
|
@histogram = ExponentialHistogram.new
|
@@ -20,17 +22,17 @@ module Metrics
|
|
20
22
|
end
|
21
23
|
|
22
24
|
def clear
|
25
|
+
@meter.clear
|
23
26
|
@histogram.clear
|
24
27
|
end
|
25
28
|
|
26
29
|
def update(duration, unit)
|
27
|
-
|
28
|
-
self.update_timer(duration * mult)
|
30
|
+
update_timer(convert_to_ns(duration, unit))
|
29
31
|
end
|
30
32
|
|
31
|
-
def time
|
33
|
+
def time
|
32
34
|
start_time = Time.now.to_f
|
33
|
-
result =
|
35
|
+
result = yield
|
34
36
|
time_diff = Time.now.to_f - start_time
|
35
37
|
time_in_ns = convert_to_ns time_diff, :seconds
|
36
38
|
update_timer(time_in_ns)
|
@@ -73,24 +75,17 @@ module Metrics
|
|
73
75
|
scale_duration_to_ns @histogram.std_dev, @duration_unit
|
74
76
|
end
|
75
77
|
|
76
|
-
def quantiles(percentiles =
|
77
|
-
result
|
78
|
-
|
79
|
-
@histogram.quantiles(percentiles).each do |k,v|
|
78
|
+
def quantiles(percentiles = DEFAULT_PERCENTILES)
|
79
|
+
@histogram.quantiles(percentiles).inject({}) do |result, (k, v)|
|
80
80
|
result[k] = scale_duration_to_ns v, @duration_unit
|
81
|
+
result
|
81
82
|
end
|
82
|
-
|
83
|
-
result
|
84
83
|
end
|
85
84
|
|
86
85
|
def values
|
87
|
-
|
88
|
-
|
89
|
-
@histogram.values.each do |value|
|
90
|
-
result << (scale_duration_to_ns value, @duration_unit)
|
86
|
+
@histogram.values.map do |value|
|
87
|
+
scale_duration_to_ns value, @duration_unit
|
91
88
|
end
|
92
|
-
|
93
|
-
result
|
94
89
|
end
|
95
90
|
|
96
91
|
def update_timer(duration)
|
@@ -113,7 +108,7 @@ module Metrics
|
|
113
108
|
:min => min,
|
114
109
|
:max => max,
|
115
110
|
:mean => mean,
|
116
|
-
:percentiles => quantiles
|
111
|
+
:percentiles => quantiles,
|
117
112
|
:unit => @duration_unit
|
118
113
|
}
|
119
114
|
}
|
@@ -125,7 +120,7 @@ module Metrics
|
|
125
120
|
|
126
121
|
private
|
127
122
|
def scale_duration_to_ns(value, unit)
|
128
|
-
value.to_f / convert_to_ns(1, unit)
|
123
|
+
value.to_f / convert_to_ns(1.0, unit)
|
129
124
|
end
|
130
125
|
end
|
131
126
|
end
|
@@ -15,54 +15,41 @@ module Metrics
|
|
15
15
|
module Rack
|
16
16
|
class Middleware
|
17
17
|
|
18
|
-
attr_accessor :app, :options, :agent
|
19
|
-
|
20
|
-
:requests, :uncaught_exceptions,
|
21
|
-
:status_codes
|
22
|
-
|
18
|
+
attr_accessor :app, :options, :agent
|
19
|
+
|
23
20
|
def initialize(app, options ={})
|
24
21
|
@app = app
|
25
22
|
@options = {:show => "/stats"}.merge(options)
|
26
23
|
@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
24
|
end
|
41
|
-
|
25
|
+
|
42
26
|
def call(env)
|
43
27
|
return show(env) if show?(env)
|
44
28
|
|
45
29
|
env['metrics.agent'] = @agent
|
46
30
|
|
47
|
-
status, headers, body =
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
end
|
52
|
-
|
31
|
+
status, headers, body = time_request { @app.call(env) }
|
32
|
+
|
33
|
+
incr_status_code_counter(status / 100)
|
34
|
+
|
53
35
|
[status, headers, body]
|
54
36
|
rescue Exception
|
55
37
|
# TODO: add "last_uncaught_exception" with string of error
|
56
|
-
|
38
|
+
incr_uncaught_exceptions
|
57
39
|
raise
|
58
40
|
end
|
59
|
-
|
60
|
-
|
41
|
+
|
42
|
+
private
|
43
|
+
def show?(env, test = options[:show])
|
61
44
|
case
|
62
|
-
when String === test
|
63
|
-
|
64
|
-
when
|
65
|
-
|
45
|
+
when String === test
|
46
|
+
env['PATH_INFO'] == test
|
47
|
+
when Regexp === test
|
48
|
+
env['PATH_INFO'] =~ test
|
49
|
+
when test.respond_to?(:call)
|
50
|
+
test.call(env)
|
51
|
+
else
|
52
|
+
test
|
66
53
|
end
|
67
54
|
end
|
68
55
|
|
@@ -75,7 +62,18 @@ module Metrics
|
|
75
62
|
[body]
|
76
63
|
]
|
77
64
|
end
|
78
|
-
|
65
|
+
|
66
|
+
def time_request(&block)
|
67
|
+
@agent.timer(:_requests).time(&block)
|
68
|
+
end
|
69
|
+
|
70
|
+
def incr_uncaught_exceptions
|
71
|
+
@agent.counter(:_uncaught_exceptions).incr
|
72
|
+
end
|
73
|
+
|
74
|
+
def incr_status_code_counter(hundred)
|
75
|
+
@agent.counter(:"_status_#{hundred}xx").incr
|
76
|
+
end
|
79
77
|
end
|
80
78
|
end
|
81
79
|
end
|
@@ -7,8 +7,7 @@ module Metrics
|
|
7
7
|
module Reporters
|
8
8
|
class GangliaReporter
|
9
9
|
|
10
|
-
attr_reader :host_ip
|
11
|
-
attr_reader :host_port
|
10
|
+
attr_reader :host_ip, :host_port
|
12
11
|
|
13
12
|
def initialize(options = {})
|
14
13
|
@host_ip = options[:host_ip]
|
@@ -17,18 +16,19 @@ module Metrics
|
|
17
16
|
|
18
17
|
def send_data(data)
|
19
18
|
puts "Sending data: #{data.inspect}"
|
20
|
-
data_type =
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
19
|
+
data_type =
|
20
|
+
case data[:value]
|
21
|
+
Fixnum
|
22
|
+
"uint32"
|
23
|
+
Float
|
24
|
+
"float"
|
25
|
+
String
|
26
|
+
"string"
|
27
|
+
else
|
28
|
+
"unknown"
|
29
|
+
end
|
30
30
|
|
31
|
-
Ganglia::GMetric.send(@host_ip, @host_port.to_i,
|
31
|
+
Ganglia::GMetric.send(@host_ip, @host_port.to_i,
|
32
32
|
:spoof => 0,
|
33
33
|
:name => data[:name],
|
34
34
|
:units => data[:units],
|
@@ -36,44 +36,45 @@ module Metrics
|
|
36
36
|
:value => data[:value],
|
37
37
|
:tmax => 60,
|
38
38
|
:dmax => 300,
|
39
|
-
|
39
|
+
)
|
40
40
|
end
|
41
41
|
|
42
42
|
def report(agent)
|
43
|
-
|
44
43
|
agent.instruments.each do |name, instrument|
|
45
|
-
nothing_to_do = false
|
46
|
-
data = { :name => name, :units => instrument.units }
|
47
44
|
case instrument
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
else
|
59
|
-
data.merge! :value => instrument.get
|
60
|
-
send_data data
|
61
|
-
end
|
62
|
-
when Metrics::Instruments::Timer
|
63
|
-
[:count, :fifteen_minute_rate, :five_minute_rate, :one_minute_rate, :min, :max, :mean].each do |attribute|
|
64
|
-
data.merge!(:name => "#{name}_#{attribute}", :value => instrument.send(attribute))
|
65
|
-
send_data data
|
45
|
+
when Metrics::Instruments::Counter
|
46
|
+
send_data :name => name,
|
47
|
+
:value => instrument.to_i,
|
48
|
+
:units => instrument.units
|
49
|
+
when Metrics::Instruments::Gauge
|
50
|
+
if instrument.get.is_a? Hash
|
51
|
+
instrument.get.each do |key, value|
|
52
|
+
send_data :name => "#{name}_#{key}",
|
53
|
+
:value => value,
|
54
|
+
:units => instrument.units
|
66
55
|
end
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
56
|
+
else
|
57
|
+
send_data :name => name,
|
58
|
+
:value => instrument.get,
|
59
|
+
:units => instrument.units
|
60
|
+
end
|
61
|
+
when Metrics::Instruments::Timer
|
62
|
+
[:count, :fifteen_minute_rate, :five_minute_rate, :one_minute_rate, :min, :max, :mean].each do |attribute|
|
63
|
+
send_data :name => "#{name}_#{attribute}",
|
64
|
+
:value => instrument.send(attribute),
|
65
|
+
:units => instrument.units
|
66
|
+
end
|
67
|
+
when Metrics::Instruments::Meter
|
68
|
+
[:count, :fifteen_minute_rate, :five_minute_rate, :one_minute_rate, :mean_rate].each do |attribute|
|
69
|
+
send_data :name => "#{name_attribute}",
|
70
|
+
:value => instrument.send(attribute),
|
71
|
+
:units => instrument.units
|
72
|
+
end
|
73
|
+
else
|
74
|
+
puts "Unhandled instrument"
|
73
75
|
end
|
74
76
|
end
|
75
77
|
end
|
76
78
|
end
|
77
79
|
end
|
78
80
|
end
|
79
|
-
|
@@ -33,48 +33,50 @@ module Metrics
|
|
33
33
|
https.start do |http|
|
34
34
|
result = http.request(req)
|
35
35
|
case result
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
36
|
+
when Net::HTTPCreated
|
37
|
+
# OK
|
38
|
+
puts "SENT!"
|
39
|
+
else
|
40
|
+
puts "FAILED TO SEND: #{https.inspect}"
|
41
41
|
end
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
45
|
def report(agent)
|
46
|
-
|
47
46
|
agent.instruments.each do |name, instrument|
|
48
|
-
nothing_to_do = false
|
49
47
|
measure_time = Time.now.to_i
|
50
48
|
|
51
|
-
case instrument
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
send_data(post_url, post_data)
|
57
|
-
when "Metrics::Instruments::Gauge"
|
58
|
-
post_url = "#{API_URL}/gauges/#{name}.json"
|
59
|
-
if instrument.get.is_a? Hash
|
60
|
-
instrument.get.each do |key, value|
|
61
|
-
post_data = {:measure_time => measure_time, :source => key, :value => value}
|
62
|
-
send_data(post_url, post_data)
|
63
|
-
end
|
64
|
-
else
|
65
|
-
post_data = {:measure_time => measure_time, :value => instrument.get}
|
66
|
-
send_data(post_url, post_data)
|
67
|
-
end
|
68
|
-
when "Metrics::Instruments::Timer"
|
69
|
-
post_url = "#{API_URL}/gauges/#{name}.json"
|
70
|
-
common_data = {:measure_time => measure_time}
|
49
|
+
case instrument
|
50
|
+
when Metrics::Instruments::Counter
|
51
|
+
send_data "#{API_URL}/counters/#{name}.json",
|
52
|
+
:measure_time => measure_time,
|
53
|
+
:value => instrument.to_i
|
71
54
|
|
72
|
-
|
73
|
-
|
74
|
-
|
55
|
+
when Metrics::Instruments::Gauge
|
56
|
+
post_url = "#{API_URL}/gauges/#{name}.json"
|
57
|
+
if instrument.get.is_a? Hash
|
58
|
+
instrument.get.each do |key, value|
|
59
|
+
send_data post_url,
|
60
|
+
:measure_time => measure_time,
|
61
|
+
:source => key,
|
62
|
+
:value => value
|
75
63
|
end
|
76
|
-
else
|
77
|
-
|
64
|
+
else
|
65
|
+
send_data post_url,
|
66
|
+
:measure_time => measure_time,
|
67
|
+
:value => instrument.get
|
68
|
+
end
|
69
|
+
|
70
|
+
when Metrics::Instruments::Timer
|
71
|
+
[:count, :fifteen_minute_rate, :five_minute_rate, :one_minute_rate, :min, :max, :mean].each do |attribute|
|
72
|
+
send_data "#{API_URL}/gauges/#{name}.json",
|
73
|
+
:measure_time => measure_time,
|
74
|
+
:source => attribute,
|
75
|
+
:value => instrument.send(attribute)
|
76
|
+
end
|
77
|
+
|
78
|
+
else
|
79
|
+
puts "Unhandled instrument"
|
78
80
|
end
|
79
81
|
end
|
80
82
|
end
|
@@ -1,24 +1,23 @@
|
|
1
1
|
module Metrics
|
2
2
|
module Statistics
|
3
3
|
class ExponentialSample
|
4
|
+
RESCALE_WINDOW_SECONDS = 60 * 60 # 1 hour
|
5
|
+
|
4
6
|
def initialize(size = 1028, alpha = 0.015)
|
5
|
-
@values = Hash.new
|
6
|
-
@count = 0
|
7
7
|
@size = size
|
8
8
|
@alpha = alpha
|
9
|
-
|
10
|
-
self.clear
|
9
|
+
clear
|
11
10
|
end
|
12
11
|
|
13
12
|
def clear
|
14
|
-
@values =
|
13
|
+
@values = { }
|
15
14
|
@start_time = tick
|
16
|
-
@next_scale_time = Time.now.to_f +
|
15
|
+
@next_scale_time = Time.now.to_f + RESCALE_WINDOW_SECONDS
|
17
16
|
@count = 0
|
18
17
|
end
|
19
18
|
|
20
19
|
def size
|
21
|
-
[@values.
|
20
|
+
[@values.size, @count].min
|
22
21
|
end
|
23
22
|
|
24
23
|
def tick
|
@@ -32,59 +31,50 @@ module Metrics
|
|
32
31
|
def update_with_timestamp(value, timestamp)
|
33
32
|
priority = weight(timestamp.to_f - @start_time.to_f) / rand
|
34
33
|
@count += 1
|
35
|
-
|
36
|
-
if (newcount <= @size)
|
34
|
+
if @count <= @size
|
37
35
|
@values[priority] = value
|
38
36
|
else
|
39
|
-
|
40
|
-
if
|
37
|
+
first_key = @values.keys.first
|
38
|
+
if first_key && first_key < priority
|
41
39
|
@values[priority] = value
|
42
40
|
|
43
|
-
while
|
44
|
-
|
41
|
+
while values.any? && @values.delete(first_key).nil?
|
42
|
+
first_key = @values.keys.first
|
45
43
|
end
|
46
44
|
end
|
47
45
|
end
|
48
46
|
|
49
47
|
now = Time.now.to_f
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
self.rescale(now, next_scale_time)
|
48
|
+
|
49
|
+
if now >= @next_scale_time
|
50
|
+
rescale(now + RESCALE_WINDOW_SECONDS)
|
54
51
|
end
|
55
52
|
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
|
-
|
53
|
+
|
79
54
|
def values
|
80
55
|
# read-lock?
|
81
|
-
|
82
|
-
|
83
|
-
keys.each do |key|
|
84
|
-
result << @values[key]
|
56
|
+
@values.keys.sort.map do |key|
|
57
|
+
@values[key]
|
85
58
|
end
|
86
|
-
|
87
|
-
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def rescale(next_scale_time)
|
63
|
+
# writelock
|
64
|
+
@next_scale_time = next_scale_time
|
65
|
+
old_start_time = @start_time
|
66
|
+
@start_time = tick
|
67
|
+
time_delta = @start_time - old_start_time
|
68
|
+
@values.keys.each do |key|
|
69
|
+
value = @values.delete(key)
|
70
|
+
new_key = key * Math.exp(-@alpha * time_delta)
|
71
|
+
@values[new_key] = value
|
72
|
+
end
|
73
|
+
# unlock
|
74
|
+
end
|
75
|
+
|
76
|
+
def weight(factor)
|
77
|
+
@alpha.to_f * factor.to_f
|
88
78
|
end
|
89
79
|
end
|
90
80
|
end
|
@@ -3,22 +3,21 @@ module Metrics
|
|
3
3
|
class UniformSample
|
4
4
|
def initialize(size = 1028)
|
5
5
|
@values = Array.new(size)
|
6
|
-
@count = 0
|
7
6
|
@size = size
|
8
|
-
|
7
|
+
clear
|
9
8
|
end
|
10
|
-
|
9
|
+
|
11
10
|
def clear
|
12
|
-
(0
|
11
|
+
(0...@values.size).each do |i|
|
13
12
|
@values[i] = 0
|
14
13
|
end
|
15
14
|
@count = 0
|
16
15
|
end
|
17
|
-
|
16
|
+
|
18
17
|
def size
|
19
|
-
@values.
|
18
|
+
@values.size
|
20
19
|
end
|
21
|
-
|
20
|
+
|
22
21
|
def update(value)
|
23
22
|
if @count < @values.length
|
24
23
|
@values[@count] = value
|
@@ -28,10 +27,10 @@ module Metrics
|
|
28
27
|
@values[index] = value
|
29
28
|
end
|
30
29
|
end
|
31
|
-
|
30
|
+
|
32
31
|
def values
|
33
32
|
@values.dup
|
34
33
|
end
|
35
34
|
end
|
36
35
|
end
|
37
|
-
end
|
36
|
+
end
|
@@ -1,42 +1,36 @@
|
|
1
1
|
module Metrics
|
2
|
-
|
3
|
-
|
4
|
-
def self.to_nsec(mult = 1)
|
5
|
-
raise NotImplementedError
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
class Nanoseconds < TimeUnit
|
2
|
+
|
3
|
+
module Nanoseconds
|
10
4
|
def self.to_nsec(mult = 1)
|
11
5
|
mult
|
12
6
|
end
|
13
7
|
end
|
14
8
|
|
15
|
-
|
9
|
+
module Microseconds
|
16
10
|
def self.to_nsec(mult = 1)
|
17
11
|
1000 * mult
|
18
12
|
end
|
19
13
|
end
|
20
14
|
|
21
|
-
|
15
|
+
module Milliseconds
|
22
16
|
def self.to_nsec(mult = 1)
|
23
17
|
1000000 * mult
|
24
18
|
end
|
25
19
|
end
|
26
20
|
|
27
|
-
|
21
|
+
module Seconds
|
28
22
|
def self.to_nsec(mult = 1)
|
29
23
|
1000000000 * mult
|
30
24
|
end
|
31
25
|
end
|
32
26
|
|
33
|
-
|
27
|
+
module Minutes
|
34
28
|
def self.to_nsec(mult = 1)
|
35
29
|
60000000000 * mult
|
36
30
|
end
|
37
31
|
end
|
38
32
|
|
39
|
-
|
33
|
+
module Hours
|
40
34
|
def self.to_nsec(mult = 1)
|
41
35
|
3600000000000 * mult
|
42
36
|
end
|
@@ -53,11 +47,11 @@ module Metrics
|
|
53
47
|
}
|
54
48
|
|
55
49
|
def convert_to_ns(value, unit)
|
56
|
-
|
50
|
+
UNITS[unit].to_nsec.to_f * value.to_f
|
57
51
|
end
|
58
52
|
|
59
53
|
def scale_time_units(source, dest)
|
60
|
-
|
54
|
+
UNITS[source].to_nsec.to_f / UNITS[dest].to_nsec.to_f
|
61
55
|
end
|
62
56
|
end
|
63
57
|
end
|
data/lib/ruby-metrics/version.rb
CHANGED
data/ruby-metrics.gemspec
CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.description = %q{A Ruby implementation of metrics inspired by @coda's JVM metrics for those of us in Ruby land}
|
18
18
|
s.license = 'MIT'
|
19
19
|
|
20
|
-
s.add_dependency 'json', '~>
|
20
|
+
s.add_dependency 'json', '~> 2.3', '>= 2.3.0'
|
21
21
|
|
22
22
|
s.add_development_dependency 'rspec', '~> 2.14', '>= 2.14.0'
|
23
23
|
s.add_development_dependency 'simplecov', '~> 0.3', '>= 0.3.8'
|
@@ -71,7 +71,7 @@ describe Metrics::Instruments::Histogram do
|
|
71
71
|
|
72
72
|
it "should clear data correctly" do
|
73
73
|
sample = Metrics::Statistics::UniformSample.new
|
74
|
-
sample.should_receive(:clear)
|
74
|
+
sample.should_receive(:clear).twice
|
75
75
|
Metrics::Statistics::UniformSample.should_receive(:new).and_return sample
|
76
76
|
|
77
77
|
histogram = Metrics::Instruments::Histogram.new
|
@@ -29,7 +29,7 @@ describe Metrics::Instruments::Timer do
|
|
29
29
|
end
|
30
30
|
|
31
31
|
it "should have quantiles of zero" do
|
32
|
-
@timer.quantiles.should == {0.
|
32
|
+
@timer.quantiles.should == {0.25=>0.0, 0.50=>0.0, 0.75=>0.0, 0.95=>0.0, 0.97=>0.0, 0.98=>0.0, 0.99=>0.0 }
|
33
33
|
end
|
34
34
|
|
35
35
|
it "should have a mean rate of zero" do
|
@@ -85,7 +85,7 @@ describe Metrics::Instruments::Timer do
|
|
85
85
|
end
|
86
86
|
|
87
87
|
it "should accurately calculate percentiles" do
|
88
|
-
@timer.quantiles.should == {0.
|
88
|
+
@timer.quantiles.should == {0.25=>20.0, 0.5=>20.0, 0.75=>30.0, 0.95=>38.0, 0.97=>38.8, 0.98=>39.2, 0.99=>39.6}
|
89
89
|
end
|
90
90
|
|
91
91
|
it "should contain the series added" do
|
@@ -54,7 +54,7 @@ describe Metrics::Integration::Rack::Middleware do
|
|
54
54
|
it "should count all requests" do
|
55
55
|
lambda do
|
56
56
|
get '/'
|
57
|
-
end.should change{ app.
|
57
|
+
end.should change{ app.agent.timer(:_requests).count }.by(1)
|
58
58
|
end
|
59
59
|
|
60
60
|
it "should count uncaught exceptions" do
|
@@ -63,22 +63,22 @@ describe Metrics::Integration::Rack::Middleware do
|
|
63
63
|
lambda do
|
64
64
|
get '/'
|
65
65
|
end.should raise_error
|
66
|
-
end.should change{ app.
|
66
|
+
end.should change{ app.agent.counter(:_uncaught_exceptions).to_i }.by(1)
|
67
67
|
end
|
68
68
|
|
69
69
|
it "should time request length" do
|
70
70
|
length = 0.1
|
71
71
|
@app = lambda{ |env| sleep(length); [200, {}, ['']] }
|
72
72
|
get '/'
|
73
|
-
app.
|
73
|
+
app.agent.timer(:_requests).mean.should be_within(length / 10).of(length)
|
74
74
|
end
|
75
|
-
|
76
|
-
[
|
75
|
+
|
76
|
+
[200, 304, 404, 500].each do |status|
|
77
77
|
it "should count #{status} HTTP status code as #{status / 100}xx" do
|
78
78
|
@app = lambda{ |env| [status, {}, []] }
|
79
79
|
lambda do
|
80
80
|
get '/'
|
81
|
-
end.should change{ app.
|
81
|
+
end.should change{ app.agent.counter(:"_status_#{status / 100}xx").to_i }.by(1)
|
82
82
|
end
|
83
83
|
end
|
84
84
|
|
metadata
CHANGED
@@ -1,102 +1,93 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-metrics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
5
|
-
prerelease:
|
4
|
+
version: 0.9.4
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- John Ewart
|
9
|
-
autorequire:
|
8
|
+
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2021-11-04 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: json
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
|
-
- - ~>
|
17
|
+
- - "~>"
|
20
18
|
- !ruby/object:Gem::Version
|
21
|
-
version: '
|
22
|
-
- -
|
19
|
+
version: '2.3'
|
20
|
+
- - ">="
|
23
21
|
- !ruby/object:Gem::Version
|
24
|
-
version:
|
22
|
+
version: 2.3.0
|
25
23
|
type: :runtime
|
26
24
|
prerelease: false
|
27
25
|
version_requirements: !ruby/object:Gem::Requirement
|
28
|
-
none: false
|
29
26
|
requirements:
|
30
|
-
- - ~>
|
27
|
+
- - "~>"
|
31
28
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
33
|
-
- -
|
29
|
+
version: '2.3'
|
30
|
+
- - ">="
|
34
31
|
- !ruby/object:Gem::Version
|
35
|
-
version:
|
32
|
+
version: 2.3.0
|
36
33
|
- !ruby/object:Gem::Dependency
|
37
34
|
name: rspec
|
38
35
|
requirement: !ruby/object:Gem::Requirement
|
39
|
-
none: false
|
40
36
|
requirements:
|
41
|
-
- - ~>
|
37
|
+
- - "~>"
|
42
38
|
- !ruby/object:Gem::Version
|
43
39
|
version: '2.14'
|
44
|
-
- -
|
40
|
+
- - ">="
|
45
41
|
- !ruby/object:Gem::Version
|
46
42
|
version: 2.14.0
|
47
43
|
type: :development
|
48
44
|
prerelease: false
|
49
45
|
version_requirements: !ruby/object:Gem::Requirement
|
50
|
-
none: false
|
51
46
|
requirements:
|
52
|
-
- - ~>
|
47
|
+
- - "~>"
|
53
48
|
- !ruby/object:Gem::Version
|
54
49
|
version: '2.14'
|
55
|
-
- -
|
50
|
+
- - ">="
|
56
51
|
- !ruby/object:Gem::Version
|
57
52
|
version: 2.14.0
|
58
53
|
- !ruby/object:Gem::Dependency
|
59
54
|
name: simplecov
|
60
55
|
requirement: !ruby/object:Gem::Requirement
|
61
|
-
none: false
|
62
56
|
requirements:
|
63
|
-
- - ~>
|
57
|
+
- - "~>"
|
64
58
|
- !ruby/object:Gem::Version
|
65
59
|
version: '0.3'
|
66
|
-
- -
|
60
|
+
- - ">="
|
67
61
|
- !ruby/object:Gem::Version
|
68
62
|
version: 0.3.8
|
69
63
|
type: :development
|
70
64
|
prerelease: false
|
71
65
|
version_requirements: !ruby/object:Gem::Requirement
|
72
|
-
none: false
|
73
66
|
requirements:
|
74
|
-
- - ~>
|
67
|
+
- - "~>"
|
75
68
|
- !ruby/object:Gem::Version
|
76
69
|
version: '0.3'
|
77
|
-
- -
|
70
|
+
- - ">="
|
78
71
|
- !ruby/object:Gem::Version
|
79
72
|
version: 0.3.8
|
80
73
|
- !ruby/object:Gem::Dependency
|
81
74
|
name: rack-test
|
82
75
|
requirement: !ruby/object:Gem::Requirement
|
83
|
-
none: false
|
84
76
|
requirements:
|
85
|
-
- - ~>
|
77
|
+
- - "~>"
|
86
78
|
- !ruby/object:Gem::Version
|
87
79
|
version: '0.5'
|
88
|
-
- -
|
80
|
+
- - ">="
|
89
81
|
- !ruby/object:Gem::Version
|
90
82
|
version: 0.5.7
|
91
83
|
type: :development
|
92
84
|
prerelease: false
|
93
85
|
version_requirements: !ruby/object:Gem::Requirement
|
94
|
-
none: false
|
95
86
|
requirements:
|
96
|
-
- - ~>
|
87
|
+
- - "~>"
|
97
88
|
- !ruby/object:Gem::Version
|
98
89
|
version: '0.5'
|
99
|
-
- -
|
90
|
+
- - ">="
|
100
91
|
- !ruby/object:Gem::Version
|
101
92
|
version: 0.5.7
|
102
93
|
description: A Ruby implementation of metrics inspired by @coda's JVM metrics for
|
@@ -107,10 +98,10 @@ executables: []
|
|
107
98
|
extensions: []
|
108
99
|
extra_rdoc_files: []
|
109
100
|
files:
|
110
|
-
- .bundle/config
|
111
|
-
- .gitignore
|
112
|
-
- .rvmrc
|
113
|
-
- .travis.yml
|
101
|
+
- ".bundle/config"
|
102
|
+
- ".gitignore"
|
103
|
+
- ".rvmrc"
|
104
|
+
- ".travis.yml"
|
114
105
|
- CHANGELOG.md
|
115
106
|
- Gemfile
|
116
107
|
- LICENSE
|
@@ -162,30 +153,40 @@ files:
|
|
162
153
|
- spec/spec_helper.rb
|
163
154
|
- spec/statistics/exponential_sample_spec.rb
|
164
155
|
- spec/statistics/uniform_sample_spec.rb
|
165
|
-
- spec/time_units_spec.rb
|
166
156
|
homepage: https://github.com/johnewart/ruby-metrics
|
167
157
|
licenses:
|
168
158
|
- MIT
|
169
|
-
|
159
|
+
metadata: {}
|
160
|
+
post_install_message:
|
170
161
|
rdoc_options: []
|
171
162
|
require_paths:
|
172
163
|
- lib
|
173
164
|
required_ruby_version: !ruby/object:Gem::Requirement
|
174
|
-
none: false
|
175
165
|
requirements:
|
176
|
-
- -
|
166
|
+
- - ">="
|
177
167
|
- !ruby/object:Gem::Version
|
178
168
|
version: '0'
|
179
169
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
180
|
-
none: false
|
181
170
|
requirements:
|
182
|
-
- -
|
171
|
+
- - ">="
|
183
172
|
- !ruby/object:Gem::Version
|
184
173
|
version: '0'
|
185
174
|
requirements: []
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
specification_version: 3
|
175
|
+
rubygems_version: 3.1.2
|
176
|
+
signing_key:
|
177
|
+
specification_version: 4
|
190
178
|
summary: Metrics for Ruby
|
191
|
-
test_files:
|
179
|
+
test_files:
|
180
|
+
- spec/agent_spec.rb
|
181
|
+
- spec/instruments/counter_spec.rb
|
182
|
+
- spec/instruments/gauge_spec.rb
|
183
|
+
- spec/instruments/histogram_spec.rb
|
184
|
+
- spec/instruments/meter_spec.rb
|
185
|
+
- spec/instruments/timer_spec.rb
|
186
|
+
- spec/integration/rack_endpoint_spec.rb
|
187
|
+
- spec/integration/rack_middleware_spec.rb
|
188
|
+
- spec/reporter_spec.rb
|
189
|
+
- spec/reporters/opentsdb_spec.rb
|
190
|
+
- spec/spec_helper.rb
|
191
|
+
- spec/statistics/exponential_sample_spec.rb
|
192
|
+
- spec/statistics/uniform_sample_spec.rb
|
data/spec/time_units_spec.rb
DELETED