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