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,83 @@
|
|
1
|
+
require File.expand_path('../base', __FILE__)
|
2
|
+
|
3
|
+
module Drone
|
4
|
+
module Interfaces
|
5
|
+
##
|
6
|
+
# This interface is meant for debug mainly, it will
|
7
|
+
# simply output all the available metrics at a regular
|
8
|
+
# interval on the console.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# require 'drone'
|
12
|
+
# Drone::init_drone()
|
13
|
+
# Drone::add_output(:console, 1)
|
14
|
+
#
|
15
|
+
class Console < Base
|
16
|
+
|
17
|
+
def output()
|
18
|
+
puts ""
|
19
|
+
puts "[#{Time.now.strftime('%M:%S')}] Drone report:"
|
20
|
+
Drone::each_metric do |m|
|
21
|
+
case m
|
22
|
+
when Metrics::Gauge
|
23
|
+
puts "[Gauge] #{m.name} : #{m.value}"
|
24
|
+
|
25
|
+
when Metrics::Counter
|
26
|
+
puts "[Counter] #{m.name} : #{m.value}"
|
27
|
+
|
28
|
+
when Metrics::Timer
|
29
|
+
puts "[Timer] #{m.name}"
|
30
|
+
print_meter(m)
|
31
|
+
print_histogram(m)
|
32
|
+
|
33
|
+
when Metrics::Meter
|
34
|
+
puts "[Meter] #{m.name}"
|
35
|
+
print_meter(m)
|
36
|
+
|
37
|
+
when Metrics::Histogram
|
38
|
+
puts "[Histogram] #{m.name}"
|
39
|
+
print_histogram(m)
|
40
|
+
|
41
|
+
else
|
42
|
+
puts "Unknown metric: #{m}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
private
|
49
|
+
def print_meter(m)
|
50
|
+
puts format("%20s : %d", "count", m.count)
|
51
|
+
|
52
|
+
{
|
53
|
+
'mean rate' => m.mean_rate,
|
54
|
+
'1-minute rate' => m.one_minute_rate,
|
55
|
+
'5-minute rate' => m.five_minutes_rate,
|
56
|
+
'15-minute rate' => m.fifteen_minutes_rate
|
57
|
+
}.each do |label, value|
|
58
|
+
puts format("%20s : %2.2f", label, value)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def print_histogram(m)
|
63
|
+
percentiles = m.percentiles(0.5, 0.75, 0.95, 0.98, 0.99, 0.999)
|
64
|
+
{
|
65
|
+
'min' => m.min,
|
66
|
+
'max' => m.max,
|
67
|
+
'mean' => m.mean,
|
68
|
+
'stddev' => m.stdDev,
|
69
|
+
'median' => percentiles[0],
|
70
|
+
'75%' => percentiles[1],
|
71
|
+
'%95' => percentiles[2],
|
72
|
+
'%98' => percentiles[3],
|
73
|
+
'%99' => percentiles[4],
|
74
|
+
'%99.9' => percentiles[5]
|
75
|
+
}.each do |label, value|
|
76
|
+
puts format("%20s : %2.2f", label, value)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Drone
|
2
|
+
module Metrics
|
3
|
+
|
4
|
+
class Counter
|
5
|
+
attr_reader :value, :name
|
6
|
+
|
7
|
+
def initialize(name, initial_value = 0)
|
8
|
+
@name = name
|
9
|
+
@value = initial_value
|
10
|
+
end
|
11
|
+
|
12
|
+
def increment(n = 1)
|
13
|
+
@value += n
|
14
|
+
end
|
15
|
+
alias :inc :increment
|
16
|
+
|
17
|
+
def decrement(n = 1)
|
18
|
+
@value -= n
|
19
|
+
end
|
20
|
+
alias :dec :decrement
|
21
|
+
|
22
|
+
def clear
|
23
|
+
@value = 0
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Drone
|
2
|
+
module Metrics
|
3
|
+
|
4
|
+
##
|
5
|
+
# Gauge are linked to a block of code which
|
6
|
+
# will be called when the value is asked, the block
|
7
|
+
# is expected to return a number
|
8
|
+
#
|
9
|
+
class Gauge
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
def initialize(name, &block)
|
13
|
+
raise "Block expected" unless block
|
14
|
+
@name = name
|
15
|
+
@block = block
|
16
|
+
end
|
17
|
+
|
18
|
+
def value
|
19
|
+
@block.call()
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require File.expand_path('../../utils/uniform_sample', __FILE__)
|
2
|
+
require File.expand_path('../../utils/exponentially_decaying_sample', __FILE__)
|
3
|
+
|
4
|
+
module Drone
|
5
|
+
class Histogram
|
6
|
+
TYPE_UNIFORM = lambda{ UniformSample.new(1028) }
|
7
|
+
TYPE_BIASED = lambda{ ExponentiallyDecayingSample.new(1028, 0.015) }
|
8
|
+
|
9
|
+
MIN = (-(2**63)).freeze
|
10
|
+
MAX = ((2**64) - 1).freeze
|
11
|
+
|
12
|
+
def initialize(sample_or_type = TYPE_UNIFORM)
|
13
|
+
if sample_or_type.is_a?(Proc)
|
14
|
+
@sample = sample_or_type.call()
|
15
|
+
else
|
16
|
+
@sample = sample_or_type
|
17
|
+
end
|
18
|
+
|
19
|
+
clear()
|
20
|
+
end
|
21
|
+
|
22
|
+
def clear
|
23
|
+
@sample.clear()
|
24
|
+
@count = 0
|
25
|
+
@_min = MAX
|
26
|
+
@_max = MIN
|
27
|
+
@_sum = 0
|
28
|
+
@varianceM = -1
|
29
|
+
@varianceS = 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def update(val)
|
33
|
+
@count += 1
|
34
|
+
@sample.update(val)
|
35
|
+
set_max(val);
|
36
|
+
set_min(val);
|
37
|
+
@_sum += val
|
38
|
+
update_variance(val)
|
39
|
+
end
|
40
|
+
|
41
|
+
def count
|
42
|
+
@count
|
43
|
+
end
|
44
|
+
|
45
|
+
def max
|
46
|
+
(@count > 0) ? @_max : 0.0
|
47
|
+
end
|
48
|
+
|
49
|
+
def min
|
50
|
+
(@count > 0) ? @_min : 0.0
|
51
|
+
end
|
52
|
+
|
53
|
+
def mean
|
54
|
+
(@count > 0) ? @_sum.to_f / @count : 0.0
|
55
|
+
end
|
56
|
+
|
57
|
+
def stdDev
|
58
|
+
(@count > 0) ? Math.sqrt( variance() ) : 0.0
|
59
|
+
end
|
60
|
+
|
61
|
+
def percentiles(*percentiles)
|
62
|
+
scores = Array.new(percentiles.size, 0)
|
63
|
+
if @count > 0
|
64
|
+
values = @sample.values.sort
|
65
|
+
percentiles.each.with_index do |p, i|
|
66
|
+
pos = p * (values.size + 1)
|
67
|
+
if pos < 1
|
68
|
+
scores[i] = values[0]
|
69
|
+
elsif pos >= values.size
|
70
|
+
scores[i] = values[-1]
|
71
|
+
else
|
72
|
+
lower = values[pos - 1]
|
73
|
+
upper = values[pos]
|
74
|
+
scores[i] = lower + (pos - pos.floor) * (upper - lower)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
scores
|
80
|
+
end
|
81
|
+
|
82
|
+
def values
|
83
|
+
@sample.values
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def doubleToLongBits(n)
|
89
|
+
[n].pack('D').unpack('q')[0]
|
90
|
+
end
|
91
|
+
|
92
|
+
def longBitsToDouble(n)
|
93
|
+
[n].pack('q').unpack('D')[0]
|
94
|
+
end
|
95
|
+
|
96
|
+
def update_variance(val)
|
97
|
+
if @varianceM == -1
|
98
|
+
@varianceM = doubleToLongBits(val)
|
99
|
+
else
|
100
|
+
oldMCas = @varianceM
|
101
|
+
oldM = longBitsToDouble(oldMCas)
|
102
|
+
newM = oldM + ((val - oldM) / count())
|
103
|
+
|
104
|
+
oldSCas = @varianceS
|
105
|
+
oldS = longBitsToDouble(oldSCas)
|
106
|
+
newS = oldS + ((val - oldM) * (val - newM))
|
107
|
+
|
108
|
+
@varianceM = doubleToLongBits(newM)
|
109
|
+
@varianceS = doubleToLongBits(newS)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def variance
|
114
|
+
if @count <= 1
|
115
|
+
0.0
|
116
|
+
else
|
117
|
+
longBitsToDouble(@varianceS) / (count() - 1)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def set_max(val)
|
122
|
+
(@_max >= val) || @_max = val
|
123
|
+
end
|
124
|
+
|
125
|
+
def set_min(val)
|
126
|
+
(@_min <= val) || @_min = val
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
require File.expand_path('../../core', __FILE__)
|
4
|
+
require File.expand_path('../../utils/ewma', __FILE__)
|
5
|
+
|
6
|
+
module Drone
|
7
|
+
module Metrics
|
8
|
+
# A meter metric which measures mean throughput and one-, five-, and
|
9
|
+
# fifteen-minute exponentially-weighted moving average throughputs.
|
10
|
+
class Meter
|
11
|
+
INTERVAL = 5
|
12
|
+
|
13
|
+
attr_reader :count, :name
|
14
|
+
|
15
|
+
def initialize(name)
|
16
|
+
@name = name
|
17
|
+
@start_time = Time.now
|
18
|
+
@count = 0
|
19
|
+
@rates = {
|
20
|
+
1 => EWMA.one_minute_ewma,
|
21
|
+
5 => EWMA.five_minutes_ewma,
|
22
|
+
15 => EWMA.fifteen_minutes_ewma
|
23
|
+
}
|
24
|
+
|
25
|
+
Drone::schedule_periodic(INTERVAL){ tick() }
|
26
|
+
end
|
27
|
+
|
28
|
+
def tick
|
29
|
+
@rates.values.each(&:tick)
|
30
|
+
end
|
31
|
+
|
32
|
+
def mark(events = 1)
|
33
|
+
@count += events
|
34
|
+
@rates.each do |_, r|
|
35
|
+
r.update(events)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def mean_rate
|
40
|
+
if @count == 0
|
41
|
+
0.0
|
42
|
+
else
|
43
|
+
elapsed = Time.now.to_f - @start_time.to_f
|
44
|
+
@count / elapsed
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def one_minute_rate
|
49
|
+
@rates[1].rate()
|
50
|
+
end
|
51
|
+
|
52
|
+
def five_minutes_rate
|
53
|
+
@rates[5].rate()
|
54
|
+
end
|
55
|
+
|
56
|
+
def fifteen_minutes_rate
|
57
|
+
@rates[15].rate()
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
require File.expand_path('../histogram', __FILE__)
|
3
|
+
require File.expand_path('..//meter', __FILE__)
|
4
|
+
|
5
|
+
module Drone
|
6
|
+
module Metrics
|
7
|
+
class Timer
|
8
|
+
def initialize(name = 'calls')
|
9
|
+
@histogram = Histogram.new(Histogram::TYPE_BIASED)
|
10
|
+
@meter = Meter.new(name)
|
11
|
+
clear()
|
12
|
+
end
|
13
|
+
|
14
|
+
def count
|
15
|
+
@histogram.count
|
16
|
+
end
|
17
|
+
|
18
|
+
[:fifteen_minutes_rate, :five_minutes_rate, :mean_rate, :one_minute_rate].each do |attr_name|
|
19
|
+
define_method(attr_name) do
|
20
|
+
@meter.send(attr_name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# may requires a conversion... or not
|
25
|
+
[:count, :min, :max, :mean, :stdDev, :percentiles, :values].each do |attr_name|
|
26
|
+
define_method(attr_name) do |*args|
|
27
|
+
@histogram.send(attr_name, *args)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def clear
|
32
|
+
@histogram.clear()
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# duration: milliseconds
|
37
|
+
def update(duration)
|
38
|
+
if duration >= 0
|
39
|
+
@histogram.update(duration)
|
40
|
+
@meter.mark()
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# time and record the duration of the block
|
46
|
+
def time
|
47
|
+
started_at = Time.now.to_f
|
48
|
+
yield()
|
49
|
+
ensure
|
50
|
+
update(Time.now.to_f - started_at.to_f)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
|
55
|
+
|
56
|
+
def name
|
57
|
+
@meter.name
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require File.expand_path('../metrics/meter', __FILE__)
|
2
|
+
require File.expand_path('../metrics/timer', __FILE__)
|
3
|
+
|
4
|
+
module Drone
|
5
|
+
##
|
6
|
+
# This module contains what is needed to instruments
|
7
|
+
# class methods easily
|
8
|
+
#
|
9
|
+
module Monitoring
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval do
|
12
|
+
extend ClassMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
Drone::register_monitored_class(base)
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
# external API
|
20
|
+
|
21
|
+
##
|
22
|
+
# Monitor the call rate of the following method
|
23
|
+
#
|
24
|
+
# @param [String] name metric name, it must be unique and will be shared
|
25
|
+
# among all the objects of this class
|
26
|
+
# @api public
|
27
|
+
#
|
28
|
+
def monitor_rate(name)
|
29
|
+
meter = Drone::find_metric(name) || Metrics::Meter.new(name)
|
30
|
+
unless meter.is_a?(Metrics::Meter)
|
31
|
+
raise(TypeError, "metric #{name} is already defined as #{rate.class}")
|
32
|
+
end
|
33
|
+
|
34
|
+
Drone::register_meter(meter)
|
35
|
+
@_rate_waiting = meter
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Monitor the time of execution as well as the
|
40
|
+
# call rate
|
41
|
+
#
|
42
|
+
# @param [String] name metric name, it must be unique and will be shared
|
43
|
+
# among all the objects of this class
|
44
|
+
#
|
45
|
+
# @api public
|
46
|
+
#
|
47
|
+
def monitor_time(name)
|
48
|
+
timer = Drone::find_metric(name) || Metrics::Timer.new(name)
|
49
|
+
unless timer.is_a?(Metrics::Timer)
|
50
|
+
raise(TypeError, "metric #{name} is already defined as #{rate.class}")
|
51
|
+
end
|
52
|
+
Drone::register_meter(timer)
|
53
|
+
@_timer_waiting = timer
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
# internals
|
58
|
+
|
59
|
+
##
|
60
|
+
# @private
|
61
|
+
#
|
62
|
+
def method_added(m)
|
63
|
+
return if @_ignore_added
|
64
|
+
|
65
|
+
@_ignore_added = true
|
66
|
+
ma_rate_meter(m) if @_rate_waiting
|
67
|
+
ma_timer_meter(m) if @_timer_waiting
|
68
|
+
@_ignore_added = false
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# @private
|
73
|
+
#
|
74
|
+
def ma_rate_meter(m)
|
75
|
+
rate = @_rate_waiting
|
76
|
+
@_rate_waiting = nil
|
77
|
+
|
78
|
+
define_method("instrumented_#{m}") do |*args, &block|
|
79
|
+
rate.mark()
|
80
|
+
send("original_#{m}", *args, &block)
|
81
|
+
end
|
82
|
+
|
83
|
+
alias_method "original_#{m}", m
|
84
|
+
alias_method m, "instrumented_#{m}"
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# @private
|
89
|
+
#
|
90
|
+
def ma_timer_meter(m)
|
91
|
+
timer = @_timer_waiting
|
92
|
+
@_timer_waiting = nil
|
93
|
+
|
94
|
+
define_method("instrumented_#{m}") do |*args, &block|
|
95
|
+
timer.time do
|
96
|
+
send("original_#{m}", *args, &block)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
alias_method "original_#{m}", m
|
101
|
+
alias_method m, "instrumented_#{m}"
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|