rubix 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.
- data/LICENSE +20 -0
- data/README.rdoc +262 -0
- data/VERSION +1 -0
- data/bin/zabbix_api +60 -0
- data/bin/zabbix_pipe +77 -0
- data/lib/rubix.rb +42 -0
- data/lib/rubix/connection.rb +111 -0
- data/lib/rubix/examples/es_monitor.rb +130 -0
- data/lib/rubix/examples/hbase_monitor.rb +87 -0
- data/lib/rubix/examples/mongo_monitor.rb +125 -0
- data/lib/rubix/log.rb +70 -0
- data/lib/rubix/model.rb +56 -0
- data/lib/rubix/models/application.rb +76 -0
- data/lib/rubix/models/host.rb +127 -0
- data/lib/rubix/models/host_group.rb +74 -0
- data/lib/rubix/models/item.rb +122 -0
- data/lib/rubix/models/template.rb +81 -0
- data/lib/rubix/monitor.rb +167 -0
- data/lib/rubix/monitors/chef_monitor.rb +82 -0
- data/lib/rubix/monitors/cluster_monitor.rb +84 -0
- data/lib/rubix/response.rb +124 -0
- data/lib/rubix/sender.rb +301 -0
- data/spec/rubix/connection_spec.rb +43 -0
- data/spec/rubix/models/host_group_spec.rb +56 -0
- data/spec/rubix/monitor_spec.rb +81 -0
- data/spec/rubix/monitors/chef_monitor_spec.rb +11 -0
- data/spec/rubix/monitors/cluster_monitor_spec.rb +11 -0
- data/spec/rubix/response_spec.rb +35 -0
- data/spec/rubix/sender_spec.rb +9 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/response_helper.rb +17 -0
- metadata +140 -0
data/lib/rubix/sender.rb
ADDED
@@ -0,0 +1,301 @@
|
|
1
|
+
require 'rubix/log'
|
2
|
+
|
3
|
+
module Rubix
|
4
|
+
|
5
|
+
class Sender
|
6
|
+
|
7
|
+
include Logs
|
8
|
+
|
9
|
+
# A Hash of options.
|
10
|
+
attr_accessor :settings
|
11
|
+
|
12
|
+
# The host the Sender will send data for.
|
13
|
+
attr_accessor :host
|
14
|
+
|
15
|
+
# The hostgroups used to create this host.
|
16
|
+
attr_accessor :host_groups
|
17
|
+
|
18
|
+
# The templates used to create this host.
|
19
|
+
attr_accessor :templates
|
20
|
+
|
21
|
+
# The applications used to create items.
|
22
|
+
attr_accessor :applications
|
23
|
+
|
24
|
+
#
|
25
|
+
# Initialization
|
26
|
+
#
|
27
|
+
|
28
|
+
def initialize settings
|
29
|
+
@settings = settings
|
30
|
+
confirm_settings
|
31
|
+
self.host = Host.new(:name => settings['host'])
|
32
|
+
@log_name = "PIPE #{host.name}"
|
33
|
+
if settings['fast']
|
34
|
+
info("Forwarding...") if settings['verbose']
|
35
|
+
else
|
36
|
+
initialize_hostgroups
|
37
|
+
initialize_templates
|
38
|
+
initialize_host
|
39
|
+
initialize_applications
|
40
|
+
info("Forwarding...") if settings['verbose'] && host.exists?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def alive?
|
45
|
+
settings['fast'] || host.exists?
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize_hostgroups
|
49
|
+
self.host_groups = settings['host_groups'].split(',').flatten.compact.map { |group_name | HostGroup.find_or_create_by_name(group_name.strip) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize_templates
|
53
|
+
self.templates = (settings['templates'] || '').split(',').flatten.compact.map { |template_name | Template.find_or_create_by_name(template_name.strip) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize_host
|
57
|
+
unless host.exists?
|
58
|
+
host.host_groups = host_groups
|
59
|
+
host.templates = templates
|
60
|
+
host.create
|
61
|
+
end
|
62
|
+
# if settings['verbose']
|
63
|
+
# puts "Forwarding data for Host '#{settings['host']}' (#{host_id}) from #{settings['pipe']} to #{settings['server']}"
|
64
|
+
# puts "Creating Items in Application '#{settings['application']}' (#{application_id}) at #{settings['api_server']} as #{settings['username']}"
|
65
|
+
# end
|
66
|
+
end
|
67
|
+
|
68
|
+
def initialize_applications
|
69
|
+
self.applications = (settings['applications'] || '').split(',').flatten.compact.map { |app_name| Application.find_or_create_by_name_and_host(app_name, host) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def confirm_settings
|
73
|
+
raise ConnectionError.new("Must specify a Zabbix server to send data to.") unless settings['server']
|
74
|
+
raise Error.new("Must specify the path to a local configuraiton file") unless settings['configuration_file'] && File.file?(settings['configuration_file'])
|
75
|
+
raise ConnectionError.new("Must specify the name of a host to send data for.") unless settings['host']
|
76
|
+
raise ValidationError.new("Must define at least one host group.") if settings['host_groups'].nil? || settings['host_groups'].empty?
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# Actions
|
81
|
+
#
|
82
|
+
|
83
|
+
def run
|
84
|
+
return unless alive?
|
85
|
+
case
|
86
|
+
when settings['pipe']
|
87
|
+
process_pipe
|
88
|
+
when settings.rest.size > 0
|
89
|
+
settings.rest.each do |path|
|
90
|
+
process_file(path)
|
91
|
+
end
|
92
|
+
else
|
93
|
+
process_stdin
|
94
|
+
end
|
95
|
+
exit(0)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Process each line of the file at +path+.
|
99
|
+
def process_file path
|
100
|
+
f = File.new(path)
|
101
|
+
process_file_handle(f)
|
102
|
+
f.close
|
103
|
+
end
|
104
|
+
|
105
|
+
# Process each line of standard input.
|
106
|
+
def process_stdin
|
107
|
+
process_file_handle($stdin)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Process each line read from the pipe.
|
111
|
+
def process_pipe
|
112
|
+
# We want to open this pipe in non-blocking read mode b/c
|
113
|
+
# otherwise this process becomes hard to kill.
|
114
|
+
f = File.new(settings['pipe'], (File::RDONLY | File::NONBLOCK))
|
115
|
+
while true
|
116
|
+
process_file_handle(f)
|
117
|
+
# In non-blocking mode, an EOFError from f.readline doesn't mean
|
118
|
+
# there's no more data to read, just that there's no more data
|
119
|
+
# right *now*. If we sleep for a bit there might be more data
|
120
|
+
# coming down the pipe.
|
121
|
+
sleep settings['pipe_read_sleep']
|
122
|
+
end
|
123
|
+
f.close
|
124
|
+
end
|
125
|
+
|
126
|
+
# Process each line of a given file handle +f+.
|
127
|
+
def process_file_handle f
|
128
|
+
begin
|
129
|
+
line = f.readline
|
130
|
+
rescue EOFError
|
131
|
+
line = nil
|
132
|
+
end
|
133
|
+
while line
|
134
|
+
process_line(line)
|
135
|
+
begin
|
136
|
+
# FIXME -- this call to File#readline blocks and doesn't let
|
137
|
+
# stuff like SIGINT (generated from Ctrl-C on a keyboard,
|
138
|
+
# say) take affect.
|
139
|
+
line = f.readline
|
140
|
+
rescue EOFError
|
141
|
+
line = nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def process_line line
|
147
|
+
if looks_like_json?(line)
|
148
|
+
process_line_of_json_in_new_pipe(line)
|
149
|
+
else
|
150
|
+
process_line_of_tsv_in_this_pipe(line)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Parse and send a single +line+ of TSV input to the Zabbix server.
|
155
|
+
# The line will be split at tabs and expects either
|
156
|
+
#
|
157
|
+
# a) two columns: an item key and a value
|
158
|
+
# b) three columns: an item key, a value, and a timestamp
|
159
|
+
#
|
160
|
+
# Unexpected input will cause an error to be logged.
|
161
|
+
def process_line_of_tsv_in_this_pipe line
|
162
|
+
parts = line.strip.split("\t")
|
163
|
+
case parts.size
|
164
|
+
when 2
|
165
|
+
timestamp = Time.now
|
166
|
+
key, value = parts
|
167
|
+
when 3
|
168
|
+
key, value = parts[0..1]
|
169
|
+
timestamp = Time.parse(parts.last)
|
170
|
+
else
|
171
|
+
error("Each line of input must be a tab separated row consisting of 2 columns (key, value) or 3 columns (timestamp, key, value)")
|
172
|
+
return
|
173
|
+
end
|
174
|
+
send(key, value, timestamp)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Parse and send a single +line+ of JSON input to the Zabbix server.
|
178
|
+
# The JSON must have a key +data+ in order to be processed. The
|
179
|
+
# value of 'data' should be an Array of Hashes each with a +key+ and
|
180
|
+
# +value+.
|
181
|
+
#
|
182
|
+
# This ZabbixPipe's settings will be merged with the remainder of
|
183
|
+
# the JSON hash. This allows sending values for 'host2' to an
|
184
|
+
# instance of ZabbixPipe already set up to receive for 'host1'.
|
185
|
+
#
|
186
|
+
# This is useful for sending data for keys from multiple hosts
|
187
|
+
#
|
188
|
+
# Example of expected input:
|
189
|
+
#
|
190
|
+
# {
|
191
|
+
# 'data': [
|
192
|
+
# {'key': 'foo.bar.baz', 'value': 10},
|
193
|
+
# {'key': 'snap.crackle.pop', 'value': 8 }
|
194
|
+
# ]
|
195
|
+
# }
|
196
|
+
#
|
197
|
+
# Or when sending for another host:
|
198
|
+
#
|
199
|
+
# {
|
200
|
+
# 'hostname': 'shazaam',
|
201
|
+
# 'application': 'silly',
|
202
|
+
# 'data': [
|
203
|
+
# {'key': 'foo.bar.baz', 'value': 10},
|
204
|
+
# {'key': 'snap.crackle.pop', 'value': 8 }
|
205
|
+
# ]
|
206
|
+
# }
|
207
|
+
def process_line_of_json_in_new_pipe line
|
208
|
+
begin
|
209
|
+
json = JSON.parse(line)
|
210
|
+
rescue JSON::ParserError => e
|
211
|
+
error("Malformed JSON")
|
212
|
+
return
|
213
|
+
end
|
214
|
+
|
215
|
+
data = json.delete('data')
|
216
|
+
unless data && data.is_a?(Array)
|
217
|
+
error("A line of JSON input must a have an Array key 'data'")
|
218
|
+
return
|
219
|
+
end
|
220
|
+
|
221
|
+
if json.empty?
|
222
|
+
# If there are no other settings then the daughter will be the
|
223
|
+
# same as the parent -- so just use 'self'.
|
224
|
+
daughter_pipe = self
|
225
|
+
else
|
226
|
+
# We merge the settings from 'self' with whatever else is
|
227
|
+
# present in the line.
|
228
|
+
begin
|
229
|
+
daughter_pipe = self.class.new(settings.stringify_keys.merge(json))
|
230
|
+
return unless daughter_pipe.alive?
|
231
|
+
rescue Error => e
|
232
|
+
error(e.message)
|
233
|
+
return
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
data.each do |point|
|
238
|
+
key = point['key']
|
239
|
+
value = point['value']
|
240
|
+
unless key && value
|
241
|
+
warn("The elements of the 'data' Array must be Hashes with a 'key' and a 'value'")
|
242
|
+
next
|
243
|
+
end
|
244
|
+
|
245
|
+
tsv_line = [key, value].map(&:to_s).join("\t")
|
246
|
+
daughter_pipe.process_line(tsv_line)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Does the line look like it might be JSON?
|
251
|
+
def looks_like_json? line
|
252
|
+
line =~ /^\s*\{/
|
253
|
+
end
|
254
|
+
|
255
|
+
# Send the +value+ for +key+ at the given +timestamp+ to the Zabbix
|
256
|
+
# server.
|
257
|
+
#
|
258
|
+
# If the +key+ doesn't exist for this local agent's host, it will be
|
259
|
+
# added.
|
260
|
+
def send key, value, timestamp
|
261
|
+
item = Item.new(:key => key, :host => host, :applications => applications, :value_type => Item.value_type_from_value(value))
|
262
|
+
unless settings['fast'] || item.exists?
|
263
|
+
return unless item.create
|
264
|
+
# There is a time lag of about 15-30 seconds between (successfully)
|
265
|
+
# creating an item on the Zabbix server and having the Zabbix accept
|
266
|
+
# new data for that item.
|
267
|
+
#
|
268
|
+
# If it is crucial that *every single* data point be written, dial
|
269
|
+
# up this sleep period. The first data point for a new key will put
|
270
|
+
# the wrapper to sleep for this period of time, in hopes that the
|
271
|
+
# Zabbix server will catch up and be ready to accept new data
|
272
|
+
# points.
|
273
|
+
#
|
274
|
+
# If you don't care that you're going to lose the first few data
|
275
|
+
# points you send to Zabbix, then don't worry about it.
|
276
|
+
sleep settings['create_item_sleep']
|
277
|
+
end
|
278
|
+
command = "#{settings['sender']} --config #{settings['configuration_file']} --zabbix-server #{settings['server']} --host #{settings['host']} --key #{key} --value '#{value}'"
|
279
|
+
process_zabbix_sender_output(key, `#{command}`)
|
280
|
+
|
281
|
+
# command = "zabbix_sender --config #{configuration_file} --zabbix-server #{server} --input-file - --with-timestamps"
|
282
|
+
# open(command, 'w') do |zabbix_sender|
|
283
|
+
# zabbix_sender.write([settings['host'], key, timestamp.to_i, value].map(&:to_s).join("\t"))
|
284
|
+
# zabbix_sender.close_write
|
285
|
+
# process_zabbix_sender_output(zabbix_sender.read)
|
286
|
+
# end
|
287
|
+
end
|
288
|
+
|
289
|
+
# Parse the +text+ output by +zabbix_sender+.
|
290
|
+
def process_zabbix_sender_output key, text
|
291
|
+
return unless settings['verbose']
|
292
|
+
lines = text.strip.split("\n")
|
293
|
+
return if lines.size < 1
|
294
|
+
status_line = lines.first
|
295
|
+
status_line =~ /Processed +(\d+) +Failed +(\d+) +Total +(\d+)/
|
296
|
+
processed, failed, total = $1, $2, $3
|
297
|
+
warn("Failed to write #{failed} values to key '#{key}'") if failed.to_i != 0
|
298
|
+
end
|
299
|
+
|
300
|
+
end
|
301
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Rubix::Connection do
|
4
|
+
|
5
|
+
before do
|
6
|
+
|
7
|
+
@mock_server = mock("Net::HTTP instance")
|
8
|
+
Net::HTTP.stub!(:new).and_return(@mock_server)
|
9
|
+
|
10
|
+
@mock_response = mock("Net::HTTP::Response instance")
|
11
|
+
@mock_server.stub!(:request).and_return(@mock_response)
|
12
|
+
@mock_response.stub!(:code).and_return('200')
|
13
|
+
|
14
|
+
|
15
|
+
@good_auth_response = '{"result": "auth_token"}'
|
16
|
+
@blah_response = '{"result": "bar"}'
|
17
|
+
|
18
|
+
@mock_response.stub!(:body).and_return(@blah_response)
|
19
|
+
|
20
|
+
@connection = Rubix::Connection.new('localhost/api.php', 'username', 'password')
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should attempt to authorize itself without being asked" do
|
24
|
+
@connection.should_receive(:authorize!)
|
25
|
+
@connection.request('foobar', {})
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should not repeatedly authorize itself" do
|
29
|
+
@mock_response.stub!(:body).and_return(@good_auth_response, @blah_response, @blah_response)
|
30
|
+
@connection.request('foobar', {})
|
31
|
+
@connection.should_not_receive(:authorize!)
|
32
|
+
@connection.request('foobar', {})
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should increment its request ID" do
|
36
|
+
@mock_response.stub!(:body).and_return(@good_auth_response, @blah_response, @blah_response)
|
37
|
+
@connection.request('foobar', {})
|
38
|
+
@connection.request('foobar', {})
|
39
|
+
@connection.request_id.should == 3 # it's the number used for the *next* request
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Rubix::HostGroup do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@name = 'foobar'
|
7
|
+
@id = 100
|
8
|
+
@group = Rubix::HostGroup.new(:name => @name)
|
9
|
+
|
10
|
+
@successful_get_response = mock_response([{'groupid' => @id, 'name' => @name, 'hosts' => [{'hostid' => 1}, {'hostid' => 2}]}])
|
11
|
+
@successful_create_response = mock_response({'groupids' => [@id]})
|
12
|
+
@empty_response = mock_response
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'loading' do
|
16
|
+
it "should retrieve properties for an existing host group" do
|
17
|
+
@group.should_receive(:request).with('hostgroup.get', kind_of(Hash)).and_return(@successful_get_response)
|
18
|
+
@group.exists?.should be_true
|
19
|
+
@group.name.should == @name
|
20
|
+
@group.id.should == @id
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should recognize a host group does not exist" do
|
24
|
+
@group.should_receive(:request).with('hostgroup.get', kind_of(Hash)).and_return(@empty_response)
|
25
|
+
@group.exists?.should be_false
|
26
|
+
@group.name.should == @name
|
27
|
+
@group.id.should be_nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'creating' do
|
32
|
+
|
33
|
+
it "can successfully create a new host group" do
|
34
|
+
@group.should_receive(:request).with('hostgroup.create', kind_of(Array)).and_return(@successful_create_response)
|
35
|
+
@group.create
|
36
|
+
@group.exists?.should be_true
|
37
|
+
@group.id.should == @id
|
38
|
+
end
|
39
|
+
|
40
|
+
it "can handle an error" do
|
41
|
+
@group.should_receive(:request).with('hostgroup.get', kind_of(Hash)).and_return(@empty_response)
|
42
|
+
@group.should_receive(:request).with('hostgroup.create', kind_of(Array)).and_return(@empty_response)
|
43
|
+
@group.create
|
44
|
+
@group.exists?.should be_false
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'updating' do
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
describe 'destroying' do
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
describe Rubix::Monitor do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@measurement = '{"data":[{"value":"bar","key":"foo"}]}'
|
8
|
+
|
9
|
+
@wrapper = Class.new(Rubix::Monitor)
|
10
|
+
@wrapper.class_eval do
|
11
|
+
def measure
|
12
|
+
write do |data|
|
13
|
+
data << ['foo', 'bar']
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe 'writing to STDOUT' do
|
20
|
+
|
21
|
+
it "should be the default behavior when run with no arguments" do
|
22
|
+
::ARGV.replace([])
|
23
|
+
$stdout.should_receive(:puts).with(@measurement)
|
24
|
+
@wrapper.run
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should flush after each write" do
|
28
|
+
::ARGV.replace([])
|
29
|
+
$stdout.stub!(:puts)
|
30
|
+
$stdout.should_receive(:flush).twice()
|
31
|
+
@wrapper.run
|
32
|
+
@wrapper.run
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'writing to files' do
|
37
|
+
|
38
|
+
before do
|
39
|
+
@file = Tempfile.new('monitor', '/tmp')
|
40
|
+
::ARGV.replace([@file.path])
|
41
|
+
end
|
42
|
+
|
43
|
+
after do
|
44
|
+
FileUtils.rm(@file.path) if File.exist?(@file.path)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should create a new file if called with a path that doesn't exist" do
|
48
|
+
FileUtils.rm(@file.path) if File.exist?(@file.path)
|
49
|
+
@wrapper.run
|
50
|
+
File.read(@file.path).should include(@measurement)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should append to an existing file" do
|
54
|
+
File.open(@file.path, 'w') { |f| f.puts('old content') }
|
55
|
+
@wrapper.run
|
56
|
+
File.read(@file.path).should include(@measurement)
|
57
|
+
File.read(@file.path).should include('old content')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe 'writing to FIFOs' do
|
62
|
+
|
63
|
+
before do
|
64
|
+
@file = Tempfile.new('monitor', '/tmp')
|
65
|
+
FileUtils.rm(@file.path) if File.exist?(@file.path)
|
66
|
+
`mkfifo #{@file.path}`
|
67
|
+
::ARGV.replace([@file.path])
|
68
|
+
end
|
69
|
+
|
70
|
+
after do
|
71
|
+
FileUtils.rm(@file.path) if File.exist?(@file.path)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should not block or error when writing to a FIFO with no listener" do
|
75
|
+
@wrapper.run
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|