drone 1.0.4 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +13 -0
- data/Guardfile +12 -0
- data/Rakefile +22 -37
- data/drone.gemspec +2 -6
- data/examples/collectd.rb +51 -0
- data/examples/common.rb +24 -0
- data/examples/json.rb +49 -0
- data/examples/redis_storage.rb +60 -0
- data/examples/simple.rb +3 -3
- data/extensions/drone_collectd/Gemfile +7 -0
- data/extensions/drone_collectd/LICENSE +20 -0
- data/extensions/drone_collectd/README.md +24 -0
- data/extensions/drone_collectd/drone_collectd.gemspec +28 -0
- data/extensions/drone_collectd/lib/drone_collectd.rb +7 -0
- data/extensions/drone_collectd/lib/drone_collectd/collectd.rb +97 -0
- data/extensions/drone_collectd/lib/drone_collectd/parser.rb +86 -0
- data/extensions/drone_collectd/specs/common.rb +3 -0
- data/extensions/drone_collectd/specs/unit/parser_spec.rb +49 -0
- data/extensions/drone_json/Gemfile +6 -0
- data/extensions/drone_json/LICENSE +20 -0
- data/extensions/drone_json/README.md +9 -0
- data/extensions/drone_json/drone_json.gemspec +32 -0
- data/extensions/drone_json/lib/drone_json.rb +9 -0
- data/extensions/drone_json/lib/drone_json/json.rb +100 -0
- data/extensions/drone_json/specs/common.rb +63 -0
- data/extensions/drone_redis/Gemfile +7 -0
- data/extensions/drone_redis/drone_redis.gemspec +22 -0
- data/extensions/drone_redis/lib/drone_redis.rb +8 -0
- data/extensions/drone_redis/lib/drone_redis/redis.rb +218 -0
- data/lib/drone.rb +1 -0
- data/lib/drone/errors.rb +11 -0
- data/lib/drone/metrics/histogram.rb +7 -6
- data/lib/drone/metrics/meter.rb +1 -1
- data/lib/drone/monitoring.rb +2 -2
- data/lib/drone/storage/memory.rb +1 -0
- data/lib/drone/utils/exponentially_decaying_sample.rb +79 -24
- data/lib/drone/version.rb +1 -1
- data/specs/{unit → metrics}/histogram_spec.rb +5 -1
- data/specs/metrics/meter_spec.rb +10 -2
- data/specs/metrics/timer_spec.rb +7 -1
- data/specs/{unit/monitoring_spec.rb → monitoring_spec.rb} +25 -1
- data/specs/{unit → utils}/ewma_spec.rb +1 -0
- data/specs/utils/exponentially_decaying_sample_spec.rb +140 -0
- data/specs/{unit → utils}/uniform_sample_spec.rb +0 -0
- metadata +72 -93
- data/specs/unit/exponentially_decaying_sample_spec.rb +0 -86
data/lib/drone.rb
CHANGED
data/lib/drone/errors.rb
ADDED
@@ -41,12 +41,13 @@ module Drone
|
|
41
41
|
|
42
42
|
def clear
|
43
43
|
@sample.clear()
|
44
|
-
|
45
|
-
@
|
46
|
-
@
|
47
|
-
@
|
48
|
-
@
|
49
|
-
@
|
44
|
+
|
45
|
+
@count = Drone::request_number("#{name}:count", 0)
|
46
|
+
@_min = Drone::request_number("#{name}:min", MAX)
|
47
|
+
@_max = Drone::request_number("#{name}:max", MIN)
|
48
|
+
@_sum = Drone::request_number("#{name}:max", 0)
|
49
|
+
@varianceM = Drone::request_number("#{name}:varianceM", -1)
|
50
|
+
@varianceS = Drone::request_number("#{name}:varianceS", 0)
|
50
51
|
end
|
51
52
|
|
52
53
|
def update(val)
|
data/lib/drone/metrics/meter.rb
CHANGED
@@ -7,7 +7,7 @@ require File.expand_path('../../utils/ewma', __FILE__)
|
|
7
7
|
module Drone
|
8
8
|
module Metrics
|
9
9
|
##
|
10
|
-
#
|
10
|
+
# This meter measures mean throughput and one-, five-, and
|
11
11
|
# fifteen-minute exponentially-weighted moving average throughputs.
|
12
12
|
#
|
13
13
|
class Meter < Metric
|
data/lib/drone/monitoring.rb
CHANGED
@@ -28,7 +28,7 @@ module Drone
|
|
28
28
|
def monitor_rate(name)
|
29
29
|
meter = Drone::find_metric(name) || Metrics::Meter.new(name)
|
30
30
|
unless meter.is_a?(Metrics::Meter)
|
31
|
-
raise
|
31
|
+
raise AlreadyDefined, "metric #{name} is already defined as #{meter.class}"
|
32
32
|
end
|
33
33
|
|
34
34
|
Drone::register_metric(meter)
|
@@ -47,7 +47,7 @@ module Drone
|
|
47
47
|
def monitor_time(name)
|
48
48
|
timer = Drone::find_metric(name) || Metrics::Timer.new(name)
|
49
49
|
unless timer.is_a?(Metrics::Timer)
|
50
|
-
raise
|
50
|
+
raise AlreadyDefined, "metric #{name} is already defined as #{timer.class}"
|
51
51
|
end
|
52
52
|
Drone::register_metric(timer)
|
53
53
|
@_timer_waiting = timer
|
data/lib/drone/storage/memory.rb
CHANGED
@@ -1,10 +1,29 @@
|
|
1
|
+
require 'flt'
|
1
2
|
require File.expand_path('../../core', __FILE__)
|
2
3
|
|
3
4
|
module Drone
|
4
|
-
class ExponentiallyDecayingSample
|
5
|
-
# 1 hour in ms
|
6
|
-
RESCALE_THRESHOLD = (1 * 60 * 60 * 1000).freeze
|
7
5
|
|
6
|
+
##
|
7
|
+
# An exponentially-decaying random sample
|
8
|
+
#
|
9
|
+
class ExponentiallyDecayingSample
|
10
|
+
# 1 hour
|
11
|
+
RESCALE_THRESHOLD = (1 * 60 * 60).freeze
|
12
|
+
|
13
|
+
##
|
14
|
+
# Create a new dataset, if the decay factor is too big
|
15
|
+
# the flt ruby library will be used for internal computations
|
16
|
+
# to allow greater precision, the switch happens if alpha is
|
17
|
+
# higher than 0.1.
|
18
|
+
#
|
19
|
+
# @param [String] id A unique id representing this
|
20
|
+
# dataset.
|
21
|
+
# @param [Integer] reservoir_size the number of samples
|
22
|
+
# to keep.
|
23
|
+
# @param [Number] alpha the decay factor, the higher this
|
24
|
+
# number, the more biased the sample will be towards
|
25
|
+
# newer values.
|
26
|
+
#
|
8
27
|
def initialize(id, reservoir_size, alpha)
|
9
28
|
@id = id
|
10
29
|
@values = Drone::request_hash("#{@id}:values")
|
@@ -28,25 +47,28 @@ module Drone
|
|
28
47
|
(@values.size < count) ? @values.size : count
|
29
48
|
end
|
30
49
|
|
31
|
-
|
32
50
|
def update(val, time = current_time)
|
33
|
-
priority = weight(time - @start_time.get) /
|
34
|
-
|
35
|
-
|
51
|
+
priority = weight(time - @start_time.get) / generate_random()
|
52
|
+
new_count = @count.inc
|
53
|
+
|
54
|
+
if new_count <= @reservoir_size
|
36
55
|
@values[priority] = val
|
37
56
|
else
|
38
|
-
first = @values.keys
|
57
|
+
first = @values.keys[0]
|
39
58
|
if first < priority
|
40
|
-
@values[priority] = val
|
41
|
-
|
42
|
-
|
59
|
+
old_val, @values[priority] = @values[priority], val
|
60
|
+
unless old_val
|
61
|
+
while @values.delete(first) == nil
|
62
|
+
first = @values.keys[0]
|
63
|
+
end
|
43
64
|
end
|
44
65
|
end
|
45
66
|
end
|
46
67
|
|
47
68
|
now = current_time()
|
48
|
-
|
49
|
-
|
69
|
+
next_scale = @next_scale_time.get
|
70
|
+
if now >= next_scale
|
71
|
+
rescale(now, next_scale)
|
50
72
|
end
|
51
73
|
end
|
52
74
|
|
@@ -55,26 +77,59 @@ module Drone
|
|
55
77
|
buff << @values[key]
|
56
78
|
end
|
57
79
|
end
|
58
|
-
|
59
|
-
def rescale(now)
|
60
|
-
@next_scale_time.set( current_time() + RESCALE_THRESHOLD )
|
61
|
-
new_start = current_time()
|
62
|
-
old_start = @start_time.get_and_set(new_start)
|
63
|
-
|
64
|
-
@values = Hash[ @values.map{ |k,v|
|
65
|
-
[k * Math.exp(-@alpha * (new_start - old_start)), v]
|
66
|
-
}]
|
67
80
|
|
81
|
+
def rescale(now, next_scale)
|
82
|
+
if @next_scale_time.compare_and_set(next_scale, now + RESCALE_THRESHOLD)
|
83
|
+
new_start = current_time()
|
84
|
+
old_start = @start_time.get_and_set( new_start )
|
85
|
+
time_diff = new_start - old_start
|
86
|
+
|
87
|
+
@values = Hash[ @values.map{ |k,v|
|
88
|
+
[k * math_exp(-@alpha * time_diff), v]
|
89
|
+
}]
|
90
|
+
|
91
|
+
end
|
68
92
|
end
|
69
93
|
|
70
94
|
private
|
95
|
+
|
96
|
+
def use_flt?
|
97
|
+
@alpha > 0.1
|
98
|
+
end
|
99
|
+
|
100
|
+
def math_exp(n)
|
101
|
+
if use_flt?
|
102
|
+
Flt::DecNum(Rational(n)).exp()
|
103
|
+
else
|
104
|
+
Math.exp(n)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Generates a non-zero random number
|
110
|
+
# According to the ruby documentation rand() could return 0
|
111
|
+
# so we ensure this will never happen
|
112
|
+
#
|
113
|
+
# @return [Float] The random number
|
114
|
+
#
|
115
|
+
def generate_random()
|
116
|
+
begin
|
117
|
+
r = Kernel.rand()
|
118
|
+
end while r == 0.0
|
119
|
+
|
120
|
+
if use_flt?
|
121
|
+
Flt::DecNum(Rational(r))
|
122
|
+
else
|
123
|
+
r
|
124
|
+
end
|
125
|
+
end
|
71
126
|
|
72
127
|
def current_time
|
73
|
-
Time.now.to_f
|
128
|
+
Time.now.to_f
|
74
129
|
end
|
75
130
|
|
76
131
|
def weight(n)
|
77
|
-
|
132
|
+
math_exp(@alpha * n)
|
78
133
|
end
|
79
134
|
|
80
135
|
end
|
data/lib/drone/version.rb
CHANGED
@@ -9,7 +9,7 @@ describe 'Histogram' do
|
|
9
9
|
Drone::init_drone()
|
10
10
|
end
|
11
11
|
|
12
|
-
describe "A histogram with zero recorded
|
12
|
+
describe "A histogram with zero recorded values" do
|
13
13
|
before do
|
14
14
|
@histogram = Histogram.new("id1", UniformSample.new("id1:sample", 100))
|
15
15
|
end
|
@@ -17,6 +17,10 @@ describe 'Histogram' do
|
|
17
17
|
should "have a count of 0" do
|
18
18
|
@histogram.count.should == 0
|
19
19
|
end
|
20
|
+
|
21
|
+
should "have a variance of 0" do
|
22
|
+
@histogram.send(:variance).should == 0
|
23
|
+
end
|
20
24
|
|
21
25
|
should "have a max of 0" do
|
22
26
|
@histogram.max.should == 0
|
data/specs/metrics/meter_spec.rb
CHANGED
@@ -48,14 +48,22 @@ EM.describe 'Meter Metrics' do
|
|
48
48
|
|
49
49
|
describe "A meter metric with three events" do
|
50
50
|
before do
|
51
|
-
|
52
|
-
|
51
|
+
Delorean.time_travel_to("2 second ago") do
|
52
|
+
@meter = Metrics::Meter.new("thangs")
|
53
|
+
@meter.mark(3)
|
54
|
+
end
|
53
55
|
end
|
54
56
|
|
55
57
|
should "have a count of three" do
|
56
58
|
@meter.count.should == 3
|
57
59
|
done
|
58
60
|
end
|
61
|
+
|
62
|
+
should "have a mean rate of 0 events/sec" do
|
63
|
+
@meter.mean_rate.should.be.close?(1.5, 0.01)
|
64
|
+
done
|
65
|
+
end
|
66
|
+
|
59
67
|
end
|
60
68
|
|
61
69
|
end
|
data/specs/metrics/timer_spec.rb
CHANGED
@@ -9,7 +9,7 @@ EM.describe 'Timer Metrics' do
|
|
9
9
|
Drone::start_monitoring()
|
10
10
|
end
|
11
11
|
|
12
|
-
describe "A
|
12
|
+
describe "A newly created timer" do
|
13
13
|
before do
|
14
14
|
@timer = Metrics::Timer.new('id')
|
15
15
|
end
|
@@ -91,6 +91,12 @@ EM.describe 'Timer Metrics' do
|
|
91
91
|
@timer.stdDev.should.be.close?(11.401, 0.001)
|
92
92
|
done
|
93
93
|
end
|
94
|
+
|
95
|
+
it 'can be cleared' do
|
96
|
+
@timer.clear()
|
97
|
+
@timer.count.should == 0
|
98
|
+
done
|
99
|
+
end
|
94
100
|
|
95
101
|
should "calculate the median/p95/p98/p99/p999" do
|
96
102
|
median, p95, p98, p99, p999 = @timer.percentiles(0.5, 0.95, 0.98, 0.99, 0.999)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require File.expand_path('
|
1
|
+
require File.expand_path('../common', __FILE__)
|
2
2
|
|
3
3
|
require 'drone'
|
4
4
|
require 'drone/monitoring'
|
@@ -21,11 +21,35 @@ EM.describe 'Monitoring' do
|
|
21
21
|
monitor_rate("users/with_block")
|
22
22
|
def method_with_block(&block); block.call; end
|
23
23
|
|
24
|
+
monitor_time("users/timed")
|
25
|
+
def timed_method; end;
|
26
|
+
|
24
27
|
end
|
25
28
|
@obj = @klass.new
|
26
29
|
|
27
30
|
end
|
28
31
|
|
32
|
+
should "raise an error if the metric name os already used (rate => time)" do
|
33
|
+
proc{
|
34
|
+
@klass.instance_eval do
|
35
|
+
monitor_time("users/no_args")
|
36
|
+
end
|
37
|
+
}.should.raise(Drone::AlreadyDefined)
|
38
|
+
|
39
|
+
done
|
40
|
+
end
|
41
|
+
|
42
|
+
should "raise an error if the metric name os already used (time => rate)" do
|
43
|
+
|
44
|
+
proc{
|
45
|
+
@klass.instance_eval do
|
46
|
+
monitor_rate("users/timed")
|
47
|
+
end
|
48
|
+
}.should.raise(Drone::AlreadyDefined)
|
49
|
+
|
50
|
+
done
|
51
|
+
end
|
52
|
+
|
29
53
|
should 'reuse same meter for every instances of this class' do
|
30
54
|
meter = Drone::find_metric("users/no_args")
|
31
55
|
meter.count.should == 0
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require File.expand_path('../../common', __FILE__)
|
2
|
+
|
3
|
+
require 'drone/utils/exponentially_decaying_sample'
|
4
|
+
include Drone
|
5
|
+
|
6
|
+
describe 'Exponentially Decaying Sample' do
|
7
|
+
before do
|
8
|
+
Drone::init_drone(nil, Storage::Memory.new)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "A sample of 100 out of 1000 elements" do
|
12
|
+
before do
|
13
|
+
@population = (0...100)
|
14
|
+
@sample = ExponentiallyDecayingSample.new('id1', 1000, 0.99)
|
15
|
+
@population.step(1){|n| @sample.update(n) }
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
should "have 100 elements" do
|
20
|
+
@sample.size.should == 100
|
21
|
+
@sample.values.size.should == 100
|
22
|
+
end
|
23
|
+
|
24
|
+
should "only have elements from the population" do
|
25
|
+
arr = @sample.values - @population.to_a
|
26
|
+
arr.should == []
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
describe "A sample of 100 out of 10 elements" do
|
33
|
+
before do
|
34
|
+
@population = (0...100)
|
35
|
+
@sample = ExponentiallyDecayingSample.new('id1', 100, 0.99)
|
36
|
+
@population.step(1){|n| @sample.update(n) }
|
37
|
+
end
|
38
|
+
|
39
|
+
should "have 100 elements" do
|
40
|
+
@sample.size.should == 100
|
41
|
+
@sample.values.size.should == 100
|
42
|
+
end
|
43
|
+
|
44
|
+
should "only have elements from the population" do
|
45
|
+
arr = @sample.values - @population.to_a
|
46
|
+
arr.should == []
|
47
|
+
end
|
48
|
+
|
49
|
+
should "rescale after 1 hour2" do
|
50
|
+
Delorean.time_travel_to("1 hours from now") do
|
51
|
+
@sample.update(42)
|
52
|
+
end
|
53
|
+
|
54
|
+
@sample.size.should == 100
|
55
|
+
@sample.values.size.should == 100
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
describe "A heavily-biased sample of 100 out of 1000 elements" do
|
62
|
+
before do
|
63
|
+
@population = (0...100)
|
64
|
+
@sample = ExponentiallyDecayingSample.new('id1', 100, 0.01)
|
65
|
+
@population.step(1){|n| @sample.update(n) }
|
66
|
+
end
|
67
|
+
|
68
|
+
should "have 100 elements" do
|
69
|
+
@sample.size.should == 100
|
70
|
+
@sample.values.size.should == 100
|
71
|
+
end
|
72
|
+
|
73
|
+
should "only have elements from the population" do
|
74
|
+
arr = @sample.values - @population.to_a
|
75
|
+
arr.should == []
|
76
|
+
end
|
77
|
+
|
78
|
+
should "rescale after 1 hour" do
|
79
|
+
Delorean.time_travel_to("1 hours from now") do
|
80
|
+
@sample.update(42)
|
81
|
+
end
|
82
|
+
|
83
|
+
@sample.size.should == 100
|
84
|
+
@sample.values.size.should == 100
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "A heavily-biased sample of 1000 out of 1000 elements" do
|
90
|
+
before do
|
91
|
+
@population = (0...1000)
|
92
|
+
@sample = ExponentiallyDecayingSample.new('id1', 1000, 0.01)
|
93
|
+
@population.step(1){|n| @sample.update(n) }
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should have 1000 elements" do
|
97
|
+
@sample.size.should == 1000
|
98
|
+
@sample.values.length.should == 1000
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should only have elements from the population" do
|
102
|
+
values = @sample.values
|
103
|
+
@population.each do |datum|
|
104
|
+
values.should.include?(datum)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should replace an element when updating" do
|
109
|
+
Delorean.time_travel_to("10 minutes from now") do
|
110
|
+
@sample.update(4242)
|
111
|
+
@sample.size.should == 1000
|
112
|
+
@sample.values.should.include?(4242)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should rescale so that newer events are higher in priority in the hash" do
|
117
|
+
Delorean.time_travel_to("1 hour from now") do
|
118
|
+
@sample.update(2121)
|
119
|
+
@sample.size.should == 1000
|
120
|
+
end
|
121
|
+
|
122
|
+
Delorean.time_travel_to("2 hours from now") do
|
123
|
+
@sample.update(4242)
|
124
|
+
@sample.size.should == 1000
|
125
|
+
|
126
|
+
values = @sample.values
|
127
|
+
|
128
|
+
values.length.should == 1000
|
129
|
+
values.should.include?(4242)
|
130
|
+
values.should.include?(2121)
|
131
|
+
|
132
|
+
# Most recently added values in time should be at the end with the highest priority
|
133
|
+
values[999].should == 4242
|
134
|
+
values[998].should == 2121
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|