drone 1.0.4 → 1.0.5
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/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
|