ganymed 0.1.2 → 0.2.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.
data/.gitignore CHANGED
@@ -6,6 +6,7 @@
6
6
  Gemfile.lock
7
7
  InstalledFiles
8
8
  _yardoc
9
+ config.yml
9
10
  coverage/
10
11
  doc/
11
12
  ganymed.pid
@@ -15,6 +16,7 @@ node_modules/
15
16
  old/
16
17
  pkg
17
18
  prof/
19
+ profile*
18
20
  rdoc
19
21
  spec/reports
20
22
  test.js
data/README.md CHANGED
@@ -2,18 +2,19 @@
2
2
 
3
3
  Ganymed is an event collection daemon.
4
4
 
5
- Ganymed processes system and application metrics in realtime and can easily
6
- handle thousands of samples per second.
5
+ Ganymed processes system and application metrics and events in realtime and can
6
+ easily handle thousands of samples per second. Ganymed provides these services
7
+ to administrators and third-party applications:
7
8
 
8
- * the *sampler* accepts high frequency samples. these samples are then
9
- consolidated based on the data source type and emitted to the *processor*.
9
+ * The *Sampler* accepts high frequency samples. these samples are then
10
+ consolidated based on the data source type and emitted to the *Processor*.
10
11
 
11
- * the *processor* accepts low frequency data points (possibly created by the
12
- *sampler*) and publishes these to interested parties via WebSocket in
13
- realtime. All samples are also stored in MongoDB for later analysis.
12
+ * The *Processor* accepts metrics (events) and stores these metrics into
13
+ MongoDB. All metrics are also published to interested parties via WebSocket.
14
14
 
15
- * the *collector* will send various system metrics (cpu, memory, network, etc)
16
- to the *sampler* and *processor*
15
+ * The *Collector* is a simple thread that polls various system-level metrics
16
+ that are not pushed to the *Sampler*/*Processor* from third-party
17
+ applications.
17
18
 
18
19
  ## Installation
19
20
 
@@ -19,11 +19,12 @@ Gem::Specification.new do |s|
19
19
  # master
20
20
  s.add_dependency "activesupport", ">= 3.2"
21
21
  s.add_dependency "eventmachine", ">= 0.12.10"
22
+ s.add_dependency "ganymed-client"
22
23
  s.add_dependency "madvertise-ext", ">= 0.1.2"
23
24
  s.add_dependency "madvertise-logging", ">= 0.3.2"
24
25
  s.add_dependency "mixlib-cli"
25
- s.add_dependency "servolux"
26
26
  s.add_dependency "RubyInline"
27
+ s.add_dependency "servolux"
27
28
 
28
29
  # processor
29
30
  s.add_dependency "msgpack"
@@ -1,4 +1,12 @@
1
+ ##
2
+ # Ganymed is an event collection daemon.
3
+ #
4
+ # For more information about Ganymed go to the {file:README.md README}
5
+ #
1
6
  module Ganymed
7
+ # @private
2
8
  GEM_ROOT = File.expand_path("../..", __FILE__).freeze
9
+
10
+ # @private
3
11
  LIB_DIR = File.join(GEM_ROOT, "lib").freeze
4
12
  end
@@ -6,96 +6,155 @@ require 'ganymed/client'
6
6
  module Ganymed
7
7
 
8
8
  ##
9
- # The Collector polls various system-level metrics that are not pushed to the
10
- # {Processor} automatically.
9
+ # The Collector is a simple thread that polls various system-level metrics
10
+ # that are not pushed to the {Sampler}/{Processor} from third-party
11
+ # applications.
12
+ #
13
+ # The Collector features a very simple DSL to implement custom collector
14
+ # plugins. Simply create a file with a +.rb+ file extension and a +collect+
15
+ # block:
16
+ #
17
+ # collect do
18
+ # File.open('/proc/foobar').each do |line|
19
+ # parse(line)
20
+ # end
21
+ # end
22
+ #
23
+ # Copy this plugin to a location of your choice and set
24
+ # +collector.load_paths+ in your configuration to point to the containing
25
+ # directory.
26
+ #
27
+ # The collector thread will then call this plugin once per resolution tick.
28
+ # See {file:doc/configuration.md} for details.
29
+ #
30
+ # Custom intervals can also be specified for high frequency sampling:
31
+ #
32
+ # collect(0.3) do
33
+ # File.open('/proc/foobar').each do |line|
34
+ # parse(line)
35
+ # end
36
+ # end
11
37
  #
12
38
  class Collector
13
39
  # The configuration object.
40
+ # @return [Section]
14
41
  attr_reader :config
15
42
 
16
- # The {Client} object.
43
+ # The client object.
44
+ # @return [Ganymed::Client]
17
45
  attr_reader :client
18
46
 
19
47
  # Create a new collector instance and initialize all configured collectors.
20
48
  #
21
- # @param [Configuration] config The configuration object.
49
+ # @param [Section] config The configuration object.
22
50
  def initialize(config)
23
51
  @config = config
24
- @client = Client.new(:processor => @config.client.processor,
25
- :sampler => @config.client.sampler)
26
-
52
+ @client = Client.new(@config.client)
27
53
  load_collectors
28
- @config.collectors.each do |collector|
29
- log.info("initializing collector #{collector.klass.demodulize}")
30
- collector.klass.constantize.new(collector, self).run
31
- end
32
54
  end
33
55
 
56
+ # @private
34
57
  def load_collectors
35
- # collectors shipped with ganymed
36
- Dir[File.join(Ganymed::LIB_DIR, 'ganymed/collector/*.rb')].each do |f|
37
- log.debug("Loading collector from #{f}")
38
- require f
39
- end
58
+ collectors.each do |file|
59
+ name = File.basename(file, '.rb')
60
+ config = @config.collectors[name.to_sym] || Section.new
61
+ config[:resolution] = @config.resolution
40
62
 
41
- # custom collectors
42
- if @config.collector.path.tap{}
43
- Dir[File.join(@config.collector.path, '*.rb')].each do |f|
44
- log.debug("Loading collector from #{f}")
45
- require f
63
+ log.debug("loading collector #{name} from #{file}")
64
+ Plugin.new(config, @client).from_file(file).tap do |collector|
65
+ log.info("collecting #{name} metrics every #{collector.interval} seconds")
66
+ collector.run
46
67
  end
47
68
  end
48
69
  end
49
70
 
71
+ # @private
72
+ def collectors
73
+ load_paths = [@config.collector.load_paths.tap{}].flatten.compact
74
+ load_paths << File.join(Ganymed::LIB_DIR, 'ganymed/collectors')
75
+
76
+ [].tap do |files|
77
+ load_paths.each do |load_path|
78
+ log.debug("loading collectors from #{load_path}")
79
+ Dir[File.join(load_path, '*.rb')].each do |file|
80
+ files << file
81
+ end
82
+ end
83
+ end.sort.uniq
84
+ end
85
+
50
86
  ##
51
- # A base class for collectors.
87
+ # Base class and DSL for {Collector} plugins.
52
88
  #
53
- class Base
89
+ class Plugin
90
+
91
+ # The configuration object.
92
+ # @return [Section]
93
+ attr_accessor :config
94
+
95
+ # Plugin interval
96
+ # @return [Fixnum,Float]
97
+ attr_accessor :interval
54
98
 
55
- # Create a new data collector instance.
99
+ # Processor socket.
100
+ # @return [Ganymed::Client::ProcessorSocket]
101
+ attr_accessor :processor
102
+
103
+ # Sampler socket.
104
+ # @return [Ganymed::Client::SamplerSocket]
105
+ attr_accessor :sampler
106
+
107
+ # Create a new plugin instance.
108
+ #
109
+ # @param [Section] config The configuration object.
110
+ # @param [Ganymed::Client] client A client instance.
111
+ def initialize(config, client)
112
+ @config, @client = config, client
113
+ @processor = @client.processor
114
+ @sampler = @client.sampler
115
+ end
116
+
117
+ # Set the block used to collect metrics with this plugin.
56
118
  #
57
- # @param [Configuration] config The configuration object.
58
- # @param [Collector] collector The collector instance.
59
- def initialize(config, collector)
60
- @config, @collector = config, collector
61
- @processor = @collector.client.processor
62
- @sampler = @collector.client.sampler
119
+ # @param [Fixnum,Float] interval Custom plugin interval.
120
+ # @return [void]
121
+ def collect(interval=nil, &block)
122
+ @interval ||= interval
123
+ @interval ||= @config.interval.tap{}
124
+ @interval ||= @config.resolution
125
+ @collector = Proc.new(&block)
63
126
  end
64
127
 
65
128
  # Start the EventMachine timer to collect metrics at the specified
66
129
  # interval.
130
+ # @return [void]
67
131
  def run
68
- timer do
69
- begin
70
- collect!
71
- rescue Exception => e
72
- log.exception(e)
73
- end
132
+ EM.add_periodic_timer(@interval) do
133
+ EM.defer { collect! }
74
134
  end
75
135
  end
76
136
 
77
137
  # @private
78
- def timer
79
- if interval
80
- EM.add_periodic_timer(interval) do
81
- EM.defer { yield }
82
- end
83
- else
84
- yield
138
+ def collect!
139
+ begin
140
+ @collector.call if @collector.is_a?(Proc)
141
+ rescue Exception => exc
142
+ log.exception(exc)
85
143
  end
86
144
  end
87
145
 
88
- # The interval used to collect metrics. Either taken from the
89
- # configuration file or overriden by subclasses.
90
- def interval
91
- @config.interval.tap{} or @collector.config.resolution
92
- end
93
-
94
- # This method must be implemented by subclasses to collect the desired
95
- # metrics. This function is called every #interval seconds after the
96
- # timer has been initialized by the #run method.
97
- def collect!
98
- raise NotImplementedError
146
+ # Loads a given ruby file, and runs instance_eval against it in the
147
+ # context of the current object.
148
+ #
149
+ # @param [String] filename The absolute path to a plugin file.
150
+ # @return [Plugin] The instance for easy method chaining.
151
+ def from_file(filename)
152
+ if File.exists?(filename) && File.readable?(filename)
153
+ self.instance_eval(IO.read(filename), filename, 1)
154
+ else
155
+ raise IOError, "Cannot open or read #{filename}!"
156
+ end
157
+ self
99
158
  end
100
159
  end
101
160
  end
@@ -0,0 +1,19 @@
1
+ collect(0.3) do
2
+ return if not File.readable?('/proc/stat')
3
+
4
+ File.open('/proc/stat').each do |line|
5
+ next if not line =~ /^cpu /
6
+
7
+ cpu = line.chomp.split[1,7].map do |x|
8
+ x.to_i / 100
9
+ end
10
+
11
+ sampler.emit(:derive, "os.cpu.user", cpu[0])
12
+ sampler.emit(:derive, "os.cpu.nice", cpu[1])
13
+ sampler.emit(:derive, "os.cpu.system", cpu[2])
14
+ sampler.emit(:derive, "os.cpu.idle", cpu[3])
15
+ sampler.emit(:derive, "os.cpu.iowait", cpu[4])
16
+ sampler.emit(:derive, "os.cpu.irq", cpu[5])
17
+ sampler.emit(:derive, "os.cpu.softirq", cpu[6])
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ require 'sys/filesystem'
2
+
3
+ collect do
4
+ Sys::Filesystem.mounts do |mount|
5
+ st = Sys::Filesystem.stat(mount.mount_point)
6
+ next if st.blocks == 0 or st.files == 0
7
+
8
+ case mp = mount.mount_point
9
+ when '/'
10
+ 'rootfs'
11
+ when /^\/.+$/
12
+ mp[1..-1].tr('^[a-z][0-9]', '-')
13
+ else
14
+ 'unknown'
15
+ end.tap do |name|
16
+ next if config.exclude.map {|e| Regexp.new(e).match(name)}.any?
17
+
18
+ block_pc = 1.0 - (st.blocks_free.to_f / st.blocks.to_f)
19
+ processor.event("os.disk.#{name}.blocks", block_pc)
20
+
21
+ files_pc = 1.0 - (st.files_free.to_f / st.files.to_f)
22
+ processor.event("os.disk.#{name}.files", block_pc)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ Struct.new("IOStat",
2
+ :major, :minor, :dev,
3
+ :rio, :rmerge, :rsect, :ruse,
4
+ :wio, :wmerge, :wsect, :wuse,
5
+ :running, :use, :aveq)
6
+
7
+ collect(0.3) do
8
+ return if not File.readable?('/proc/diskstats')
9
+ File.open('/proc/diskstats').each do |line|
10
+ ios = Struct::IOStat.new(*line.strip.split(/\s+/))
11
+ next if config.skip_numbered.tap{} and ios.dev =~ /\d+$/
12
+ next if config.exclude.map {|e| Regexp.new(e).match(ios.dev)}.any?
13
+ sampler.emit(:derive, "os.iostat.#{ios.dev}.rsect", ios.rsect)
14
+ sampler.emit(:derive, "os.iostat.#{ios.dev}.wsect", ios.wsect)
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ collect(5) do
2
+ return if not File.readable?('/proc/loadavg')
3
+ File.open('/proc/loadavg') do |f|
4
+ loadavg = f.read.chomp.split[0,3].map(&:to_f)
5
+ sampler.emit(:gauge, "os.loadavg", loadavg[0])
6
+ end
7
+ end
@@ -0,0 +1,39 @@
1
+ collect(5) do
2
+ return if not File.readable?('/proc/meminfo')
3
+
4
+ # calculate app memory from total
5
+ apps = 0
6
+
7
+ File.open('/proc/meminfo').each do |line|
8
+ key, value, unit = line.chomp.split
9
+ key.gsub!(/:/, '')
10
+ value = value.to_i * 1024
11
+
12
+ case key
13
+ when "MemTotal"
14
+ apps = value
15
+ when "MemFree"
16
+ sampler.emit(:gauge, "os.mem.unused", value)
17
+ apps -= value
18
+ when "Buffers"
19
+ sampler.emit(:gauge, "os.mem.buffers", value)
20
+ apps -= value
21
+ when "Cached"
22
+ sampler.emit(:gauge, "os.mem.cache", value)
23
+ apps -= value
24
+ when "SwapCached"
25
+ sampler.emit(:gauge, "os.mem.swap_cache", value)
26
+ apps -= value
27
+ when "Slab"
28
+ sampler.emit(:gauge, "os.mem.slab_cache", value)
29
+ apps -= value
30
+ when "PageTables"
31
+ sampler.emit(:gauge, "os.mem.page_tables", value)
32
+ apps -= value
33
+ when "Mapped"
34
+ sampler.emit(:gauge, "os.mem.mapped", value)
35
+ end
36
+ end
37
+
38
+ sampler.emit(:gauge, "os.mem.apps", apps)
39
+ end
@@ -0,0 +1,22 @@
1
+ require 'ohai'
2
+
3
+ collect do
4
+ Ohai::System.new.tap do |ohai|
5
+ %w(
6
+ os
7
+ kernel
8
+ hostname
9
+ keys
10
+ network
11
+ platform
12
+ uptime
13
+ virtualization
14
+ ).each do |plugin|
15
+ ohai.require_plugin(plugin)
16
+ end
17
+ processor.metadata(ohai.data)
18
+
19
+ # ohai doesn't cleanup after itself
20
+ true while Process.wait(-1, Process::WNOHANG) rescue nil
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ # bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
2
+ Struct.new("NetworkStat", :dev,
3
+ :rbytes, :rpackets, :rerrs, :rdrop, :rfifo, :rframe, :rcompressed, :rmulticast,
4
+ :wbytes, :wpackets, :werrs, :wdrop, :wfifo, :wcolls, :wcarrier, :wcompressed)
5
+
6
+ collect(0.3) do
7
+ return if not File.readable?('/proc/net/dev')
8
+
9
+ File.open('/proc/net/dev') do |f|
10
+ f.each do |line|
11
+ next unless line =~ /:/
12
+
13
+ ns = Struct::NetworkStat.new(*line.strip.split(/\s+/))
14
+ ns.dev.sub!(/:$/, '')
15
+
16
+ next if config.exclude.map {|e| Regexp.new(e).match(ns.dev)}.any?
17
+
18
+ sampler.emit(:derive, "os.net.#{ns.dev}.rbytes", ns.rbytes)
19
+ sampler.emit(:derive, "os.net.#{ns.dev}.wbytes", ns.wbytes)
20
+ sampler.emit(:derive, "os.net.#{ns.dev}.rpackets", ns.rpackets)
21
+ sampler.emit(:derive, "os.net.#{ns.dev}.wpackets", ns.wpackets)
22
+ sampler.emit(:derive, "os.net.#{ns.dev}.rerrs", ns.rerrs)
23
+ sampler.emit(:derive, "os.net.#{ns.dev}.werrs", ns.werrs)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ collect(0.3) do
2
+ return if not File.readable?('/proc/stat')
3
+ File.open('/proc/stat').each do |line|
4
+ key, value = line.chomp.split
5
+ case key
6
+ when "ctxt"
7
+ sampler.emit(:derive, "os.procs.switch", value.to_i)
8
+ when "processes"
9
+ sampler.emit(:derive, "os.procs.forks", value.to_i)
10
+ when "procs_running"
11
+ sampler.emit(:gauge, "os.procs.running", value.to_i)
12
+ when "procs_blocked"
13
+ sampler.emit(:gauge, "os.procs.blocked", value.to_i)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ collect do
2
+ return if not File.readable?('/proc/stat')
3
+ File.open('/proc/stat').each do |line|
4
+ next if not line =~ /^btime /
5
+ boottime = Time.at(line.chomp.split[1].to_i)
6
+ processor.event("os.reboot", 1, :now => boottime)
7
+ end
8
+ end
@@ -38,22 +38,18 @@ generic:
38
38
  port: 1337
39
39
 
40
40
  collectors:
41
- - klass: Ganymed::Collector::Metadata
42
- - klass: Ganymed::Collector::Uptime
43
- - klass: Ganymed::Collector::Load
44
- - klass: Ganymed::Collector::CPU
45
- - klass: Ganymed::Collector::Procs
46
- - klass: Ganymed::Collector::Network
41
+ network:
47
42
  exclude:
48
43
  - ^lo$
49
44
  - ^sit
50
- - klass: Ganymed::Collector::Disk
45
+ disk:
51
46
  exclude:
52
47
  - ^dev$
53
48
  - ^dev-shm$
54
49
  - rc-init-d$
55
50
  - ^run$
56
51
  - ^sys-fs-cgroup$
57
- - klass: Ganymed::Collector::IOStat
52
+ - ^var-tmp-metro
53
+ iostat:
58
54
  skip_numbered: true
59
55
  exclude: []
@@ -5,11 +5,11 @@ module Ganymed
5
5
  #
6
6
  # n:: namespace (e.g. +os.cpu.idle+, +nginx.requests+, etc)
7
7
  # c:: a consolidation function used to build this event from multiple
8
- # samples
8
+ # samples (_optional_)
9
9
  # o:: origin (e.g. +web1.example.com+, +myapplication+)
10
10
  # t:: timestamp as seconds since epoch
11
- # r:: resolution of this event (i.e. how often is this event sent to the processor)
12
- # v:: the value in integer or floating-point representation
11
+ # r:: resolution of this event (i.e. how often is this event sent to the processor, _optional_)
12
+ # v:: the event value (any object that can be serialized with BSON and JSON)
13
13
  #
14
14
  class Event
15
15
  # The namespace of this event.
@@ -17,7 +17,7 @@ module Ganymed
17
17
  attr_accessor :ns
18
18
 
19
19
  # The consolidation function used to build this event from multiple
20
- # samples.
20
+ # samples if any.
21
21
  # @return [String]
22
22
  attr_accessor :cf
23
23
 
@@ -31,6 +31,9 @@ module Ganymed
31
31
 
32
32
  # The event resolution
33
33
  # @return [Fixnum]
34
+ #
35
+ # @note +nil+ or +0+ will bypass range checks for MongoDB, refer to the
36
+ # {file:doc/configuration.md configuration} for details.
34
37
  attr_accessor :resolution
35
38
 
36
39
  # The actual value of this sample.
@@ -74,6 +77,8 @@ module Ganymed
74
77
  end
75
78
 
76
79
  # Validate this event.
80
+ #
81
+ # @return [void]
77
82
  def validate
78
83
  unless @ns =~ /^[a-z][a-z0-9_\.-]+[a-z0-9]$/
79
84
  raise "invalid namespace: #{@ns.inspect}"
@@ -86,6 +86,7 @@ module Ganymed
86
86
 
87
87
  if config.profiling.perftools
88
88
  PerfTools::CpuProfiler.stop
89
+ `pprof.rb --svg profile > profile.svg`
89
90
  end
90
91
 
91
92
  if config.profiling.rubyprof
@@ -7,6 +7,7 @@ module Ganymed
7
7
 
8
8
  def initialize(config)
9
9
  @config = config
10
+ @collections = {}
10
11
  log.info("using MongoDB at #{config.host}:#{config.port}/#{config.database}")
11
12
  end
12
13
 
@@ -22,7 +23,7 @@ module Ganymed
22
23
  end
23
24
 
24
25
  def collection(ns)
25
- db[ns].tap do |col|
26
+ @collections[ns] ||= db[ns].tap do |col|
26
27
  col.ensure_index([['c', ::Mongo::ASCENDING]])
27
28
  col.ensure_index([['o', ::Mongo::ASCENDING]])
28
29
  col.ensure_index([['t', ::Mongo::ASCENDING]])
@@ -1,18 +1,35 @@
1
1
  require 'eventmachine'
2
2
  require 'msgpack'
3
3
 
4
+ require 'ganymed/client'
4
5
  require 'ganymed/event'
5
6
  require 'ganymed/mongodb'
6
7
  require 'ganymed/websocket'
7
8
 
8
9
  module Ganymed
10
+
11
+ ##
12
+ # The Processor accept {Event events} from a UDP socket and stores these
13
+ # metrics into MongoDB. All metrics are also published to interested parties
14
+ # via WebSocket.
15
+ #
9
16
  class Processor
10
17
  def initialize(config)
11
18
  @config = config
12
19
  @db = MongoDB.new(config.processor.mongodb)
13
20
  @websocket = Websocket.new(config.processor.websocket, @db)
14
21
  @client = Client.new(:sampler => @config.client.sampler)
15
- listen!
22
+ cache! and listen!
23
+ end
24
+
25
+ def cache!
26
+ @metadata = Hash.new do |hsh, key|
27
+ hsh[key] = []
28
+ end
29
+
30
+ @db['metadata'].find({}, fields: [:_id, :namespaces]).each do |result|
31
+ @metadata[result['_id']] = result['namespaces']
32
+ end
16
33
  end
17
34
 
18
35
  def listen!
@@ -36,20 +53,19 @@ module Ganymed
36
53
  end
37
54
 
38
55
  def process_event(event)
39
- # insert origin + namespace into metadata
40
- metadata = @db['metadata'].find_and_modify({
41
- query: {:_id => event.origin},
42
- update: {:$addToSet => {:namespaces => event.ns}},
43
- upsert: true,
44
- })
56
+ if not @metadata[event.origin].include?(event.ns)
57
+ # insert origin + namespace into metadata
58
+ metadata = @db['metadata'].find_and_modify({
59
+ query: {:_id => event.origin},
60
+ update: {:$addToSet => {:namespaces => event.ns}},
61
+ upsert: true,
62
+ })
45
63
 
46
- # set defaults in case find_and_modify returned an empty document
47
- metadata['_id'] ||= event.origin
48
- metadata['namespaces'] ||= []
64
+ # set defaults in case find_and_modify returned an empty document
65
+ metadata['_id'] ||= event.origin
66
+ metadata['namespaces'] ||= [event.ns]
49
67
 
50
- # if metadata has changed notify websocket clients
51
- if not metadata['namespaces'].include?(event.ns)
52
- metadata['namespaces'] |= [event.ns]
68
+ # notify websocket clients
53
69
  @websocket.send(:metadata, [metadata])
54
70
  end
55
71
 
@@ -59,7 +75,7 @@ module Ganymed
59
75
  end
60
76
 
61
77
  # skip events that are produced below the mongodb storage threshold
62
- return if (1..@config.resolution).include?(event.resolution)
78
+ return if (1...@config.resolution).include?(event.resolution)
63
79
 
64
80
  # insert event into ns collection
65
81
  @db.collection(event.ns).update({
@@ -1,11 +1,10 @@
1
1
  require 'active_support/inflector'
2
2
  require 'eventmachine'
3
- require 'madvertise/ext/enumerable'
4
- require 'msgpack'
5
- require 'socket'
6
3
  require 'inline'
4
+ require 'madvertise/ext/enumerable'
7
5
 
8
6
  require 'ganymed'
7
+ require 'ganymed/client'
9
8
 
10
9
  module Ganymed
11
10
 
@@ -1,3 +1,4 @@
1
1
  module Ganymed
2
- VERSION = '0.1.2'
2
+ # @private
3
+ VERSION = '0.2.0'
3
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ganymed
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-25 00:00:00.000000000 Z
12
+ date: 2012-05-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
16
- requirement: &12783700 !ruby/object:Gem::Requirement
16
+ requirement: &19204520 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '3.2'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *12783700
24
+ version_requirements: *19204520
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: eventmachine
27
- requirement: &12781700 !ruby/object:Gem::Requirement
27
+ requirement: &19202420 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,21 @@ dependencies:
32
32
  version: 0.12.10
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *12781700
35
+ version_requirements: *19202420
36
+ - !ruby/object:Gem::Dependency
37
+ name: ganymed-client
38
+ requirement: &19227700 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *19227700
36
47
  - !ruby/object:Gem::Dependency
37
48
  name: madvertise-ext
38
- requirement: &12780400 !ruby/object:Gem::Requirement
49
+ requirement: &19226300 !ruby/object:Gem::Requirement
39
50
  none: false
40
51
  requirements:
41
52
  - - ! '>='
@@ -43,10 +54,10 @@ dependencies:
43
54
  version: 0.1.2
44
55
  type: :runtime
45
56
  prerelease: false
46
- version_requirements: *12780400
57
+ version_requirements: *19226300
47
58
  - !ruby/object:Gem::Dependency
48
59
  name: madvertise-logging
49
- requirement: &12806280 !ruby/object:Gem::Requirement
60
+ requirement: &19225180 !ruby/object:Gem::Requirement
50
61
  none: false
51
62
  requirements:
52
63
  - - ! '>='
@@ -54,10 +65,10 @@ dependencies:
54
65
  version: 0.3.2
55
66
  type: :runtime
56
67
  prerelease: false
57
- version_requirements: *12806280
68
+ version_requirements: *19225180
58
69
  - !ruby/object:Gem::Dependency
59
70
  name: mixlib-cli
60
- requirement: &12805060 !ruby/object:Gem::Requirement
71
+ requirement: &19224340 !ruby/object:Gem::Requirement
61
72
  none: false
62
73
  requirements:
63
74
  - - ! '>='
@@ -65,10 +76,10 @@ dependencies:
65
76
  version: '0'
66
77
  type: :runtime
67
78
  prerelease: false
68
- version_requirements: *12805060
79
+ version_requirements: *19224340
69
80
  - !ruby/object:Gem::Dependency
70
- name: servolux
71
- requirement: &12803060 !ruby/object:Gem::Requirement
81
+ name: RubyInline
82
+ requirement: &19223140 !ruby/object:Gem::Requirement
72
83
  none: false
73
84
  requirements:
74
85
  - - ! '>='
@@ -76,10 +87,10 @@ dependencies:
76
87
  version: '0'
77
88
  type: :runtime
78
89
  prerelease: false
79
- version_requirements: *12803060
90
+ version_requirements: *19223140
80
91
  - !ruby/object:Gem::Dependency
81
- name: RubyInline
82
- requirement: &12800700 !ruby/object:Gem::Requirement
92
+ name: servolux
93
+ requirement: &19220580 !ruby/object:Gem::Requirement
83
94
  none: false
84
95
  requirements:
85
96
  - - ! '>='
@@ -87,10 +98,10 @@ dependencies:
87
98
  version: '0'
88
99
  type: :runtime
89
100
  prerelease: false
90
- version_requirements: *12800700
101
+ version_requirements: *19220580
91
102
  - !ruby/object:Gem::Dependency
92
103
  name: msgpack
93
- requirement: &12815620 !ruby/object:Gem::Requirement
104
+ requirement: &19234260 !ruby/object:Gem::Requirement
94
105
  none: false
95
106
  requirements:
96
107
  - - ! '>='
@@ -98,10 +109,10 @@ dependencies:
98
109
  version: '0'
99
110
  type: :runtime
100
111
  prerelease: false
101
- version_requirements: *12815620
112
+ version_requirements: *19234260
102
113
  - !ruby/object:Gem::Dependency
103
114
  name: mongo
104
- requirement: &12812240 !ruby/object:Gem::Requirement
115
+ requirement: &19231900 !ruby/object:Gem::Requirement
105
116
  none: false
106
117
  requirements:
107
118
  - - ! '>='
@@ -109,10 +120,10 @@ dependencies:
109
120
  version: '1.6'
110
121
  type: :runtime
111
122
  prerelease: false
112
- version_requirements: *12812240
123
+ version_requirements: *19231900
113
124
  - !ruby/object:Gem::Dependency
114
125
  name: em-websocket
115
- requirement: &12810000 !ruby/object:Gem::Requirement
126
+ requirement: &19229040 !ruby/object:Gem::Requirement
116
127
  none: false
117
128
  requirements:
118
129
  - - ! '>='
@@ -120,10 +131,10 @@ dependencies:
120
131
  version: '0'
121
132
  type: :runtime
122
133
  prerelease: false
123
- version_requirements: *12810000
134
+ version_requirements: *19229040
124
135
  - !ruby/object:Gem::Dependency
125
136
  name: yajl-ruby
126
- requirement: &12807700 !ruby/object:Gem::Requirement
137
+ requirement: &19257780 !ruby/object:Gem::Requirement
127
138
  none: false
128
139
  requirements:
129
140
  - - ! '>='
@@ -131,10 +142,10 @@ dependencies:
131
142
  version: '0'
132
143
  type: :runtime
133
144
  prerelease: false
134
- version_requirements: *12807700
145
+ version_requirements: *19257780
135
146
  - !ruby/object:Gem::Dependency
136
147
  name: sys-filesystem
137
- requirement: &12823980 !ruby/object:Gem::Requirement
148
+ requirement: &19255820 !ruby/object:Gem::Requirement
138
149
  none: false
139
150
  requirements:
140
151
  - - ! '>='
@@ -142,10 +153,10 @@ dependencies:
142
153
  version: '0'
143
154
  type: :runtime
144
155
  prerelease: false
145
- version_requirements: *12823980
156
+ version_requirements: *19255820
146
157
  - !ruby/object:Gem::Dependency
147
158
  name: ohai
148
- requirement: &12822240 !ruby/object:Gem::Requirement
159
+ requirement: &19252280 !ruby/object:Gem::Requirement
149
160
  none: false
150
161
  requirements:
151
162
  - - ! '>='
@@ -153,7 +164,7 @@ dependencies:
153
164
  version: 0.6.12
154
165
  type: :runtime
155
166
  prerelease: false
156
- version_requirements: *12822240
167
+ version_requirements: *19252280
157
168
  description: Ganymed is an event collection daemon
158
169
  email:
159
170
  - bb@xnull.de
@@ -175,16 +186,16 @@ files:
175
186
  - contrib/cpuhog.c
176
187
  - ganymed.gemspec
177
188
  - lib/ganymed.rb
178
- - lib/ganymed/client.rb
179
189
  - lib/ganymed/collector.rb
180
- - lib/ganymed/collector/cpu.rb
181
- - lib/ganymed/collector/disk.rb
182
- - lib/ganymed/collector/iostat.rb
183
- - lib/ganymed/collector/load.rb
184
- - lib/ganymed/collector/metadata.rb
185
- - lib/ganymed/collector/network.rb
186
- - lib/ganymed/collector/procs.rb
187
- - lib/ganymed/collector/uptime.rb
190
+ - lib/ganymed/collectors/cpu.rb
191
+ - lib/ganymed/collectors/disk.rb
192
+ - lib/ganymed/collectors/iostat.rb
193
+ - lib/ganymed/collectors/load.rb
194
+ - lib/ganymed/collectors/memory.rb
195
+ - lib/ganymed/collectors/metadata.rb
196
+ - lib/ganymed/collectors/network.rb
197
+ - lib/ganymed/collectors/process.rb
198
+ - lib/ganymed/collectors/uptime.rb
188
199
  - lib/ganymed/config.yml
189
200
  - lib/ganymed/event.rb
190
201
  - lib/ganymed/master.rb
@@ -228,7 +239,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
228
239
  version: '0'
229
240
  segments:
230
241
  - 0
231
- hash: -813213877420202571
242
+ hash: -4122495108718523153
232
243
  required_rubygems_version: !ruby/object:Gem::Requirement
233
244
  none: false
234
245
  requirements:
@@ -237,7 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
237
248
  version: '0'
238
249
  segments:
239
250
  - 0
240
- hash: -813213877420202571
251
+ hash: -4122495108718523153
241
252
  requirements: []
242
253
  rubyforge_project:
243
254
  rubygems_version: 1.8.17
@@ -1,132 +0,0 @@
1
- require 'active_support/core_ext/array/extract_options'
2
- require 'socket'
3
- require 'msgpack'
4
-
5
- module Ganymed
6
-
7
- ##
8
- # The Client connects to the {Sampler} and/or {Processor} and emits samples
9
- # and/or {Event events} respectively.
10
- #
11
- class Client
12
-
13
- # The {Processor} {ProcessorSocket socket}.
14
- attr_reader :processor
15
-
16
- # The {Sampler} {SamplerSocket socket}.
17
- attr_reader :sampler
18
-
19
- # Create a new client instance.
20
- #
21
- # @param [Hash] opts Client options.
22
- # @option opts [Hash] :processor Options passed to the {ProcessorSocket}.
23
- # @option opts [Hash] :sampler Options passed to the {SamplerSocket}.
24
- def initialize(opts)
25
- @processor = ProcessorSocket.new(opts[:processor]) if opts[:processor]
26
- @sampler = SamplerSocket.new(opts[:sampler]) if opts[:sampler]
27
- end
28
-
29
- ##
30
- # A simple UDPSocket wrapper.
31
- #
32
- class Socket
33
-
34
- # Create a new Socket instance.
35
- #
36
- # @param [Hash] opts Socket options.
37
- # @option opts [String] :host Host to connect to.
38
- # @option opts [Fixnum] :port Port to connect to.
39
- # @option opts [String] :origin Origin of Samples/Events. Defaults to the
40
- # fully-qualified hostname.
41
- def initialize(opts)
42
- @host, @port = opts[:host], opts[:port]
43
- @socket = UDPSocket.new
44
- @origin = opts[:origin] || ::Socket.gethostbyname(::Socket.gethostname).first
45
- end
46
-
47
- # Send data to the socket.
48
- #
49
- # @param [String] data The data to send.
50
- # @param [Fixnum] flags Socket flags.
51
- def send(data, flags=0)
52
- @socket.send(data, flags, @host, @port)
53
- end
54
- end
55
-
56
- ##
57
- # A {Socket} that emits samples to a {Sampler}.
58
- #
59
- class SamplerSocket < Socket
60
-
61
- # Emit a new sample.
62
- #
63
- # @param [String, Symbol] ds Sample data source.
64
- # @param [String] ns {Event} namespace.
65
- # @param [Fixnum, Float] value Sample value.
66
- # @param [Time] now Sample timestamp.
67
- def emit(ds, ns, value, now=Time.now.utc)
68
- data = [ds.to_s, ns, @origin, now.to_f, value.to_f]
69
- send(data.pack("Z*Z*Z*GG"))
70
- end
71
- end
72
-
73
- ##
74
- # A {Socket} that emits {Event events} to a {Processor}.
75
- #
76
- class ProcessorSocket < Socket
77
-
78
- # Emit a new {Event}.
79
- #
80
- # @param [String] ns {Event} namespace.
81
- # @param [Object] value {Event} value.
82
- # @param [Hash] opts Options
83
- # @option opts [String] cf Consolidation function used in this event.
84
- # @option opts [Time] now {Event} timestamp.
85
- # @option opts [Fixnum] resolution {Event} resolution.
86
- # @option opts [String] origin {Event} origin.
87
- def event(ns, value, opts={})
88
- opts = {
89
- :cf => nil,
90
- :now => Time.now.utc,
91
- :resolution => 0,
92
- :origin => @origin,
93
- }.merge(opts)
94
-
95
- {
96
- :_type => :event,
97
- :n => ns,
98
- :c => opts[:cf],
99
- :o => opts[:origin],
100
- :t => opts[:now].to_i,
101
- :r => opts[:resolution].to_i,
102
- :v => value
103
- }.tap do |data|
104
- send(data.to_msgpack)
105
- end
106
- end
107
-
108
- # Emit a metadata object to the {Processor}.
109
- #
110
- # A metadata object contains arbitrary data related to the origin. This
111
- # can be queried by {Websocket} clients for displaying information about
112
- # origins.
113
- #
114
- # @param [Hash] data Metadata object.
115
- # @param [Hash] opts Options
116
- # @option opts [String] origin Metadata origin. Defaults to the FQDN.
117
- def metadata(data, opts={})
118
- opts = {
119
- :origin => @origin,
120
- }.merge(opts)
121
-
122
- {
123
- :_type => :metadata,
124
- :origin => opts[:origin],
125
- :data => data,
126
- }.tap do |data|
127
- send(data.to_msgpack)
128
- end
129
- end
130
- end
131
- end
132
- end
@@ -1,32 +0,0 @@
1
- require 'ganymed/collector'
2
-
3
- module Ganymed
4
- class Collector
5
- class CPU < Base
6
- def collect!
7
- return if not File.readable?('/proc/stat')
8
- File.open('/proc/stat').each do |line|
9
- next if not line =~ /^cpu /
10
- cpu = line.chomp.split[1,7].map do |x|
11
- x.to_i / hz
12
- end
13
- @sampler.emit(:derive, "os.cpu.user", cpu[0])
14
- @sampler.emit(:derive, "os.cpu.nice", cpu[1])
15
- @sampler.emit(:derive, "os.cpu.system", cpu[2])
16
- @sampler.emit(:derive, "os.cpu.idle", cpu[3])
17
- @sampler.emit(:derive, "os.cpu.iowait", cpu[4])
18
- @sampler.emit(:derive, "os.cpu.irq", cpu[5])
19
- @sampler.emit(:derive, "os.cpu.softirq", cpu[6])
20
- end
21
- end
22
-
23
- def interval
24
- @config.interval.tap{} or 0.2
25
- end
26
-
27
- def hz
28
- @config.hz.tap{} or 100
29
- end
30
- end
31
- end
32
- end
@@ -1,36 +0,0 @@
1
- require 'sys/filesystem'
2
-
3
- module Ganymed
4
- class Collector
5
- class Disk < Base
6
- def collect!
7
- Sys::Filesystem.mounts do |mount|
8
- st = Sys::Filesystem.stat(mount.mount_point)
9
- next if st.blocks == 0 or st.files == 0
10
-
11
- name = name(mount.mount_point)
12
- next if @config.exclude.map {|e| Regexp.new(e).match(name)}.any?
13
-
14
- block_pc = 1.0 - (st.blocks_free.to_f / st.blocks.to_f)
15
- @processor.event("os.disk.#{name}.blocks", block_pc)
16
-
17
- files_pc = 1.0 - (st.files_free.to_f / st.files.to_f)
18
- @processor.event("os.disk.#{name}.files", block_pc)
19
- end
20
- end
21
-
22
- private
23
-
24
- def name(mount)
25
- case mount
26
- when '/'
27
- 'rootfs'
28
- when /^\/.+$/
29
- mount[1..-1].tr('^[a-z][0-9]', '-')
30
- else
31
- 'unknown'
32
- end
33
- end
34
- end
35
- end
36
- end
@@ -1,26 +0,0 @@
1
- Struct.new("IOStat",
2
- :major, :minor, :dev,
3
- :rio, :rmerge, :rsect, :ruse,
4
- :wio, :wmerge, :wsect, :wuse,
5
- :running, :use, :aveq)
6
-
7
- module Ganymed
8
- class Collector
9
- class IOStat < Base
10
- def collect!
11
- return if not File.readable?('/proc/diskstats')
12
- File.open('/proc/diskstats').each do |line|
13
- ios = Struct::IOStat.new(*line.strip.split(/\s+/))
14
- next if @config.skip_numbered.tap{} and ios.dev =~ /\d+$/
15
- next if @config.exclude.map {|e| Regexp.new(e).match(ios.dev)}.any?
16
- @sampler.emit(:derive, "os.iostat.#{ios.dev}.rsect", ios.rsect)
17
- @sampler.emit(:derive, "os.iostat.#{ios.dev}.wsect", ios.wsect)
18
- end
19
- end
20
-
21
- def interval
22
- @config.interval.tap{} or 0.2
23
- end
24
- end
25
- end
26
- end
@@ -1,19 +0,0 @@
1
- require 'ganymed/collector'
2
-
3
- module Ganymed
4
- class Collector
5
- class Load < Base
6
- def collect!
7
- return if not File.readable?('/proc/loadavg')
8
- File.open('/proc/loadavg') do |f|
9
- loadavg = f.read.chomp.split[0,3].map(&:to_f)
10
- @sampler.emit(:gauge, "os.loadavg", loadavg[0])
11
- end
12
- end
13
-
14
- def interval
15
- @config.interval.tap{} or 0.2
16
- end
17
- end
18
- end
19
- end
@@ -1,34 +0,0 @@
1
- require 'ohai'
2
-
3
- require 'ganymed/collector'
4
-
5
- module Ganymed
6
- class Collector
7
- class Metadata < Base
8
- def collect!
9
- Ohai::System.new.tap do |ohai|
10
- %w(
11
- os
12
- kernel
13
- hostname
14
- keys
15
- network
16
- platform
17
- uptime
18
- virtualization
19
- ).each do |plugin|
20
- ohai.require_plugin(plugin)
21
- end
22
- @processor.metadata(ohai.data)
23
-
24
- # ohai doesn't cleanup after itself
25
- true while Process.wait(-1, Process::WNOHANG) rescue nil
26
- end
27
- end
28
-
29
- def interval
30
- nil
31
- end
32
- end
33
- end
34
- end
@@ -1,36 +0,0 @@
1
- # bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
2
- Struct.new("NetworkStat", :dev,
3
- :rbytes, :rpackets, :rerrs, :rdrop, :rfifo, :rframe, :rcompressed, :rmulticast,
4
- :wbytes, :wpackets, :werrs, :wdrop, :wfifo, :wcolls, :wcarrier, :wcompressed)
5
-
6
- module Ganymed
7
- class Collector
8
- class Network < Base
9
- def collect!
10
- return if not File.readable?('/proc/net/dev')
11
-
12
- File.open('/proc/net/dev') do |f|
13
- f.each do |line|
14
- next unless line =~ /:/
15
-
16
- ns = Struct::NetworkStat.new(*line.strip.split(/\s+/))
17
- ns.dev.sub!(/:$/, '')
18
-
19
- next if @config.exclude.map {|e| Regexp.new(e).match(ns.dev)}.any?
20
-
21
- @sampler.emit(:derive, "os.net.#{ns.dev}.rbytes", ns.rbytes)
22
- @sampler.emit(:derive, "os.net.#{ns.dev}.wbytes", ns.wbytes)
23
- @sampler.emit(:derive, "os.net.#{ns.dev}.rpackets", ns.rpackets)
24
- @sampler.emit(:derive, "os.net.#{ns.dev}.wpackets", ns.wpackets)
25
- @sampler.emit(:derive, "os.net.#{ns.dev}.rerrs", ns.rerrs)
26
- @sampler.emit(:derive, "os.net.#{ns.dev}.werrs", ns.werrs)
27
- end
28
- end
29
- end
30
-
31
- def interval
32
- @config.interval.tap{} or 0.1
33
- end
34
- end
35
- end
36
- end
@@ -1,22 +0,0 @@
1
- module Ganymed
2
- class Collector
3
- class Procs < Base
4
- def collect!
5
- return if not File.readable?('/proc/stat')
6
- File.open('/proc/stat').each do |line|
7
- next if not line =~ /^procs_/
8
- key, value = line.chomp.split
9
- if key == "procs_running"
10
- @sampler.emit(:gauge, "os.procs.running", value.to_i)
11
- elsif key == "procs_blocked"
12
- @sampler.emit(:gauge, "os.procs.blocked", value.to_i)
13
- end
14
- end
15
- end
16
-
17
- def interval
18
- @config.interval.tap{} or 0.2
19
- end
20
- end
21
- end
22
- end
@@ -1,16 +0,0 @@
1
- require 'ganymed/collector'
2
-
3
- module Ganymed
4
- class Collector
5
- class Uptime < Base
6
- def collect!
7
- return if not File.readable?('/proc/stat')
8
- File.open('/proc/stat').each do |line|
9
- next if not line =~ /^btime /
10
- boottime = Time.at(line.chomp.split[1].to_i)
11
- @processor.event("os.reboot", 1, :now => boottime)
12
- end
13
- end
14
- end
15
- end
16
- end