reacter 0.0.1
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.
- 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: []
|