drone 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|