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,61 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module Drone
|
4
|
+
module Schedulers
|
5
|
+
module EMScheduler
|
6
|
+
|
7
|
+
@started = false
|
8
|
+
@timers_once = []
|
9
|
+
@timers_periodic = []
|
10
|
+
|
11
|
+
##
|
12
|
+
# Schedule a block to be called immediatly and after
|
13
|
+
# that at a specified interval
|
14
|
+
#
|
15
|
+
# @param [Number] delay the interval
|
16
|
+
#
|
17
|
+
def self.schedule_periodic(delay, &block)
|
18
|
+
raise "Block required" unless block
|
19
|
+
if @started
|
20
|
+
block.call()
|
21
|
+
EM::add_periodic_timer(delay, &block)
|
22
|
+
else
|
23
|
+
@timers_periodic << [delay, block]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
##
|
29
|
+
# Schedule a block to be called after a specified
|
30
|
+
# delay
|
31
|
+
#
|
32
|
+
# @param [Number] delay the interval
|
33
|
+
#
|
34
|
+
def self.schedule_once(delay, &block)
|
35
|
+
raise "Block required" unless block
|
36
|
+
if @started
|
37
|
+
EM::add_timer(delay, &block)
|
38
|
+
else
|
39
|
+
@timers_once << [delay, block]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
##
|
45
|
+
# Start the timers.
|
46
|
+
#
|
47
|
+
def self.start
|
48
|
+
@started = true
|
49
|
+
@timers_once.each do |(delay, block)|
|
50
|
+
schedule_once(delay, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
@timers_periodic.each do |(delay, block)|
|
54
|
+
schedule_periodic(delay, &block)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# ruby adaptation of the metrics library version by Coda Hale
|
2
|
+
class EWMA
|
3
|
+
M1_ALPHA = (1 - Math.exp(-5 / 60.0)).freeze
|
4
|
+
M5_ALPHA = (1 - Math.exp(-5 / 60.0 / 5)).freeze
|
5
|
+
M15_ALPHA = (1 - Math.exp(-5 / 60.0 / 15)).freeze
|
6
|
+
|
7
|
+
def self.one_minute_ewma
|
8
|
+
new(M1_ALPHA, 5000)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.five_minutes_ewma
|
12
|
+
new(M5_ALPHA, 5000)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.fifteen_minutes_ewma
|
16
|
+
new(M15_ALPHA, 5000)
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# interval: in ms
|
21
|
+
def initialize(alpha, interval)
|
22
|
+
@alpha = alpha
|
23
|
+
@interval = interval.to_f # * (1000*1000)
|
24
|
+
@uncounted = 0
|
25
|
+
@rate = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def update(n)
|
29
|
+
@uncounted += n
|
30
|
+
end
|
31
|
+
|
32
|
+
def tick()
|
33
|
+
count = @uncounted
|
34
|
+
@uncounted = 0
|
35
|
+
instant_rate = count / @interval
|
36
|
+
if @rate
|
37
|
+
@rate += (@alpha * (instant_rate - @rate))
|
38
|
+
else
|
39
|
+
@rate = instant_rate
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def rate(as = :seconds)
|
44
|
+
case as
|
45
|
+
when :ms then @rate
|
46
|
+
when :seconds then @rate * 1000
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
|
2
|
+
class ExponentiallyDecayingSample
|
3
|
+
# 1 hour in ms
|
4
|
+
RESCALE_THRESHOLD = (1 * 60 * 60 * 1000).freeze
|
5
|
+
|
6
|
+
def initialize(reservoir_size, alpha)
|
7
|
+
@values = {}
|
8
|
+
@alpha = alpha
|
9
|
+
@reservoir_size = reservoir_size
|
10
|
+
clear()
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear
|
14
|
+
@values.clear()
|
15
|
+
@count = 0
|
16
|
+
@start_time = Time.now
|
17
|
+
@next_scale_time = current_time() + RESCALE_THRESHOLD
|
18
|
+
end
|
19
|
+
|
20
|
+
def size
|
21
|
+
(@values.size < @count) ? @values.size : @count
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def update(val, time = Time.now)
|
26
|
+
priority = weight(time - @start_time) / rand()
|
27
|
+
@count += 1
|
28
|
+
if @count <= @reservoir_size
|
29
|
+
@values[priority] = val
|
30
|
+
else
|
31
|
+
first = @values.keys.min
|
32
|
+
if first < priority
|
33
|
+
@values[priority] = val
|
34
|
+
while @values.delete(first) == nil
|
35
|
+
first = @values.keys.min
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
now = current_time()
|
41
|
+
if now >= @next_scale_time
|
42
|
+
rescale(now, @next_scale_time)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def values
|
47
|
+
@values.values
|
48
|
+
end
|
49
|
+
|
50
|
+
def rescale(now)
|
51
|
+
@next_scale_time = current_time() + RESCALE_THRESHOLD
|
52
|
+
old_start = @start_time
|
53
|
+
@start_time = Time.now
|
54
|
+
|
55
|
+
@values = Hash[ @values.map{ |k,v|
|
56
|
+
[k * Math.exp(-@alpha * (@start_time - old_start)), v]
|
57
|
+
}]
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def current_time
|
64
|
+
Time.now.to_f * 1000
|
65
|
+
end
|
66
|
+
|
67
|
+
def weight(n)
|
68
|
+
Math.exp(@alpha * n)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class UniformSample
|
2
|
+
|
3
|
+
def initialize(size)
|
4
|
+
@values = Array.new(size)
|
5
|
+
clear()
|
6
|
+
end
|
7
|
+
|
8
|
+
def clear
|
9
|
+
@values.size.times do |n|
|
10
|
+
@values[n] = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
@count = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def size
|
17
|
+
(@count > @values.size) ? @values.size : @count
|
18
|
+
end
|
19
|
+
|
20
|
+
def update(val)
|
21
|
+
@count += 1
|
22
|
+
if @count <= @values.size
|
23
|
+
@values[@count - 1] = val
|
24
|
+
else
|
25
|
+
r = rand(2**64 - 1) % @count
|
26
|
+
if r < @values.size
|
27
|
+
@values[r] = val
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def values
|
33
|
+
# only return @count elements
|
34
|
+
@values[0,@count]
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
data/specs/common.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
$:.reject! { |e| e.include? 'TextMate' }
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
puts "Testing with ruby #{RUBY_VERSION} and rubygems #{Gem::VERSION}"
|
6
|
+
|
7
|
+
require 'bundler/setup'
|
8
|
+
|
9
|
+
if (RUBY_VERSION >= "1.9") && ENV['COVERAGE']
|
10
|
+
require 'simplecov'
|
11
|
+
ROOT = File.expand_path('../../', __FILE__)
|
12
|
+
|
13
|
+
puts "[[ SimpleCov enabled ]]"
|
14
|
+
|
15
|
+
SimpleCov.start do
|
16
|
+
add_filter '/gems/'
|
17
|
+
add_filter '/specs/'
|
18
|
+
|
19
|
+
root(ROOT)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'bacon'
|
24
|
+
require 'mocha'
|
25
|
+
require 'delorean'
|
26
|
+
require 'em-spec/bacon'
|
27
|
+
EM.spec_backend = EventMachine::Spec::Bacon
|
28
|
+
|
29
|
+
$LOAD_PATH << File.expand_path('../../lib', __FILE__)
|
30
|
+
|
31
|
+
module Bacon
|
32
|
+
module MochaRequirementsCounter
|
33
|
+
def self.increment
|
34
|
+
Counter[:requirements] += 1
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Context
|
39
|
+
include Mocha::API
|
40
|
+
|
41
|
+
alias_method :it_before_mocha, :it
|
42
|
+
|
43
|
+
def it(description)
|
44
|
+
it_before_mocha(description) do
|
45
|
+
begin
|
46
|
+
mocha_setup
|
47
|
+
yield
|
48
|
+
mocha_verify(MochaRequirementsCounter)
|
49
|
+
rescue Mocha::ExpectationError => e
|
50
|
+
raise Error.new(:failed, "#{e.message}\n#{e.backtrace[0...10].join("\n")}")
|
51
|
+
ensure
|
52
|
+
mocha_teardown
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def focus(test_label)
|
60
|
+
Bacon.const_set(:RestrictName, %r{#{test_label}})
|
61
|
+
end
|
62
|
+
|
63
|
+
Bacon.summary_on_exit()
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.expand_path('../../common', __FILE__)
|
2
|
+
|
3
|
+
require 'drone/metrics/counter'
|
4
|
+
include Drone
|
5
|
+
|
6
|
+
describe 'Counter Metrics' do
|
7
|
+
before do
|
8
|
+
@counter = Metrics::Counter.new('something')
|
9
|
+
end
|
10
|
+
|
11
|
+
should "start at zero" do
|
12
|
+
@counter.value.should == 0
|
13
|
+
end
|
14
|
+
|
15
|
+
should "increment by one" do
|
16
|
+
@counter.inc()
|
17
|
+
@counter.value.should == 1
|
18
|
+
end
|
19
|
+
|
20
|
+
should "increment by an arbitrary delta" do
|
21
|
+
@counter.inc(3)
|
22
|
+
@counter.value.should == 3
|
23
|
+
end
|
24
|
+
|
25
|
+
should "decrement by one" do
|
26
|
+
@counter.dec()
|
27
|
+
@counter.value.should == -1
|
28
|
+
end
|
29
|
+
|
30
|
+
should "decrement by an arbitrary delta" do
|
31
|
+
@counter.dec(3)
|
32
|
+
@counter.value.should == -3
|
33
|
+
end
|
34
|
+
|
35
|
+
should "be zero after being cleared" do
|
36
|
+
@counter.inc(3)
|
37
|
+
@counter.clear()
|
38
|
+
@counter.value.should == 0
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.expand_path('../../common', __FILE__)
|
2
|
+
|
3
|
+
require 'drone/metrics/gauge'
|
4
|
+
include Drone
|
5
|
+
|
6
|
+
describe 'Geuge Metric' do
|
7
|
+
before do
|
8
|
+
@n = 0
|
9
|
+
@gauge = Metrics::Gauge.new("temperature"){ @n+= 1 }
|
10
|
+
end
|
11
|
+
|
12
|
+
should 'require a block' do
|
13
|
+
err = proc{
|
14
|
+
Metrics::Gauge.new('dummy')
|
15
|
+
}.should.raise(RuntimeError)
|
16
|
+
|
17
|
+
err.message.should.include?('Block expected')
|
18
|
+
end
|
19
|
+
|
20
|
+
should 'call block when value is asked' do
|
21
|
+
@n.should == 0
|
22
|
+
@gauge.value.should == 1
|
23
|
+
@n.should == 1
|
24
|
+
|
25
|
+
@gauge.value.should == 2
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.expand_path('../../common', __FILE__)
|
2
|
+
|
3
|
+
require 'drone/metrics/meter'
|
4
|
+
|
5
|
+
include Drone
|
6
|
+
|
7
|
+
EM.describe 'Meter Metrics' do
|
8
|
+
before do
|
9
|
+
Drone::init_drone()
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "A meter metric with no events" do
|
13
|
+
before do
|
14
|
+
@meter = Metrics::Meter.new("thangs")
|
15
|
+
end
|
16
|
+
|
17
|
+
should "have a count of zero" do
|
18
|
+
@meter.count.should == 0
|
19
|
+
done
|
20
|
+
end
|
21
|
+
|
22
|
+
should "have a mean rate of 0 events/sec" do
|
23
|
+
@meter.mean_rate.should == 0.0
|
24
|
+
done
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "A meter metric with three events" do
|
29
|
+
before do
|
30
|
+
@meter = Metrics::Meter.new("thangs")
|
31
|
+
@meter.mark(3)
|
32
|
+
end
|
33
|
+
|
34
|
+
should "have a count of three" do
|
35
|
+
@meter.count.should == 3
|
36
|
+
done
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require File.expand_path('../../common', __FILE__)
|
2
|
+
|
3
|
+
require 'drone/metrics/timer'
|
4
|
+
include Drone
|
5
|
+
|
6
|
+
EM.describe 'Timer Metrics' do
|
7
|
+
before do
|
8
|
+
Drone::init_drone()
|
9
|
+
Drone::start_monitoring()
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "A blank timer" do
|
13
|
+
before do
|
14
|
+
@timer = Metrics::Timer.new()
|
15
|
+
end
|
16
|
+
|
17
|
+
should "have a max of zero" do
|
18
|
+
@timer.max.should.be.close?(0, 0.001)
|
19
|
+
done
|
20
|
+
end
|
21
|
+
|
22
|
+
should "have a min of zero" do
|
23
|
+
@timer.min.should.be.close?(0, 0.001)
|
24
|
+
done
|
25
|
+
end
|
26
|
+
|
27
|
+
should "have a mean of zero" do
|
28
|
+
@timer.mean.should.be.close?(0, 0.001)
|
29
|
+
done
|
30
|
+
end
|
31
|
+
|
32
|
+
should "have a count of zero" do
|
33
|
+
@timer.count.should == 0
|
34
|
+
done
|
35
|
+
end
|
36
|
+
|
37
|
+
should "have a standard deviation of zero" do
|
38
|
+
@timer.stdDev.should.be.close?(0, 0.001)
|
39
|
+
done
|
40
|
+
end
|
41
|
+
|
42
|
+
should "have a median/p95/p98/p99/p999 of zero" do
|
43
|
+
median, p95, p98, p99, p999 = @timer.percentiles(0.5, 0.95, 0.98, 0.99, 0.999)
|
44
|
+
median.should.be.close?(0, 0.001)
|
45
|
+
p95.should.be.close?(0, 0.001)
|
46
|
+
p98.should.be.close?(0, 0.001)
|
47
|
+
p99.should.be.close?(0, 0.001)
|
48
|
+
p999.should.be.close?(0, 0.001)
|
49
|
+
done
|
50
|
+
end
|
51
|
+
|
52
|
+
should "have a mean rate of zero" do
|
53
|
+
@timer.mean_rate.should.be.close?(0, 0.001)
|
54
|
+
done
|
55
|
+
end
|
56
|
+
|
57
|
+
should "have a one-minute rate of zero" do
|
58
|
+
@timer.one_minute_rate.should.be.close?(0, 0.001)
|
59
|
+
done
|
60
|
+
end
|
61
|
+
|
62
|
+
should "have a five-minute rate of zero" do
|
63
|
+
@timer.five_minutes_rate.should.be.close?(0, 0.001)
|
64
|
+
done
|
65
|
+
end
|
66
|
+
|
67
|
+
should "have a fifteen-minute rate of zero" do
|
68
|
+
@timer.fifteen_minutes_rate.should.be.close?(0, 0.001)
|
69
|
+
done
|
70
|
+
end
|
71
|
+
|
72
|
+
should "have no values" do
|
73
|
+
@timer.values.should == []
|
74
|
+
done
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
|
80
|
+
describe "Timing a series of events" do
|
81
|
+
before do
|
82
|
+
@timer = Metrics::Timer.new()
|
83
|
+
@timer.update(10)
|
84
|
+
@timer.update(20)
|
85
|
+
@timer.update(20)
|
86
|
+
@timer.update(30)
|
87
|
+
@timer.update(40)
|
88
|
+
end
|
89
|
+
|
90
|
+
should "record the count" do
|
91
|
+
@timer.count.should == 5
|
92
|
+
done
|
93
|
+
end
|
94
|
+
|
95
|
+
should "calculate the minimum duration" do
|
96
|
+
@timer.min.should.be.close?(10, 0.001)
|
97
|
+
done
|
98
|
+
end
|
99
|
+
|
100
|
+
should "calclate the maximum duration" do
|
101
|
+
@timer.max.should.be.close?(40, 0.001)
|
102
|
+
done
|
103
|
+
end
|
104
|
+
|
105
|
+
should "calclate the mean duration" do
|
106
|
+
@timer.mean.should.be.close?(24, 0.001)
|
107
|
+
done
|
108
|
+
end
|
109
|
+
|
110
|
+
should "calclate the standard deviation" do
|
111
|
+
@timer.stdDev.should.be.close?(11.401, 0.001)
|
112
|
+
done
|
113
|
+
end
|
114
|
+
|
115
|
+
should "calculate the median/p95/p98/p99/p999" do
|
116
|
+
median, p95, p98, p99, p999 = @timer.percentiles(0.5, 0.95, 0.98, 0.99, 0.999)
|
117
|
+
median.should.be.close?(20, 0.001)
|
118
|
+
p95.should.be.close?(40, 0.001)
|
119
|
+
p98.should.be.close?(40, 0.001)
|
120
|
+
p99.should.be.close?(40, 0.001)
|
121
|
+
p999.should.be.close?(40, 0.001)
|
122
|
+
done
|
123
|
+
end
|
124
|
+
|
125
|
+
should "have a series of values" do
|
126
|
+
@timer.values.sort.should == [10, 20, 20, 30, 40]
|
127
|
+
done
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|