reacter 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/reacter.rb +1 -0
- data/lib/reacter/adapter.rb +61 -0
- data/lib/reacter/adapters/amqp.rb +45 -0
- data/lib/reacter/adapters/file.rb +39 -0
- data/lib/reacter/agent.rb +43 -0
- data/lib/reacter/agents/decider.rb +159 -0
- data/lib/reacter/agents/logger.rb +12 -0
- data/lib/reacter/config.rb +81 -0
- data/lib/reacter/core.rb +102 -0
- data/lib/reacter/message.rb +90 -0
- data/lib/reacter/parsers/collectd.rb +28 -0
- data/lib/reacter/parsers/default.rb +15 -0
- data/lib/reacter/parsers/graphite.rb +26 -0
- data/lib/reacter/parsers/json.rb +19 -0
- data/lib/reacter/parsers/tsdb.rb +24 -0
- data/lib/reacter/util.rb +38 -0
- metadata +101 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 78f3df07bc48af15f0de952a22a18e0ef37cec7d
|
4
|
+
data.tar.gz: e5269dca6726a6a13ff2caeb2ea637f32f35b4c2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 28b7fdd002e758c901dcf64968633c16b473a9517da311ef423eefce35fa3fa7c8975514980a00a702e2439f59efc13e156a308e24400c63c38f4ed16e526d80
|
7
|
+
data.tar.gz: 0042e5ec4cba2ec33bad8d3562b33058c20c0847cb3ce07aafc8628d575c0ba4617447fae6e75618dd39dcc7439aa7e736b1f69ebc5052ecb2bb3cb229b25c7f
|
data/lib/reacter.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'reacter/core'
|
@@ -0,0 +1,61 @@
|
|
1
|
+
#------------------------------------------------------------------------------#
|
2
|
+
# Adapter
|
3
|
+
#
|
4
|
+
require 'reacter/message'
|
5
|
+
require 'reacter/config'
|
6
|
+
require 'reacter/util'
|
7
|
+
|
8
|
+
class AdapterConnectionFailed < Exception; end
|
9
|
+
class AdapterConnectionFaulted < Exception; end
|
10
|
+
class AdapterConnectionClosed < Exception; end
|
11
|
+
|
12
|
+
class Reacter
|
13
|
+
class Adapter
|
14
|
+
attr :config
|
15
|
+
attr :type
|
16
|
+
|
17
|
+
def initialize()
|
18
|
+
@config = Reacter.get('global.adapter', {})
|
19
|
+
@type = @config.get('type')
|
20
|
+
Util.info("Loading adapter #{@type}...")
|
21
|
+
end
|
22
|
+
|
23
|
+
# implement: connect to the data source
|
24
|
+
def connect(args={})
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
# implement: send a message suitable for consumption by an instance of reacter
|
29
|
+
# in listen mode
|
30
|
+
def send(message)
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
# implement: poll for a new message and return it
|
35
|
+
def poll()
|
36
|
+
false
|
37
|
+
end
|
38
|
+
|
39
|
+
# implement: manual disconnect / cleanup
|
40
|
+
def disconnect()
|
41
|
+
raise AdapterConnectionClosed
|
42
|
+
end
|
43
|
+
|
44
|
+
class<<self
|
45
|
+
def create(type)
|
46
|
+
if type
|
47
|
+
begin
|
48
|
+
require "reacter/adapters/#{type}"
|
49
|
+
rv = (Reacter.const_get("#{type.capitalize}Adapter").new())
|
50
|
+
return rv
|
51
|
+
|
52
|
+
rescue LoadError
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#------------------------------------------------------------------------------#
|
2
|
+
# AMQP Adapter
|
3
|
+
#
|
4
|
+
require 'reacter/adapter'
|
5
|
+
require 'reacter/config'
|
6
|
+
require 'reacter/util'
|
7
|
+
|
8
|
+
class Reacter
|
9
|
+
class AmqpAdapter < Adapter
|
10
|
+
require 'amqp'
|
11
|
+
|
12
|
+
DEFAULT_QUEUENAME='reacter'
|
13
|
+
|
14
|
+
def connect(args={})
|
15
|
+
@_connection = AMQP.connect({
|
16
|
+
:host => @config.get(:hostname, 'localhost'),
|
17
|
+
:port => @config.get(:port, 5672),
|
18
|
+
:username => @config.get(:username, 'guest'),
|
19
|
+
:password => @config.get(:password, 'guest'),
|
20
|
+
:vhost => @config.get(:vhost, '/')
|
21
|
+
})
|
22
|
+
|
23
|
+
@_channel = AMQP::Channel.new(@_connection)
|
24
|
+
@_queue = @_channel.queue(@config.get(:queue, DEFAULT_QUEUENAME), {
|
25
|
+
:auto_delete => @config.get(:autodelete, true)
|
26
|
+
})
|
27
|
+
|
28
|
+
@_exchange = @config.get(:exchange, '')
|
29
|
+
end
|
30
|
+
|
31
|
+
def send(message)
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
def poll(&block)
|
36
|
+
@_queue.bind(@_exchange).subscribe do |payload|
|
37
|
+
yield Message.parse(payload)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def disconnect()
|
42
|
+
raise AdapterConnectionClosed
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
#------------------------------------------------------------------------------#
|
2
|
+
# Standard Input Adapter
|
3
|
+
#
|
4
|
+
require 'reacter/adapter'
|
5
|
+
require 'reacter/config'
|
6
|
+
require 'reacter/util'
|
7
|
+
|
8
|
+
class Reacter
|
9
|
+
class FileAdapter < Adapter
|
10
|
+
def connect(args={})
|
11
|
+
if @config.get(:filename) == 'stdin'
|
12
|
+
@_stdin = true
|
13
|
+
else
|
14
|
+
@_stdin = false
|
15
|
+
@_input = File.open(File.expand_path(@config.get(:filename)), 'r+')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def send(message)
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
def poll(&block)
|
24
|
+
loop do
|
25
|
+
line = @_input.gets
|
26
|
+
yield Message.parse(line)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def disconnect()
|
31
|
+
raise AdapterConnectionClosed
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def stdin?()
|
36
|
+
@_stdin
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#------------------------------------------------------------------------------#
|
2
|
+
# Agent
|
3
|
+
#
|
4
|
+
require 'reacter/message'
|
5
|
+
require 'reacter/config'
|
6
|
+
require 'reacter/util'
|
7
|
+
|
8
|
+
class Reacter
|
9
|
+
class Agent
|
10
|
+
attr :config
|
11
|
+
attr :type
|
12
|
+
|
13
|
+
def initialize()
|
14
|
+
@type = self.class.name.sub('Agent', '').split('::').last.downcase
|
15
|
+
@config = Reacter.get("agents.#{@type}", {})
|
16
|
+
Util.info("Loading agent #{@type}...")
|
17
|
+
end
|
18
|
+
|
19
|
+
def received(message)
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
class<<self
|
24
|
+
def create(type)
|
25
|
+
if type
|
26
|
+
begin
|
27
|
+
agentpath = Reacter['global.agents.path']
|
28
|
+
$: << File.expand_path(agentpath) if agentpath
|
29
|
+
|
30
|
+
require "reacter/agents/#{type}"
|
31
|
+
rv = (Reacter.const_get("#{type.capitalize}Agent").new())
|
32
|
+
return rv
|
33
|
+
|
34
|
+
rescue LoadError
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
#------------------------------------------------------------------------------#
|
2
|
+
# DeciderAgent
|
3
|
+
#
|
4
|
+
require 'reacter/agent'
|
5
|
+
|
6
|
+
class Reacter
|
7
|
+
class DeciderAgent < Agent
|
8
|
+
class EmitAlert < Exception; end
|
9
|
+
|
10
|
+
STATES=[:okay, :warning, :critical]
|
11
|
+
COMPARISONS=%w{not is above below}
|
12
|
+
|
13
|
+
def initialize()
|
14
|
+
super
|
15
|
+
|
16
|
+
@_state_names = {}
|
17
|
+
STATES.each_index do |i|
|
18
|
+
@_state_names[STATES[i]] = i
|
19
|
+
end
|
20
|
+
|
21
|
+
@_alerts = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def received(message)
|
25
|
+
# early exit conditions
|
26
|
+
return false unless @config
|
27
|
+
return false unless @config['sources']
|
28
|
+
return false unless message.metric
|
29
|
+
unless message.source
|
30
|
+
return false unless @config['sources']['any']
|
31
|
+
end
|
32
|
+
|
33
|
+
# build effective configuration rule for this message
|
34
|
+
general = (@config['sources']['any'][message.metric] or {} rescue {})
|
35
|
+
specific = (@config['sources'][message.source][message.metric] or {} rescue {})
|
36
|
+
rule = specific.deep_merge!(general)
|
37
|
+
|
38
|
+
# quit early for empty rules
|
39
|
+
return false if rule.empty?
|
40
|
+
|
41
|
+
# quit early for invalid rules
|
42
|
+
return false unless (rule['threshold'] and rule['actions'])
|
43
|
+
|
44
|
+
# default return value: false
|
45
|
+
rv = false
|
46
|
+
|
47
|
+
# initialize in-memory alert tracking (per source, per metric)
|
48
|
+
@_alerts[message.source] ||= {}
|
49
|
+
@_alerts[message.source][message.metric] ||= ({
|
50
|
+
:count => 1,
|
51
|
+
:last_state => nil,
|
52
|
+
:last_seen => nil,
|
53
|
+
:new_alert => false,
|
54
|
+
:has_ever_failed => false
|
55
|
+
})
|
56
|
+
|
57
|
+
# get current state of this message according to the rule
|
58
|
+
state, comparison = state_of(message, rule)
|
59
|
+
|
60
|
+
begin
|
61
|
+
# a non-okay alert has occurred, this metric can now be said to have failed
|
62
|
+
unless state == :okay
|
63
|
+
@_alerts[message.source][message.metric][:has_ever_failed] = true
|
64
|
+
end
|
65
|
+
|
66
|
+
# the state has changed, reset counter
|
67
|
+
if @_alerts[message.source][message.metric][:last_state] != state
|
68
|
+
@_alerts[message.source][message.metric][:new_alert] = true
|
69
|
+
@_alerts[message.source][message.metric][:count] = 1
|
70
|
+
end
|
71
|
+
|
72
|
+
hits = ((rule['threshold'][state.to_s][hits] rescue rule['hits']) or rule['hits'] or 1).to_i
|
73
|
+
persist = ((rule['threshold'][state.to_s]['persist'] === true) rescue false)
|
74
|
+
|
75
|
+
# only raise every n alerts
|
76
|
+
if @_alerts[message.source][message.metric][:count] >= hits
|
77
|
+
# raise the alert if it's new or if we're persistently reminding of alerts
|
78
|
+
if @_alerts[message.source][message.metric][:new_alert] or persist
|
79
|
+
# only emit if this metric has ever failed
|
80
|
+
if @_alerts[message.source][message.metric][:has_ever_failed]
|
81
|
+
raise EmitAlert
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
rescue EmitAlert
|
88
|
+
message[:state] = state
|
89
|
+
message[:comparison] = comparison
|
90
|
+
message[:check_value] = (rule['threshold'][state][comparison] rescue nil)
|
91
|
+
message[:alerted_at] = Time.now.to_i
|
92
|
+
|
93
|
+
rv = message
|
94
|
+
|
95
|
+
# alert is emitted, it's not new anymore
|
96
|
+
@_alerts[message.source][message.metric][:new_alert] = false
|
97
|
+
|
98
|
+
ensure
|
99
|
+
@_alerts[message.source][message.metric][:count] += 1
|
100
|
+
@_alerts[message.source][message.metric][:last_state] = state
|
101
|
+
@_alerts[message.source][message.metric][:last_seen] = Time.now.to_i
|
102
|
+
end
|
103
|
+
|
104
|
+
return rv
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def state_of(message, rule)
|
111
|
+
worst_state = 0
|
112
|
+
comparison = nil
|
113
|
+
|
114
|
+
# for each state, add all breached states
|
115
|
+
rule['threshold'].each do |state, check|
|
116
|
+
state = state.to_sym
|
117
|
+
|
118
|
+
check.each do |comp, test|
|
119
|
+
if compare(message.value, comp, test)
|
120
|
+
# this breached state is worse than what we've seen, make it the worst
|
121
|
+
if @_state_names[state] > worst_state
|
122
|
+
worst_state = @_state_names[state]
|
123
|
+
comparison = comp
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
return [STATES[worst_state], comparison]
|
130
|
+
end
|
131
|
+
|
132
|
+
def compare(value, comparison, test)
|
133
|
+
case comparison.to_sym
|
134
|
+
when :not
|
135
|
+
return (value != test)
|
136
|
+
when :is
|
137
|
+
return (value == test)
|
138
|
+
when :above
|
139
|
+
return (value > test)
|
140
|
+
when :below
|
141
|
+
return (value < test)
|
142
|
+
else
|
143
|
+
return false
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
# {"complements"=>"df-var.df_complex-used",
|
151
|
+
# "attributes"=>{
|
152
|
+
# "owner"=>"This Guy <tguy@outbrain.com>",
|
153
|
+
# "description"=>"",
|
154
|
+
# "neon"=>{"url"=>"http://localhost:5000/alert/new"}},
|
155
|
+
# "actions"=>{
|
156
|
+
# "exec"=>["~/Dropbox/Outbrain/reacter-notify"]},
|
157
|
+
# "threshold"=>{
|
158
|
+
# "warning"=>{
|
159
|
+
# "below"=>"5%"}}}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
#------------------------------------------------------------------------------#
|
2
|
+
# LoggerAgent
|
3
|
+
#
|
4
|
+
require 'reacter/agent'
|
5
|
+
|
6
|
+
class Reacter
|
7
|
+
class LoggerAgent < Agent
|
8
|
+
def received(message)
|
9
|
+
Util.info("LOGGER: [#{message.state or :unknown}] #{message.source}/#{message.metric}")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
class Reacter
|
2
|
+
class<<self
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
DEFAULT_CONFIG={
|
6
|
+
'global' => {
|
7
|
+
'searchpath' => ['~/.reacter', '/etc/reacter'],
|
8
|
+
'configs' => [
|
9
|
+
'~/.reacter/reacter.yaml',
|
10
|
+
'/etc/reacter/reacter.yaml'
|
11
|
+
]
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
def load_config(config={})
|
16
|
+
@_config = DEFAULT_CONFIG
|
17
|
+
default_path = DEFAULT_CONFIG.get('global.configs')
|
18
|
+
|
19
|
+
# load from default paths
|
20
|
+
default_path.each do |path|
|
21
|
+
_load_file(path)
|
22
|
+
end
|
23
|
+
|
24
|
+
# load any supplemental configs
|
25
|
+
@_config.get('global.searchpath').each do |dir|
|
26
|
+
# adapters
|
27
|
+
adapter = @_config.get('global.adapter.type')
|
28
|
+
_load_file(File.join(dir, 'adapters', "#{adapter}.yaml")) if adapter
|
29
|
+
|
30
|
+
# agents
|
31
|
+
@_config.get('global.agents.enabled', []).each do |agent|
|
32
|
+
_load_file(File.join(dir, 'agents', "#{agent}.yaml"))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# load from any new paths that were loaded
|
37
|
+
(@_config.get('global.configs') - default_path).each do |path|
|
38
|
+
_load_file(path)
|
39
|
+
end
|
40
|
+
|
41
|
+
@_config = config.deep_merge!(@_config, {
|
42
|
+
:merge_hash_arrays => true
|
43
|
+
})
|
44
|
+
end
|
45
|
+
|
46
|
+
def [](key)
|
47
|
+
get(key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def []=(key, value)
|
51
|
+
set(key, value)
|
52
|
+
end
|
53
|
+
|
54
|
+
def get(path, default=nil)
|
55
|
+
@_config.get(path, default)
|
56
|
+
end
|
57
|
+
|
58
|
+
def set(path, value)
|
59
|
+
@_config.set(path, value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def dump_config()
|
63
|
+
@_config
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
private
|
68
|
+
def _load_file(path)
|
69
|
+
path = File.expand_path(path)
|
70
|
+
|
71
|
+
if File.exists?(path)
|
72
|
+
Util.info("Loading configuration #{path}...")
|
73
|
+
yaml = YAML.load(File.read(path))
|
74
|
+
|
75
|
+
@_config = yaml.deep_merge!(@_config, {
|
76
|
+
:merge_hash_arrays => true
|
77
|
+
})
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/reacter/core.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'deep_merge'
|
3
|
+
require 'hashlib'
|
4
|
+
require 'reacter/message'
|
5
|
+
require 'reacter/config'
|
6
|
+
require 'reacter/util'
|
7
|
+
require 'reacter/adapter'
|
8
|
+
require 'reacter/agent'
|
9
|
+
|
10
|
+
class Reacter
|
11
|
+
class Core < EM::Connection
|
12
|
+
def initialize(*args)
|
13
|
+
super
|
14
|
+
|
15
|
+
@_dispatch_queue = EM::Queue.new
|
16
|
+
@_adapter = nil
|
17
|
+
@_agents = []
|
18
|
+
|
19
|
+
Reacter.load_config()
|
20
|
+
|
21
|
+
load_adapters()
|
22
|
+
load_agents()
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_adapters()
|
26
|
+
adapter_config = Reacter.get('global.adapter', nil)
|
27
|
+
|
28
|
+
if adapter_config
|
29
|
+
type = adapter_config.get('type')
|
30
|
+
|
31
|
+
@_adapter = Adapter.create(type)
|
32
|
+
|
33
|
+
if @_adapter
|
34
|
+
@_adapter.connect()
|
35
|
+
else
|
36
|
+
raise "Adapter '#{type}' not found, exiting"
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
else
|
40
|
+
Util.fatal("No adapters specified, exiting")
|
41
|
+
exit 1
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def load_agents()
|
46
|
+
Reacter.get('global.agents.enabled', []).each do |agent|
|
47
|
+
agent = Agent.create(agent)
|
48
|
+
@_agents << agent if agent
|
49
|
+
end
|
50
|
+
|
51
|
+
if @_agents.empty?
|
52
|
+
Util.fatal("No agents enabled, exiting")
|
53
|
+
exit 1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def run()
|
58
|
+
Util.info("Start listening using #{@_adapter.type} adapter...")
|
59
|
+
|
60
|
+
|
61
|
+
# agent message dispatch subprocess
|
62
|
+
dispatch = proc do |messages|
|
63
|
+
messages.each do |message|
|
64
|
+
rv = message
|
65
|
+
|
66
|
+
# send this message to all agents
|
67
|
+
@_agents.each do |agent|
|
68
|
+
next if rv === false
|
69
|
+
rv = agent.received(rv)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# enter polling loop
|
75
|
+
begin
|
76
|
+
@_adapter.poll do |messages|
|
77
|
+
@_dispatch_queue.push(messages)
|
78
|
+
@_dispatch_queue.pop(dispatch)
|
79
|
+
end
|
80
|
+
|
81
|
+
rescue AdapterConnectionFailed => e
|
82
|
+
Util.error("Adapter connection failed: #{e.message}")
|
83
|
+
|
84
|
+
rescue AdapterConnectionFaulted => e
|
85
|
+
Util.error("Adapter connection error: #{e.message}")
|
86
|
+
|
87
|
+
rescue AdapterConnectionClosed => e
|
88
|
+
Util.info("Adapter closed connection")
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class<<self
|
95
|
+
def start()
|
96
|
+
EM.run do
|
97
|
+
reacter = EM.connect('127.0.0.1', 9000, Reacter::Core)
|
98
|
+
reacter.run()
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'reacter/util'
|
2
|
+
require 'reacter/config'
|
3
|
+
|
4
|
+
class Reacter
|
5
|
+
class Message
|
6
|
+
require 'hashlib'
|
7
|
+
DEFAULT_SOURCE_NAME='default'
|
8
|
+
|
9
|
+
def initialize(message={})
|
10
|
+
@_data = {
|
11
|
+
:raw => message,
|
12
|
+
:time => (Time.now.to_i * 1000),
|
13
|
+
:attributes => {},
|
14
|
+
:metric => nil,
|
15
|
+
:source => DEFAULT_SOURCE_NAME
|
16
|
+
}.merge(message)
|
17
|
+
|
18
|
+
if not @_data.has_key?(:event) and not @_data.has_key?(:value)
|
19
|
+
@_data[:event] = true
|
20
|
+
else
|
21
|
+
@_data[:event] = false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_h
|
26
|
+
@_data
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](key)
|
30
|
+
@_data[key.to_sym]
|
31
|
+
end
|
32
|
+
|
33
|
+
def []=(key, value)
|
34
|
+
@_data[key.to_sym] = value
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(name, *args)
|
38
|
+
@_data[name.to_sym] or args.first
|
39
|
+
end
|
40
|
+
|
41
|
+
class<<self
|
42
|
+
def load_parsers()
|
43
|
+
unless defined?(@@_parsers)
|
44
|
+
@@_parsers = {}
|
45
|
+
|
46
|
+
# load and initialize parsers
|
47
|
+
Dir[File.join(File.dirname(__FILE__), 'parsers', '*.rb')].each do |i|
|
48
|
+
i = File.basename(i,'.rb')
|
49
|
+
require "reacter/parsers/#{i}"
|
50
|
+
@@_parsers[i.to_sym] = Message.const_get("#{i.capitalize}Parser")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse(body)
|
56
|
+
load_parsers()
|
57
|
+
|
58
|
+
# split strings into lines
|
59
|
+
body = body.lines if body.is_a?(String)
|
60
|
+
messages = []
|
61
|
+
|
62
|
+
if body.respond_to?(:each)
|
63
|
+
body.each do |message|
|
64
|
+
# strings need to be detected and parsed
|
65
|
+
if message.is_a?(String)
|
66
|
+
next if message.strip.chomp.empty?
|
67
|
+
|
68
|
+
# use first parser that claims it can handle this string
|
69
|
+
@@_parsers.each do |name, parser|
|
70
|
+
if parser.detected?(message)
|
71
|
+
#Util.debug("Using parser #{name}")
|
72
|
+
m = parser.parse(message)
|
73
|
+
(m.is_a?(Array) ? messages += m : messages << m)
|
74
|
+
next
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# hashes go directly into the stack
|
79
|
+
elsif message.is_a?(Hash)
|
80
|
+
messages << message
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
messages.collect{|i| Message.new(i) }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'reacter/parsers/default'
|
2
|
+
|
3
|
+
class Reacter
|
4
|
+
class Message
|
5
|
+
class CollectdParser < DefaultParser
|
6
|
+
class<<self
|
7
|
+
def detected?(message)
|
8
|
+
(message[0..6] == 'PUTVAL ')
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse(message)
|
12
|
+
message = message.split(' ')
|
13
|
+
source, plugin, type = message[1].split('/')
|
14
|
+
attributes = Hash[message[2].split(';').collect{|i| i.split('=',2) }]
|
15
|
+
time, value = message[3].split(':')
|
16
|
+
|
17
|
+
{
|
18
|
+
:source => source,
|
19
|
+
:metric => [plugin, type].join('.'),
|
20
|
+
:time => (time.to_i * 1000),
|
21
|
+
:value => value.to_f,
|
22
|
+
:attributes => attributes
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'reacter/parsers/default'
|
2
|
+
|
3
|
+
class Reacter
|
4
|
+
class Message
|
5
|
+
class GraphiteParser < DefaultParser
|
6
|
+
class<<self
|
7
|
+
# Graphite
|
8
|
+
# |metric-----|value|epoch----|
|
9
|
+
# => metric.name 12345 123456789
|
10
|
+
def detected?(message)
|
11
|
+
(message =~ /^[\w\.]+ [\d\.\-]+ \d+$/ ? true : false)
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse(message)
|
15
|
+
message = message.split(' ')
|
16
|
+
|
17
|
+
{
|
18
|
+
:metric => message[0],
|
19
|
+
:value => message[1].to_f,
|
20
|
+
:time => (message[2].to_i * 1000)
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'reacter/parsers/default'
|
2
|
+
|
3
|
+
class Reacter
|
4
|
+
class Message
|
5
|
+
class JsonParser < DefaultParser
|
6
|
+
class<<self
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
def detected?(message)
|
10
|
+
(message[0] == '{' or message[0] == '[')
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(message)
|
14
|
+
([*JSON.load(message)] rescue [])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'reacter/parsers/default'
|
2
|
+
|
3
|
+
class Reacter
|
4
|
+
class Message
|
5
|
+
class TsdbParser < DefaultParser
|
6
|
+
class<<self
|
7
|
+
def detected?(message)
|
8
|
+
return (message.upcase[0..4] == 'PUT ')
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse(message)
|
12
|
+
message = message.split(' ')
|
13
|
+
|
14
|
+
{
|
15
|
+
:metric => message[1],
|
16
|
+
:time => (message[2].to_i * 1000),
|
17
|
+
:value => message[3].to_f,
|
18
|
+
:attributes => Hash[message[4..-1].collect{|i| i.split('=',2) }]
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/reacter/util.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'reacter/config'
|
2
|
+
|
3
|
+
class Reacter
|
4
|
+
class Util
|
5
|
+
class<<self
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
@@_logger = {
|
9
|
+
:default => Logger.new(STDOUT)
|
10
|
+
}
|
11
|
+
|
12
|
+
def log(message, severity=:info, log=:default)
|
13
|
+
@@_logger[log] = Logger.new(STDOUT) unless @@_logger[log]
|
14
|
+
@@_logger[log].send(severity, [*message].join(' '))
|
15
|
+
end
|
16
|
+
|
17
|
+
def info(message, log=:default)
|
18
|
+
log(message, :info, log)
|
19
|
+
end
|
20
|
+
|
21
|
+
def debug(message, log=:default)
|
22
|
+
log(message, :debug, log)
|
23
|
+
end
|
24
|
+
|
25
|
+
def warn(message, log=:default)
|
26
|
+
log(message, :warn, log)
|
27
|
+
end
|
28
|
+
|
29
|
+
def error(message, log=:default)
|
30
|
+
log(message, :error, log)
|
31
|
+
end
|
32
|
+
|
33
|
+
def fatal(message, log=:default)
|
34
|
+
log(message, :fatal, log)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: reacter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gary Hetzel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-03-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: eventmachine
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: deep_merge
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: hashlib
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: A utility for consuming, transforming, and routing monitoring data from
|
56
|
+
various sources
|
57
|
+
email: ghetzel@outbrain.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- lib/reacter.rb
|
63
|
+
- lib/reacter/adapter.rb
|
64
|
+
- lib/reacter/adapters/amqp.rb
|
65
|
+
- lib/reacter/adapters/file.rb
|
66
|
+
- lib/reacter/agent.rb
|
67
|
+
- lib/reacter/agents/decider.rb
|
68
|
+
- lib/reacter/agents/logger.rb
|
69
|
+
- lib/reacter/config.rb
|
70
|
+
- lib/reacter/core.rb
|
71
|
+
- lib/reacter/message.rb
|
72
|
+
- lib/reacter/parsers/collectd.rb
|
73
|
+
- lib/reacter/parsers/default.rb
|
74
|
+
- lib/reacter/parsers/graphite.rb
|
75
|
+
- lib/reacter/parsers/json.rb
|
76
|
+
- lib/reacter/parsers/tsdb.rb
|
77
|
+
- lib/reacter/util.rb
|
78
|
+
homepage: http://outbrain.github.com/reacter/
|
79
|
+
licenses: []
|
80
|
+
metadata: {}
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 2.0.0
|
98
|
+
signing_key:
|
99
|
+
specification_version: 4
|
100
|
+
summary: Reacter monitoring processor
|
101
|
+
test_files: []
|