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,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