ganymed 0.1.0

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 (50) hide show
  1. data/.gitignore +22 -0
  2. data/.rspec +1 -0
  3. data/.rvmrc +1 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +20 -0
  6. data/LICENSE +20 -0
  7. data/README.md +28 -0
  8. data/Rakefile +8 -0
  9. data/bin/ganymed +20 -0
  10. data/ganymed.gemspec +38 -0
  11. data/lib/ganymed/client.rb +132 -0
  12. data/lib/ganymed/collector/cpu.rb +30 -0
  13. data/lib/ganymed/collector/disk.rb +35 -0
  14. data/lib/ganymed/collector/iostat.rb +25 -0
  15. data/lib/ganymed/collector/load.rb +18 -0
  16. data/lib/ganymed/collector/metadata.rb +31 -0
  17. data/lib/ganymed/collector/network.rb +30 -0
  18. data/lib/ganymed/collector/uptime.rb +15 -0
  19. data/lib/ganymed/collector.rb +89 -0
  20. data/lib/ganymed/config.yml +49 -0
  21. data/lib/ganymed/event.rb +102 -0
  22. data/lib/ganymed/master.rb +150 -0
  23. data/lib/ganymed/mongodb.rb +39 -0
  24. data/lib/ganymed/processor.rb +93 -0
  25. data/lib/ganymed/sampler/counter.rb +32 -0
  26. data/lib/ganymed/sampler/datasource.rb +92 -0
  27. data/lib/ganymed/sampler/derive.rb +36 -0
  28. data/lib/ganymed/sampler/gauge.rb +24 -0
  29. data/lib/ganymed/sampler.rb +106 -0
  30. data/lib/ganymed/version.rb +3 -0
  31. data/lib/ganymed/websocket/authentication.rb +37 -0
  32. data/lib/ganymed/websocket/connection.rb +71 -0
  33. data/lib/ganymed/websocket/filter.rb +23 -0
  34. data/lib/ganymed/websocket/metadata.rb +21 -0
  35. data/lib/ganymed/websocket/query.rb +26 -0
  36. data/lib/ganymed/websocket/subscribe.rb +45 -0
  37. data/lib/ganymed/websocket.rb +45 -0
  38. data/lib/ganymed.rb +6 -0
  39. data/spec/sampler/counter_spec.rb +11 -0
  40. data/spec/sampler/datasource_examples.rb +49 -0
  41. data/spec/sampler/datasource_spec.rb +23 -0
  42. data/spec/sampler/derive_spec.rb +34 -0
  43. data/spec/sampler/gauge_spec.rb +35 -0
  44. data/spec/sampler_spec.rb +5 -0
  45. data/spec/spec.opts +1 -0
  46. data/spec/spec_helper.rb +10 -0
  47. data/tasks/reek.rake +5 -0
  48. data/tasks/rspec.rake +7 -0
  49. data/tasks/yard.rake +5 -0
  50. metadata +242 -0
@@ -0,0 +1,37 @@
1
+ require 'digest'
2
+
3
+ require 'ganymed/websocket'
4
+
5
+ module Ganymed
6
+ class Websocket
7
+ module Authentication
8
+ def self.included(base)
9
+ base.command :authenticate
10
+ end
11
+
12
+ def authenticate(data)
13
+ @authenticated = db['users'].find({
14
+ :_id => data['username'],
15
+ :password => Digest::SHA256.hexdigest(data['password'])
16
+ }).count > 0
17
+
18
+ if authenticated?
19
+ log.info("successful auth for #{data['username']} from #{peer}")
20
+ send(:state, {authenticated: true})
21
+ metadata # trigger metadata push
22
+ else
23
+ log.warn("bad auth for #{data['username']} from #{peer}")
24
+ send(:state, {authenticated: false})
25
+ end
26
+ rescue Exception => exc
27
+ log.warn("failed to authenticate #{peer}")
28
+ log.exception(exc)
29
+ send(:state, {authenticated: false})
30
+ end
31
+
32
+ def authenticated?
33
+ @authenticated ||= false
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,71 @@
1
+ require 'em-websocket'
2
+ require 'yajl'
3
+
4
+ require 'ganymed/websocket'
5
+ require 'ganymed/websocket/authentication'
6
+ require 'ganymed/websocket/subscribe'
7
+ require 'ganymed/websocket/metadata'
8
+ require 'ganymed/websocket/query'
9
+
10
+ module Ganymed
11
+ class Websocket
12
+ class Connection < EventMachine::WebSocket::Connection
13
+ attr_accessor :config, :db
14
+
15
+ def self.command(command)
16
+ @commands ||= []
17
+ @commands << command
18
+ end
19
+
20
+ def self.commands
21
+ @commands
22
+ end
23
+
24
+ include Authentication
25
+ include Metadata
26
+ include Subscribe
27
+ include Query
28
+
29
+ def peer
30
+ sin = Socket.unpack_sockaddr_in(get_peername)
31
+ "#{sin[1]}:#{sin[0]}"
32
+ rescue
33
+ "unknown"
34
+ end
35
+
36
+ def convert(data)
37
+ if data.is_a?(Hash)
38
+ data
39
+ elsif data.is_a?(Array)
40
+ data.map {|elem| convert(elem)}
41
+ else
42
+ if data.respond_to?(:as_json)
43
+ data.as_json
44
+ elsif data.respond_to?(:to_h)
45
+ data.to_h
46
+ elsif data.respond_to?(:to_a)
47
+ data.to_a
48
+ else
49
+ data
50
+ end
51
+ end
52
+ end
53
+
54
+ def send(type, data)
55
+ super(Yajl::Encoder.encode({type.to_s => convert(data)}))
56
+ end
57
+
58
+ def trigger_on_message(data)
59
+ data = Yajl::Parser.parse(data)
60
+ data.each do |command, data|
61
+ log.debug("command #{command}(#{data.inspect})")
62
+ if self.class.commands.include?(command.to_sym)
63
+ __send__(command.to_sym, data)
64
+ else
65
+ send(:error, "invalid command")
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,23 @@
1
+ require 'ganymed/websocket'
2
+
3
+ module Ganymed
4
+ class Websocket
5
+ class Filter
6
+ def initialize(spec)
7
+ @spec = spec
8
+ end
9
+
10
+ def match?(event)
11
+ @spec.map do |skey, svalue|
12
+ evalue = event.send(skey.to_sym)
13
+ case svalue
14
+ when String
15
+ svalue == evalue
16
+ when Array
17
+ svalue.include?(evalue)
18
+ end
19
+ end.all?
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ require 'digest'
2
+
3
+ require 'ganymed/websocket'
4
+
5
+ module Ganymed
6
+ class Websocket
7
+ module Metadata
8
+ def self.included(base)
9
+ base.command :metadata
10
+ end
11
+
12
+ def metadata(data=nil)
13
+ metadata = db['metadata'].find()
14
+ send(:metadata, metadata)
15
+ rescue Exception => exc
16
+ log.warn("failed to send metadata to #{peer}")
17
+ log.exception(exc)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ require 'ganymed/websocket'
2
+ require 'ganymed/websocket/filter'
3
+
4
+ module Ganymed
5
+ class Websocket
6
+ module Query
7
+ def self.included(base)
8
+ base.command :query
9
+ end
10
+
11
+ def query(data)
12
+ return if not authenticated?
13
+ data.each do |ns, query|
14
+ query_id = query.delete('_id')
15
+ log.debug("query #{query_id} from #{peer}: #{ns}(#{query.inspect})")
16
+
17
+ events = db.collection(ns).find(query).map do |event|
18
+ Event.parse(event.merge({'n' => ns}))
19
+ end
20
+
21
+ send(:result, {query_id => convert(events)})
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,45 @@
1
+ require 'ganymed/websocket'
2
+ require 'ganymed/websocket/filter'
3
+
4
+ module Ganymed
5
+ class Websocket
6
+ module Subscribe
7
+ def self.included(base)
8
+ base.command :subscribe
9
+ base.command :unsubscribe
10
+ end
11
+
12
+ def filters
13
+ @filters ||= {}
14
+ end
15
+
16
+ def subscribe(data)
17
+ return if not authenticated?
18
+ data.each do |prefix, filter|
19
+ log.debug("client #{peer} subscribed to #{prefix} with filter=#{filter.inspect}")
20
+ filters[prefix] = Filter.new(filter)
21
+ end
22
+ end
23
+
24
+ def unsubscribe(data)
25
+ return if not authenticated?
26
+ data.each do |prefix, filter|
27
+ log.debug("client #{peer} unsubscribed prefix #{prefix}")
28
+ filters.delete(prefix)
29
+ end
30
+ end
31
+
32
+ def subscribed?(event)
33
+ filters.map do |prefix, filter|
34
+ filter if prefix == "all" or event.ns =~ /^#{prefix}/
35
+ end.compact.map do |filter|
36
+ filter.match?(event)
37
+ end.any?
38
+ end
39
+
40
+ def publish(event)
41
+ send(:event, [event]) if subscribed?(event)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ require 'em-websocket'
2
+ require 'yajl'
3
+
4
+ require 'ganymed'
5
+ require 'ganymed/websocket/connection'
6
+
7
+ module Ganymed
8
+ class Websocket
9
+ attr_accessor :config, :db
10
+
11
+ def initialize(config, db)
12
+ @config, @db = config, db
13
+ @connections = []
14
+ run
15
+ end
16
+
17
+ def run
18
+ log.info("accepting WebSocket connections on tcp##{config.host}:#{config.port}")
19
+ EM.start_server(config.host, config.port, Connection, {}) do |connection|
20
+ connection.config = config
21
+ connection.db = db
22
+
23
+ connection.onopen do
24
+ log.info("new connection from #{connection.peer}")
25
+ @connections << connection
26
+ end
27
+
28
+ connection.onclose do
29
+ log.info("#{connection.peer} has closed the connection")
30
+ @connections.delete(connection)
31
+ end
32
+ end
33
+ end
34
+
35
+ def each(&block)
36
+ @connections.each(&block)
37
+ end
38
+
39
+ def send(type, data)
40
+ each do |connection|
41
+ connection.send(type, data)
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/ganymed.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'madvertise/ext/logging'
2
+
3
+ module Ganymed
4
+ GEM_ROOT = File.expand_path("../..", __FILE__).freeze
5
+ LIB_DIR = File.join(GEM_ROOT, "lib").freeze
6
+ end
@@ -0,0 +1,11 @@
1
+ require 'ganymed/sampler/counter'
2
+ require 'sampler/datasource_examples'
3
+
4
+ describe Ganymed::Sampler::Counter do
5
+ include_context 'DataSource'
6
+ subject { Ganymed::Sampler::Counter.new(ticks) }
7
+
8
+ let(:values) { 10.times.collect { rand } }
9
+
10
+ it_behaves_like Ganymed::Sampler::DataSource
11
+ end
@@ -0,0 +1,49 @@
1
+ require 'ganymed/sampler/datasource'
2
+
3
+ shared_context 'DataSource' do
4
+ let(:ns) { 'foo.bar' }
5
+ let(:origin) { 'example.com' }
6
+ let(:ticks) { [300, 1] }
7
+ let(:value) { rand }
8
+ end
9
+
10
+ shared_examples Ganymed::Sampler::DataSource do
11
+ %w(
12
+ add
13
+ buffer
14
+ each
15
+ feed
16
+ flush
17
+ ticks
18
+ ).each do |method|
19
+ it { should respond_to method }
20
+ end
21
+
22
+ describe "#add" do
23
+ it "should add the given value to each tick buffer" do
24
+ subject.add(ns, origin, value)
25
+ ticks.each do |tick|
26
+ subject.buffer[tick][ns][origin].should include(value)
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "#each" do
32
+ before(:each) do
33
+ values.each do |value|
34
+ subject.add(ns, origin, value)
35
+ end
36
+ end
37
+
38
+ it "should clear the buffer" do
39
+ subject.each(1) { |ns, origin, values| }
40
+ subject.buffer[1].should be_empty
41
+ end
42
+
43
+ it "should yield all values" do
44
+ subject.should_receive(:each).with(1).once.and_yield(ns, origin, values).once
45
+ subject.each(1) { |ns, origin, values| }
46
+ end
47
+ end
48
+
49
+ end
@@ -0,0 +1,23 @@
1
+ require 'ganymed/sampler/derive'
2
+ require 'sampler/datasource_examples'
3
+
4
+ describe Ganymed::Sampler::DataSource do
5
+ include_context 'DataSource'
6
+ subject { Ganymed::Sampler::DataSource.new(ticks) }
7
+
8
+ let(:values) { 10.times.collect { rand } }
9
+
10
+ it_behaves_like Ganymed::Sampler::DataSource
11
+
12
+ describe "#flush" do
13
+ it "is not implemented" do
14
+ expect { subject.flush(1) }.to raise_error(NotImplementedError)
15
+ end
16
+ end
17
+
18
+ describe "#feed" do
19
+ it "is not implemented" do
20
+ expect { subject.feed(ns, origin, nil, value) }.to raise_error(NotImplementedError)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ require 'ganymed/sampler/derive'
2
+ require 'sampler/datasource_examples'
3
+
4
+ describe Ganymed::Sampler::Derive do
5
+ include_context 'DataSource'
6
+ subject { Ganymed::Sampler::Derive.new(ticks) }
7
+
8
+ let(:ts) { now = Time.now.utc; now.to_i + (now.usec * 1e-6) }
9
+ let(:values) { 10.times.collect { rand } }
10
+
11
+ it_behaves_like Ganymed::Sampler::DataSource
12
+
13
+ describe "#flush" do
14
+ before(:each) do
15
+ values.each do |value|
16
+ subject.feed(ns, origin, ts, value)
17
+ end
18
+ end
19
+
20
+ it "should call each" do
21
+ subject.should_receive(:each).with(1).once
22
+ subject.flush(1)
23
+ end
24
+
25
+ it "should yield derived values"
26
+ end
27
+
28
+ describe "#feed" do
29
+ it "should add fed samples" do
30
+ subject.should_receive(:add).with(ns, origin, [ts, value])
31
+ subject.feed(ns, origin, ts, value)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,35 @@
1
+ require 'ganymed/sampler/gauge'
2
+ require 'sampler/datasource_examples'
3
+
4
+ describe Ganymed::Sampler::Gauge do
5
+ include_context 'DataSource'
6
+ subject { Ganymed::Sampler::Gauge.new(ticks) }
7
+
8
+ let(:values) { 10.times.collect { rand } }
9
+
10
+ it_behaves_like Ganymed::Sampler::DataSource
11
+
12
+ describe "#flush" do
13
+ before(:each) do
14
+ values.each do |value|
15
+ subject.feed(ns, origin, nil, value)
16
+ end
17
+ end
18
+
19
+ it "should call each" do
20
+ subject.should_receive(:each).with(1).once
21
+ subject.flush(1)
22
+ end
23
+
24
+ it "should yield all values" do
25
+ expect { |b| subject.flush(1, &b) }.to yield_with_args(ns, origin, values)
26
+ end
27
+ end
28
+
29
+ describe "#feed" do
30
+ it "should add fed samples" do
31
+ subject.should_receive(:add).with(ns, origin, value)
32
+ subject.feed(ns, origin, nil, value)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ require 'ganymed/sampler'
2
+
3
+ describe Ganymed::Sampler do
4
+
5
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper --color --format documentation
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ $:.unshift(File.join(File.expand_path("../..", __FILE__), 'lib'))
3
+
4
+ require 'rspec'
5
+ require 'debugger'
6
+ require 'simplecov'
7
+ SimpleCov.start
8
+
9
+ RSpec.configure do |config|
10
+ end
data/tasks/reek.rake ADDED
@@ -0,0 +1,5 @@
1
+ require 'reek/rake/task'
2
+
3
+ Reek::Rake::Task.new do |t|
4
+ t.fail_on_error = false
5
+ end
data/tasks/rspec.rake ADDED
@@ -0,0 +1,7 @@
1
+ require 'rspec'
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc "Run the specs"
5
+ RSpec::Core::RakeTask.new do |t|
6
+ t.rspec_opts = ['--options', "spec/spec.opts"]
7
+ end
data/tasks/yard.rake ADDED
@@ -0,0 +1,5 @@
1
+ require 'yard'
2
+
3
+ YARD::Rake::YardocTask.new do |t|
4
+ t.files = ['lib/**/*.rb', 'README.rdoc']
5
+ end