ganymed 0.2.3 → 0.3.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/Gemfile CHANGED
@@ -3,6 +3,7 @@ source :rubygems
3
3
  gemspec
4
4
 
5
5
  group :development, :test do
6
+ gem 'ganymed-client', :path => '../ganymed-client'
6
7
  gem 'bson_ext'
7
8
  gem 'bundler'
8
9
  gem 'debugger'
data/ganymed.gemspec CHANGED
@@ -19,7 +19,7 @@ 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
+ s.add_dependency "ganymed-client", ">= 0.2.0"
23
23
  s.add_dependency "madvertise-ext", ">= 0.1.2"
24
24
  s.add_dependency "madvertise-logging", ">= 0.3.2"
25
25
  s.add_dependency "mixlib-cli"
@@ -49,7 +49,17 @@ module Ganymed
49
49
  # @param [Section] config The configuration object.
50
50
  def initialize(config)
51
51
  @config = config
52
- @client = Client.new(@config.client)
52
+
53
+ @config.client.sampler.tap do |sampler|
54
+ log.info("emitting collected samples to tcp##{sampler.host}:#{sampler.port}")
55
+ @sampler = Ganymed::Client::Sampler.connect(sampler.host, sampler.port)
56
+ end
57
+
58
+ @config.client.processor.tap do |processor|
59
+ log.info("emitting collected events to tcp##{processor.host}:#{processor.port}")
60
+ @processor = Ganymed::Client::Processor.connect(processor.host, processor.port)
61
+ end
62
+
53
63
  load_collectors
54
64
  end
55
65
 
@@ -58,10 +68,9 @@ module Ganymed
58
68
  collectors.each do |file|
59
69
  name = File.basename(file, '.rb')
60
70
  config = @config.collectors[name.to_sym] || Section.new
61
- config[:resolution] = @config.resolution
62
71
 
63
72
  log.debug("loading collector #{name} from #{file}")
64
- Plugin.new(config, @client).from_file(file).tap do |collector|
73
+ Plugin.new(config, @sampler, @processor).from_file(file).tap do |collector|
65
74
  log.info("collecting #{name} metrics every #{collector.interval} seconds")
66
75
  collector.run
67
76
  end
@@ -97,21 +106,19 @@ module Ganymed
97
106
  attr_accessor :interval
98
107
 
99
108
  # Processor socket.
100
- # @return [Ganymed::Client::ProcessorSocket]
109
+ # @return [Ganymed::Client::Processor]
101
110
  attr_accessor :processor
102
111
 
103
112
  # Sampler socket.
104
- # @return [Ganymed::Client::SamplerSocket]
113
+ # @return [Ganymed::Client::Sampler]
105
114
  attr_accessor :sampler
106
115
 
107
116
  # Create a new plugin instance.
108
117
  #
109
118
  # @param [Section] config The configuration object.
110
119
  # @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
120
+ def initialize(config, sampler, processor)
121
+ @config, @sampler, @processor = config, sampler, processor
115
122
  end
116
123
 
117
124
  # Set the block used to collect metrics with this plugin.
@@ -119,9 +126,7 @@ module Ganymed
119
126
  # @param [Fixnum,Float] interval Custom plugin interval.
120
127
  # @return [void]
121
128
  def collect(interval=nil, &block)
122
- @interval ||= interval
123
- @interval ||= @config.interval.tap{}
124
- @interval ||= @config.resolution
129
+ @interval = interval || config.interval.tap{} || 1
125
130
  @collector = Proc.new(&block)
126
131
  end
127
132
 
@@ -129,7 +134,7 @@ module Ganymed
129
134
  # interval.
130
135
  # @return [void]
131
136
  def run
132
- EM.add_periodic_timer(@interval) do
137
+ EM.add_periodic_timer(interval) do
133
138
  EM.defer { collect! }
134
139
  end
135
140
  end
@@ -1,19 +1,21 @@
1
- collect(0.3) do
1
+ collect do
2
2
  break if not File.readable?('/proc/stat')
3
3
 
4
- File.open('/proc/stat').each do |line|
5
- next if not line =~ /^cpu /
4
+ File.open('/proc/stat') do | file|
5
+ file.each do |line|
6
+ next if not line =~ /^cpu /
6
7
 
7
- cpu = line.chomp.split[1,7].map do |x|
8
- x.to_i / 100
9
- end
8
+ cpu = line.chomp.split[1,7].map do |x|
9
+ x.to_i / 100
10
+ end
10
11
 
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])
12
+ sampler.emit(:derive, "os.cpu.user", cpu[0])
13
+ sampler.emit(:derive, "os.cpu.nice", cpu[1])
14
+ sampler.emit(:derive, "os.cpu.system", cpu[2])
15
+ sampler.emit(:derive, "os.cpu.idle", cpu[3])
16
+ sampler.emit(:derive, "os.cpu.iowait", cpu[4])
17
+ sampler.emit(:derive, "os.cpu.irq", cpu[5])
18
+ sampler.emit(:derive, "os.cpu.softirq", cpu[6])
19
+ end
18
20
  end
19
21
  end
@@ -19,7 +19,7 @@ collect do
19
19
  processor.event("os.disk.#{name}.blocks", block_pc)
20
20
 
21
21
  files_pc = 1.0 - (st.files_free.to_f / st.files.to_f)
22
- processor.event("os.disk.#{name}.files", block_pc)
22
+ processor.event("os.disk.#{name}.files", files_pc)
23
23
  end
24
24
  end
25
25
  end
@@ -4,13 +4,15 @@ Struct.new("IOStat",
4
4
  :wio, :wmerge, :wsect, :wuse,
5
5
  :running, :use, :aveq)
6
6
 
7
- collect(0.3) do
7
+ collect do
8
8
  next 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)
9
+ File.open('/proc/diskstats') do |file|
10
+ file.each do |line|
11
+ ios = Struct::IOStat.new(*line.strip.split(/\s+/))
12
+ next if config.skip_numbered.tap{} and ios.dev =~ /\d+$/
13
+ next if config.exclude.map {|e| Regexp.new(e).match(ios.dev)}.any?
14
+ sampler.emit(:derive, "os.iostat.#{ios.dev}.rsect", ios.rsect)
15
+ sampler.emit(:derive, "os.iostat.#{ios.dev}.wsect", ios.wsect)
16
+ end
15
17
  end
16
18
  end
@@ -1,7 +1,7 @@
1
- collect(5) do
1
+ collect do
2
2
  next if not File.readable?('/proc/loadavg')
3
- File.open('/proc/loadavg') do |f|
4
- loadavg = f.read.chomp.split[0,3].map(&:to_f)
3
+ File.open('/proc/loadavg') do |file|
4
+ loadavg = file.read.chomp.split[0,3].map(&:to_f)
5
5
  sampler.emit(:gauge, "os.loadavg", loadavg[0])
6
6
  end
7
7
  end
@@ -1,39 +1,41 @@
1
- collect(5) do
1
+ collect do
2
2
  next if not File.readable?('/proc/meminfo')
3
3
 
4
4
  # calculate app memory from total
5
5
  apps = 0
6
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
7
+ File.open('/proc/meminfo') do |file|
8
+ file.each do |line|
9
+ key, value, unit = line.chomp.split
10
+ key.gsub!(/:/, '')
11
+ value = value.to_i * 1024
11
12
 
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)
13
+ case key
14
+ when "MemTotal"
15
+ apps = value
16
+ when "MemFree"
17
+ sampler.emit(:gauge, "os.mem.unused", value)
18
+ apps -= value
19
+ when "Buffers"
20
+ sampler.emit(:gauge, "os.mem.buffers", value)
21
+ apps -= value
22
+ when "Cached"
23
+ sampler.emit(:gauge, "os.mem.cache", value)
24
+ apps -= value
25
+ when "SwapCached"
26
+ sampler.emit(:gauge, "os.mem.swap_cache", value)
27
+ apps -= value
28
+ when "Slab"
29
+ sampler.emit(:gauge, "os.mem.slab_cache", value)
30
+ apps -= value
31
+ when "PageTables"
32
+ sampler.emit(:gauge, "os.mem.page_tables", value)
33
+ apps -= value
34
+ when "Mapped"
35
+ sampler.emit(:gauge, "os.mem.mapped", value)
36
+ end
35
37
  end
36
- end
37
38
 
38
- sampler.emit(:gauge, "os.mem.apps", apps)
39
+ sampler.emit(:gauge, "os.mem.apps", apps)
40
+ end
39
41
  end
@@ -3,11 +3,11 @@ Struct.new("NetworkStat", :dev,
3
3
  :rbytes, :rpackets, :rerrs, :rdrop, :rfifo, :rframe, :rcompressed, :rmulticast,
4
4
  :wbytes, :wpackets, :werrs, :wdrop, :wfifo, :wcolls, :wcarrier, :wcompressed)
5
5
 
6
- collect(0.3) do
6
+ collect do
7
7
  break if not File.readable?('/proc/net/dev')
8
8
 
9
- File.open('/proc/net/dev') do |f|
10
- f.each do |line|
9
+ File.open('/proc/net/dev') do |file|
10
+ file.each do |line|
11
11
  next unless line =~ /:/
12
12
 
13
13
  ns = Struct::NetworkStat.new(*line.strip.split(/\s+/))
@@ -1,16 +1,18 @@
1
- collect(0.3) do
1
+ collect do
2
2
  break 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)
3
+ File.open('/proc/stat') do |file|
4
+ file.each do |line|
5
+ key, value = line.chomp.split
6
+ case key
7
+ when "ctxt"
8
+ sampler.emit(:derive, "os.procs.switch", value.to_i)
9
+ when "processes"
10
+ sampler.emit(:derive, "os.procs.forks", value.to_i)
11
+ when "procs_running"
12
+ sampler.emit(:gauge, "os.procs.running", value.to_i)
13
+ when "procs_blocked"
14
+ sampler.emit(:gauge, "os.procs.blocked", value.to_i)
15
+ end
14
16
  end
15
17
  end
16
18
  end
@@ -1,8 +1,10 @@
1
1
  collect do
2
2
  break 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)
3
+ File.open('/proc/stat') do |file|
4
+ file.each do |line|
5
+ next if not line =~ /^btime /
6
+ boottime = Time.at(line.chomp.split[1].to_i)
7
+ processor.event("os.reboot", 1, :now => boottime)
8
+ end
7
9
  end
8
10
  end
@@ -1,6 +1,4 @@
1
1
  generic:
2
- resolution: 300
3
-
4
2
  eventmachine:
5
3
  threadpool_size: 2
6
4
 
@@ -24,10 +22,16 @@ generic:
24
22
 
25
23
  sampler:
26
24
  enabled: true
27
- ticks: [1]
25
+ window: 300
28
26
  listen:
29
27
  host: 127.0.0.1
30
28
  port: 1337
29
+ consolidations:
30
+ min: [min]
31
+ max: [max]
32
+ avg: [mean]
33
+ stdev: [stdev]
34
+ u90: [percentile, 0.9]
31
35
 
32
36
  client:
33
37
  processor:
@@ -0,0 +1,38 @@
1
+ require 'inline'
2
+
3
+ class Array
4
+ # this is incredibly faster than doing ary.inject(0, :+)
5
+ # should only be called on numbers-only arrays, though ;-)
6
+ inline :C do |builder|
7
+ builder.c_raw <<-EOC
8
+ static VALUE sum(int argc, VALUE *argv, VALUE self) {
9
+ double result = 0;
10
+ VALUE *arr = RARRAY_PTR(self);
11
+ long i, len = RARRAY_LEN(self);
12
+
13
+ for (i = 0; i < len; i++) {
14
+ result += NUM2DBL(arr[i]);
15
+ }
16
+
17
+ return rb_float_new(result);
18
+ }
19
+ EOC
20
+ end
21
+
22
+ def mean
23
+ sum.to_f / length
24
+ end
25
+
26
+ def variance
27
+ m = mean
28
+ reduce(0) {|accum, item| accum + (item - m) ** 2}.to_f / (length - 1)
29
+ end
30
+
31
+ def stdev
32
+ Math.sqrt(variance)
33
+ end
34
+
35
+ def percentile(pc)
36
+ sort[(pc * length).ceil - 1]
37
+ end
38
+ end
@@ -31,74 +31,77 @@ module Ganymed
31
31
  end
32
32
 
33
33
  def run
34
- unless EventMachine.reactor_running?
35
- @reactor.join if @reactor.is_a?(Thread)
36
- log.info("starting Ganymed reactor in #{Env.mode} mode")
37
-
38
- # configure eventmachine
39
- log.debug("EventMachine threadpool_size=#{config.eventmachine.threadpool_size}")
40
- EventMachine.threadpool_size = config.eventmachine.threadpool_size
41
-
42
- @reactor = Thread.new do
43
- begin
44
- EventMachine.run { setup_reactor }
45
- rescue Exception => exc
46
- log.exception(exc)
47
- end
48
- end
49
- end
50
-
51
- if config.profiling.gcprofiler
52
- puts GC::Profiler.report
34
+ if pid = Process.fork
35
+ @supervisor = true
36
+ Process.wait(pid)
37
+ else
38
+ child
53
39
  end
54
40
  end
55
41
 
56
- def setup_reactor
57
- Processor.new(config) if config.processor.enabled
58
- Sampler.new(config) if config.sampler.enabled
59
- Collector.new(config) if config.collectors.any?
42
+ def child
43
+ log.info("starting Ganymed reactor in #{Env.mode} mode")
44
+
45
+ # configure eventmachine
46
+ log.debug("EventMachine threadpool_size=#{config.eventmachine.threadpool_size}")
47
+ EventMachine.threadpool_size = config.eventmachine.threadpool_size
48
+
49
+ EventMachine.epoll # use epoll
50
+ EventMachine.run do
51
+ Processor.new(config) if config.processor.enabled
52
+ Sampler.new(config) if config.sampler.enabled
53
+ Collector.new(config) if config.collectors.any?
54
+ end
55
+ rescue Exception => exc
56
+ log.exception(exc)
60
57
  end
61
58
 
62
59
  def before_starting
63
- if config.profiling.perftools
64
- log.info("activating PerfTools CPU profiler")
65
- require 'perftools'
66
- PerfTools::CpuProfiler.start('profile')
67
- end
60
+ unless @supervisor
61
+ if config.profiling.perftools
62
+ log.info("activating PerfTools CPU profiler")
63
+ require 'perftools'
64
+ PerfTools::CpuProfiler.start('profile')
65
+ end
68
66
 
69
- if config.profiling.gcprofiler
70
- log.info("activating GC::Profiler")
71
- GC::Profiler.enable
72
- end
67
+ if config.profiling.gcprofiler
68
+ log.info("activating GC::Profiler")
69
+ GC::Profiler.enable
70
+ end
73
71
 
74
- if config.profiling.rubyprof
75
- require 'ruby-prof'
76
- log.info("activating RubyProf")
77
- RubyProf.start
72
+ if config.profiling.rubyprof
73
+ require 'ruby-prof'
74
+ log.info("activating RubyProf")
75
+ RubyProf.start
76
+ end
78
77
  end
79
78
  end
80
79
 
80
+ def before_stopping
81
+ if EventMachine.reactor_running?
82
+ log.info("shutting down Ganymed reactor")
83
+ EventMachine.stop_event_loop
84
+ end
85
+ end
81
86
 
82
87
  def after_stopping
83
- log.info("shutting down Ganymed reactor")
84
- EventMachine.stop_event_loop if EventMachine.reactor_running?
85
- @reactor.join if @reactor.is_a?(Thread)
86
-
87
- if config.profiling.perftools
88
- PerfTools::CpuProfiler.stop
89
- `pprof.rb --svg profile > profile.svg`
90
- end
88
+ unless @supervisor
89
+ if config.profiling.perftools
90
+ PerfTools::CpuProfiler.stop
91
+ `pprof.rb --svg profile > profile.svg`
92
+ end
91
93
 
92
- if config.profiling.rubyprof
93
- result = RubyProf.stop
94
- result.eliminate_methods!([
95
- /Queue#pop/,
96
- /Queue#push/,
97
- /Mutex#synchronize/,
98
- /Mutex#sleep/,
99
- ])
100
- printer = RubyProf::MultiPrinter.new(result)
101
- printer.print(:path => "prof", :profile => "ganymed")
94
+ if config.profiling.rubyprof
95
+ result = RubyProf.stop
96
+ result.eliminate_methods!([
97
+ /Queue#pop/,
98
+ /Queue#push/,
99
+ /Mutex#synchronize/,
100
+ /Mutex#sleep/,
101
+ ])
102
+ printer = RubyProf::MultiPrinter.new(result)
103
+ printer.print(:path => "prof", :profile => "ganymed")
104
+ end
102
105
  end
103
106
 
104
107
  log.info("done shutting down. exiting ...")
@@ -9,7 +9,7 @@ require 'ganymed/websocket'
9
9
  module Ganymed
10
10
 
11
11
  ##
12
- # The Processor accept {Event events} from a UDP socket and stores these
12
+ # The Processor accepts {Event events} from a UDP socket and stores these
13
13
  # metrics into MongoDB. All metrics are also published to interested parties
14
14
  # via WebSocket.
15
15
  #
@@ -18,7 +18,14 @@ module Ganymed
18
18
  @config = config
19
19
  @db = MongoDB.new(config.processor.mongodb)
20
20
  @websocket = Websocket.new(config.processor.websocket, @db)
21
- @client = Client.new(:sampler => @config.client.sampler)
21
+
22
+ @config.client.sampler.tap do |sampler|
23
+ # schedule connect on next tick so that our sampler socket is
24
+ # available. Processor#initialize is called from EM.run and therefore
25
+ # no sockets have been opened yet.
26
+ EM.next_tick { @sampler = Ganymed::Client::Sampler.connect(sampler.host, sampler.port) }
27
+ end
28
+
22
29
  cache!
23
30
  listen!
24
31
  end
@@ -39,21 +46,28 @@ module Ganymed
39
46
  EM.open_datagram_socket(listen.host, listen.port, Connection) do |connection|
40
47
  connection.processor = self
41
48
  end
49
+
50
+ log.info("processing metrics on tcp##{listen.host}:#{listen.port}")
51
+ EM.start_server(listen.host, listen.port, Connection) do |connection|
52
+ connection.processor = self
53
+ end
42
54
  end
43
55
  end
44
56
 
45
57
  def process(data)
46
- data = MessagePack.unpack(data)
47
58
  case data['_type'].to_sym
48
59
  when :event
49
60
  process_event(Event.parse(data))
50
- @client.sampler.emit(:counter, 'ganymed.processor.events', 1)
51
61
  when :metadata
52
62
  process_metadata(data)
53
63
  end
64
+ rescue Exception => exc
65
+ log.exception(exc)
54
66
  end
55
67
 
56
68
  def process_event(event)
69
+ @sampler.emit(:counter, 'ganymed.processor.events', 1) rescue nil
70
+
57
71
  if not @metadata[event.origin].include?(event.ns)
58
72
  # insert origin + namespace into metadata
59
73
  metadata = @db['metadata'].find_and_modify({
@@ -70,19 +84,11 @@ module Ganymed
70
84
  @websocket.send(:metadata, [metadata])
71
85
  end
72
86
 
73
- # send the event to websocket clients
74
- @websocket.each do |connection|
75
- connection.publish(event)
76
- end
77
-
78
87
  # normalize timestamp for events with a resolution
79
88
  if event.resolution > 1
80
89
  event.timestamp -= event.timestamp % event.resolution
81
90
  end
82
91
 
83
- # skip events that are produced below the resolution threshold
84
- return if (1...@config.resolution).include?(event.resolution)
85
-
86
92
  # insert event into ns collection
87
93
  @db.collection(event.ns).update({
88
94
  :c => event.cf,
@@ -91,6 +97,11 @@ module Ganymed
91
97
  }, {
92
98
  :$set => {:v => event.value}
93
99
  }, :upsert => true)
100
+
101
+ # send the event to websocket clients
102
+ @websocket.each do |connection|
103
+ connection.publish(event)
104
+ end
94
105
  end
95
106
 
96
107
  def process_metadata(data)
@@ -105,17 +116,21 @@ module Ganymed
105
116
  end
106
117
 
107
118
  # @private
108
- module Connection
119
+ class Connection < EM::Connection
109
120
  attr_accessor :processor
110
121
 
122
+ def initialize
123
+ @pac = MessagePack::Unpacker.new
124
+ end
125
+
111
126
  def receive_data(data)
112
- EM.defer do
113
- begin
114
- processor.process(data)
115
- rescue Exception => exc
116
- log.exception(exc)
117
- end
127
+ @pac.feed(data)
128
+ @pac.each do |obj|
129
+ EM.defer { processor.process(obj) }
118
130
  end
131
+ rescue Exception => exc
132
+ log.exception(exc)
133
+ close_connection
119
134
  end
120
135
  end
121
136
  end
@@ -1,3 +1,4 @@
1
+ require 'ganymed/ext/array'
1
2
  require 'ganymed/sampler/datasource'
2
3
 
3
4
  module Ganymed
@@ -8,25 +9,11 @@ module Ganymed
8
9
  # as a webserver request, user login, etc) and produces a rate/s gauge.
9
10
  #
10
11
  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
12
+ def flush
13
+ each do |ns, origin, ts|
14
+ yield ns, origin, ts.values.map(&:sum)
24
15
  end
25
16
  end
26
-
27
- def feed(ns, origin, ts, value)
28
- add(ns, origin, [ts, value])
29
- end
30
17
  end
31
18
  end
32
19
  end
@@ -1,10 +1,13 @@
1
+ require 'inline'
2
+
1
3
  module Ganymed
2
4
  class Sampler
3
5
 
4
6
  ##
5
7
  # 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
+ # to an in-memory buffer using a sliding-window algorithm. Values are kept
9
+ # for a configurable time and consolidated values can be flushed to the
10
+ # processor at any time.
8
11
  #
9
12
  # All DataSources interpolate their samples to gauge style values before
10
13
  # flushing to the processor, e.g. a counter (like requests, or bytes sent
@@ -13,79 +16,69 @@ module Ganymed
13
16
  #
14
17
  class DataSource
15
18
 
16
- attr_reader :ticks, :buffer
19
+ attr_reader :buffer, :window
17
20
 
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
+ # Create a new DataSource.
21
22
  #
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] = []
23
+ # @param [Fixnum] window Sliding window size.
24
+ def initialize(window)
25
+ @window = window
26
+ @buffer = Hash.new do |buffer, origin|
27
+ buffer[origin] = Hash.new do |origin, ns|
28
+ origin[ns] = Hash.new do |ns, timestamp|
29
+ ns[timestamp] = []
29
30
  end
30
31
  end
31
32
  end
32
33
  end
33
34
 
34
- # @group Methods for subclasses
35
-
36
35
  # Feed data from the source.
37
36
  #
38
37
  # @param [String] ns The namespace for this value.
39
38
  # @param [String] origin The origin of this value.
40
- # @param [Time] ts The timestamp of this value.
41
39
  # @param [Fixnum,Float] value The actual value.
42
40
  # @return [void]
43
- def feed(ns, origin, ts, value)
44
- raise NotImplementedError
41
+ def feed(ns, origin, value)
42
+ @buffer[origin][ns][now] << value
45
43
  end
46
44
 
47
- # Flush and consolidate the buffer for the given +tick+.
45
+ # Flush and consolidate the buffer.
48
46
  #
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.
47
+ # @yield [ns, origin, values] Run the block once for every
48
+ # namespace/origin, passing in all the
49
+ # consolidated values.
52
50
  # @return [void]
53
- def flush(tick, &block)
51
+ def flush
54
52
  raise NotImplementedError
55
53
  end
56
54
 
57
- # @endgroup
58
-
59
- # Add a value to all tick buffers. Usually called from {#feed}.
55
+ # Yield values for every origin/ns combination and remove old elements
56
+ # from the buffer (sliding window). This is used internally to iterate
57
+ # through the values in the current window.
60
58
  #
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
59
+ # @yield [ns, origin, ts] Run the block once for every namespace
60
+ # passing in all the collected samples grouped
61
+ # by timestamp.
62
+ def each(&block)
63
+ oldest = now - window
64
+ @buffer.each do |origin, ns|
65
+ ns.each do |ns, ts|
66
+ ts.delete_if { |ts, _| ts < oldest }
67
+ yield ns, origin, ts
68
+ end
68
69
  end
69
70
  end
70
71
 
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
72
+ # Time.now.utc.to_i is way too expensive
73
+ inline :C do |builder|
74
+ builder.include "<sys/time.h>"
75
+ builder.c <<-EOC
76
+ long now(void) {
77
+ struct timeval tv;
78
+ gettimeofday(&tv, NULL);
79
+ return tv.tv_sec;
80
+ }
81
+ EOC
89
82
  end
90
83
  end
91
84
  end
@@ -10,27 +10,18 @@ module Ganymed
10
10
  # rate/s gauge, but only accepts absolute values and produces the
11
11
  # derivative instead of summation of all samples.
12
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
13
  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
14
+ def flush
15
+ each do |ns, origin, ts|
16
+ values = ts.each_cons(2).map do |a, b|
17
+ (b[1].last - a[1].last)/(b[0] - a[0]) # ∆value / ∆ts
18
+ end.reject do |value|
19
+ value < 0 # ignore overflow/counter wrap
25
20
  end
26
21
 
27
22
  yield ns, origin, values
28
23
  end
29
24
  end
30
-
31
- def feed(ns, origin, ts, value)
32
- add(ns, origin, [ts, value])
33
- end
34
25
  end
35
26
  end
36
27
  end
@@ -10,15 +10,11 @@ module Ganymed
10
10
  # currently logged in, etc).
11
11
  #
12
12
  class Gauge < DataSource
13
- def flush(tick, &block)
14
- each(tick) do |ns, origin, values|
15
- yield ns, origin, values
13
+ def flush
14
+ each do |ns, origin, ts|
15
+ yield ns, origin, ts.values.flatten
16
16
  end
17
17
  end
18
-
19
- def feed(ns, origin, ts, value)
20
- add(ns, origin, value)
21
- end
22
18
  end
23
19
  end
24
20
  end
@@ -1,33 +1,29 @@
1
1
  require 'active_support/inflector'
2
2
  require 'eventmachine'
3
- require 'inline'
4
- require 'madvertise/ext/enumerable'
5
3
 
6
4
  require 'ganymed'
7
5
  require 'ganymed/client'
6
+ require 'ganymed/ext/array'
8
7
 
9
8
  module Ganymed
10
9
 
11
10
  ##
12
11
  # The Sampler processes samples from a UDP socket, stores these samples into
13
12
  # memory and flushes a possibly interpolated and consolidated value to the
14
- # {Processor} for every tick.
13
+ # {Processor}.
15
14
  #
16
15
  class Sampler
17
16
  def initialize(config)
18
17
  @config = config
19
- @config.sampler.ticks |= [@config.resolution]
20
- @fqdn = @config.fqdn
21
18
  listen!
22
19
  emit!
23
- tick!
24
20
  end
25
21
 
26
22
  def listen!
27
23
  @config.sampler.listen.tap do |listen|
28
24
  log.info("processing samples on udp##{listen.host}:#{listen.port}")
29
25
  EM.open_datagram_socket(listen.host, listen.port, Connection) do |connection|
30
- connection.fqdn = @fqdn
26
+ connection.fqdn = @config.fqdn
31
27
  connection.datasources = datasources
32
28
  end
33
29
  end
@@ -35,53 +31,37 @@ module Ganymed
35
31
 
36
32
  def emit!
37
33
  @config.client.processor.tap do |processor|
38
- log.info("emitting consolidated samples to udp##{processor.host}:#{processor.port}")
39
- @client = Client.new(:processor => processor)
40
- @processor = @client.processor
34
+ log.info("emitting consolidated samples to tcp##{processor.host}:#{processor.port}")
35
+ @processor = Ganymed::Client::Processor.connect(processor.host, processor.port)
41
36
  end
42
- end
43
37
 
44
- def tick!
45
- @config.sampler.ticks.each do |tick|
46
- EM.add_periodic_timer(tick) { flush(tick) }
47
- end
38
+ # emit consolidated samples 5 times per window
39
+ interval = [1, @config.sampler.window / 5].max
40
+ EM.add_periodic_timer(interval) { flush }
48
41
  end
49
42
 
50
43
  def datasources
51
44
  @datasources ||= Hash.new do |hsh, key|
52
45
  klass = "Ganymed::Sampler::#{key.classify}".constantize
53
- hsh[key] = klass.new(@config.sampler.ticks)
46
+ hsh[key] = klass.new(@config.sampler.window)
54
47
  end
55
48
  end
56
49
 
57
- def flush(tick)
50
+ def flush
58
51
  datasources.each do |_, datasource|
59
- EM.defer do
60
- datasource.flush(tick) do |ns, origin, values|
61
- emit(ns, origin, tick, values)
62
- end
52
+ datasource.flush do |ns, origin, values|
53
+ emit(ns, origin, values)
63
54
  end
64
55
  end
65
56
  end
66
57
 
67
- # Consolidation functions.
68
- CFS = {
69
- :min => [:min],
70
- :max => [:max],
71
- :avg => [:mean],
72
- :stdev => [:stdev],
73
- :u90 => [:percentile, 0.9],
74
- }
75
-
76
- def emit(ns, origin, tick, values)
77
- CFS.each do |cf, args|
78
- # do not calculate the consolidations below threshold
79
- next if tick < @config.resolution and cf != :avg
58
+ def emit(ns, origin, values)
59
+ @config.sampler.consolidations.each do |cf, args|
80
60
  begin
81
61
  @processor.event(ns, values.send(*args), {
82
62
  :cf => cf,
83
63
  :origin => origin,
84
- :resolution => tick,
64
+ :resolution => @config.sampler.window,
85
65
  })
86
66
  rescue Exception => exc
87
67
  log.warn("failed to emit #{ns}@#{origin}")
@@ -94,24 +74,12 @@ module Ganymed
94
74
  module Connection
95
75
  attr_accessor :datasources, :fqdn
96
76
 
97
- # Time.now.utc.to_f is way too expensive
98
- inline :C do |builder|
99
- builder.include "<sys/time.h>"
100
- builder.c <<-EOC
101
- double timestamp(void) {
102
- struct timeval tv;
103
- gettimeofday(&tv, NULL);
104
- return ((double)tv.tv_sec + ((double)tv.tv_usec / 10000000.0));
105
- }
106
- EOC
107
- end
108
-
109
77
  def receive_data(data)
110
78
  begin
111
- # pack format: <datasource><namespace><origin><timestamp><value>
112
- data = data.unpack("Z*Z*Z*GG")
79
+ # pack format: <datasource><namespace><origin><value>
80
+ data = data.unpack("Z*Z*Z*G")
113
81
  datasources[data.slice!(0)].feed(*data)
114
- datasources['counter'].feed('ganymed.sampler.samples', @fqdn, timestamp, 1)
82
+ datasources['counter'].feed('ganymed.sampler.samples', fqdn, 1)
115
83
  rescue Exception => exc
116
84
  log.exception(exc)
117
85
  end
@@ -1,4 +1,4 @@
1
1
  module Ganymed
2
2
  # @private
3
- VERSION = '0.2.3'
3
+ VERSION = '0.3.0'
4
4
  end
@@ -12,12 +12,16 @@ module Ganymed
12
12
  return if not authenticated?
13
13
  data.each do |ns, query|
14
14
  query_id = query.delete('_id')
15
+
15
16
  log.debug("query #{query_id} from #{peer}: #{ns}(#{query.inspect})")
17
+ t = Time.now
16
18
 
17
19
  events = db.collection(ns).find(query).map do |event|
18
20
  Event.parse(event.merge({'n' => ns}))
19
21
  end
20
22
 
23
+ log.debug("query #{query_id} returned #{events.length} results in #{Time.now - t}s")
24
+
21
25
  send(:result, {query_id => convert(events)})
22
26
  end
23
27
  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.2.3
4
+ version: 0.3.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-30 00:00:00.000000000 Z
12
+ date: 2012-06-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
16
- requirement: &5360620 !ruby/object:Gem::Requirement
16
+ requirement: &16400780 !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: *5360620
24
+ version_requirements: *16400780
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: eventmachine
27
- requirement: &5358160 !ruby/object:Gem::Requirement
27
+ requirement: &16399280 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,21 +32,21 @@ dependencies:
32
32
  version: 0.12.10
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *5358160
35
+ version_requirements: *16399280
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: ganymed-client
38
- requirement: &5355500 !ruby/object:Gem::Requirement
38
+ requirement: &16397660 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
42
42
  - !ruby/object:Gem::Version
43
- version: '0'
43
+ version: 0.2.0
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *5355500
46
+ version_requirements: *16397660
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: madvertise-ext
49
- requirement: &5353920 !ruby/object:Gem::Requirement
49
+ requirement: &16396540 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 0.1.2
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *5353920
57
+ version_requirements: *16396540
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: madvertise-logging
60
- requirement: &5381900 !ruby/object:Gem::Requirement
60
+ requirement: &16395360 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: 0.3.2
66
66
  type: :runtime
67
67
  prerelease: false
68
- version_requirements: *5381900
68
+ version_requirements: *16395360
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: mixlib-cli
71
- requirement: &5380600 !ruby/object:Gem::Requirement
71
+ requirement: &16394880 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0'
77
77
  type: :runtime
78
78
  prerelease: false
79
- version_requirements: *5380600
79
+ version_requirements: *16394880
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: RubyInline
82
- requirement: &5378540 !ruby/object:Gem::Requirement
82
+ requirement: &16410420 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: '0'
88
88
  type: :runtime
89
89
  prerelease: false
90
- version_requirements: *5378540
90
+ version_requirements: *16410420
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: servolux
93
- requirement: &5376720 !ruby/object:Gem::Requirement
93
+ requirement: &16408420 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ! '>='
@@ -98,10 +98,10 @@ dependencies:
98
98
  version: '0'
99
99
  type: :runtime
100
100
  prerelease: false
101
- version_requirements: *5376720
101
+ version_requirements: *16408420
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: msgpack
104
- requirement: &5375220 !ruby/object:Gem::Requirement
104
+ requirement: &16406820 !ruby/object:Gem::Requirement
105
105
  none: false
106
106
  requirements:
107
107
  - - ! '>='
@@ -109,10 +109,10 @@ dependencies:
109
109
  version: '0'
110
110
  type: :runtime
111
111
  prerelease: false
112
- version_requirements: *5375220
112
+ version_requirements: *16406820
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: mongo
115
- requirement: &5396280 !ruby/object:Gem::Requirement
115
+ requirement: &16405220 !ruby/object:Gem::Requirement
116
116
  none: false
117
117
  requirements:
118
118
  - - ! '>='
@@ -120,10 +120,10 @@ dependencies:
120
120
  version: '1.6'
121
121
  type: :runtime
122
122
  prerelease: false
123
- version_requirements: *5396280
123
+ version_requirements: *16405220
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: em-websocket
126
- requirement: &5392360 !ruby/object:Gem::Requirement
126
+ requirement: &16403660 !ruby/object:Gem::Requirement
127
127
  none: false
128
128
  requirements:
129
129
  - - ! '>='
@@ -131,10 +131,10 @@ dependencies:
131
131
  version: '0'
132
132
  type: :runtime
133
133
  prerelease: false
134
- version_requirements: *5392360
134
+ version_requirements: *16403660
135
135
  - !ruby/object:Gem::Dependency
136
136
  name: yajl-ruby
137
- requirement: &5391160 !ruby/object:Gem::Requirement
137
+ requirement: &16402580 !ruby/object:Gem::Requirement
138
138
  none: false
139
139
  requirements:
140
140
  - - ! '>='
@@ -142,10 +142,10 @@ dependencies:
142
142
  version: '0'
143
143
  type: :runtime
144
144
  prerelease: false
145
- version_requirements: *5391160
145
+ version_requirements: *16402580
146
146
  - !ruby/object:Gem::Dependency
147
147
  name: sys-filesystem
148
- requirement: &5414000 !ruby/object:Gem::Requirement
148
+ requirement: &16433420 !ruby/object:Gem::Requirement
149
149
  none: false
150
150
  requirements:
151
151
  - - ! '>='
@@ -153,10 +153,10 @@ dependencies:
153
153
  version: '0'
154
154
  type: :runtime
155
155
  prerelease: false
156
- version_requirements: *5414000
156
+ version_requirements: *16433420
157
157
  - !ruby/object:Gem::Dependency
158
158
  name: ohai
159
- requirement: &5406240 !ruby/object:Gem::Requirement
159
+ requirement: &16432680 !ruby/object:Gem::Requirement
160
160
  none: false
161
161
  requirements:
162
162
  - - ! '>='
@@ -164,7 +164,7 @@ dependencies:
164
164
  version: 0.6.12
165
165
  type: :runtime
166
166
  prerelease: false
167
- version_requirements: *5406240
167
+ version_requirements: *16432680
168
168
  description: Ganymed is an event collection daemon
169
169
  email:
170
170
  - bb@xnull.de
@@ -197,6 +197,7 @@ files:
197
197
  - lib/ganymed/collectors/uptime.rb
198
198
  - lib/ganymed/config.yml
199
199
  - lib/ganymed/event.rb
200
+ - lib/ganymed/ext/array.rb
200
201
  - lib/ganymed/master.rb
201
202
  - lib/ganymed/mongodb.rb
202
203
  - lib/ganymed/processor.rb
@@ -238,7 +239,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
238
239
  version: '0'
239
240
  segments:
240
241
  - 0
241
- hash: 1127904829280483897
242
+ hash: 4174202345634086766
242
243
  required_rubygems_version: !ruby/object:Gem::Requirement
243
244
  none: false
244
245
  requirements:
@@ -247,7 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
247
248
  version: '0'
248
249
  segments:
249
250
  - 0
250
- hash: 1127904829280483897
251
+ hash: 4174202345634086766
251
252
  requirements: []
252
253
  rubyforge_project:
253
254
  rubygems_version: 1.8.17