drone 1.0.2 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/.rvmrc +1 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +162 -0
- data/Rakefile +49 -0
- data/drone.gemspec +29 -0
- data/examples/simple.rb +50 -0
- data/lib/drone.rb +23 -0
- data/lib/drone/core.rb +141 -0
- data/lib/drone/interfaces/base.rb +17 -0
- data/lib/drone/interfaces/console.rb +82 -0
- data/lib/drone/metrics/counter.rb +40 -0
- data/lib/drone/metrics/gauge.rb +25 -0
- data/lib/drone/metrics/histogram.rb +153 -0
- data/lib/drone/metrics/meter.rb +82 -0
- data/lib/drone/metrics/metric.rb +16 -0
- data/lib/drone/metrics/timer.rb +57 -0
- data/lib/drone/monitoring.rb +107 -0
- data/lib/drone/schedulers/eventmachine.rb +70 -0
- data/lib/drone/storage/base.rb +122 -0
- data/lib/drone/storage/memory.rb +58 -0
- data/lib/drone/utils/ewma.rb +55 -0
- data/lib/drone/utils/exponentially_decaying_sample.rb +81 -0
- data/lib/drone/utils/uniform_sample.rb +52 -0
- data/lib/drone/version.rb +3 -0
- data/specs/all.rb +11 -0
- data/specs/common.rb +63 -0
- data/specs/metrics/counter_spec.rb +43 -0
- data/specs/metrics/gauge_spec.rb +28 -0
- data/specs/metrics/meter_spec.rb +61 -0
- data/specs/metrics/timer_spec.rb +111 -0
- data/specs/schedulers/eventmachine_spec.rb +76 -0
- data/specs/unit/ewma_spec.rb +141 -0
- data/specs/unit/exponentially_decaying_sample_spec.rb +86 -0
- data/specs/unit/histogram_spec.rb +91 -0
- data/specs/unit/monitoring_spec.rb +129 -0
- data/specs/unit/uniform_sample_spec.rb +46 -0
- metadata +42 -5
@@ -0,0 +1,82 @@
|
|
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_histogram(m)
|
31
|
+
|
32
|
+
when Metrics::Meter
|
33
|
+
puts "[Meter] #{m.name}"
|
34
|
+
print_meter(m)
|
35
|
+
|
36
|
+
when Metrics::Histogram
|
37
|
+
puts "[Histogram] #{m.name}"
|
38
|
+
print_histogram(m)
|
39
|
+
|
40
|
+
else
|
41
|
+
puts "Unknown metric: #{m}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
private
|
48
|
+
def print_meter(m)
|
49
|
+
puts format("%20s : %d", "count", m.count)
|
50
|
+
|
51
|
+
{
|
52
|
+
'mean rate' => m.mean_rate,
|
53
|
+
'1-minute rate' => m.one_minute_rate,
|
54
|
+
'5-minute rate' => m.five_minutes_rate,
|
55
|
+
'15-minute rate' => m.fifteen_minutes_rate
|
56
|
+
}.each do |label, value|
|
57
|
+
puts format("%20s : %2.2f", label, value)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def print_histogram(m)
|
62
|
+
percentiles = m.percentiles(0.5, 0.75, 0.95, 0.98, 0.99, 0.999)
|
63
|
+
{
|
64
|
+
'min' => m.min,
|
65
|
+
'max' => m.max,
|
66
|
+
'mean' => m.mean,
|
67
|
+
'stddev' => m.stdDev,
|
68
|
+
'median' => percentiles[0],
|
69
|
+
'75%' => percentiles[1],
|
70
|
+
'%95' => percentiles[2],
|
71
|
+
'%98' => percentiles[3],
|
72
|
+
'%99' => percentiles[4],
|
73
|
+
'%99.9' => percentiles[5]
|
74
|
+
}.each do |label, value|
|
75
|
+
puts format("%20s : %2.2f", label, value)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.expand_path('../metric', __FILE__)
|
2
|
+
|
3
|
+
module Drone
|
4
|
+
module Metrics
|
5
|
+
|
6
|
+
##
|
7
|
+
# A Counter store a number which can go up or down,
|
8
|
+
# the counter can change a counter value with
|
9
|
+
# the methods increment and decrement aliased
|
10
|
+
# as inc and dec
|
11
|
+
#
|
12
|
+
class Counter < Metric
|
13
|
+
|
14
|
+
def initialize(name, initial_value = 0)
|
15
|
+
super(name)
|
16
|
+
|
17
|
+
@value = Drone::request_number("#{name}:value", initial_value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def value
|
21
|
+
@value.get
|
22
|
+
end
|
23
|
+
|
24
|
+
def increment(n = 1)
|
25
|
+
@value.inc(n)
|
26
|
+
end
|
27
|
+
alias :inc :increment
|
28
|
+
|
29
|
+
def decrement(n = 1)
|
30
|
+
@value.dec(n)
|
31
|
+
end
|
32
|
+
alias :dec :decrement
|
33
|
+
|
34
|
+
def clear
|
35
|
+
@value.set(0)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.expand_path('../metric', __FILE__)
|
2
|
+
|
3
|
+
module Drone
|
4
|
+
module Metrics
|
5
|
+
|
6
|
+
##
|
7
|
+
# Gauge are linked to a block of code which
|
8
|
+
# will be called when the value is asked, the block
|
9
|
+
# is expected to return a number
|
10
|
+
#
|
11
|
+
class Gauge < Metric
|
12
|
+
|
13
|
+
def initialize(name, &block)
|
14
|
+
raise "Block expected" unless block
|
15
|
+
super(name)
|
16
|
+
@block = block
|
17
|
+
end
|
18
|
+
|
19
|
+
def value
|
20
|
+
@block.call()
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require File.expand_path('../../utils/uniform_sample', __FILE__)
|
2
|
+
require File.expand_path('../../utils/exponentially_decaying_sample', __FILE__)
|
3
|
+
require File.expand_path('../metric', __FILE__)
|
4
|
+
|
5
|
+
module Drone
|
6
|
+
|
7
|
+
##
|
8
|
+
# An Histogram store a list of values (1028) and can
|
9
|
+
# compute on demand statistics on those values:
|
10
|
+
# - min/max
|
11
|
+
# - mean
|
12
|
+
# - stddev
|
13
|
+
# - percentiles
|
14
|
+
#
|
15
|
+
class Histogram < Metric
|
16
|
+
MIN = (-(2**63)).freeze
|
17
|
+
MAX = ((2**64) - 1).freeze
|
18
|
+
|
19
|
+
def initialize(name, sample_or_type = :uniform)
|
20
|
+
super(name)
|
21
|
+
|
22
|
+
if sample_or_type.is_a?(Symbol)
|
23
|
+
case sample_or_type
|
24
|
+
when :uniform then @sample = UniformSample.new("#{name}:sample", 1028)
|
25
|
+
when :biased then @sample = ExponentiallyDecayingSample.new("#{name}:sample", 1028, 0.015)
|
26
|
+
else
|
27
|
+
raise ArgumentError, "unknown type: #{sample_or_type}"
|
28
|
+
end
|
29
|
+
else
|
30
|
+
@sample = sample_or_type
|
31
|
+
end
|
32
|
+
|
33
|
+
@count = Drone::request_number("#{name}:count", 0)
|
34
|
+
@_min = Drone::request_number("#{name}:min", MAX)
|
35
|
+
@_max = Drone::request_number("#{name}:max", MIN)
|
36
|
+
@_sum = Drone::request_number("#{name}:max", 0)
|
37
|
+
@varianceM = Drone::request_number("#{name}:varianceM", -1)
|
38
|
+
@varianceS = Drone::request_number("#{name}:varianceS", 0)
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
def clear
|
43
|
+
@sample.clear()
|
44
|
+
@count = 0
|
45
|
+
@_min = MAX
|
46
|
+
@_max = MIN
|
47
|
+
@_sum = 0
|
48
|
+
@varianceM = -1
|
49
|
+
@varianceS = 0
|
50
|
+
end
|
51
|
+
|
52
|
+
def update(val)
|
53
|
+
@count.inc
|
54
|
+
@sample.update(val)
|
55
|
+
set_max(val);
|
56
|
+
set_min(val);
|
57
|
+
@_sum.inc(val)
|
58
|
+
update_variance(val)
|
59
|
+
end
|
60
|
+
|
61
|
+
def count
|
62
|
+
@count.get
|
63
|
+
end
|
64
|
+
|
65
|
+
def max
|
66
|
+
(@count.get > 0) ? @_max.get : 0.0
|
67
|
+
end
|
68
|
+
|
69
|
+
def min
|
70
|
+
(@count.get > 0) ? @_min.get : 0.0
|
71
|
+
end
|
72
|
+
|
73
|
+
def mean
|
74
|
+
(@count.get > 0) ? @_sum.get.to_f / @count.get : 0.0
|
75
|
+
end
|
76
|
+
|
77
|
+
def stdDev
|
78
|
+
(@count.get > 0) ? Math.sqrt( variance() ) : 0.0
|
79
|
+
end
|
80
|
+
|
81
|
+
def percentiles(*percentiles)
|
82
|
+
scores = Array.new(percentiles.size, 0)
|
83
|
+
if @count.get > 0
|
84
|
+
values = @sample.values.sort
|
85
|
+
percentiles.each.with_index do |p, i|
|
86
|
+
pos = p * (values.size + 1)
|
87
|
+
if pos < 1
|
88
|
+
scores[i] = values[0]
|
89
|
+
elsif pos >= values.size
|
90
|
+
scores[i] = values[-1]
|
91
|
+
else
|
92
|
+
lower = values[pos - 1]
|
93
|
+
upper = values[pos]
|
94
|
+
scores[i] = lower + (pos - pos.floor) * (upper - lower)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
scores
|
100
|
+
end
|
101
|
+
|
102
|
+
def values
|
103
|
+
@sample.values
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def doubleToLongBits(n)
|
109
|
+
[n].pack('D').unpack('q')[0]
|
110
|
+
end
|
111
|
+
|
112
|
+
def longBitsToDouble(n)
|
113
|
+
[n].pack('q').unpack('D')[0]
|
114
|
+
end
|
115
|
+
|
116
|
+
def update_variance(val)
|
117
|
+
if @varianceM.get == -1
|
118
|
+
@varianceM.set( doubleToLongBits(val) )
|
119
|
+
else
|
120
|
+
oldMCas = @varianceM.get
|
121
|
+
oldM = longBitsToDouble(oldMCas)
|
122
|
+
newM = oldM + ((val - oldM) / count())
|
123
|
+
|
124
|
+
oldSCas = @varianceS.get
|
125
|
+
oldS = longBitsToDouble(oldSCas)
|
126
|
+
newS = oldS + ((val - oldM) * (val - newM))
|
127
|
+
|
128
|
+
@varianceM.set( doubleToLongBits(newM) )
|
129
|
+
@varianceS.set( doubleToLongBits(newS) )
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def variance
|
134
|
+
count = @count.get
|
135
|
+
if count <= 1
|
136
|
+
0.0
|
137
|
+
else
|
138
|
+
longBitsToDouble(@varianceS.get) / (count - 1)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def set_max(val)
|
143
|
+
(@_max.get >= val) || @_max.set(val)
|
144
|
+
end
|
145
|
+
|
146
|
+
def set_min(val)
|
147
|
+
(@_min.get <= val) || @_min.set(val)
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
require File.expand_path('../metric', __FILE__)
|
4
|
+
require File.expand_path('../../core', __FILE__)
|
5
|
+
require File.expand_path('../../utils/ewma', __FILE__)
|
6
|
+
|
7
|
+
module Drone
|
8
|
+
module Metrics
|
9
|
+
##
|
10
|
+
# A meter measures mean throughput and one-, five-, and
|
11
|
+
# fifteen-minute exponentially-weighted moving average throughputs.
|
12
|
+
#
|
13
|
+
class Meter < Metric
|
14
|
+
INTERVAL = 5
|
15
|
+
|
16
|
+
def initialize(name)
|
17
|
+
super(name)
|
18
|
+
@start_time = Drone::request_number("#{name}:start_time", Time.now)
|
19
|
+
@next_tick = Drone::request_number("#{name}:next_tick_lock", 1)
|
20
|
+
|
21
|
+
@count = Drone::request_number("#{name}:count", 0)
|
22
|
+
@rates = {
|
23
|
+
1 => EWMA.one_minute_ewma("#{name}:rate1"),
|
24
|
+
5 => EWMA.five_minutes_ewma("#{name}:rate5"),
|
25
|
+
15 => EWMA.fifteen_minutes_ewma("#{name}:rate15")
|
26
|
+
}
|
27
|
+
|
28
|
+
Drone::schedule_periodic(INTERVAL) do
|
29
|
+
Fiber.new{ tick() }.resume
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def tick
|
34
|
+
# init if required
|
35
|
+
@local_next_tick ||= @next_tick.get
|
36
|
+
|
37
|
+
# ensure only one process will trigger the tick
|
38
|
+
if @next_tick.compare_and_set(@local_next_tick, @local_next_tick + 1)
|
39
|
+
@rates.values.each(&:tick)
|
40
|
+
@local_next_tick += 1
|
41
|
+
else
|
42
|
+
# reset the tick counter to give a chance to this
|
43
|
+
# process to trigger the next tick
|
44
|
+
@local_next_tick = @next_tick.get()
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def mark(events = 1)
|
49
|
+
@count.inc(events)
|
50
|
+
@rates.each do |_, r|
|
51
|
+
r.update(events)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def count
|
56
|
+
@count.get
|
57
|
+
end
|
58
|
+
|
59
|
+
def mean_rate
|
60
|
+
count = @count.get
|
61
|
+
if count == 0
|
62
|
+
0.0
|
63
|
+
else
|
64
|
+
count / (Time.now.to_f - @start_time.get.to_f)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def one_minute_rate
|
69
|
+
@rates[1].rate()
|
70
|
+
end
|
71
|
+
|
72
|
+
def five_minutes_rate
|
73
|
+
@rates[5].rate()
|
74
|
+
end
|
75
|
+
|
76
|
+
def fifteen_minutes_rate
|
77
|
+
@rates[15].rate()
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Drone
|
2
|
+
class Metric
|
3
|
+
##
|
4
|
+
# Every metric must have a name to be referenced by
|
5
|
+
#
|
6
|
+
# @attr_reader [String] name The metric's name
|
7
|
+
# (which is also its id)
|
8
|
+
#
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
@name = name
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require File.expand_path('../histogram', __FILE__)
|
3
|
+
require File.expand_path('../meter', __FILE__)
|
4
|
+
require File.expand_path('../metric', __FILE__)
|
5
|
+
|
6
|
+
module Drone
|
7
|
+
module Metrics
|
8
|
+
##
|
9
|
+
# The timer metric will record the time spent in a given method
|
10
|
+
# or any block of code.
|
11
|
+
#
|
12
|
+
# All the times are in milliseconds.
|
13
|
+
#
|
14
|
+
class Timer < Metric
|
15
|
+
extend Forwardable
|
16
|
+
|
17
|
+
def_delegators :@histogram, :count, :min, :max, :mean, :stdDev, :percentiles, :values
|
18
|
+
|
19
|
+
def initialize(name)
|
20
|
+
super(name)
|
21
|
+
|
22
|
+
@histogram = Histogram.new("#{name}:histogram", :biased)
|
23
|
+
end
|
24
|
+
|
25
|
+
def count
|
26
|
+
@histogram.count
|
27
|
+
end
|
28
|
+
|
29
|
+
def clear
|
30
|
+
@histogram.clear()
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Method used to record a new duration
|
35
|
+
#
|
36
|
+
# @param [Float] duration A duration in milliseconds
|
37
|
+
#
|
38
|
+
def update(duration)
|
39
|
+
if duration >= 0
|
40
|
+
@histogram.update(duration)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# time and record the duration of the block
|
46
|
+
# @yield [] The block to time
|
47
|
+
#
|
48
|
+
def time
|
49
|
+
started_at = Time.now.to_f
|
50
|
+
yield()
|
51
|
+
ensure
|
52
|
+
update((Time.now.to_f - started_at.to_f) * 1000)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|