drone 0.0.3 → 1.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/.rvmrc +1 -0
- data/README.md +73 -49
- data/Rakefile +1 -1
- data/examples/simple.rb +10 -11
- data/lib/drone/core.rb +46 -30
- data/lib/drone/metrics/counter.rb +19 -7
- data/lib/drone/metrics/gauge.rb +4 -3
- data/lib/drone/metrics/histogram.rb +47 -26
- data/lib/drone/metrics/meter.rb +36 -16
- data/lib/drone/metrics/metric.rb +16 -0
- data/lib/drone/metrics/timer.rb +26 -19
- data/lib/drone/monitoring.rb +2 -2
- data/lib/drone/storage/base.rb +122 -0
- data/lib/drone/storage/memory.rb +58 -0
- data/lib/drone/utils/ewma.rb +44 -39
- data/lib/drone/utils/exponentially_decaying_sample.rb +61 -51
- data/lib/drone/utils/uniform_sample.rb +43 -28
- data/lib/drone/version.rb +1 -1
- data/specs/metrics/counter_spec.rb +2 -0
- data/specs/metrics/timer_spec.rb +2 -2
- data/specs/unit/ewma_spec.rb +6 -3
- data/specs/unit/exponentially_decaying_sample_spec.rb +6 -3
- data/specs/unit/histogram_spec.rb +6 -2
- data/specs/unit/monitoring_spec.rb +3 -3
- data/specs/unit/uniform_sample_spec.rb +5 -2
- metadata +6 -2
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.2
|
data/README.md
CHANGED
@@ -14,74 +14,87 @@ A fully working example is included in examples/simple, to run it
|
|
14
14
|
|
15
15
|
The example will output the collected statistics directly on the console every second.
|
16
16
|
|
17
|
+
# Supported Runtimes
|
18
|
+
|
19
|
+
- MRI 1.8.7+
|
20
|
+
- Rubinius 1.2.2+
|
21
|
+
|
22
|
+
|
23
|
+
# Status
|
24
|
+
- Most of the features I wanted in are (see below for usage examples):
|
25
|
+
- timing method calls
|
26
|
+
- method calls rate
|
27
|
+
- counters
|
28
|
+
- gauges
|
29
|
+
|
30
|
+
- Good test coverage
|
31
|
+
|
32
|
+
The gem was created for a specific need and is currently used in preproduction environment,
|
33
|
+
no major bugs until now.
|
34
|
+
|
35
|
+
|
17
36
|
# How is it done
|
18
37
|
|
19
38
|
The library is split in different parts
|
20
39
|
|
21
|
-
- the core
|
40
|
+
- the core:<br/>
|
22
41
|
it contains all the API used to declare which data to collect and how as well as the storage for them
|
23
42
|
|
24
|
-
- the metrics
|
43
|
+
- the metrics:<br/>
|
25
44
|
that is all the metrics type the library know.
|
26
45
|
|
27
|
-
- the interfaces
|
46
|
+
- the interfaces:<br/>
|
28
47
|
those are the parts which will decides how the stored data are made available.
|
29
48
|
|
30
|
-
- the schedulers
|
49
|
+
- the schedulers:</br>
|
31
50
|
this is where the timers are scheduled, currently there is only one scheduler: eventmachine
|
32
51
|
|
52
|
+
- the storage:<br/>
|
53
|
+
this part decides where the actual data for the metrics are stored, the default is to store them
|
54
|
+
in memory but other possible options are: redis, memcached, etc...
|
55
|
+
The goal for external storage is to allow concurrent applications to share the same metrics, an
|
56
|
+
immediate example of such application is a rails application ran under passenger or any other spawner
|
57
|
+
|
33
58
|
## Constraints
|
34
59
|
|
35
60
|
- the name of each metric can be formatted how it pleases you (note that output interfaces may expect some format)
|
36
61
|
but the name is expected to be unique or you could end up reusing the same metric without wanting it.
|
37
62
|
(this only applies to monitor_time and monitor_rate helpers but could apply anywhere else as needed)
|
38
63
|
|
39
|
-
|
40
|
-
# Supported Runtimes
|
41
|
-
|
42
|
-
- MRI 1.8.7+
|
43
|
-
- Rubinius 1.2.2+
|
44
|
-
|
45
|
-
|
46
|
-
# Status
|
47
|
-
- Most of the features I wanted in are:
|
48
|
-
- timing method calls
|
49
|
-
- method calls rate
|
50
|
-
- counters
|
51
|
-
- gauges
|
52
|
-
|
53
|
-
- Decent test coverage (Simplecov report ~ 87% for what is worth)
|
54
|
-
|
55
64
|
# Usage
|
56
65
|
|
57
66
|
I try to keep things as simple as possible, there is currently two ways to use
|
58
67
|
this library:
|
59
68
|
|
60
69
|
- the first one is to just instantiate metrics by hand and use them directly
|
70
|
+
|
71
|
+
``` ruby
|
72
|
+
require 'drone'
|
73
|
+
Drone::init_drone()
|
74
|
+
@counter = Drone::Metris::Counter.new('my_counter')
|
61
75
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
def some_method
|
67
|
-
@counter.inc()
|
68
|
-
end
|
76
|
+
def some_method
|
77
|
+
@counter.inc()
|
78
|
+
end
|
79
|
+
```
|
69
80
|
|
70
81
|
- the other way is to instrument a class:
|
71
82
|
|
72
|
-
|
73
|
-
|
83
|
+
``` ruby
|
84
|
+
require 'drone'
|
85
|
+
Drone::init_drone()
|
86
|
+
|
87
|
+
class User
|
88
|
+
include Drone::Monitoring
|
89
|
+
|
90
|
+
monitor_rate("users/new")
|
91
|
+
def initialize(login, pass); end
|
74
92
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
monitor_time("users/rename")
|
82
|
-
def rename(new_login); end
|
83
|
-
|
84
|
-
end
|
93
|
+
monitor_time("users/rename")
|
94
|
+
def rename(new_login); end
|
95
|
+
|
96
|
+
end
|
97
|
+
```
|
85
98
|
|
86
99
|
This code will create three metrics:
|
87
100
|
- "users/new" : how many users are created each second
|
@@ -93,13 +106,23 @@ Once you have your data you need to add a way to serve them, each lives in a sep
|
|
93
106
|
gem to limit the core's dependencies so the only one in core is:
|
94
107
|
|
95
108
|
- console output (puts), mainly for debug:
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
109
|
+
|
110
|
+
``` ruby
|
111
|
+
require 'drone'
|
112
|
+
Drone::init_drone()
|
113
|
+
Drone::add_output(:console, 1)
|
114
|
+
```
|
100
115
|
|
101
116
|
The values will be printed on the console at the inter
|
102
117
|
|
118
|
+
The others output are available in their own gems:
|
119
|
+
|
120
|
+
- drone_json:<br/>
|
121
|
+
The stats are served by a thin server in json
|
122
|
+
|
123
|
+
- drone_collectd:<br/>
|
124
|
+
The stats are send to a collectd daemon.
|
125
|
+
|
103
126
|
# Goals
|
104
127
|
|
105
128
|
My goal is to be able to serve stats efficiently from any ruby 1.9 application built
|
@@ -113,23 +136,24 @@ gem to limit the core's dependencies so the only one in core is:
|
|
113
136
|
optional part instead of in the core. There should not be any problem to implements it
|
114
137
|
in an includable module not included as default (it may requires some modifications in the core):
|
115
138
|
|
116
|
-
|
117
|
-
|
139
|
+
``` ruby
|
140
|
+
require 'drone'
|
141
|
+
require 'drone/threadsafe'
|
118
142
|
|
119
|
-
|
120
|
-
|
143
|
+
# [...]
|
144
|
+
```
|
121
145
|
|
122
146
|
# Development
|
123
147
|
|
124
148
|
Installing the development environment is pretty simple thanks to bundler:
|
125
|
-
|
149
|
+
|
126
150
|
gem install bundler
|
127
151
|
bundle
|
128
152
|
|
129
153
|
## Running specs
|
130
154
|
|
131
155
|
The specs are written with bacon, mocha and em-spec, they can be ran with:
|
132
|
-
|
156
|
+
|
133
157
|
rake spec
|
134
158
|
|
135
159
|
## Build the doc
|
data/Rakefile
CHANGED
data/examples/simple.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
require '
|
2
|
-
|
1
|
+
require File.expand_path('../common', __FILE__)
|
2
|
+
init_environment()
|
3
3
|
|
4
|
-
|
5
|
-
require 'drone'
|
4
|
+
require 'fiber'
|
6
5
|
|
7
6
|
Drone::init_drone()
|
8
7
|
Drone::register_gauge("cpu:0/user"){ rand(200) }
|
@@ -22,11 +21,9 @@ class User
|
|
22
21
|
monitor_time("users:do_something:time")
|
23
22
|
monitor_rate("users:do_something:rate")
|
24
23
|
def do_something
|
25
|
-
#
|
26
|
-
|
27
|
-
|
28
|
-
200.times{ str << "b" }
|
29
|
-
end
|
24
|
+
# fb = Fiber.current
|
25
|
+
# EM::add_timer(1){ fb.resume() }
|
26
|
+
# Fiber.yield
|
30
27
|
end
|
31
28
|
end
|
32
29
|
|
@@ -45,7 +42,9 @@ EM::run do
|
|
45
42
|
counter1.increment()
|
46
43
|
end
|
47
44
|
|
48
|
-
EM::add_periodic_timer(
|
49
|
-
|
45
|
+
EM::add_periodic_timer(2) do
|
46
|
+
Fiber.new do
|
47
|
+
a.do_something()
|
48
|
+
end.resume
|
50
49
|
end
|
51
50
|
end
|
data/lib/drone/core.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
1
3
|
|
2
4
|
require File.expand_path('../schedulers/eventmachine', __FILE__)
|
3
5
|
|
6
|
+
require File.expand_path('../storage/memory', __FILE__)
|
7
|
+
|
4
8
|
module Drone
|
5
9
|
##
|
6
10
|
# This module contains all the metrics you can use to collect data
|
@@ -20,11 +24,20 @@ module Drone
|
|
20
24
|
#
|
21
25
|
module Schedulers; end
|
22
26
|
|
27
|
+
|
28
|
+
##
|
29
|
+
# This module contains the class used for storage,
|
30
|
+
# they determine where the metric's data are stored
|
31
|
+
#
|
32
|
+
module Storage; end
|
33
|
+
|
23
34
|
class <<self
|
35
|
+
extend Forwardable
|
24
36
|
|
25
|
-
def init_drone(scheduler = Schedulers::EMScheduler)
|
26
|
-
@
|
37
|
+
def init_drone(scheduler = Schedulers::EMScheduler, storage = Storage::Memory.new)
|
38
|
+
@metrics = []
|
27
39
|
@scheduler = scheduler
|
40
|
+
@storage = storage
|
28
41
|
@monitored_classes = []
|
29
42
|
@output_modules = []
|
30
43
|
end
|
@@ -41,7 +54,7 @@ module Drone
|
|
41
54
|
|
42
55
|
def each_metric
|
43
56
|
raise "Block expected" unless block_given?
|
44
|
-
@
|
57
|
+
@metrics.each{|m| yield(m) }
|
45
58
|
end
|
46
59
|
|
47
60
|
|
@@ -51,7 +64,7 @@ module Drone
|
|
51
64
|
# @param [String] name The mtric's name
|
52
65
|
#
|
53
66
|
def find_metric(name)
|
54
|
-
@
|
67
|
+
@metrics.detect{|m| m.name == name }
|
55
68
|
end
|
56
69
|
|
57
70
|
##
|
@@ -67,59 +80,62 @@ module Drone
|
|
67
80
|
@output_modules << klass.new(*args)
|
68
81
|
end
|
69
82
|
|
70
|
-
##
|
71
|
-
# Register a monitored class.
|
72
|
-
# @private
|
73
|
-
#
|
74
|
-
def register_monitored_class(klass)
|
75
|
-
@monitored_classes << klass
|
76
|
-
end
|
77
|
-
|
78
83
|
##
|
79
84
|
# Register a new counter
|
85
|
+
# @see Drone::Metrics::Counter
|
80
86
|
# @param [String] type Name of this metric
|
81
87
|
# @api public
|
82
88
|
#
|
83
89
|
def register_counter(type)
|
84
|
-
|
90
|
+
register_metric( Drone::Metrics::Counter.new(type) )
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
##
|
95
|
+
# Register an Histogram
|
96
|
+
# @see Drone::Metrics::Histogram
|
97
|
+
# @param [String] name Name of this metric
|
98
|
+
# @param [optional,Enum] type one of Drone::Metrics::Histogram::TYPE_UNIFORM or Drone::Metrics::Histogram::TYPE_BIASED
|
99
|
+
#
|
100
|
+
def register_histogram(name, type = :uniform)
|
101
|
+
register_metric( Drone::Metrics::Histogram.new(name, type) )
|
85
102
|
end
|
86
103
|
|
87
104
|
##
|
88
105
|
# Register a new gauge
|
106
|
+
# @see Drone::Metrics::Gauge
|
89
107
|
# @param [String] type Name of this metric
|
90
108
|
# @api public
|
91
109
|
#
|
92
110
|
def register_gauge(type, &block)
|
93
|
-
|
111
|
+
register_metric( Drone::Metrics::Gauge.new(type, &block) )
|
94
112
|
end
|
95
113
|
|
96
114
|
##
|
97
|
-
# Register a new
|
115
|
+
# Register a new metric
|
98
116
|
# This method can be used bu the user but the prefered
|
99
117
|
# way is to use the register_counter / register_gauge methods
|
100
118
|
#
|
101
|
-
# @param [
|
102
|
-
# @api private
|
119
|
+
# @param [Metric] metric The Metric to register
|
103
120
|
# @private
|
104
121
|
#
|
105
|
-
def
|
106
|
-
@
|
107
|
-
|
122
|
+
def register_metric(metric)
|
123
|
+
@metrics << metric
|
124
|
+
metric
|
108
125
|
end
|
109
126
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
end
|
127
|
+
|
128
|
+
def_delegators :@storage, :request_fixed_size_array, :request_number, :request_hash
|
129
|
+
def_delegators :@scheduler, :schedule_periodic, :schedule_once
|
130
|
+
|
131
|
+
|
116
132
|
|
117
133
|
##
|
118
|
-
#
|
134
|
+
# Register a monitored class.
|
135
|
+
# @private
|
119
136
|
#
|
120
|
-
def
|
121
|
-
@
|
137
|
+
def register_monitored_class(klass)
|
138
|
+
@monitored_classes << klass
|
122
139
|
end
|
123
|
-
|
124
140
|
end
|
125
141
|
end
|
@@ -1,26 +1,38 @@
|
|
1
|
+
require File.expand_path('../metric', __FILE__)
|
2
|
+
|
1
3
|
module Drone
|
2
4
|
module Metrics
|
3
5
|
|
4
|
-
|
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
|
6
13
|
|
7
14
|
def initialize(name, initial_value = 0)
|
8
|
-
|
9
|
-
|
15
|
+
super(name)
|
16
|
+
|
17
|
+
@value = Drone::request_number("#{name}:value", initial_value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def value
|
21
|
+
@value.get
|
10
22
|
end
|
11
23
|
|
12
24
|
def increment(n = 1)
|
13
|
-
@value
|
25
|
+
@value.inc(n)
|
14
26
|
end
|
15
27
|
alias :inc :increment
|
16
28
|
|
17
29
|
def decrement(n = 1)
|
18
|
-
@value
|
30
|
+
@value.dec(n)
|
19
31
|
end
|
20
32
|
alias :dec :decrement
|
21
33
|
|
22
34
|
def clear
|
23
|
-
@value
|
35
|
+
@value.set(0)
|
24
36
|
end
|
25
37
|
end
|
26
38
|
|
data/lib/drone/metrics/gauge.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require File.expand_path('../metric', __FILE__)
|
2
|
+
|
1
3
|
module Drone
|
2
4
|
module Metrics
|
3
5
|
|
@@ -6,12 +8,11 @@ module Drone
|
|
6
8
|
# will be called when the value is asked, the block
|
7
9
|
# is expected to return a number
|
8
10
|
#
|
9
|
-
class Gauge
|
10
|
-
attr_reader :name
|
11
|
+
class Gauge < Metric
|
11
12
|
|
12
13
|
def initialize(name, &block)
|
13
14
|
raise "Block expected" unless block
|
14
|
-
|
15
|
+
super(name)
|
15
16
|
@block = block
|
16
17
|
end
|
17
18
|
|
@@ -1,22 +1,42 @@
|
|
1
1
|
require File.expand_path('../../utils/uniform_sample', __FILE__)
|
2
2
|
require File.expand_path('../../utils/exponentially_decaying_sample', __FILE__)
|
3
|
+
require File.expand_path('../metric', __FILE__)
|
3
4
|
|
4
5
|
module Drone
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
9
16
|
MIN = (-(2**63)).freeze
|
10
17
|
MAX = ((2**64) - 1).freeze
|
11
18
|
|
12
|
-
def initialize(sample_or_type =
|
13
|
-
|
14
|
-
|
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
|
15
29
|
else
|
16
30
|
@sample = sample_or_type
|
17
31
|
end
|
18
32
|
|
19
|
-
|
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
|
+
|
20
40
|
end
|
21
41
|
|
22
42
|
def clear
|
@@ -30,37 +50,37 @@ module Drone
|
|
30
50
|
end
|
31
51
|
|
32
52
|
def update(val)
|
33
|
-
@count
|
53
|
+
@count.inc
|
34
54
|
@sample.update(val)
|
35
55
|
set_max(val);
|
36
56
|
set_min(val);
|
37
|
-
@_sum
|
57
|
+
@_sum.inc(val)
|
38
58
|
update_variance(val)
|
39
59
|
end
|
40
60
|
|
41
61
|
def count
|
42
|
-
@count
|
62
|
+
@count.get
|
43
63
|
end
|
44
64
|
|
45
65
|
def max
|
46
|
-
(@count > 0) ? @_max : 0.0
|
66
|
+
(@count.get > 0) ? @_max.get : 0.0
|
47
67
|
end
|
48
68
|
|
49
69
|
def min
|
50
|
-
(@count > 0) ? @_min : 0.0
|
70
|
+
(@count.get > 0) ? @_min.get : 0.0
|
51
71
|
end
|
52
72
|
|
53
73
|
def mean
|
54
|
-
(@count > 0) ? @_sum.to_f / @count : 0.0
|
74
|
+
(@count.get > 0) ? @_sum.get.to_f / @count.get : 0.0
|
55
75
|
end
|
56
76
|
|
57
77
|
def stdDev
|
58
|
-
(@count > 0) ? Math.sqrt( variance() ) : 0.0
|
78
|
+
(@count.get > 0) ? Math.sqrt( variance() ) : 0.0
|
59
79
|
end
|
60
80
|
|
61
81
|
def percentiles(*percentiles)
|
62
82
|
scores = Array.new(percentiles.size, 0)
|
63
|
-
if @count > 0
|
83
|
+
if @count.get > 0
|
64
84
|
values = @sample.values.sort
|
65
85
|
percentiles.each.with_index do |p, i|
|
66
86
|
pos = p * (values.size + 1)
|
@@ -94,36 +114,37 @@ module Drone
|
|
94
114
|
end
|
95
115
|
|
96
116
|
def update_variance(val)
|
97
|
-
if @varianceM == -1
|
98
|
-
@varianceM
|
117
|
+
if @varianceM.get == -1
|
118
|
+
@varianceM.set( doubleToLongBits(val) )
|
99
119
|
else
|
100
|
-
oldMCas = @varianceM
|
120
|
+
oldMCas = @varianceM.get
|
101
121
|
oldM = longBitsToDouble(oldMCas)
|
102
122
|
newM = oldM + ((val - oldM) / count())
|
103
123
|
|
104
|
-
oldSCas = @varianceS
|
124
|
+
oldSCas = @varianceS.get
|
105
125
|
oldS = longBitsToDouble(oldSCas)
|
106
126
|
newS = oldS + ((val - oldM) * (val - newM))
|
107
127
|
|
108
|
-
@varianceM
|
109
|
-
@varianceS
|
128
|
+
@varianceM.set( doubleToLongBits(newM) )
|
129
|
+
@varianceS.set( doubleToLongBits(newS) )
|
110
130
|
end
|
111
131
|
end
|
112
132
|
|
113
133
|
def variance
|
114
|
-
|
134
|
+
count = @count.get
|
135
|
+
if count <= 1
|
115
136
|
0.0
|
116
137
|
else
|
117
|
-
longBitsToDouble(@varianceS) / (count
|
138
|
+
longBitsToDouble(@varianceS.get) / (count - 1)
|
118
139
|
end
|
119
140
|
end
|
120
141
|
|
121
142
|
def set_max(val)
|
122
|
-
(@_max >= val) || @_max
|
143
|
+
(@_max.get >= val) || @_max.set(val)
|
123
144
|
end
|
124
145
|
|
125
146
|
def set_min(val)
|
126
|
-
(@_min <= val) || @_min
|
147
|
+
(@_min.get <= val) || @_min.set(val)
|
127
148
|
end
|
128
149
|
|
129
150
|
|