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,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
|