drone 1.0.4 → 1.0.5

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.
Files changed (46) hide show
  1. data/Gemfile +13 -0
  2. data/Guardfile +12 -0
  3. data/Rakefile +22 -37
  4. data/drone.gemspec +2 -6
  5. data/examples/collectd.rb +51 -0
  6. data/examples/common.rb +24 -0
  7. data/examples/json.rb +49 -0
  8. data/examples/redis_storage.rb +60 -0
  9. data/examples/simple.rb +3 -3
  10. data/extensions/drone_collectd/Gemfile +7 -0
  11. data/extensions/drone_collectd/LICENSE +20 -0
  12. data/extensions/drone_collectd/README.md +24 -0
  13. data/extensions/drone_collectd/drone_collectd.gemspec +28 -0
  14. data/extensions/drone_collectd/lib/drone_collectd.rb +7 -0
  15. data/extensions/drone_collectd/lib/drone_collectd/collectd.rb +97 -0
  16. data/extensions/drone_collectd/lib/drone_collectd/parser.rb +86 -0
  17. data/extensions/drone_collectd/specs/common.rb +3 -0
  18. data/extensions/drone_collectd/specs/unit/parser_spec.rb +49 -0
  19. data/extensions/drone_json/Gemfile +6 -0
  20. data/extensions/drone_json/LICENSE +20 -0
  21. data/extensions/drone_json/README.md +9 -0
  22. data/extensions/drone_json/drone_json.gemspec +32 -0
  23. data/extensions/drone_json/lib/drone_json.rb +9 -0
  24. data/extensions/drone_json/lib/drone_json/json.rb +100 -0
  25. data/extensions/drone_json/specs/common.rb +63 -0
  26. data/extensions/drone_redis/Gemfile +7 -0
  27. data/extensions/drone_redis/drone_redis.gemspec +22 -0
  28. data/extensions/drone_redis/lib/drone_redis.rb +8 -0
  29. data/extensions/drone_redis/lib/drone_redis/redis.rb +218 -0
  30. data/lib/drone.rb +1 -0
  31. data/lib/drone/errors.rb +11 -0
  32. data/lib/drone/metrics/histogram.rb +7 -6
  33. data/lib/drone/metrics/meter.rb +1 -1
  34. data/lib/drone/monitoring.rb +2 -2
  35. data/lib/drone/storage/memory.rb +1 -0
  36. data/lib/drone/utils/exponentially_decaying_sample.rb +79 -24
  37. data/lib/drone/version.rb +1 -1
  38. data/specs/{unit → metrics}/histogram_spec.rb +5 -1
  39. data/specs/metrics/meter_spec.rb +10 -2
  40. data/specs/metrics/timer_spec.rb +7 -1
  41. data/specs/{unit/monitoring_spec.rb → monitoring_spec.rb} +25 -1
  42. data/specs/{unit → utils}/ewma_spec.rb +1 -0
  43. data/specs/utils/exponentially_decaying_sample_spec.rb +140 -0
  44. data/specs/{unit → utils}/uniform_sample_spec.rb +0 -0
  45. metadata +72 -93
  46. data/specs/unit/exponentially_decaying_sample_spec.rb +0 -86
@@ -0,0 +1,86 @@
1
+ module DroneCollectd
2
+ class CollectdPacket
3
+ # part type
4
+ HOST = 0x0000
5
+ TIME = 0x0001
6
+ PLUGIN = 0x0002
7
+ PLUGIN_INSTANCE = 0x0003
8
+ TYPE = 0x0004
9
+ TYPE_INSTANCE = 0x0005
10
+ VALUES = 0x0006
11
+ INTERVAL = 0x0007
12
+ MESSAGE = 0x0100
13
+ SEVERITY = 0x0101
14
+
15
+ # data type
16
+ COUNTER = 0
17
+ GAUGE = 1
18
+ DERIVE = 2
19
+ ABSOLUTE = 3
20
+
21
+
22
+ attr_accessor :host, :time, :interval
23
+ attr_accessor :plugin, :plugin_instance
24
+ attr_accessor :type, :type_instance
25
+
26
+ def initialize
27
+ @values = []
28
+ @values_type = []
29
+ end
30
+
31
+ def add_value(type, value)
32
+ raise(ArgumentError, "unknown type: #{type}") unless CollectdPacket::const_defined?(type.to_s.upcase)
33
+ data_type = CollectdPacket::const_get(type.to_s.upcase)
34
+
35
+ @values_type << data_type
36
+ @values << value
37
+ end
38
+
39
+ def build_packet
40
+ @pkt = CollectdGenerator::string(HOST, @host)
41
+ @pkt << CollectdGenerator::number(TIME, @time)
42
+ @pkt << CollectdGenerator::number(INTERVAL, @interval)
43
+ @pkt << CollectdGenerator::string(PLUGIN, @plugin)
44
+ @pkt << CollectdGenerator::string(PLUGIN_INSTANCE, @plugin_instance)
45
+ @pkt << CollectdGenerator::string(TYPE, @type)
46
+ @pkt << CollectdGenerator::string(TYPE_INSTANCE, @type_instance)
47
+
48
+ # values part header
49
+ @pkt << [VALUES, 4 + 2 + (@values.size * 9), @values.size].pack('nnn')
50
+ # types
51
+ @pkt << @values_type.pack('C*')
52
+
53
+ # and the values
54
+ @values.each.with_index do |v, i|
55
+ case @values_type[i]
56
+ when COUNTER, ABSOLUTE, DERIVE
57
+ @pkt << [v >> 32, v & 0xffffffff].pack("NN")
58
+
59
+ when GAUGE
60
+ @pkt << [v].pack('E')
61
+
62
+ else
63
+ raise "unknown type: #{@values_type[i]}"
64
+ end
65
+ end
66
+
67
+ @pkt
68
+ end
69
+
70
+ end
71
+
72
+ module CollectdGenerator
73
+ # Encode a string (type 0, null terminated string)
74
+ def self.string(type, str)
75
+ str += "\000"
76
+ str_size = str.respond_to?(:bytesize) ? str.bytesize : str.size
77
+ [type, 4 + str_size].pack("nn") + str
78
+ end
79
+
80
+ # Encode an integer
81
+ def self.number(type, num)
82
+ [type, 12].pack("nn") + [num >> 32, num & 0xffffffff].pack("NN")
83
+ end
84
+ end
85
+
86
+ end
@@ -0,0 +1,3 @@
1
+ require File.expand_path('../../../../specs/common', __FILE__)
2
+
3
+ $LOAD_PATH << File.expand_path('../../lib', __FILE__)
@@ -0,0 +1,49 @@
1
+ require File.expand_path('../../common', __FILE__)
2
+
3
+ require 'drone_collectd/parser'
4
+ include DroneCollectd
5
+
6
+ describe 'Packet Parser' do
7
+ before do
8
+ @packet = CollectdPacket.new()
9
+
10
+ @packet.host = "localhost"
11
+ @packet.time = 1
12
+ @packet.interval = 10
13
+ @packet.plugin = "plugin"
14
+ @packet.plugin_instance = "plugin_instance"
15
+ @packet.type = "type"
16
+ @packet.type_instance = "type_instance"
17
+ @packet.add_value(:counter, 42)
18
+ end
19
+
20
+ it 'can generate a packet' do
21
+ expected = [
22
+ "\x00\x00\x00\x0elocalhost\x00", # host
23
+ "\x00\x01\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x01", # time
24
+ "\x00\x07\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x0a", # interval
25
+ "\x00\x02\x00\x0bplugin\x00", # plugin
26
+ "\x00\x03\x00\x14plugin_instance\x00", # plugin_instance
27
+ "\x00\x04\x00\x09type\x00", # type
28
+ "\x00\x05\x00\x12type_instance\x00", # type_instance
29
+ "\x00\x06\x00\x0f\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x2a" # value
30
+
31
+ ]
32
+
33
+ if "".respond_to?(:encode)
34
+ expected = expected.map{|s| s.encode('ASCII') }
35
+ end
36
+
37
+ data = @packet.build_packet
38
+
39
+ data[0,14].should == expected[0]
40
+ data[14,12].should == expected[1]
41
+ data[26,12].should == expected[2]
42
+ data[38,11].should == expected[3]
43
+ data[49,20].should == expected[4]
44
+ data[69, 9].should == expected[5]
45
+ data[78,18].should == expected[6]
46
+ data[96,15].should == expected[7]
47
+ end
48
+
49
+ end
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in drone_json.gemspec
4
+ gemspec
5
+
6
+ gem 'drone', :path => "/Users/schmurfy/Dev/personal/drone"
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011-2011 Julien Ammous
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,9 @@
1
+ # What is this ?
2
+
3
+ It's a module for Drone to serve the data via json with a Thin server.<br/>
4
+ You can find more about Drone [here](https://github.com/schmurfy/drone)
5
+
6
+ # Supported Runtimes
7
+
8
+ - MRI 1.8.7+
9
+ - Rubinius 1.2.2+
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../../../lib/drone/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "drone_json"
6
+ s.version = Drone::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Julien Ammous"]
9
+ s.email = []
10
+ s.homepage = ""
11
+ s.summary = %q{Drone Interface}
12
+ s.description = %q{JSON Interface for Drone}
13
+
14
+ s.rubyforge_project = "drone_json"
15
+
16
+ s.files = Dir['LICENSE', 'README.md', 'lib/**/*']
17
+ s.test_files = Dir['specs/**/*']
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_dependency('drone', '~> 1.0.4')
21
+ s.add_dependency('thin')
22
+
23
+ if RUBY_VERSION < "1.9.1"
24
+ s.add_dependency('json')
25
+ end
26
+
27
+ s.add_development_dependency("mocha")
28
+ s.add_development_dependency("bacon")
29
+ s.add_development_dependency("schmurfy-em-spec")
30
+ s.add_development_dependency("delorean")
31
+ s.add_development_dependency("simplecov")
32
+ end
@@ -0,0 +1,9 @@
1
+ require 'drone'
2
+
3
+ module DroneJson
4
+ def self.require_lib(path)
5
+ require File.expand_path("../#{path}", __FILE__)
6
+ end
7
+
8
+ require_lib('drone_json/json')
9
+ end
@@ -0,0 +1,100 @@
1
+ require 'thin'
2
+ require 'json'
3
+
4
+ Thin::Logging.silent = true
5
+
6
+ module Drone
7
+ module Interfaces
8
+
9
+ class Json < Base
10
+ def initialize(address = '0.0.0.0', port = 3001)
11
+ me = self
12
+ Thin::Server.start(address, port) do
13
+ map('/'){ run(me) }
14
+ end
15
+ end
16
+
17
+ def call(env)
18
+ ret = {}
19
+
20
+ Drone::each_metric do |m|
21
+ case m
22
+ when Metrics::Gauge
23
+ ret[m.name] = gauge_hash(m)
24
+
25
+ when Metrics::Counter
26
+ ret[m.name] = counter_hash(m)
27
+
28
+ when Metrics::Timer
29
+ tmp = {
30
+ 'type' => 'timer'
31
+ }
32
+
33
+ tmp = histogram_hash(m, tmp)
34
+
35
+ ret[m.name] = tmp
36
+
37
+ when Metrics::Meter
38
+ ret[m.name] = meter_hash(m, { 'type' => 'meter' })
39
+
40
+ when Metrics::Histogram
41
+ ret[m.name] = histogram_hash(m, { 'type' => 'histogram' })
42
+
43
+ else
44
+ puts "Unknown metric: #{m}"
45
+ end
46
+ end
47
+
48
+
49
+ [
50
+ 200,
51
+ {'Content-Type' => 'application/json'},
52
+ ret.to_json
53
+ ]
54
+ end
55
+
56
+ private
57
+ def gauge_hash(m)
58
+ {
59
+ 'type' => 'gauge',
60
+ 'value' => m.value
61
+ }
62
+ end
63
+
64
+ def counter_hash(m)
65
+ {
66
+ 'type' => 'counter',
67
+ 'value' => m.value
68
+ }
69
+ end
70
+
71
+ def meter_hash(m, h = {})
72
+ h.merge({
73
+ 'count' => m.count,
74
+ 'mean_rate' => m.mean_rate,
75
+ 'rate_1' => m.one_minute_rate,
76
+ 'rate_5' => m.five_minutes_rate,
77
+ 'rate_15' => m.fifteen_minutes_rate
78
+ })
79
+ end
80
+
81
+ def histogram_hash(m, h = {})
82
+ percentiles = m.percentiles(0.5, 0.75, 0.95, 0.98, 0.99, 0.999)
83
+
84
+ h.merge({
85
+ 'min' => m.min,
86
+ 'max' => m.max,
87
+ 'mean' => m.mean,
88
+ 'stddev' => m.stdDev,
89
+ 'median' => percentiles[0],
90
+ '75p' => percentiles[1],
91
+ '95p' => percentiles[2],
92
+ '98p' => percentiles[3],
93
+ '99p' => percentiles[4],
94
+ '999p' => percentiles[5]
95
+ })
96
+ end
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,63 @@
1
+ $:.reject! { |e| e.include? 'TextMate' }
2
+
3
+ require 'rubygems'
4
+
5
+ puts "Testing with ruby #{RUBY_VERSION} and rubygems #{Gem::VERSION}"
6
+
7
+ require 'bundler/setup'
8
+
9
+ if (RUBY_VERSION >= "1.9") && ENV['COVERAGE']
10
+ require 'simplecov'
11
+ ROOT = File.expand_path('../../', __FILE__)
12
+
13
+ puts "[[ SimpleCov enabled ]]"
14
+
15
+ SimpleCov.start do
16
+ add_filter '/gems/'
17
+ add_filter '/specs/'
18
+
19
+ root(ROOT)
20
+ end
21
+ end
22
+
23
+ require 'bacon'
24
+ require 'mocha'
25
+ require 'delorean'
26
+ require 'em-spec/bacon'
27
+ EM.spec_backend = EventMachine::Spec::Bacon
28
+
29
+ $LOAD_PATH << File.expand_path('../../lib', __FILE__)
30
+
31
+ module Bacon
32
+ module MochaRequirementsCounter
33
+ def self.increment
34
+ Counter[:requirements] += 1
35
+ end
36
+ end
37
+
38
+ class Context
39
+ include Mocha::API
40
+
41
+ alias_method :it_before_mocha, :it
42
+
43
+ def it(description)
44
+ it_before_mocha(description) do
45
+ begin
46
+ mocha_setup
47
+ yield
48
+ mocha_verify(MochaRequirementsCounter)
49
+ rescue Mocha::ExpectationError => e
50
+ raise Error.new(:failed, "#{e.message}\n#{e.backtrace[0...10].join("\n")}")
51
+ ensure
52
+ mocha_teardown
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def focus(test_label)
60
+ Bacon.const_set(:RestrictName, %r{#{test_label}})
61
+ end
62
+
63
+ Bacon.summary_on_exit()
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in drone_redis.gemspec
4
+ gemspec
5
+
6
+ gem 'drone', :path => "/Users/schmurfy/Dev/personal/drone"
7
+
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../../../lib/drone/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "drone_redis"
6
+ s.version = Drone::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Julien ammous"]
9
+ s.email = []
10
+ s.homepage = ""
11
+ s.summary = %q{Redis Storage for Drone}
12
+ s.description = %q{-}
13
+
14
+ s.rubyforge_project = "drone_redis"
15
+
16
+ s.files = Dir['LICENSE', 'README.md', 'lib/**/*']
17
+ s.test_files = Dir['specs/**/*']
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_dependency('superfeedr-em-redis')
21
+ s.add_dependency('drone', '~> 1.0.4')
22
+ end
@@ -0,0 +1,8 @@
1
+
2
+ module DroneRedis
3
+ def self.require_lib(path)
4
+ require File.expand_path("../#{path}", __FILE__)
5
+ end
6
+
7
+ require_lib("drone_redis/redis")
8
+ end
@@ -0,0 +1,218 @@
1
+ require 'drone'
2
+ require 'em-redis'
3
+ require 'fiber'
4
+
5
+ module Drone
6
+ module Storage
7
+
8
+ class Redis < Base
9
+
10
+ class SharedNumber
11
+ def initialize(key, store, initial_value = 0)
12
+ @store = store
13
+ @key = key
14
+ @initial_value = initial_value
15
+ end
16
+
17
+ def init_if_required
18
+ unless @init_done
19
+ @init_done = true
20
+ # initialize the key to 0 if it does not exists
21
+ @store.setnx(@key, @initial_value)
22
+ @initial_value = nil
23
+ end
24
+ end
25
+
26
+ def inc(n = 1)
27
+ init_if_required()
28
+ @store.incr(@key, n)
29
+ end
30
+
31
+ def dec(n = 1)
32
+ init_if_required()
33
+ @store.decr(@key, n)
34
+ end
35
+
36
+ def set(n)
37
+ @store.set(@key, n)
38
+ end
39
+
40
+ def get
41
+ init_if_required()
42
+ @store.get(@key).to_f
43
+ end
44
+
45
+ def get_and_set(n)
46
+ init_if_required()
47
+ @store.getset(@key, n).to_f
48
+ end
49
+
50
+ def compare_and_set(expected, new_value)
51
+ @store.watch(@key)
52
+ if @store.get == expected
53
+ @store.multi
54
+ set(new_value)
55
+ @store.exec
56
+ true
57
+ else
58
+ false
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+
65
+ class SharedFixedSizeArray
66
+ def initialize(key, size, store, initial_value = 0)
67
+ @store = store
68
+ @key = key
69
+ @size = size
70
+ @initial_value = initial_value
71
+ @init_done = false
72
+ raise "wtf"
73
+ end
74
+
75
+ def init_if_required
76
+ unless @init_done
77
+ @init_done = true
78
+ # initialize the key to 0 if it does not exists
79
+ @size.times do |n|
80
+ self[n] = @initial_value
81
+ end
82
+ @initial_value = nil
83
+ end
84
+ end
85
+
86
+ def [](index, to = nil)
87
+ init_if_required()
88
+
89
+ if to
90
+ @store.lrange(@key, index, to)
91
+ else
92
+ @store.lindex(@key, index)
93
+ end
94
+ end
95
+
96
+ def []=(index, val)
97
+ init_if_required()
98
+
99
+ @store.lset(@key, index, val)
100
+ end
101
+
102
+ def size
103
+ init_if_required()
104
+ # size is fixed so no need to ask redis
105
+ # for it
106
+ @size
107
+ end
108
+ end
109
+
110
+
111
+ class SharedHash
112
+ def initialize(key, store)
113
+ @store = store
114
+ @key = key
115
+ end
116
+
117
+ def [](k)
118
+ @store.hget(@key, k)
119
+ end
120
+
121
+ def []=(k, val)
122
+ @store.hset(@key, k, val)
123
+ end
124
+
125
+ def clear
126
+ @store.del()
127
+ end
128
+
129
+ def size
130
+ @store.hlen(@key)
131
+ end
132
+
133
+ def keys
134
+ @store.hkeys(@key)
135
+ end
136
+
137
+ def values
138
+ @store.hvals(@key)
139
+ end
140
+
141
+ def map(&block)
142
+ # Hash[1, "v1", 2, "v2"] => {1 => "v1", 2 => "v2"}
143
+ Hash[*@store.hgetall(@key)].map(&block)
144
+ end
145
+
146
+ end
147
+
148
+
149
+
150
+ def initialize(address = '127.0.0.1', db = 1, port = nil)
151
+ @address = address
152
+ @db = db
153
+ @port = port
154
+ end
155
+
156
+ def connect()
157
+ @connection ||= EM::Protocols::Redis.connect(
158
+ :host => @address,
159
+ :port => @port,
160
+ :db => @db
161
+ )
162
+
163
+ @connection
164
+ end
165
+
166
+ [
167
+ # transactions
168
+ :multi, :exec, :watch,
169
+
170
+ # for SharedNumber
171
+ :incr, :incrby, :decr, :decrby, :get, :set, :setnx, :getset,
172
+
173
+ # SharedArray
174
+ :lindex, :lset, :llen,
175
+
176
+ # SharedHash
177
+ :hget, :hset, :hlen, :hkeys, :hvals, :hgetall
178
+
179
+ ].each do |m|
180
+ define_method(m) do |*args|
181
+ suspend_and_execute(m, *args)
182
+ end
183
+ end
184
+
185
+ def suspend_and_execute(cmd, *args)
186
+ connect()
187
+ raise "No connection" unless @connection
188
+ # raise(NoMethodError, "command not found: #{cmd}") unless @connection.respond_to?(cmd)
189
+
190
+ fb = Fiber.current
191
+ @connection.send(cmd, *args) do |response|
192
+ fb.resume(response)
193
+ end
194
+ Fiber.yield
195
+ end
196
+
197
+ # def method_missing(method, *args)
198
+ # suspend_and_execute(method, *args)
199
+ # end
200
+
201
+
202
+
203
+ # external api
204
+ def request_fixed_size_array(id, size)
205
+ SharedFixedSizeArray.new(id, size, self)
206
+ end
207
+
208
+ def request_hash(id)
209
+ SharedHash.new(id, self)
210
+ end
211
+
212
+ def request_number(id, initial_value = 0)
213
+ SharedNumber.new(id, self, initial_value)
214
+ end
215
+ end
216
+
217
+ end
218
+ end