ganymed 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,102 @@
1
+ module Ganymed
2
+
3
+ ##
4
+ # An incoming event is a hash-like object and contains the following keys:
5
+ #
6
+ # n:: namespace (e.g. +os.cpu.idle+, +nginx.requests+, etc)
7
+ # m:: event modifiers (e.g. +realtime+)
8
+ # c:: a consolidation function used to build this event from multiple
9
+ # samples
10
+ # o:: origin (e.g. +web1.example.com+, +myapplication+)
11
+ # t:: timestamp as seconds since epoch
12
+ # v:: the value in integer or floating-point representation
13
+ #
14
+ class Event
15
+ # The namespace of this event.
16
+ # @return [String]
17
+ attr_accessor :ns
18
+
19
+ # The event modifiers.
20
+ # @return [Array]
21
+ attr_accessor :modifiers
22
+
23
+ # The consolidation function used to build this event from multiple
24
+ # samples.
25
+ # @return [String]
26
+ attr_accessor :cf
27
+
28
+ # The origin of this event.
29
+ # @return [String]
30
+ attr_accessor :origin
31
+
32
+ # The timestamp of this event.
33
+ # @return [Fixnum]
34
+ attr_accessor :timestamp
35
+
36
+ # The actual value of this sample.
37
+ # @return [Fixnum, Float]
38
+ attr_accessor :value
39
+
40
+ # Create a new {Event} instance from +obj+.
41
+ #
42
+ # @return [Event] The parsed event instance.
43
+ def self.parse(obj)
44
+ new.tap do |event|
45
+ event.ns = obj['n']
46
+ event.modifiers = obj['m']
47
+ event.cf = obj['c']
48
+ event.origin = obj['o']
49
+ event.timestamp = obj['t'].to_i
50
+
51
+ value = obj['v']
52
+ if value.is_a?(Float) and (value.nan? or value.infinite?)
53
+ event.value = nil
54
+ else
55
+ event.value = value
56
+ end
57
+
58
+ event.validate
59
+ end
60
+ end
61
+
62
+ # Convert this event into a hash with long keys. This hash cannot be parsed
63
+ # by #parse which accepts short keys for effiency.
64
+ #
65
+ # @return [Hash]
66
+ def to_h
67
+ {
68
+ :ns => ns,
69
+ :cf => cf,
70
+ :origin => origin,
71
+ :timestamp => timestamp,
72
+ :value => value
73
+ }
74
+ end
75
+
76
+ # Check if this event is a realtime event.
77
+ #
78
+ # @return [TrueClass,FalseClass]
79
+ def realtime?
80
+ @modifiers.include?("realtime")
81
+ end
82
+
83
+ # Validate this event.
84
+ def validate
85
+ unless @ns =~ /^[a-z][a-z0-9_\.-]+[a-z0-9]$/
86
+ raise "invalid namespace: #{@ns.inspect}"
87
+ end
88
+
89
+ if @ns =~ /^(system|ganymed)\./
90
+ raise "namespace is reserved: #{@ns.inspect}"
91
+ end
92
+
93
+ if @timestamp < 0
94
+ raise "invalid timestamp: #{@timestamp.inspect}"
95
+ end
96
+
97
+ unless @value.is_a?(Fixnum) or @value.is_a?(Float)
98
+ raise "invalid value in #{@ns}: #{@value.inspect} (#{@value.class.name})"
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,150 @@
1
+ require 'eventmachine'
2
+ require 'madvertise/ext/config'
3
+ require 'madvertise/ext/environment'
4
+ require 'mixlib/cli'
5
+ require 'servolux'
6
+ require 'socket'
7
+
8
+ require 'ganymed'
9
+ require 'ganymed/collector'
10
+ require 'ganymed/processor'
11
+ require 'ganymed/sampler'
12
+ require 'ganymed/version'
13
+
14
+ module Ganymed
15
+ # @private
16
+ class Master < ::Servolux::Server
17
+ include Configuration::Helpers
18
+
19
+ def initialize(cli)
20
+ $0 = "ganymed"
21
+
22
+ log.level = :debug if cli[:debug]
23
+
24
+ @default_config_file = File.join(LIB_DIR, 'ganymed/config.yml')
25
+ @config_file = cli[:config_file]
26
+
27
+ @pid_file = config.pid_file.tap{}
28
+
29
+ super('ganymed', :interval => 1, :logger => log, :pid_file => @pid_file)
30
+ end
31
+
32
+ def run
33
+ unless EventMachine.reactor_running?
34
+ @reactor.join if @reactor.is_a?(Thread)
35
+ log.info("starting Ganymed reactor in #{Env.mode} mode")
36
+ @reactor = Thread.new do
37
+ begin
38
+ EventMachine.run { setup_reactor }
39
+ rescue Exception => exc
40
+ log.exception(exc)
41
+ end
42
+ end
43
+ end
44
+
45
+ if config.profiling.gcprofiler
46
+ puts GC::Profiler.report
47
+ end
48
+ end
49
+
50
+ def setup_reactor
51
+ Processor.new(config) if config.processor.enabled
52
+ Sampler.new(config) if config.sampler.enabled
53
+ Collector.new(config) if config.collector.enabled
54
+ end
55
+
56
+ def before_starting
57
+ if config.profiling.perftools
58
+ log.info("activating PerfTools CPU profiler")
59
+ require 'perftools'
60
+ PerfTools::CpuProfiler.start('profile')
61
+ end
62
+
63
+ if config.profiling.gcprofiler
64
+ log.info("activating GC::Profiler")
65
+ GC::Profiler.enable
66
+ end
67
+
68
+ if config.profiling.rubyprof
69
+ require 'ruby-prof'
70
+ log.info("activating RubyProf")
71
+ RubyProf.start
72
+ end
73
+ end
74
+
75
+
76
+ def after_stopping
77
+ log.info("shutting down Ganymed reactor")
78
+ EventMachine.stop_event_loop if EventMachine.reactor_running?
79
+ @reactor.join if @reactor.is_a?(Thread)
80
+
81
+ if config.profiling.perftools
82
+ PerfTools::CpuProfiler.stop
83
+ end
84
+
85
+ if config.profiling.rubyprof
86
+ result = RubyProf.stop
87
+ result.eliminate_methods!([
88
+ /Queue#pop/,
89
+ /Queue#push/,
90
+ /Mutex#synchronize/,
91
+ /Mutex#sleep/,
92
+ ])
93
+ printer = RubyProf::MultiPrinter.new(result)
94
+ printer.print(:path => "prof", :profile => "ganymed")
95
+ end
96
+
97
+ log.info("done shutting down. exiting ...")
98
+ end
99
+
100
+ class CLI
101
+ include Mixlib::CLI
102
+
103
+ option :config_file,
104
+ :short => '-c CONFIG',
105
+ :long => '--config CONFIG',
106
+ :description => "The configuration file to use"
107
+
108
+ option :debug,
109
+ :short => '-D',
110
+ :long => '--debug',
111
+ :description => "Enable debug output",
112
+ :boolean => true
113
+
114
+ option :daemonize,
115
+ :short => '-d',
116
+ :long => '--daemonize',
117
+ :description => "Daemonize the server process",
118
+ :boolean => true
119
+
120
+ option :kill,
121
+ :short => '-k',
122
+ :long => '--kill',
123
+ :description => "Kill the currently running daemon instance",
124
+ :boolean => true
125
+
126
+ option :environment,
127
+ :short => '-e ENVIRONMENT',
128
+ :long => '--environment ENVIRONMENT',
129
+ :description => "Set the daemon environment",
130
+ :default => "development",
131
+ :proc => Proc.new { |value| ENV[Env.key] = value }
132
+
133
+ option :help,
134
+ :short => '-h',
135
+ :long => '--help',
136
+ :description => "Show this message",
137
+ :on => :tail,
138
+ :boolean => true,
139
+ :show_options => true,
140
+ :exit => 0
141
+
142
+ def self.parse_options
143
+ Env.key = 'GANYMED_ENV'
144
+ new.tap do |cli|
145
+ cli.parse_options
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,39 @@
1
+ require 'mongo'
2
+
3
+ module Ganymed
4
+ # @private
5
+ class MongoDB
6
+ attr_reader :config
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ log.info("using MongoDB at #{config.host}:#{config.port}/#{config.database}")
11
+ end
12
+
13
+ def connection
14
+ @connection ||= ::Mongo::Connection.new(config.host,
15
+ config.port,
16
+ :pool_size => 5,
17
+ :pool_timeout => 5)
18
+ end
19
+
20
+ def db
21
+ @db ||= connection.db(config.database)
22
+ end
23
+
24
+ def collection(ns)
25
+ db[ns].tap do |col|
26
+ col.ensure_index([['c', ::Mongo::ASCENDING]])
27
+ col.ensure_index([['o', ::Mongo::ASCENDING]])
28
+ col.ensure_index([['t', ::Mongo::ASCENDING]])
29
+ col.ensure_index([['c', ::Mongo::ASCENDING], ['o', ::Mongo::ASCENDING]])
30
+ col.ensure_index([['o', ::Mongo::ASCENDING], ['t', ::Mongo::ASCENDING]])
31
+ col.ensure_index([['c', ::Mongo::ASCENDING], ['o', ::Mongo::ASCENDING], ['t', ::Mongo::ASCENDING]], :unique => true)
32
+ end
33
+ end
34
+
35
+ def method_missing(method, *args, &block)
36
+ db.__send__(method, *args, &block)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,93 @@
1
+ require 'eventmachine'
2
+ require 'msgpack'
3
+
4
+ require 'ganymed/event'
5
+ require 'ganymed/mongodb'
6
+ require 'ganymed/websocket'
7
+
8
+ module Ganymed
9
+ class Processor
10
+ def initialize(config)
11
+ @config = config
12
+ @db = MongoDB.new(config.processor.mongodb)
13
+ @websocket = Websocket.new(config.processor.websocket, @db)
14
+ listen!
15
+ end
16
+
17
+ def listen!
18
+ @config.processor.listen.tap do |listen|
19
+ log.info("processing metrics on udp##{listen.host}:#{listen.port}")
20
+ EM.open_datagram_socket(listen.host, listen.port, Connection) do |connection|
21
+ connection.db = @db
22
+ connection.websocket = @websocket
23
+ end
24
+ end
25
+ end
26
+
27
+ module Connection
28
+ attr_accessor :db, :websocket
29
+
30
+ def receive_data(data)
31
+ EM.defer do
32
+ begin
33
+ data = MessagePack.unpack(data)
34
+ case data['_type'].to_sym
35
+ when :event
36
+ process_event(data)
37
+ when :metadata
38
+ process_metadata(data)
39
+ end
40
+ rescue Exception => exc
41
+ log.exception(exc)
42
+ end
43
+ end
44
+ end
45
+
46
+ def process_event(data)
47
+ event = Event.parse(data)
48
+
49
+ # insert origin + namespace into metadata
50
+ metadata = db['metadata'].find_and_modify({
51
+ query: {:_id => event.origin},
52
+ update: {:$addToSet => {:namespaces => event.ns}},
53
+ upsert: true,
54
+ })
55
+
56
+ # set defaults in case find_and_modify returned an empty document
57
+ metadata['_id'] ||= event.origin
58
+ metadata['namespaces'] ||= []
59
+
60
+ # if metadata has changed notify websocket clients
61
+ if not metadata['namespaces'].include?(event.ns)
62
+ metadata['namespaces'] |= [event.ns]
63
+ websocket.send(:metadata, [metadata])
64
+ end
65
+
66
+ # send the event to websocket clients
67
+ websocket.each do |connection|
68
+ connection.publish(event)
69
+ end
70
+
71
+ # do not store realtime events into mongodb
72
+ return if event.realtime?
73
+
74
+ # insert event into ns collection
75
+ db.collection(event.ns).update({
76
+ :c => event.cf,
77
+ :o => event.origin,
78
+ :t => event.timestamp
79
+ }, {
80
+ :$set => {:v => event.value}
81
+ }, :upsert => true)
82
+ end
83
+
84
+ def process_metadata(data)
85
+ db['metadata'].update({
86
+ :_id => data['origin'],
87
+ }, {
88
+ :$set => {:data => data['data']}
89
+ }, :upsert => true)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,32 @@
1
+ require 'ganymed/sampler/datasource'
2
+
3
+ module Ganymed
4
+ class Sampler
5
+
6
+ ##
7
+ # A Counter {DataSource} processes samples counting a specific event (such
8
+ # as a webserver request, user login, etc) and produces a rate/s gauge.
9
+ #
10
+ class Counter < DataSource
11
+ def flush(tick, &block)
12
+ each(tick) do |ns, origin, events|
13
+ values = events.sort_by do |event| # sort by timestamp
14
+ event[0]
15
+ end.group_by do |event| # group by timestamp
16
+ event[0].to_i
17
+ end.map do |_, events| # calculate counts for each second.
18
+ events.reduce(0) do |memo, event| # sum(event.count)
19
+ memo + event[1]
20
+ end
21
+ end
22
+
23
+ yield ns, origin, values
24
+ end
25
+ end
26
+
27
+ def feed(ns, origin, ts, value)
28
+ add(ns, origin, [ts, value])
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,92 @@
1
+ module Ganymed
2
+ class Sampler
3
+
4
+ ##
5
+ # A DataSource processes samples from various sources. Samples are stored
6
+ # to an in-memory buffer and flushed to the {Processor} once for every
7
+ # tick.
8
+ #
9
+ # All DataSources interpolate their samples to gauge style values before
10
+ # flushing to the processor, e.g. a counter (like requests, or bytes sent
11
+ # over the network) is interpolated to a rate/s gauge (e.g. requests/s or
12
+ # bytes/s).
13
+ #
14
+ class DataSource
15
+
16
+ attr_reader :ticks, :buffer
17
+
18
+ # Create a new DataSource with buffers for every +tick+ in +ticks+.
19
+ #
20
+ # @param [Array] ticks All ticks that should be stored in the buffer.
21
+ #
22
+ def initialize(ticks)
23
+ @ticks = ticks
24
+ @mutex = Mutex.new
25
+ @buffer = Hash.new do |buffer, tick|
26
+ buffer[tick] = Hash.new do |tick, ns|
27
+ tick[ns] = Hash.new do |ns, origin|
28
+ ns[origin] = []
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ # @group Methods for subclasses
35
+
36
+ # Feed data from the source.
37
+ #
38
+ # @param [String] ns The namespace for this value.
39
+ # @param [String] origin The origin of this value.
40
+ # @param [Fixnum,Float] ts The timestamp of this value.
41
+ # @param [Fixnum,Float] value The actual value.
42
+ # @return [void]
43
+ def feed(ns, origin, ts, value)
44
+ raise NotImplementedError
45
+ end
46
+
47
+ # Flush and consolidate the buffer for the given +tick+.
48
+ #
49
+ # @param [Fixnum] tick The tick to flush.
50
+ # @yield [ns, values] Run the block once for every namespace, passing in
51
+ # all the consolidated values.
52
+ # @return [void]
53
+ def flush(tick, &block)
54
+ raise NotImplementedError
55
+ end
56
+
57
+ # @endgroup
58
+
59
+ # Add a value to all tick buffers. Usually called from {#feed}.
60
+ #
61
+ # @param [String] ns The namespace for this value.
62
+ # @param [String] origin The origin of this value.
63
+ # @param [Object] value Datasource specific value object.
64
+ # @return [void]
65
+ def add(ns, origin, value)
66
+ @ticks.each do |tick|
67
+ @buffer[tick][ns][origin] << value
68
+ end
69
+ end
70
+
71
+ # Atomically create a new buffer for +tick+ and iterate over the old one.
72
+ # Typically used by {#flush} to consolidate the samples.
73
+ #
74
+ # @param [Fixnum] tick The tick buffer to flush.
75
+ # @yield [ns, origin, values] Run the block once for every namespace
76
+ # and passing in all the consolidated
77
+ # values.
78
+ def each(tick, &block)
79
+ {}.tap do |result|
80
+ @mutex.synchronize do
81
+ result.replace(@buffer[tick].dup)
82
+ @buffer[tick].clear
83
+ end
84
+ end.each do |ns, origins|
85
+ origins.each do |origin, values|
86
+ yield ns, origin, values
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,36 @@
1
+ require 'ganymed/sampler/datasource'
2
+
3
+ module Ganymed
4
+ class Sampler
5
+
6
+ ##
7
+ # A Derive {DataSource data source} will store the derivative of the line
8
+ # going from the last to the current value of the data source. Derive is
9
+ # similar to a {Counter} {DataSource data source} in that it produces a
10
+ # rate/s gauge, but only accepts absolute values and produces the
11
+ # derivative instead of summation of all samples.
12
+ #
13
+ # In order to get (meaningful) realtime events from a Derive {DataSource}
14
+ # samples must be sent multiple times per second. Derive does not store the
15
+ # previously flushed value for performance reasons and therefore cannot
16
+ # infer a rate/s gauge with only one event per flush.
17
+ #
18
+ class Derive < DataSource
19
+ def flush(tick, &block)
20
+ each(tick) do |ns, origin, events|
21
+ values = events.sort_by do |event| # sort by timestamp
22
+ event[0]
23
+ end.each_cons(2).map do |a, b| # derive each consective sample
24
+ (b[1] - a[1])/(b[0] - a[0]) # ∆value / ∆ts
25
+ end
26
+
27
+ yield ns, origin, values
28
+ end
29
+ end
30
+
31
+ def feed(ns, origin, ts, value)
32
+ add(ns, origin, [ts, value])
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ require 'ganymed/sampler/datasource'
2
+
3
+ module Ganymed
4
+ class Sampler
5
+
6
+ ##
7
+ # A Gauge is the simplest DataSource type. It simply records the given
8
+ # value in the buffer and emits all values in the buffer upon flush
9
+ # assuming the given values are in gauge-style (e.g. free memory, users
10
+ # currently logged in, etc).
11
+ #
12
+ class Gauge < DataSource
13
+ def flush(tick, &block)
14
+ each(tick) do |ns, origin, values|
15
+ yield ns, origin, values
16
+ end
17
+ end
18
+
19
+ def feed(ns, origin, ts, value)
20
+ add(ns, origin, value)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,106 @@
1
+ require 'active_support/inflector'
2
+ require 'eventmachine'
3
+ require 'madvertise/ext/enumerable'
4
+ require 'msgpack'
5
+ require 'socket'
6
+
7
+ require 'ganymed'
8
+
9
+ module Ganymed
10
+
11
+ ##
12
+ # The Sampler processes samples from a UDP socket, stores these samples into
13
+ # memory and flushes a possibly interpolated and consolidated value to the
14
+ # {Processor} for every tick.
15
+ #
16
+ class Sampler
17
+ def initialize(config)
18
+ @config = config
19
+ @ticks = [1, config.resolution]
20
+ listen! and emit! and tick!
21
+ end
22
+
23
+ def listen!
24
+ @config.sampler.listen.tap do |listen|
25
+ log.info("processing samples on udp##{listen.host}:#{listen.port}")
26
+ EM.open_datagram_socket(listen.host, listen.port, Connection) do |connection|
27
+ connection.datasources = datasources
28
+ end
29
+ end
30
+ end
31
+
32
+ def emit!
33
+ @config.sampler.emit.tap do |emit|
34
+ log.info("emitting consolidated samples to udp##{emit.host}:#{emit.port}")
35
+ @client = Client.new(:processor => emit)
36
+ @processor = @client.processor
37
+ end
38
+ end
39
+
40
+ def tick!
41
+ @ticks.each do |tick|
42
+ EM.add_periodic_timer(tick) { flush(tick) }
43
+ end
44
+ end
45
+
46
+ def datasources
47
+ @datasources ||= Hash.new do |hsh, key|
48
+ klass = "Ganymed::Sampler::#{key.classify}".constantize
49
+ hsh[key] = klass.new(@ticks)
50
+ end
51
+ end
52
+
53
+ def flush(tick)
54
+ modifiers = tick == 1 ? ['realtime'] : []
55
+ datasources.each do |_, datasource|
56
+ datasource.flush(tick) do |ns, origin, values|
57
+ emit(ns, modifiers, origin, values)
58
+ end
59
+ end
60
+ end
61
+
62
+ # Consolidation functions.
63
+ CFS = {
64
+ :min => [:min],
65
+ :max => [:max],
66
+ :avg => [:mean],
67
+ :stdev => [:stdev],
68
+ :u90 => [:percentile, 0.9],
69
+ }
70
+
71
+ def emit(ns, modifiers, origin, values)
72
+ CFS.each do |cf, args|
73
+ begin
74
+ @processor.event(ns, values.send(*args), {
75
+ :cf => cf,
76
+ :origin => origin,
77
+ :modifiers => modifiers,
78
+ })
79
+ rescue Exception => exc
80
+ log.warn("failed to emit #{ns}@#{origin}")
81
+ log.exception(exc)
82
+ end
83
+ end
84
+ end
85
+
86
+ # @private
87
+ module Connection
88
+ attr_accessor :datasources
89
+
90
+ def receive_data(data)
91
+ begin
92
+ # pack format: <datasource><namespace><origin><timestamp><value>
93
+ data = data.unpack("Z*Z*Z*GG")
94
+ datasource = datasources[data.slice!(0)]
95
+ datasource.feed(*data)
96
+ rescue Exception => exc
97
+ log.exception(exc)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ Dir[File.join(Ganymed::LIB_DIR, 'ganymed/sampler/*.rb')].each do |f|
105
+ require f
106
+ end
@@ -0,0 +1,3 @@
1
+ module Ganymed
2
+ VERSION = '0.1.0'
3
+ end