ganymed 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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