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,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_metric(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_metric(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("#{m}_with_meter") do |*args, &block|
|
79
|
+
rate.mark()
|
80
|
+
send("#{m}_without_meter", *args, &block)
|
81
|
+
end
|
82
|
+
|
83
|
+
alias_method "#{m}_without_meter", m
|
84
|
+
alias_method m, "#{m}_with_meter"
|
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("#{m}_with_timer") do |*args, &block|
|
95
|
+
timer.time do
|
96
|
+
send("#{m}_without_timer", *args, &block)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
alias_method "#{m}_without_timer", m
|
101
|
+
alias_method m, "#{m}_with_timer"
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,70 @@
|
|
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
|
+
##
|
60
|
+
# @private
|
61
|
+
#
|
62
|
+
def self.reset
|
63
|
+
@timers_once.clear()
|
64
|
+
@timers_periodic.clear()
|
65
|
+
@started = false
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Drone
|
2
|
+
module Storage
|
3
|
+
|
4
|
+
##
|
5
|
+
# Represents a number but procide a specific api
|
6
|
+
# allowing this number to shared anywhere
|
7
|
+
#
|
8
|
+
class SharedNumber
|
9
|
+
##
|
10
|
+
# Constructor
|
11
|
+
#
|
12
|
+
# @param [Number] initial_value The initial value
|
13
|
+
#
|
14
|
+
def initialize(initial_value)
|
15
|
+
raise "needs to be redefined"
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Increment the value
|
20
|
+
#
|
21
|
+
# @param [Number] n Increment by n
|
22
|
+
#
|
23
|
+
def inc(n = 1)
|
24
|
+
raise "needs to be redefined"
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Decrement the value
|
29
|
+
#
|
30
|
+
# @param [Number] n Decrement by n
|
31
|
+
#
|
32
|
+
def dec(n = 1)
|
33
|
+
raise "needs to be redefined"
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Set the value
|
38
|
+
#
|
39
|
+
# @param [Number] n The new value
|
40
|
+
#
|
41
|
+
def set(n)
|
42
|
+
raise "needs to be redefined"
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Get the current value
|
47
|
+
#
|
48
|
+
# @return [Number] The current value
|
49
|
+
#
|
50
|
+
def get
|
51
|
+
raise "needs to be redefined"
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Set a new value and return the old one
|
56
|
+
#
|
57
|
+
# @param [Number] n The new value
|
58
|
+
#
|
59
|
+
# @return [Number] The old value
|
60
|
+
#
|
61
|
+
def get_and_set(n)
|
62
|
+
raise "needs to be redefined"
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Set the new value but only if the current value
|
67
|
+
# is equal to expected.
|
68
|
+
#
|
69
|
+
# @param [Number] expected The expected current value
|
70
|
+
# @param [Number] new_value The new value
|
71
|
+
#
|
72
|
+
def compare_and_set(expected, new_value)
|
73
|
+
raise "needs to be redefined"
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
class Base
|
79
|
+
|
80
|
+
##
|
81
|
+
# Request a fixed size array.
|
82
|
+
#
|
83
|
+
# @param [String] id Any string which makes sense in the context
|
84
|
+
# @param [Number] size The Array size
|
85
|
+
# @param [Number] initial_value The default value for the cells
|
86
|
+
#
|
87
|
+
# @return [Object] Returns an object which share the same external interface as
|
88
|
+
# the Array class
|
89
|
+
#
|
90
|
+
def request_fixed_size_array(id, size, initial_value = nil)
|
91
|
+
raise "needs to be redefined"
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Request a hash.
|
96
|
+
#
|
97
|
+
# @param [String] id Any string which makes sense in the context
|
98
|
+
#
|
99
|
+
# @return [Object] Returns an object which share the same external interface as
|
100
|
+
# the Hash class
|
101
|
+
#
|
102
|
+
def request_hash(id)
|
103
|
+
raise "needs to be redefined"
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Request a number "slot".
|
108
|
+
#
|
109
|
+
# @param [String] id Any string which makes sense in the context
|
110
|
+
# @param [Number] initial_value The intial value
|
111
|
+
#
|
112
|
+
# @return [SharedNumber] An intance of a class inheriting SharedNumber
|
113
|
+
#
|
114
|
+
# @see SharedNumber
|
115
|
+
#
|
116
|
+
def request_number(id, initial_value = 0)
|
117
|
+
raise "needs to be redefined"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require File.expand_path('../base', __FILE__)
|
2
|
+
|
3
|
+
module Drone
|
4
|
+
module Storage
|
5
|
+
|
6
|
+
class Memory < Base
|
7
|
+
|
8
|
+
class MemorySharedNumber
|
9
|
+
def initialize(initial_value)
|
10
|
+
@store = initial_value
|
11
|
+
end
|
12
|
+
|
13
|
+
def inc(n = 1)
|
14
|
+
@store += n
|
15
|
+
end
|
16
|
+
|
17
|
+
def dec(n = 1)
|
18
|
+
@store -= n
|
19
|
+
end
|
20
|
+
|
21
|
+
def set(n)
|
22
|
+
@store = n
|
23
|
+
end
|
24
|
+
|
25
|
+
def get
|
26
|
+
@store
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_and_set(n)
|
30
|
+
ret = @store
|
31
|
+
set(n)
|
32
|
+
ret
|
33
|
+
end
|
34
|
+
|
35
|
+
def compare_and_set(expected, new_value)
|
36
|
+
# dummy implementation, with memory storage nothing can
|
37
|
+
# happen to our data
|
38
|
+
set(new_value)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
def request_fixed_size_array(id, size, initial_value = nil)
|
44
|
+
Array.new(size, initial_value)
|
45
|
+
end
|
46
|
+
|
47
|
+
def request_hash(id)
|
48
|
+
Hash.new
|
49
|
+
end
|
50
|
+
|
51
|
+
def request_number(id, initial_value = 0)
|
52
|
+
MemorySharedNumber.new(initial_value)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require File.expand_path('../../core', __FILE__)
|
2
|
+
|
3
|
+
module Drone
|
4
|
+
class EWMA
|
5
|
+
M1_ALPHA = (1 - Math.exp(-5 / 60.0)).freeze
|
6
|
+
M5_ALPHA = (1 - Math.exp(-5 / 60.0 / 5)).freeze
|
7
|
+
M15_ALPHA = (1 - Math.exp(-5 / 60.0 / 15)).freeze
|
8
|
+
|
9
|
+
def self.one_minute_ewma(id)
|
10
|
+
new(id, M1_ALPHA, 5000)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.five_minutes_ewma(id)
|
14
|
+
new(id, M5_ALPHA, 5000)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.fifteen_minutes_ewma(id)
|
18
|
+
new(id, M15_ALPHA, 5000)
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
# interval: in ms
|
23
|
+
def initialize(name, alpha, interval)
|
24
|
+
@alpha = alpha
|
25
|
+
@interval = interval.to_f # * (1000*1000)
|
26
|
+
@uncounted = Drone::request_number("#{name}:uncounted", 0)
|
27
|
+
@rate = Drone::request_number("#{name}:rate", nil)
|
28
|
+
end
|
29
|
+
|
30
|
+
def update(n)
|
31
|
+
@uncounted.inc(n)
|
32
|
+
end
|
33
|
+
|
34
|
+
def tick()
|
35
|
+
count = @uncounted.get_and_set(0)
|
36
|
+
|
37
|
+
instant_rate = count / @interval
|
38
|
+
rate = @rate.get
|
39
|
+
|
40
|
+
if rate
|
41
|
+
@rate.inc( @alpha * (instant_rate - rate) )
|
42
|
+
else
|
43
|
+
@rate.set( instant_rate )
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def rate(as = :seconds)
|
48
|
+
case as
|
49
|
+
when :ms then @rate.get
|
50
|
+
when :seconds then @rate.get * 1000
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require File.expand_path('../../core', __FILE__)
|
2
|
+
|
3
|
+
module Drone
|
4
|
+
class ExponentiallyDecayingSample
|
5
|
+
# 1 hour in ms
|
6
|
+
RESCALE_THRESHOLD = (1 * 60 * 60 * 1000).freeze
|
7
|
+
|
8
|
+
def initialize(id, reservoir_size, alpha)
|
9
|
+
@id = id
|
10
|
+
@values = Drone::request_hash("#{@id}:values")
|
11
|
+
@count = Drone::request_number("#{@id}:count", 0)
|
12
|
+
@start_time = Drone::request_number("#{@id}:start_time", current_time())
|
13
|
+
@next_scale_time = Drone::request_number("#{@id}:next_scale_time", current_time() + RESCALE_THRESHOLD)
|
14
|
+
|
15
|
+
@alpha = alpha
|
16
|
+
@reservoir_size = reservoir_size
|
17
|
+
end
|
18
|
+
|
19
|
+
def clear
|
20
|
+
@values.clear()
|
21
|
+
@count.set(0)
|
22
|
+
@start_time.set(current_time())
|
23
|
+
@next_scale_time.set( current_time() + RESCALE_THRESHOLD )
|
24
|
+
end
|
25
|
+
|
26
|
+
def size
|
27
|
+
count = @count.get
|
28
|
+
(@values.size < count) ? @values.size : count
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def update(val, time = current_time)
|
33
|
+
priority = weight(time - @start_time.get) / rand()
|
34
|
+
count = @count.inc
|
35
|
+
if count <= @reservoir_size
|
36
|
+
@values[priority] = val
|
37
|
+
else
|
38
|
+
first = @values.keys.min
|
39
|
+
if first < priority
|
40
|
+
@values[priority] = val
|
41
|
+
while @values.delete(first) == nil
|
42
|
+
first = @values.keys.min
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
now = current_time()
|
48
|
+
if now >= @next_scale_time.get
|
49
|
+
rescale(now)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def values
|
54
|
+
@values.keys.sort.inject([]) do |buff, key|
|
55
|
+
buff << @values[key]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def rescale(now)
|
60
|
+
@next_scale_time.set( current_time() + RESCALE_THRESHOLD )
|
61
|
+
new_start = current_time()
|
62
|
+
old_start = @start_time.get_and_set(new_start)
|
63
|
+
|
64
|
+
@values = Hash[ @values.map{ |k,v|
|
65
|
+
[k * Math.exp(-@alpha * (new_start - old_start)), v]
|
66
|
+
}]
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def current_time
|
73
|
+
Time.now.to_f * 1000
|
74
|
+
end
|
75
|
+
|
76
|
+
def weight(n)
|
77
|
+
Math.exp(@alpha * n)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|