reacter 0.0.1 → 0.0.2
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 +4 -4
- data/bin/reacter +68 -0
- data/lib/reacter/adapter.rb +6 -6
- data/lib/reacter/adapters/amqp.rb +1 -1
- data/lib/reacter/adapters/file.rb +21 -6
- data/lib/reacter/agents/decider.rb +86 -23
- data/lib/reacter/agents/logger.rb +2 -1
- data/lib/reacter/core.rb +3 -3
- data/lib/reacter/message.rb +15 -0
- data/lib/reacter/parsers/collectd.rb +16 -0
- data/lib/reacter/parsers/default.rb +4 -0
- data/lib/reacter/parsers/graphite.rb +11 -0
- data/lib/reacter/parsers/json.rb +6 -0
- data/lib/reacter/parsers/tsdb.rb +21 -0
- data/lib/reacter/util.rb +5 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85466ac92fce993fd08a3b321ac6080013cc0fba
|
4
|
+
data.tar.gz: 02b305a9c8195c60239e4801ab0507e7fd3748df
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 74fea13a574c9385fab7783b7d220c47acb692c4701123e093c518f0806e8e9c59e04c1502a84945359709f6d881f58cbd6bd193d0702fad12d4fffee1b302d4
|
7
|
+
data.tar.gz: 1eaeef3dc876f4b85f3e32dcb1700cfa91dffa76b35283c881c1e56e0f8407ec4c1caf73f4ebfcf9d84c61520ad15df4a2fb9dc9389e051320210373b4e14e46
|
data/bin/reacter
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# reacter - a utility for consuming, transforming, and routing monitoring data from various sources
|
4
|
+
#
|
5
|
+
# DESCRIPTION
|
6
|
+
# This script creates a mirror of a source directory, manipulating
|
7
|
+
# path components based on rules defined in dirmangle.yaml. The
|
8
|
+
# purpose of this is to provide links to (or copies of) data using
|
9
|
+
# an alternative directory tree.
|
10
|
+
#
|
11
|
+
# -C, --config FILE
|
12
|
+
# The location of the configuration YAML to load
|
13
|
+
#
|
14
|
+
# -o, --option NAME VALUE
|
15
|
+
# Set the option NAME to VALUE
|
16
|
+
#
|
17
|
+
# -v, --verbosity LEVEL
|
18
|
+
# Set the log verbosity level [debug, info (default), warn, error, fatal]
|
19
|
+
#
|
20
|
+
# AUTHOR
|
21
|
+
# Gary Hetzel <garyhetzel@gmail.com>
|
22
|
+
#
|
23
|
+
require 'reacter'
|
24
|
+
require 'optparse'
|
25
|
+
|
26
|
+
class Reacter
|
27
|
+
class CLI
|
28
|
+
class<<self
|
29
|
+
def run()
|
30
|
+
@options = {}
|
31
|
+
|
32
|
+
(OptionParser.new do |opts|
|
33
|
+
opts.banner = "Usage: reacter [options]"
|
34
|
+
|
35
|
+
# -----------------------------------------------------------------------------
|
36
|
+
opts.on('-C', '--config FILE', 'Configuration YAML location') do |file|
|
37
|
+
if File.exists?(file)
|
38
|
+
@options[:configfile] = file
|
39
|
+
else
|
40
|
+
raise "Configuration file #{file} does not exist"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# -----------------------------------------------------------------------------
|
45
|
+
@options[:options] = {}
|
46
|
+
opts.on('-o', '--option NAME=VALUE', 'Set the option NAME to VALUE') do |pair|
|
47
|
+
name, value = pair.split('=', 2)
|
48
|
+
@options[:options].set(name.strip.chomp, value) unless name.empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
# -----------------------------------------------------------------------------
|
52
|
+
opts.on('-v', '--verbosity LEVEL', 'Logging verbosity') do |level|
|
53
|
+
level = level.to_s.downcase
|
54
|
+
|
55
|
+
if ['debug', 'info', 'warn', 'error', 'fatal'].include?(level)
|
56
|
+
@options[:loglevel] = level.to_sym
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end).parse!
|
60
|
+
|
61
|
+
# options parsed, pass them to a new Reacter
|
62
|
+
Reacter.start(@options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
Reacter::CLI.run()
|
data/lib/reacter/adapter.rb
CHANGED
@@ -14,8 +14,8 @@ class Reacter
|
|
14
14
|
attr :config
|
15
15
|
attr :type
|
16
16
|
|
17
|
-
def initialize()
|
18
|
-
@config = Reacter.get('global.adapter', {})
|
17
|
+
def initialize(config=nil)
|
18
|
+
@config = (config || Reacter.get('global.adapter', {}))
|
19
19
|
@type = @config.get('type')
|
20
20
|
Util.info("Loading adapter #{@type}...")
|
21
21
|
end
|
@@ -27,12 +27,12 @@ class Reacter
|
|
27
27
|
|
28
28
|
# implement: send a message suitable for consumption by an instance of reacter
|
29
29
|
# in listen mode
|
30
|
-
def send(message)
|
30
|
+
def send(message, format=nil)
|
31
31
|
false
|
32
32
|
end
|
33
33
|
|
34
34
|
# implement: poll for a new message and return it
|
35
|
-
def poll()
|
35
|
+
def poll(&block)
|
36
36
|
false
|
37
37
|
end
|
38
38
|
|
@@ -42,11 +42,11 @@ class Reacter
|
|
42
42
|
end
|
43
43
|
|
44
44
|
class<<self
|
45
|
-
def create(type)
|
45
|
+
def create(type, config=nil)
|
46
46
|
if type
|
47
47
|
begin
|
48
48
|
require "reacter/adapters/#{type}"
|
49
|
-
rv = (Reacter.const_get("#{type.capitalize}Adapter").new())
|
49
|
+
rv = (Reacter.const_get("#{type.capitalize}Adapter").new(config))
|
50
50
|
return rv
|
51
51
|
|
52
52
|
rescue LoadError
|
@@ -12,18 +12,33 @@ class Reacter
|
|
12
12
|
@_stdin = true
|
13
13
|
else
|
14
14
|
@_stdin = false
|
15
|
-
|
15
|
+
|
16
|
+
readfile = (@config.get('filename') || @config.get('file.read'))
|
17
|
+
writefile = @config.get('file.write')
|
18
|
+
|
19
|
+
@_input = File.open(File.expand_path(readfile), 'r+') if readfile
|
20
|
+
@_output = File.open(File.expand_path(writefile), 'a') if writefile
|
16
21
|
end
|
17
22
|
end
|
18
23
|
|
19
|
-
def send(message)
|
20
|
-
|
24
|
+
def send(message, format=nil)
|
25
|
+
if defined?(@_output)
|
26
|
+
message = Message.dump(message, format)
|
27
|
+
@_output.puts(message) if message
|
28
|
+
@_output.flush()
|
29
|
+
else
|
30
|
+
Util.warn("file: Attempting to send without a valid output file handle")
|
31
|
+
end
|
21
32
|
end
|
22
33
|
|
23
34
|
def poll(&block)
|
24
|
-
|
25
|
-
|
26
|
-
|
35
|
+
if @_input
|
36
|
+
loop do
|
37
|
+
line = @_input.gets
|
38
|
+
yield Message.parse(line)
|
39
|
+
end
|
40
|
+
else
|
41
|
+
Util.warn("file: Attempting to poll without a valid input file handle")
|
27
42
|
end
|
28
43
|
end
|
29
44
|
|
@@ -7,8 +7,56 @@ class Reacter
|
|
7
7
|
class DeciderAgent < Agent
|
8
8
|
class EmitAlert < Exception; end
|
9
9
|
|
10
|
+
# local memory persistence mechanism for state tracking
|
11
|
+
class MemoryPersist
|
12
|
+
class<<self
|
13
|
+
def setup()
|
14
|
+
@_alerts = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def init(source, metric)
|
18
|
+
# initialize in-memory alert tracking (per source, per metric)
|
19
|
+
@_alerts[source] ||= {}
|
20
|
+
@_alerts[source][metric] ||= ({
|
21
|
+
:count => 1,
|
22
|
+
:last_state => nil,
|
23
|
+
:last_seen => nil,
|
24
|
+
:new_alert => false,
|
25
|
+
:has_ever_failed => false
|
26
|
+
})
|
27
|
+
end
|
28
|
+
|
29
|
+
def get(source, metric, key)
|
30
|
+
@_alerts[source][metric][key] rescue nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def set(source, metric, key, value)
|
34
|
+
(@_alerts[source][metric][key] = value) rescue nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# redis persistence mechanism for shared state tracking
|
40
|
+
class RedisPersist
|
41
|
+
#require 'redis'
|
42
|
+
class<<self
|
43
|
+
def setup()
|
44
|
+
end
|
45
|
+
|
46
|
+
def init(source, metric)
|
47
|
+
end
|
48
|
+
|
49
|
+
def get(source, metric, key)
|
50
|
+
end
|
51
|
+
|
52
|
+
def set(source, metric, key, value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
10
57
|
STATES=[:okay, :warning, :critical]
|
11
58
|
COMPARISONS=%w{not is above below}
|
59
|
+
DEFAULT_PERSISTENCE='memory'
|
12
60
|
|
13
61
|
def initialize()
|
14
62
|
super
|
@@ -18,7 +66,8 @@ class Reacter
|
|
18
66
|
@_state_names[STATES[i]] = i
|
19
67
|
end
|
20
68
|
|
21
|
-
@
|
69
|
+
@_persistence = DeciderAgent.const_get(@config.get('options.persistence.type', DEFAULT_PERSISTENCE).capitalize+'Persist')
|
70
|
+
@_persistence.setup()
|
22
71
|
end
|
23
72
|
|
24
73
|
def received(message)
|
@@ -30,9 +79,12 @@ class Reacter
|
|
30
79
|
return false unless @config['sources']['any']
|
31
80
|
end
|
32
81
|
|
82
|
+
s = message.source
|
83
|
+
m = message.metric
|
84
|
+
|
33
85
|
# build effective configuration rule for this message
|
34
|
-
general = (@config['sources']['any'][
|
35
|
-
specific = (@config['sources'][
|
86
|
+
general = (@config['sources']['any'][m] or {} rescue {})
|
87
|
+
specific = (@config['sources'][s][m] or {} rescue {})
|
36
88
|
rule = specific.deep_merge!(general)
|
37
89
|
|
38
90
|
# quit early for empty rules
|
@@ -41,18 +93,17 @@ class Reacter
|
|
41
93
|
# quit early for invalid rules
|
42
94
|
return false unless (rule['threshold'] and rule['actions'])
|
43
95
|
|
96
|
+
# passthrough mode
|
97
|
+
# if in passthrough mode, all messages that are present in the configuration
|
98
|
+
# will be passed regardless of state. this is useful in conjunction with
|
99
|
+
# the relay agent to offload the decision making to another reacter instance
|
100
|
+
return message if (@config['options'] and @config['options']['passthrough'])
|
101
|
+
|
44
102
|
# default return value: false
|
45
103
|
rv = false
|
46
104
|
|
47
|
-
# initialize
|
48
|
-
|
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
|
-
})
|
105
|
+
# initialize persistence for this source/metric
|
106
|
+
persist_init(s,m)
|
56
107
|
|
57
108
|
# get current state of this message according to the rule
|
58
109
|
state, comparison = state_of(message, rule)
|
@@ -60,24 +111,24 @@ class Reacter
|
|
60
111
|
begin
|
61
112
|
# a non-okay alert has occurred, this metric can now be said to have failed
|
62
113
|
unless state == :okay
|
63
|
-
|
114
|
+
persist_set(s,m, :has_ever_failed, true)
|
64
115
|
end
|
65
116
|
|
66
117
|
# the state has changed, reset counter
|
67
|
-
if
|
68
|
-
|
69
|
-
|
118
|
+
if persist_get(s,m, :last_state) != state
|
119
|
+
persist_set(s,m, :new_alert, true)
|
120
|
+
persist_set(s,m, :count, 1)
|
70
121
|
end
|
71
122
|
|
72
123
|
hits = ((rule['threshold'][state.to_s][hits] rescue rule['hits']) or rule['hits'] or 1).to_i
|
73
124
|
persist = ((rule['threshold'][state.to_s]['persist'] === true) rescue false)
|
74
125
|
|
75
126
|
# only raise every n alerts
|
76
|
-
if
|
127
|
+
if persist_get(s,m, :count) >= hits
|
77
128
|
# raise the alert if it's new or if we're persistently reminding of alerts
|
78
|
-
if
|
129
|
+
if persist_get(s,m, :new_alert) or persist
|
79
130
|
# only emit if this metric has ever failed
|
80
|
-
if
|
131
|
+
if persist_get(s,m, :has_ever_failed)
|
81
132
|
raise EmitAlert
|
82
133
|
end
|
83
134
|
end
|
@@ -93,12 +144,12 @@ class Reacter
|
|
93
144
|
rv = message
|
94
145
|
|
95
146
|
# alert is emitted, it's not new anymore
|
96
|
-
|
147
|
+
persist_set(s,m, :new_alert, false)
|
97
148
|
|
98
149
|
ensure
|
99
|
-
|
100
|
-
|
101
|
-
|
150
|
+
persist_set(s,m, :count, persist_get(s,m, :count) + 1)
|
151
|
+
persist_set(s,m, :last_state, state)
|
152
|
+
persist_set(s,m, :last_seen, Time.now.to_i)
|
102
153
|
end
|
103
154
|
|
104
155
|
return rv
|
@@ -143,6 +194,18 @@ class Reacter
|
|
143
194
|
return false
|
144
195
|
end
|
145
196
|
end
|
197
|
+
|
198
|
+
def persist_init(source, metric)
|
199
|
+
@_persistence.init(source, metric)
|
200
|
+
end
|
201
|
+
|
202
|
+
def persist_get(source, metric, key)
|
203
|
+
@_persistence.get(source, metric, key)
|
204
|
+
end
|
205
|
+
|
206
|
+
def persist_set(source, metric, key, value)
|
207
|
+
@_persistence.set(source, metric, key, value)
|
208
|
+
end
|
146
209
|
end
|
147
210
|
end
|
148
211
|
|
@@ -6,7 +6,8 @@ require 'reacter/agent'
|
|
6
6
|
class Reacter
|
7
7
|
class LoggerAgent < Agent
|
8
8
|
def received(message)
|
9
|
-
Util.info("
|
9
|
+
Util.info("logger: [#{message.state or :unknown}] #{message.source}/#{message.metric}")
|
10
|
+
message
|
10
11
|
end
|
11
12
|
end
|
12
13
|
end
|
data/lib/reacter/core.rb
CHANGED
@@ -16,7 +16,7 @@ class Reacter
|
|
16
16
|
@_adapter = nil
|
17
17
|
@_agents = []
|
18
18
|
|
19
|
-
Reacter.load_config()
|
19
|
+
Reacter.load_config(args.first || {})
|
20
20
|
|
21
21
|
load_adapters()
|
22
22
|
load_agents()
|
@@ -92,9 +92,9 @@ class Reacter
|
|
92
92
|
end
|
93
93
|
|
94
94
|
class<<self
|
95
|
-
def start()
|
95
|
+
def start(config={})
|
96
96
|
EM.run do
|
97
|
-
reacter = EM.connect('127.0.0.1', 9000, Reacter::Core)
|
97
|
+
reacter = EM.connect('127.0.0.1', 9000, Reacter::Core, config[:options])
|
98
98
|
reacter.run()
|
99
99
|
end
|
100
100
|
end
|
data/lib/reacter/message.rb
CHANGED
@@ -5,6 +5,7 @@ class Reacter
|
|
5
5
|
class Message
|
6
6
|
require 'hashlib'
|
7
7
|
DEFAULT_SOURCE_NAME='default'
|
8
|
+
DEFAULT_FORMAT=:json
|
8
9
|
|
9
10
|
def initialize(message={})
|
10
11
|
@_data = {
|
@@ -52,6 +53,20 @@ class Reacter
|
|
52
53
|
end
|
53
54
|
end
|
54
55
|
|
56
|
+
def dump(message, format=nil)
|
57
|
+
load_parsers() unless defined?(@@_parsers)
|
58
|
+
|
59
|
+
if @@_parsers
|
60
|
+
format = DEFAULT_FORMAT unless format
|
61
|
+
|
62
|
+
if @@_parsers.has_key?(format.to_sym)
|
63
|
+
return @@_parsers[format.to_sym].dump(message)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
return nil
|
68
|
+
end
|
69
|
+
|
55
70
|
def parse(body)
|
56
71
|
load_parsers()
|
57
72
|
|
@@ -22,6 +22,22 @@ class Reacter
|
|
22
22
|
:attributes => attributes
|
23
23
|
}
|
24
24
|
end
|
25
|
+
|
26
|
+
def dump(message)
|
27
|
+
message = message.to_h if message.is_a?(Message)
|
28
|
+
return nil unless message.is_a?(Hash)
|
29
|
+
return nil unless message[:source]
|
30
|
+
return nil unless message[:metric]
|
31
|
+
return nil unless message[:value]
|
32
|
+
return nil unless message[:time]
|
33
|
+
|
34
|
+
([
|
35
|
+
'PUTVAL',
|
36
|
+
"#{message[:source]}/#{message[:metric]}",
|
37
|
+
(message[:attributes] || {}).collect{|k,v| "#{k}=#{v}"}.join(';'),
|
38
|
+
"#{(message[:time] / 1000).to_i}:#{message[:value]}"
|
39
|
+
].join(' ')).strip
|
40
|
+
end
|
25
41
|
end
|
26
42
|
end
|
27
43
|
end
|
@@ -20,6 +20,17 @@ class Reacter
|
|
20
20
|
:time => (message[2].to_i * 1000)
|
21
21
|
}
|
22
22
|
end
|
23
|
+
|
24
|
+
def dump(message)
|
25
|
+
message = message.to_h if message.is_a?(Message)
|
26
|
+
return nil unless message.is_a?(Hash)
|
27
|
+
#return nil unless message[:source]
|
28
|
+
return nil unless message[:metric]
|
29
|
+
return nil unless message[:value]
|
30
|
+
return nil unless message[:time]
|
31
|
+
|
32
|
+
"#{message[:metric]} #{message[:value]} #{(message[:time] / 1000).to_i}".strip
|
33
|
+
end
|
23
34
|
end
|
24
35
|
end
|
25
36
|
end
|
data/lib/reacter/parsers/json.rb
CHANGED
data/lib/reacter/parsers/tsdb.rb
CHANGED
@@ -18,6 +18,27 @@ class Reacter
|
|
18
18
|
:attributes => Hash[message[4..-1].collect{|i| i.split('=',2) }]
|
19
19
|
}
|
20
20
|
end
|
21
|
+
|
22
|
+
def dump(message)
|
23
|
+
message = message.to_h if message.is_a?(Message)
|
24
|
+
return nil unless message.is_a?(Hash)
|
25
|
+
return nil unless message[:source]
|
26
|
+
return nil unless message[:metric]
|
27
|
+
return nil unless message[:value]
|
28
|
+
return nil unless message[:time]
|
29
|
+
|
30
|
+
attributes = {
|
31
|
+
:host => message[:source]
|
32
|
+
}.merge(message[:attributes] || {})
|
33
|
+
|
34
|
+
([
|
35
|
+
'PUT',
|
36
|
+
message[:metric],
|
37
|
+
(message[:time] / 1000).to_i,
|
38
|
+
message[:value],
|
39
|
+
attributes.collect{|k,v| "#{k}=#{v}" }.join(' ')
|
40
|
+
].join(' ')).strip
|
41
|
+
end
|
21
42
|
end
|
22
43
|
end
|
23
44
|
end
|
data/lib/reacter/util.rb
CHANGED
@@ -4,11 +4,16 @@ class Reacter
|
|
4
4
|
class Util
|
5
5
|
class<<self
|
6
6
|
require 'logger'
|
7
|
+
require 'socket'
|
7
8
|
|
8
9
|
@@_logger = {
|
9
10
|
:default => Logger.new(STDOUT)
|
10
11
|
}
|
11
12
|
|
13
|
+
def signature(custom=nil)
|
14
|
+
[%x{hostname -f}, Process.pid, custom].compact.join(':')
|
15
|
+
end
|
16
|
+
|
12
17
|
def log(message, severity=:info, log=:default)
|
13
18
|
@@_logger[log] = Logger.new(STDOUT) unless @@_logger[log]
|
14
19
|
@@_logger[log].send(severity, [*message].join(' '))
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reacter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gary Hetzel
|
@@ -55,7 +55,8 @@ dependencies:
|
|
55
55
|
description: A utility for consuming, transforming, and routing monitoring data from
|
56
56
|
various sources
|
57
57
|
email: ghetzel@outbrain.com
|
58
|
-
executables:
|
58
|
+
executables:
|
59
|
+
- reacter
|
59
60
|
extensions: []
|
60
61
|
extra_rdoc_files: []
|
61
62
|
files:
|
@@ -75,6 +76,7 @@ files:
|
|
75
76
|
- lib/reacter/parsers/json.rb
|
76
77
|
- lib/reacter/parsers/tsdb.rb
|
77
78
|
- lib/reacter/util.rb
|
79
|
+
- bin/reacter
|
78
80
|
homepage: http://outbrain.github.com/reacter/
|
79
81
|
licenses: []
|
80
82
|
metadata: {}
|