drone 0.0.1
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 +8 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +138 -0
- data/Rakefile +20 -0
- data/drone.gemspec +29 -0
- data/examples/simple.rb +50 -0
- data/lib/drone.rb +23 -0
- data/lib/drone/core.rb +125 -0
- data/lib/drone/interfaces/base.rb +17 -0
- data/lib/drone/interfaces/console.rb +83 -0
- data/lib/drone/metrics/counter.rb +28 -0
- data/lib/drone/metrics/gauge.rb +24 -0
- data/lib/drone/metrics/histogram.rb +132 -0
- data/lib/drone/metrics/meter.rb +62 -0
- data/lib/drone/metrics/timer.rb +62 -0
- data/lib/drone/monitoring.rb +107 -0
- data/lib/drone/schedulers/eventmachine.rb +61 -0
- data/lib/drone/utils/ewma.rb +50 -0
- data/lib/drone/utils/exponentially_decaying_sample.rb +71 -0
- data/lib/drone/utils/uniform_sample.rb +37 -0
- data/lib/drone/version.rb +3 -0
- data/specs/common.rb +63 -0
- data/specs/metrics/counter_spec.rb +41 -0
- data/specs/metrics/gauge_spec.rb +28 -0
- data/specs/metrics/meter_spec.rb +40 -0
- data/specs/metrics/timer_spec.rb +131 -0
- data/specs/unit/ewma_spec.rb +138 -0
- data/specs/unit/exponentially_decaying_sample_spec.rb +83 -0
- data/specs/unit/histogram_spec.rb +87 -0
- data/specs/unit/monitoring_spec.rb +129 -0
- data/specs/unit/uniform_sample_spec.rb +43 -0
- metadata +154 -0
@@ -0,0 +1,138 @@
|
|
1
|
+
require File.expand_path('../../common', __FILE__)
|
2
|
+
|
3
|
+
require 'drone/utils/ewma'
|
4
|
+
|
5
|
+
describe 'EWMA' do
|
6
|
+
|
7
|
+
describe 'A 1min EWMA with a value of 3' do
|
8
|
+
before do
|
9
|
+
@ewma = EWMA.one_minute_ewma
|
10
|
+
@ewma.update(3)
|
11
|
+
@ewma.tick()
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def mark_minutes(minutes)
|
16
|
+
1.upto( (minutes*60.0) / 5 ) do
|
17
|
+
@ewma.tick()
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
should "have a rate of 0.6 events/sec after the first tick" do
|
22
|
+
@ewma.rate.should.be.close(0.6, 0.000001)
|
23
|
+
end
|
24
|
+
|
25
|
+
{
|
26
|
+
1 => 0.22072766,
|
27
|
+
2 => 0.08120117,
|
28
|
+
3 => 0.02987224,
|
29
|
+
4 => 0.01098938,
|
30
|
+
5 => 0.00404277,
|
31
|
+
6 => 0.00148725,
|
32
|
+
7 => 0.00054713,
|
33
|
+
8 => 0.00020128,
|
34
|
+
9 => 0.00007405,
|
35
|
+
10 => 0.00002724,
|
36
|
+
11 => 0.00001002,
|
37
|
+
12 => 0.00000369,
|
38
|
+
13 => 0.00000136,
|
39
|
+
14 => 0.00000050,
|
40
|
+
15 => 0.00000018
|
41
|
+
|
42
|
+
}.each do |minutes, expected|
|
43
|
+
should "have a rate of #{expected} events/sec after #{minutes} minute(s)" do
|
44
|
+
mark_minutes(minutes)
|
45
|
+
@ewma.rate.should.be.close(expected, 0.00000001)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
describe 'A 5min EWMA with a value of 3' do
|
54
|
+
before do
|
55
|
+
@ewma = EWMA.five_minutes_ewma
|
56
|
+
@ewma.update(3)
|
57
|
+
@ewma.tick()
|
58
|
+
end
|
59
|
+
|
60
|
+
should "have a rate of 0.6 events/sec after the first tick" do
|
61
|
+
@ewma.rate.should.be.close(0.6, 0.000001)
|
62
|
+
end
|
63
|
+
|
64
|
+
def mark_minutes(minutes)
|
65
|
+
1.upto( (minutes*60.0) / 5 ) do
|
66
|
+
@ewma.tick()
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
{
|
71
|
+
1 => 0.49123845,
|
72
|
+
2 => 0.40219203,
|
73
|
+
3 => 0.32928698,
|
74
|
+
4 => 0.26959738,
|
75
|
+
5 => 0.22072766,
|
76
|
+
6 => 0.18071653,
|
77
|
+
7 => 0.14795818,
|
78
|
+
8 => 0.12113791,
|
79
|
+
9 => 0.09917933,
|
80
|
+
10 => 0.08120117,
|
81
|
+
11 => 0.06648190,
|
82
|
+
12 => 0.05443077,
|
83
|
+
13 => 0.04456415,
|
84
|
+
14 => 0.03648604,
|
85
|
+
15 => 0.02987224
|
86
|
+
}.each do |minutes, expected|
|
87
|
+
should "have a rate of #{expected} events/sec after #{minutes} minute(s)" do
|
88
|
+
mark_minutes(minutes)
|
89
|
+
@ewma.rate.should.be.close(expected, 0.00000001)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
describe 'A 15min EWMA with a value of 3' do
|
97
|
+
before do
|
98
|
+
@ewma = EWMA.fifteen_minutes_ewma
|
99
|
+
@ewma.update(3)
|
100
|
+
@ewma.tick()
|
101
|
+
end
|
102
|
+
|
103
|
+
should "have a rate of 0.6 events/sec after the first tick" do
|
104
|
+
@ewma.rate.should.be.close(0.6, 0.000001)
|
105
|
+
end
|
106
|
+
|
107
|
+
def mark_minutes(minutes)
|
108
|
+
1.upto( (minutes*60.0) / 5 ) do
|
109
|
+
@ewma.tick()
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
{
|
114
|
+
1 => 0.56130419,
|
115
|
+
2 => 0.52510399,
|
116
|
+
3 => 0.49123845,
|
117
|
+
4 => 0.45955700,
|
118
|
+
5 => 0.42991879,
|
119
|
+
6 => 0.40219203,
|
120
|
+
7 => 0.37625345,
|
121
|
+
8 => 0.35198773,
|
122
|
+
9 => 0.32928698,
|
123
|
+
10 => 0.30805027,
|
124
|
+
11 => 0.28818318,
|
125
|
+
12 => 0.26959738,
|
126
|
+
13 => 0.25221023,
|
127
|
+
14 => 0.23594443,
|
128
|
+
15 => 0.22072766
|
129
|
+
}.each do |minutes, expected|
|
130
|
+
should "have a rate of #{expected} events/sec after #{minutes} minute(s)" do
|
131
|
+
mark_minutes(minutes)
|
132
|
+
@ewma.rate.should.be.close(expected, 0.00000001)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require File.expand_path('../../common', __FILE__)
|
2
|
+
|
3
|
+
require 'drone/utils/exponentially_decaying_sample'
|
4
|
+
|
5
|
+
describe 'Exponentially Decaying Sample' do
|
6
|
+
describe "A sample of 100 out of 1000 elements" do
|
7
|
+
before do
|
8
|
+
@population = (0...100)
|
9
|
+
@sample = ExponentiallyDecayingSample.new(1000, 0.99)
|
10
|
+
@population.step(1){|n| @sample.update(n) }
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
should "have 100 elements" do
|
15
|
+
@sample.size.should == 100
|
16
|
+
@sample.values.size.should == 100
|
17
|
+
end
|
18
|
+
|
19
|
+
should "only have elements from the population" do
|
20
|
+
arr = @sample.values - @population.to_a
|
21
|
+
arr.should == []
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
describe "A sample of 100 out of 10 elements" do
|
28
|
+
before do
|
29
|
+
@population = (0...10)
|
30
|
+
@sample = ExponentiallyDecayingSample.new(100, 0.99)
|
31
|
+
@population.step(1){|n| @sample.update(n) }
|
32
|
+
end
|
33
|
+
|
34
|
+
should "have 10 elements" do
|
35
|
+
@sample.size.should == 10
|
36
|
+
@sample.values.size.should == 10
|
37
|
+
end
|
38
|
+
|
39
|
+
should "only have elements from the population" do
|
40
|
+
arr = @sample.values - @population.to_a
|
41
|
+
arr.should == []
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
describe "A heavily-biased sample of 100 out of 1000 elements" do
|
48
|
+
before do
|
49
|
+
@population = (0...100)
|
50
|
+
@sample = ExponentiallyDecayingSample.new(1000, 0.99)
|
51
|
+
@population.step(1){|n| @sample.update(n) }
|
52
|
+
end
|
53
|
+
|
54
|
+
should "have 100 elements" do
|
55
|
+
@sample.size.should == 100
|
56
|
+
@sample.values.size.should == 100
|
57
|
+
end
|
58
|
+
|
59
|
+
should "only have elements from the population" do
|
60
|
+
arr = @sample.values - @population.to_a
|
61
|
+
arr.should == []
|
62
|
+
end
|
63
|
+
|
64
|
+
should "rescale after 1 hour" do
|
65
|
+
@sample.expects(:rescale)
|
66
|
+
|
67
|
+
Delorean.time_travel_to("2 hours from now") do
|
68
|
+
@sample.update(1)
|
69
|
+
end
|
70
|
+
|
71
|
+
@sample.size.should == 101
|
72
|
+
@sample.values.size.should == 101
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'can rescale' do
|
76
|
+
@sample.rescale(Time.now)
|
77
|
+
@sample.values.should.not == []
|
78
|
+
# TODO: add a real test here, for now it only tests
|
79
|
+
# that the code actually runs
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require File.expand_path('../../common', __FILE__)
|
2
|
+
|
3
|
+
require 'drone/metrics/histogram'
|
4
|
+
|
5
|
+
include Drone
|
6
|
+
|
7
|
+
describe 'Histogram' do
|
8
|
+
describe "A histogram with zero recorded valeus" do
|
9
|
+
before do
|
10
|
+
@histogram = Histogram.new(UniformSample.new(100))
|
11
|
+
end
|
12
|
+
|
13
|
+
should "have a count of 0" do
|
14
|
+
@histogram.count.should == 0
|
15
|
+
end
|
16
|
+
|
17
|
+
should "have a max of 0" do
|
18
|
+
@histogram.max.should == 0
|
19
|
+
end
|
20
|
+
|
21
|
+
should "have a min of 0" do
|
22
|
+
@histogram.min.should == 0
|
23
|
+
end
|
24
|
+
|
25
|
+
should "have a mean of 0" do
|
26
|
+
@histogram.mean.should == 0.0
|
27
|
+
end
|
28
|
+
|
29
|
+
should "have a standard deviation of 0" do
|
30
|
+
@histogram.stdDev.should == 0
|
31
|
+
end
|
32
|
+
|
33
|
+
should "calculate percentiles" do
|
34
|
+
percentiles = @histogram.percentiles(0.5, 0.75, 0.99)
|
35
|
+
|
36
|
+
percentiles[0].should.be.close?(0, 0.01)
|
37
|
+
percentiles[1].should.be.close?(0, 0.01)
|
38
|
+
percentiles[2].should.be.close?(0, 0.01)
|
39
|
+
end
|
40
|
+
|
41
|
+
should "have no values" do
|
42
|
+
@histogram.values.should == []
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
describe "A histogram of the numbers 1 through 10000" do
|
48
|
+
before do
|
49
|
+
@histogram = Histogram.new( UniformSample.new(100000) )
|
50
|
+
(1..10000).each{|n| @histogram.update(n) }
|
51
|
+
end
|
52
|
+
|
53
|
+
should "have a count of 10000" do
|
54
|
+
@histogram.count.should == 10000
|
55
|
+
end
|
56
|
+
|
57
|
+
should "have a max value of 10000" do
|
58
|
+
@histogram.max.should == 10000
|
59
|
+
end
|
60
|
+
|
61
|
+
should "have a min value of 1" do
|
62
|
+
@histogram.min.should == 1
|
63
|
+
end
|
64
|
+
|
65
|
+
should "have a mean value of 5000.5" do
|
66
|
+
@histogram.mean.should.be.close?(5000.5, 0.01)
|
67
|
+
end
|
68
|
+
|
69
|
+
should "have a standard deviation of X" do
|
70
|
+
@histogram.stdDev.should.be.close?(2886.89, 0.1)
|
71
|
+
end
|
72
|
+
|
73
|
+
should "calculate percentiles" do
|
74
|
+
percentiles = @histogram.percentiles(0.5, 0.75, 0.99)
|
75
|
+
|
76
|
+
percentiles[0].should.be.close?(5000.5, 0.01)
|
77
|
+
percentiles[1].should.be.close?(7500.75, 0.01)
|
78
|
+
percentiles[2].should.be.close?(9900.99, 0.01)
|
79
|
+
end
|
80
|
+
|
81
|
+
should "have 10000 values" do
|
82
|
+
@histogram.values.should == (1..10000).to_a
|
83
|
+
# histogram.values.toList must beEqualTo((1 to 10000).toList)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require File.expand_path('../../common', __FILE__)
|
2
|
+
|
3
|
+
require 'drone'
|
4
|
+
require 'drone/monitoring'
|
5
|
+
|
6
|
+
EM.describe 'Monitoring' do
|
7
|
+
describe 'rate monitor' do
|
8
|
+
before do
|
9
|
+
|
10
|
+
Drone::init_drone()
|
11
|
+
|
12
|
+
@klass = Class.new() do
|
13
|
+
include Drone::Monitoring
|
14
|
+
|
15
|
+
monitor_rate("users/no_args")
|
16
|
+
def a_method_without_args; 42; end
|
17
|
+
|
18
|
+
monitor_rate("users/with_args")
|
19
|
+
def method_with_args(a, b); a + b; end
|
20
|
+
|
21
|
+
monitor_rate("users/with_block")
|
22
|
+
def method_with_block(&block); block.call; end
|
23
|
+
|
24
|
+
end
|
25
|
+
@obj = @klass.new
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
should 'reuse same meter for every instances of this class' do
|
30
|
+
meter = Drone::find_metric("users/no_args")
|
31
|
+
meter.count.should == 0
|
32
|
+
|
33
|
+
obj1 = @klass.new
|
34
|
+
obj2 = @klass.new
|
35
|
+
|
36
|
+
obj1.a_method_without_args()
|
37
|
+
meter.count.should == 1
|
38
|
+
|
39
|
+
obj2.a_method_without_args()
|
40
|
+
meter.count.should == 2
|
41
|
+
|
42
|
+
done
|
43
|
+
end
|
44
|
+
|
45
|
+
should 'increment counter on call' do
|
46
|
+
Drone::Metrics::Meter.any_instance.expects(:mark)
|
47
|
+
|
48
|
+
ret = @obj.a_method_without_args()
|
49
|
+
ret.should == 42
|
50
|
+
done
|
51
|
+
end
|
52
|
+
|
53
|
+
should 'be transparent for method with arguments' do
|
54
|
+
Drone::Metrics::Meter.any_instance.expects(:mark)
|
55
|
+
|
56
|
+
ret = @obj.method_with_args(4, 5)
|
57
|
+
ret.should == 9
|
58
|
+
done
|
59
|
+
end
|
60
|
+
|
61
|
+
should 'be transparent for method with block argument' do
|
62
|
+
Drone::Metrics::Meter.any_instance.expects(:mark)
|
63
|
+
|
64
|
+
ret = @obj.method_with_block(){ 32 }
|
65
|
+
ret.should == 32
|
66
|
+
done
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
describe 'timing monitor' do
|
73
|
+
before do
|
74
|
+
|
75
|
+
Drone::init_drone()
|
76
|
+
|
77
|
+
klass = Class.new() do
|
78
|
+
include Drone::Monitoring
|
79
|
+
|
80
|
+
monitor_time("users/no_args")
|
81
|
+
def a_method_without_args; 42; end
|
82
|
+
|
83
|
+
monitor_time("users/with_args")
|
84
|
+
def method_with_args(a, b); a + b; end
|
85
|
+
|
86
|
+
monitor_time("users/with_block")
|
87
|
+
def method_with_block(&block); block.call; end
|
88
|
+
|
89
|
+
end
|
90
|
+
@obj = klass.new
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
should 'time call with no args' do
|
95
|
+
Drone::Metrics::Timer.any_instance.expects(:update).with{|delay|
|
96
|
+
delay.should.be.close?(0, 0.001)
|
97
|
+
true
|
98
|
+
}
|
99
|
+
|
100
|
+
ret = @obj.a_method_without_args()
|
101
|
+
ret.should == 42
|
102
|
+
done
|
103
|
+
end
|
104
|
+
|
105
|
+
should 'time call with args' do
|
106
|
+
Drone::Metrics::Timer.any_instance.expects(:update).with{|delay|
|
107
|
+
delay.should.be.close?(0, 0.001)
|
108
|
+
true
|
109
|
+
}
|
110
|
+
|
111
|
+
ret = @obj.method_with_args(2, 4)
|
112
|
+
ret.should == 6
|
113
|
+
done
|
114
|
+
end
|
115
|
+
|
116
|
+
should 'time call with a block' do
|
117
|
+
Drone::Metrics::Timer.any_instance.expects(:update).with{|delay|
|
118
|
+
delay.should.be.close?(0, 0.001)
|
119
|
+
true
|
120
|
+
}
|
121
|
+
|
122
|
+
ret = @obj.method_with_block(){ 42 }
|
123
|
+
ret.should == 42
|
124
|
+
done
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.expand_path('../../common', __FILE__)
|
2
|
+
|
3
|
+
require 'drone/utils/uniform_sample'
|
4
|
+
|
5
|
+
describe 'EWMA' do
|
6
|
+
describe "A sample of 100 out of 1000 elements" do
|
7
|
+
before do
|
8
|
+
@population = (0...1000)
|
9
|
+
@sample = UniformSample.new(100)
|
10
|
+
@population.step(1){|n| @sample.update(n) }
|
11
|
+
end
|
12
|
+
|
13
|
+
should "have 100 elements" do
|
14
|
+
@sample.size.should == 100
|
15
|
+
@sample.values.size.should == 100
|
16
|
+
end
|
17
|
+
|
18
|
+
should "only have elements from the population" do
|
19
|
+
arr = @sample.values - @population.to_a
|
20
|
+
arr.should == []
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "A sample of 100 out of 10 elements" do
|
25
|
+
before do
|
26
|
+
@population = (0...10)
|
27
|
+
@sample = UniformSample.new(100)
|
28
|
+
@population.step(1){|n| @sample.update(n) }
|
29
|
+
end
|
30
|
+
|
31
|
+
should "have 10 elements" do
|
32
|
+
@sample.size.should == 10
|
33
|
+
@sample.values.size.should == 10
|
34
|
+
end
|
35
|
+
|
36
|
+
should "only have elements from the population" do
|
37
|
+
arr = @sample.values - @population.to_a
|
38
|
+
arr.should == []
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|