drone 0.0.3 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|