ganymed 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|