rubix 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/bin/zabbix_pipe +1 -1
- data/lib/rubix.rb +1 -0
- data/lib/rubix/auto_sender.rb +433 -0
- data/lib/rubix/monitors/monitor.rb +2 -14
- data/lib/rubix/sender.rb +74 -375
- data/spec/rubix/auto_sender_spec.rb +18 -0
- data/spec/rubix/monitors/monitor_spec.rb +14 -0
- data/spec/rubix/sender_spec.rb +8 -10
- metadata +5 -3
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/bin/zabbix_pipe
CHANGED
data/lib/rubix.rb
CHANGED
@@ -0,0 +1,433 @@
|
|
1
|
+
require 'rubix/log'
|
2
|
+
|
3
|
+
module Rubix
|
4
|
+
|
5
|
+
# A class used to send data to Zabbix.
|
6
|
+
#
|
7
|
+
# This sender is used to implement the logic for the +zabbix_pipe+
|
8
|
+
# utility. It is initialized with some metadata about a host, its
|
9
|
+
# host groups and templates, and applications into which items
|
10
|
+
# should be written, and it can then accept data and forward it to a
|
11
|
+
# Zabbix server using the +zabbix_sender+ utility that comes with
|
12
|
+
# Zabbix.
|
13
|
+
#
|
14
|
+
# A sender can be given data in either TSV or JSON formats. With
|
15
|
+
# the JSON format, it is possible to embed data for hosts, host
|
16
|
+
# groups, &c. distinct from that with which this sender was
|
17
|
+
# initialized. This is a useful way to send many different kinds of
|
18
|
+
# data through the same process.
|
19
|
+
#
|
20
|
+
# The sender will also auto-vivify any hosts, host gruops,
|
21
|
+
# templates, applications, and items it needs in order to be able to
|
22
|
+
# write data. This is expensive in terms of time so it can be
|
23
|
+
# turned off using the <tt>--fast</tt> option.
|
24
|
+
class AutoSender
|
25
|
+
|
26
|
+
include Logs
|
27
|
+
|
28
|
+
# @return [Hash] settings
|
29
|
+
attr_accessor :settings
|
30
|
+
|
31
|
+
# @return [Rubix::Host] the host the Sender will send data for
|
32
|
+
attr_accessor :host
|
33
|
+
|
34
|
+
# @return [Array<Rubix::HostGroup>] the hostgroups used to create this host
|
35
|
+
attr_accessor :host_groups
|
36
|
+
|
37
|
+
# @return [Array<Rubix::Template>] the templates used to create this host
|
38
|
+
attr_accessor :templates
|
39
|
+
|
40
|
+
# @return [Array<Rubix::Application>] The applications used to create items
|
41
|
+
attr_accessor :applications
|
42
|
+
|
43
|
+
#
|
44
|
+
# == Initialization ==
|
45
|
+
#
|
46
|
+
|
47
|
+
# Create a new sender with the given +settings+.
|
48
|
+
#
|
49
|
+
# @param [Hash, Configliere::Param] settings
|
50
|
+
# @param settings [String] host the name of the Zabbix host to write data for
|
51
|
+
# @param settings [String] host_groups comma-separated names of Zabbix host groups the host should belong to
|
52
|
+
# @param settings [String] templates comma-separated names of Zabbix templates the host should belong to
|
53
|
+
# @param settings [String] applications comma-separated names of applications created items should be scoped under
|
54
|
+
# @param settings [String] server URL for the Zabbix server -- *not* the URL for the Zabbix API
|
55
|
+
# @param settings [String] configuration_file path to a local Zabbix configuration file as used by the +zabbix_sender+ utility
|
56
|
+
# @param settings [true, false] verbose be verbose during execution
|
57
|
+
# @param settings [true, false] fast auto-vivify (slow) or not (fast)
|
58
|
+
# @param settings [String] pipe path to a named pipe to be read from
|
59
|
+
# @param settings [Fixnum] pipe_read_sleep seconds to sleep after an empty read from the a named pipe
|
60
|
+
# @param settings [Fixnum] create_item_sleep seconds to sleep after creating a new item
|
61
|
+
def initialize settings
|
62
|
+
@settings = settings
|
63
|
+
confirm_settings
|
64
|
+
if fast?
|
65
|
+
info("Forwarding for #{settings['host']}...") if settings['verbose']
|
66
|
+
else
|
67
|
+
initialize_host_groups
|
68
|
+
initialize_templates
|
69
|
+
initialize_host
|
70
|
+
initialize_applications
|
71
|
+
info("Forwarding for #{host.name}...") if settings['verbose']
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Is this sender running in 'fast' mode? If so, it will *not*
|
76
|
+
# auto-vivify any hosts, groups, items, &c.
|
77
|
+
#
|
78
|
+
# @return [true, false]
|
79
|
+
def fast?
|
80
|
+
settings['fast']
|
81
|
+
end
|
82
|
+
|
83
|
+
# Will this sender auto-vivify hosts, groups, items, &c.?
|
84
|
+
#
|
85
|
+
# @return [true, false]
|
86
|
+
def auto_vivify?
|
87
|
+
!fast?
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
# Find or create necessary host groups.
|
93
|
+
#
|
94
|
+
# @return [Array<Rubix::HostGroup>]
|
95
|
+
def initialize_host_groups
|
96
|
+
self.host_groups = settings['host_groups'].split(',').flatten.compact.map(&:strip).uniq.map { |group_name | HostGroup.find_or_create(:name => group_name.strip) }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Find necessary templates.
|
100
|
+
#
|
101
|
+
# @return [Array<Rubix::Template>]
|
102
|
+
def initialize_templates
|
103
|
+
self.templates = (settings['templates'] || '').split(',').flatten.compact.map(&:strip).uniq.map { |template_name | Template.find(:name => template_name.strip) }.compact
|
104
|
+
end
|
105
|
+
|
106
|
+
# Find or create the host for this data. Host groups and
|
107
|
+
# templates will automatically be attached.
|
108
|
+
#
|
109
|
+
# @return [Rubix::Host]
|
110
|
+
def initialize_host
|
111
|
+
self.host = (Host.find(:name => settings['host']) || Host.new(:name => settings['host']))
|
112
|
+
|
113
|
+
current_host_group_names = (host.host_groups || []).map(&:name)
|
114
|
+
current_template_names = (host.templates || []).map(&:name)
|
115
|
+
|
116
|
+
host_groups_to_add, templates_to_add = [], []
|
117
|
+
|
118
|
+
(self.host_groups || []).each do |hg|
|
119
|
+
host_groups_to_add << hg unless current_host_group_names.include?(hg.name)
|
120
|
+
end
|
121
|
+
|
122
|
+
(self.templates || []).each do |t|
|
123
|
+
templates_to_add << t unless current_template_names.include?(t.name)
|
124
|
+
end
|
125
|
+
|
126
|
+
host.host_groups = ((host.host_groups || []) + host_groups_to_add).flatten.compact.uniq
|
127
|
+
host.templates = ((host.templates || []) + templates_to_add).flatten.compact.uniq
|
128
|
+
host.save
|
129
|
+
host
|
130
|
+
end
|
131
|
+
|
132
|
+
# Find or create the applications for this data.
|
133
|
+
#
|
134
|
+
# @return [Array<Rubix::Application>]
|
135
|
+
def initialize_applications
|
136
|
+
application_names = (settings['applications'] || '').split(',').flatten.compact.map(&:strip).uniq
|
137
|
+
self.applications = []
|
138
|
+
application_names.each do |app_name|
|
139
|
+
app = Application.find(:name => app_name, :host_id => host.id)
|
140
|
+
if app
|
141
|
+
self.applications << app
|
142
|
+
else
|
143
|
+
app = Application.new(:name => app_name, :host_id => host.id)
|
144
|
+
if app.save
|
145
|
+
self.applications << app
|
146
|
+
else
|
147
|
+
warn("Could not create application '#{app_name}' for host #{host.name}")
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
self.applications
|
152
|
+
end
|
153
|
+
|
154
|
+
# Check that all settings are correct in order to be able to
|
155
|
+
# successfully write data to Zabbix.
|
156
|
+
def confirm_settings
|
157
|
+
raise ConnectionError.new("Must specify a Zabbix server to send data to.") unless settings['server']
|
158
|
+
raise Error.new("Must specify the path to a local configuraiton file") unless settings['configuration_file'] && File.file?(settings['configuration_file'])
|
159
|
+
raise ConnectionError.new("Must specify the name of a host to send data for.") unless settings['host']
|
160
|
+
raise ValidationError.new("Must define at least one host group.") if auto_vivify? && (settings['host_groups'].nil? || settings['host_groups'].empty?)
|
161
|
+
end
|
162
|
+
|
163
|
+
public
|
164
|
+
|
165
|
+
#
|
166
|
+
# == Sending Data ==
|
167
|
+
#
|
168
|
+
|
169
|
+
# Run this sender.
|
170
|
+
#
|
171
|
+
# Will read from the correct source of data and exit the Ruby
|
172
|
+
# process once the source is consumed.
|
173
|
+
def run
|
174
|
+
case
|
175
|
+
when settings['pipe']
|
176
|
+
process_pipe
|
177
|
+
when settings.rest.size > 0
|
178
|
+
settings.rest.each do |path|
|
179
|
+
process_file(path)
|
180
|
+
end
|
181
|
+
else
|
182
|
+
process_stdin
|
183
|
+
end
|
184
|
+
exit(0)
|
185
|
+
end
|
186
|
+
|
187
|
+
protected
|
188
|
+
|
189
|
+
# Process each line of a file.
|
190
|
+
#
|
191
|
+
# @param [String] path the path to the file to process
|
192
|
+
def process_file path
|
193
|
+
f = File.new(path)
|
194
|
+
process_file_handle(f)
|
195
|
+
f.close
|
196
|
+
end
|
197
|
+
|
198
|
+
# Process each line of standard input.
|
199
|
+
def process_stdin
|
200
|
+
process_file_handle($stdin)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Process each line read from the pipe.
|
204
|
+
#
|
205
|
+
# The pipe will be opened in a non-blocking read mode. This
|
206
|
+
# sender will wait 'pipe_read_sleep' seconds between successive
|
207
|
+
# empty reads.
|
208
|
+
def process_pipe
|
209
|
+
# We want to open this pipe in non-blocking read mode b/c
|
210
|
+
# otherwise this process becomes hard to kill.
|
211
|
+
f = File.new(settings['pipe'], (File::RDONLY | File::NONBLOCK))
|
212
|
+
while true
|
213
|
+
process_file_handle(f)
|
214
|
+
# In non-blocking mode, an EOFError from f.readline doesn't mean
|
215
|
+
# there's no more data to read, just that there's no more data
|
216
|
+
# right *now*. If we sleep for a bit there might be more data
|
217
|
+
# coming down the pipe.
|
218
|
+
sleep settings['pipe_read_sleep']
|
219
|
+
end
|
220
|
+
f.close
|
221
|
+
end
|
222
|
+
|
223
|
+
# Process each line of a given file handle.
|
224
|
+
#
|
225
|
+
# @param [File] f the file to process
|
226
|
+
def process_file_handle f
|
227
|
+
begin
|
228
|
+
line = f.readline
|
229
|
+
rescue EOFError
|
230
|
+
line = nil
|
231
|
+
end
|
232
|
+
while line
|
233
|
+
process_line(line)
|
234
|
+
begin
|
235
|
+
# FIXME -- this call to File#readline blocks and doesn't let
|
236
|
+
# stuff like SIGINT (generated from Ctrl-C on a keyboard,
|
237
|
+
# say) take affect.
|
238
|
+
line = f.readline
|
239
|
+
rescue EOFError
|
240
|
+
line = nil
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
public
|
246
|
+
|
247
|
+
# Process a single line of text.
|
248
|
+
#
|
249
|
+
# @param [String] line
|
250
|
+
def process_line line
|
251
|
+
if looks_like_json?(line)
|
252
|
+
process_line_of_json_in_new_pipe(line)
|
253
|
+
else
|
254
|
+
process_line_of_tsv_in_this_pipe(line)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
protected
|
259
|
+
|
260
|
+
# Parse and send a single +line+ of TSV input to the Zabbix server.
|
261
|
+
# The line will be split at tabs and expects either
|
262
|
+
#
|
263
|
+
# a) two columns: an item key and a value
|
264
|
+
# b) three columns: an item key, a value, and a timestamp
|
265
|
+
#
|
266
|
+
# Unexpected input will cause an error to be logged.
|
267
|
+
#
|
268
|
+
# @param [String] line a line of TSV data
|
269
|
+
def process_line_of_tsv_in_this_pipe line
|
270
|
+
parts = line.strip.split("\t")
|
271
|
+
case parts.size
|
272
|
+
when 2
|
273
|
+
timestamp = Time.now
|
274
|
+
key, value = parts
|
275
|
+
when 3
|
276
|
+
key, value = parts[0..1]
|
277
|
+
timestamp = Time.parse(parts.last)
|
278
|
+
else
|
279
|
+
error("Each line of input must be a tab separated row consisting of 2 columns (key, value) or 3 columns (timestamp, key, value)")
|
280
|
+
return
|
281
|
+
end
|
282
|
+
send_data(key, value, timestamp)
|
283
|
+
end
|
284
|
+
|
285
|
+
# Parse and send a single +line+ of JSON input to the Zabbix server.
|
286
|
+
# The JSON must have a key +data+ in order to be processed. The
|
287
|
+
# value of 'data' should be an Array of Hashes each with a +key+ and
|
288
|
+
# +value+.
|
289
|
+
#
|
290
|
+
# This ZabbixPipe's settings will be merged with the remainder of
|
291
|
+
# the JSON hash. This allows sending values for 'host2' to an
|
292
|
+
# instance of ZabbixPipe already set up to receive for 'host1'.
|
293
|
+
#
|
294
|
+
# This is useful for sending data for keys from multiple hosts
|
295
|
+
#
|
296
|
+
# Example of expected input:
|
297
|
+
#
|
298
|
+
# {
|
299
|
+
# 'data': [
|
300
|
+
# {'key': 'foo.bar.baz', 'value': 10},
|
301
|
+
# {'key': 'snap.crackle.pop', 'value': 8 }
|
302
|
+
# ]
|
303
|
+
# }
|
304
|
+
#
|
305
|
+
# Or when sending for another host:
|
306
|
+
#
|
307
|
+
# {
|
308
|
+
# 'host': 'shazaam',
|
309
|
+
# 'applications': 'silly',
|
310
|
+
# 'data': [
|
311
|
+
# {'key': 'foo.bar.baz', 'value': 10},
|
312
|
+
# {'key': 'snap.crackle.pop', 'value': 8 }
|
313
|
+
# ]
|
314
|
+
# }
|
315
|
+
#
|
316
|
+
# @param [String] line a line of JSON data
|
317
|
+
def process_line_of_json_in_new_pipe line
|
318
|
+
begin
|
319
|
+
json = JSON.parse(line)
|
320
|
+
rescue JSON::ParserError => e
|
321
|
+
error("Malformed JSON")
|
322
|
+
return
|
323
|
+
end
|
324
|
+
|
325
|
+
data = json.delete('data')
|
326
|
+
unless data && data.is_a?(Array)
|
327
|
+
error("A line of JSON input must a have an Array key 'data'")
|
328
|
+
return
|
329
|
+
end
|
330
|
+
|
331
|
+
if json.empty?
|
332
|
+
# If there are no other settings then the daughter will be the
|
333
|
+
# same as the parent -- so just use 'self'.
|
334
|
+
daughter_pipe = self
|
335
|
+
else
|
336
|
+
# We merge the settings from 'self' with whatever else is
|
337
|
+
# present in the line.
|
338
|
+
begin
|
339
|
+
daughter_pipe = self.class.new(settings.stringify_keys.merge(json))
|
340
|
+
rescue Error => e
|
341
|
+
error(e.message)
|
342
|
+
return
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
data.each do |point|
|
347
|
+
key = point['key']
|
348
|
+
value = point['value']
|
349
|
+
unless key && value
|
350
|
+
warn("The elements of the 'data' Array must be Hashes with a 'key' and a 'value'")
|
351
|
+
next
|
352
|
+
end
|
353
|
+
|
354
|
+
tsv_line = [key, value].map(&:to_s).join("\t")
|
355
|
+
daughter_pipe.process_line(tsv_line)
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
# Does the +line+ look like it might be JSON?
|
360
|
+
#
|
361
|
+
# @param [String] line
|
362
|
+
# @return [true, false]
|
363
|
+
def looks_like_json? line
|
364
|
+
!!(line =~ /^\s*\{/)
|
365
|
+
end
|
366
|
+
|
367
|
+
# Send the +value+ for +key+ at the given +timestamp+ to the Zabbix
|
368
|
+
# server.
|
369
|
+
#
|
370
|
+
# If the +key+ doesn't exist for this local agent's host, it will be
|
371
|
+
# added.
|
372
|
+
#
|
373
|
+
# FIXME passing +timestamp+ has no effect at present...
|
374
|
+
#
|
375
|
+
# @param [String] key
|
376
|
+
# @param [String, Fixnum, Float] value
|
377
|
+
# @param [Time] timestamp
|
378
|
+
def send_data key, value, timestamp
|
379
|
+
ensure_item_exists(key, value) unless fast?
|
380
|
+
command = "#{settings['sender']} --config #{settings['configuration_file']} --zabbix-server #{settings['server']} --host #{settings['host']} --key #{key} --value '#{value}'"
|
381
|
+
process_zabbix_sender_output(key, `#{command}`)
|
382
|
+
|
383
|
+
# command = "zabbix_sender --config #{configuration_file} --zabbix-server #{server} --input-file - --with-timestamps"
|
384
|
+
# open(command, 'w') do |zabbix_sender|
|
385
|
+
# zabbix_sender.write([settings['host'], key, timestamp.to_i, value].map(&:to_s).join("\t"))
|
386
|
+
# zabbix_sender.close_write
|
387
|
+
# process_zabbix_sender_output(zabbix_sender.read)
|
388
|
+
# end
|
389
|
+
end
|
390
|
+
|
391
|
+
# Create an item for the given +key+ if necessary.
|
392
|
+
#
|
393
|
+
# @param [String] key
|
394
|
+
# @param [String, Fixnum, Float] value
|
395
|
+
def ensure_item_exists key, value
|
396
|
+
item = Item.find(:key => key, :host_id => host.id)
|
397
|
+
unless item
|
398
|
+
Item.new(:key => key, :host_id => host.id, :applications => applications, :value_type => Item.value_type_from_value(value)).save
|
399
|
+
|
400
|
+
# There is a time lag of about 15-30 seconds between (successfully)
|
401
|
+
# creating an item on the Zabbix server and having the Zabbix accept
|
402
|
+
# new data for that item.
|
403
|
+
#
|
404
|
+
# If it is crucial that *every single* data point be written, dial
|
405
|
+
# up this sleep period. The first data point for a new key will put
|
406
|
+
# the wrapper to sleep for this period of time, in hopes that the
|
407
|
+
# Zabbix server will catch up and be ready to accept new data
|
408
|
+
# points.
|
409
|
+
#
|
410
|
+
# If you don't care that you're going to lose the first few data
|
411
|
+
# points you send to Zabbix, then don't worry about it.
|
412
|
+
sleep settings['create_item_sleep']
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
# Parse the +text+ output by +zabbix_sender+.
|
417
|
+
#
|
418
|
+
# @param [String] key
|
419
|
+
# @param [String] text the output from +zabbix_sender+
|
420
|
+
# @return [Fixnum] the number of data points processed
|
421
|
+
def process_zabbix_sender_output key, text
|
422
|
+
return unless settings['verbose']
|
423
|
+
lines = text.strip.split("\n")
|
424
|
+
return if lines.size < 1
|
425
|
+
status_line = lines.first
|
426
|
+
status_line =~ /Processed +(\d+) +Failed +(\d+) +Total +(\d+)/
|
427
|
+
processed, failed, total = $1, $2, $3
|
428
|
+
warn("Failed to write #{failed} values to key '#{key}'") if failed.to_i != 0
|
429
|
+
processed
|
430
|
+
end
|
431
|
+
|
432
|
+
end
|
433
|
+
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'configliere'
|
2
|
-
require 'open3'
|
3
2
|
|
4
3
|
module Rubix
|
5
4
|
|
@@ -186,22 +185,14 @@ module Rubix
|
|
186
185
|
end
|
187
186
|
|
188
187
|
def sender?
|
189
|
-
|
190
|
-
%w[server port host config].each do |var|
|
191
|
-
raise Rubix::Error.new("Cannot send values to Zabbix: Set value of --#{var}.") if settings[var.to_sym].nil?
|
192
|
-
end
|
193
|
-
true
|
194
|
-
else
|
195
|
-
false
|
196
|
-
end
|
188
|
+
settings[:send] == true
|
197
189
|
end
|
198
190
|
|
199
191
|
def output
|
200
192
|
return @output if @output
|
201
193
|
case
|
202
194
|
when sender?
|
203
|
-
@
|
204
|
-
@output = @sender_stdin
|
195
|
+
@output = Sender.new(:host => settings[:host], :server => settings[:server], :port => settings[:port], :config => settings[:config])
|
205
196
|
when stdout?
|
206
197
|
@output = $stdout
|
207
198
|
when fifo?
|
@@ -220,9 +211,6 @@ module Rubix
|
|
220
211
|
return unless output
|
221
212
|
output.flush
|
222
213
|
case
|
223
|
-
when sender?
|
224
|
-
# puts @sender_stdout.read
|
225
|
-
[@sender_stdin, @sender_stdout, @sender_stderr].each { |fh| fh.close } if sender?
|
226
214
|
when stdout?
|
227
215
|
return
|
228
216
|
else
|
data/lib/rubix/sender.rb
CHANGED
@@ -1,433 +1,132 @@
|
|
1
1
|
require 'rubix/log'
|
2
|
+
require 'open3'
|
2
3
|
|
3
4
|
module Rubix
|
4
5
|
|
5
6
|
# A class used to send data to Zabbix.
|
6
7
|
#
|
7
|
-
# This sender is used to
|
8
|
-
# utility. It is initialized with some metadata about a host, its
|
9
|
-
# host groups and templates, and applications into which items
|
10
|
-
# should be written, and it can then accept data and forward it to a
|
11
|
-
# Zabbix server using the +zabbix_sender+ utility that comes with
|
12
|
-
# Zabbix.
|
13
|
-
#
|
14
|
-
# A sender can be given data in either TSV or JSON formats. With
|
15
|
-
# the JSON format, it is possible to embed data for hosts, host
|
16
|
-
# groups, &c. distinct from that with which this sender was
|
17
|
-
# initialized. This is a useful way to send many different kinds of
|
18
|
-
# data through the same process.
|
19
|
-
#
|
20
|
-
# The sender will also auto-vivify any hosts, host gruops,
|
21
|
-
# templates, applications, and items it needs in order to be able to
|
22
|
-
# write data. This is expensive in terms of time so it can be
|
23
|
-
# turned off using the <tt>--fast</tt> option.
|
8
|
+
# This sender is used to wrap +zabbix_sender+.
|
24
9
|
class Sender
|
25
10
|
|
26
11
|
include Logs
|
27
12
|
|
28
|
-
# @return [Hash] settings
|
29
|
-
attr_accessor :settings
|
30
|
-
|
31
|
-
# @return [Rubix::Host] the host the Sender will send data for
|
32
|
-
attr_accessor :host
|
33
|
-
|
34
|
-
# @return [Array<Rubix::HostGroup>] the hostgroups used to create this host
|
35
|
-
attr_accessor :host_groups
|
36
|
-
|
37
|
-
# @return [Array<Rubix::Template>] the templates used to create this host
|
38
|
-
attr_accessor :templates
|
39
|
-
|
40
|
-
# @return [Array<Rubix::Application>] The applications used to create items
|
41
|
-
attr_accessor :applications
|
42
|
-
|
43
13
|
#
|
44
|
-
# ==
|
14
|
+
# == Properties ==
|
45
15
|
#
|
46
16
|
|
47
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
# @param settings [String] host_groups comma-separated names of Zabbix host groups the host should belong to
|
52
|
-
# @param settings [String] templates comma-separated names of Zabbix templates the host should belong to
|
53
|
-
# @param settings [String] applications comma-separated names of applications created items should be scoped under
|
54
|
-
# @param settings [String] server URL for the Zabbix server -- *not* the URL for the Zabbix API
|
55
|
-
# @param settings [String] configuration_file path to a local Zabbix configuration file as used by the +zabbix_sender+ utility
|
56
|
-
# @param settings [true, false] verbose be verbose during execution
|
57
|
-
# @param settings [true, false] fast auto-vivify (slow) or not (fast)
|
58
|
-
# @param settings [String] pipe path to a named pipe to be read from
|
59
|
-
# @param settings [Fixnum] pipe_read_sleep seconds to sleep after an empty read from the a named pipe
|
60
|
-
# @param settings [Fixnum] create_item_sleep seconds to sleep after creating a new item
|
61
|
-
def initialize settings
|
62
|
-
@settings = settings
|
63
|
-
confirm_settings
|
64
|
-
if fast?
|
65
|
-
info("Forwarding for #{settings['host']}...") if settings['verbose']
|
66
|
-
else
|
67
|
-
initialize_host_groups
|
68
|
-
initialize_templates
|
69
|
-
initialize_host
|
70
|
-
initialize_applications
|
71
|
-
info("Forwarding for #{host.name}...") if settings['verbose']
|
72
|
-
end
|
17
|
+
# @return [String] The IP of the Zabbix server
|
18
|
+
attr_writer :server
|
19
|
+
def server
|
20
|
+
@server ||= 'localhost'
|
73
21
|
end
|
74
22
|
|
75
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
def fast?
|
80
|
-
settings['fast']
|
23
|
+
# @return [String, Rubix::Hosts] the Zabbix host name or Rubix::Host the sender will use by default
|
24
|
+
attr_reader :host
|
25
|
+
def host= nh
|
26
|
+
@host = (nh.respond_to?(:name) ? nh.name : nh.to_s)
|
81
27
|
end
|
82
28
|
|
83
|
-
#
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
!fast?
|
29
|
+
# @return [Fixnum] the port to connect to on the Zabbix server
|
30
|
+
attr_writer :port
|
31
|
+
def port
|
32
|
+
@port ||= 10051
|
88
33
|
end
|
89
34
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
# @return [Array<Rubix::HostGroup>]
|
95
|
-
def initialize_host_groups
|
96
|
-
self.host_groups = settings['host_groups'].split(',').flatten.compact.map(&:strip).uniq.map { |group_name | HostGroup.find_or_create(:name => group_name.strip) }
|
35
|
+
# @return [String] the path to the local Zabbix agent configuration file.
|
36
|
+
attr_writer :config
|
37
|
+
def config
|
38
|
+
@config ||= '/etc/zabbix/zabbix_agentd.conf'
|
97
39
|
end
|
98
40
|
|
99
|
-
#
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
self.templates = (settings['templates'] || '').split(',').flatten.compact.map(&:strip).uniq.map { |template_name | Template.find(:name => template_name.strip) }.compact
|
41
|
+
# Whether or not to include timestamps with the data.
|
42
|
+
attr_writer :timestamps
|
43
|
+
def timestamps?
|
44
|
+
@timestamps
|
104
45
|
end
|
105
46
|
|
106
|
-
# Find or create the host for this data. Host groups and
|
107
|
-
# templates will automatically be attached.
|
108
47
|
#
|
109
|
-
#
|
110
|
-
|
111
|
-
self.host = (Host.find(:name => settings['host']) || Host.new(:name => settings['host']))
|
112
|
-
|
113
|
-
current_host_group_names = (host.host_groups || []).map(&:name)
|
114
|
-
current_template_names = (host.templates || []).map(&:name)
|
115
|
-
|
116
|
-
host_groups_to_add, templates_to_add = [], []
|
117
|
-
|
118
|
-
(self.host_groups || []).each do |hg|
|
119
|
-
host_groups_to_add << hg unless current_host_group_names.include?(hg.name)
|
120
|
-
end
|
121
|
-
|
122
|
-
(self.templates || []).each do |t|
|
123
|
-
templates_to_add << t unless current_template_names.include?(t.name)
|
124
|
-
end
|
125
|
-
|
126
|
-
host.host_groups = ((host.host_groups || []) + host_groups_to_add).flatten.compact.uniq
|
127
|
-
host.templates = ((host.templates || []) + templates_to_add).flatten.compact.uniq
|
128
|
-
host.save
|
129
|
-
host
|
130
|
-
end
|
48
|
+
# == Initialization ==
|
49
|
+
#
|
131
50
|
|
132
|
-
#
|
51
|
+
# Create a new sender with the given +settings+.
|
133
52
|
#
|
134
|
-
# @
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
warn("Could not create application '#{app_name}' for host #{host.name}")
|
148
|
-
end
|
149
|
-
end
|
150
|
-
end
|
151
|
-
self.applications
|
53
|
+
# @param [Hash, Configliere::Param] settings
|
54
|
+
# @param settings [String, Rubix::Host] host the name of the Zabbix host to write data for
|
55
|
+
# @param settings [String] server the IP of the Zabbix server
|
56
|
+
# @param settings [Fixnum] port the port to connect to on the Zabbix server
|
57
|
+
# @param settings [String] config the path to the local configuration file
|
58
|
+
def initialize settings={}
|
59
|
+
@settings = settings
|
60
|
+
self.server = settings[:server] if settings[:server]
|
61
|
+
self.host = settings[:host] if settings[:host]
|
62
|
+
self.port = settings[:port] if settings[:port]
|
63
|
+
self.config = settings[:config] if settings[:config]
|
64
|
+
self.timestamps = settings[:timestamps]
|
65
|
+
confirm_settings
|
152
66
|
end
|
153
67
|
|
154
68
|
# Check that all settings are correct in order to be able to
|
155
69
|
# successfully write data to Zabbix.
|
156
70
|
def confirm_settings
|
157
|
-
raise
|
158
|
-
raise Error.new("Must specify the
|
159
|
-
raise
|
160
|
-
raise
|
71
|
+
raise Error.new("Must specify a path to a local configuraiton file") unless config
|
72
|
+
raise Error.new("Must specify the IP of a Zabbix server") unless server
|
73
|
+
raise Error.new("Must specify the port of a Zabbix server") unless port && port.to_i > 0
|
74
|
+
raise Error.new("Must specify a default Zabbix host to write data for") unless host
|
161
75
|
end
|
162
|
-
|
163
|
-
public
|
164
76
|
|
165
77
|
#
|
166
78
|
# == Sending Data ==
|
167
79
|
#
|
168
80
|
|
169
|
-
#
|
81
|
+
# The environment for the Zabbix sender invocation.
|
170
82
|
#
|
171
|
-
#
|
172
|
-
|
173
|
-
|
174
|
-
case
|
175
|
-
when settings['pipe']
|
176
|
-
process_pipe
|
177
|
-
when settings.rest.size > 0
|
178
|
-
settings.rest.each do |path|
|
179
|
-
process_file(path)
|
180
|
-
end
|
181
|
-
else
|
182
|
-
process_stdin
|
183
|
-
end
|
184
|
-
exit(0)
|
83
|
+
# @return [Hash]
|
84
|
+
def zabbix_sender_env
|
85
|
+
{}
|
185
86
|
end
|
186
87
|
|
187
|
-
|
188
|
-
|
189
|
-
# Process each line of a file.
|
88
|
+
# Construct the command that invokes Zabbix sender.
|
190
89
|
#
|
191
|
-
# @
|
192
|
-
def
|
193
|
-
|
194
|
-
|
195
|
-
f.close
|
196
|
-
end
|
197
|
-
|
198
|
-
# Process each line of standard input.
|
199
|
-
def process_stdin
|
200
|
-
process_file_handle($stdin)
|
201
|
-
end
|
202
|
-
|
203
|
-
# Process each line read from the pipe.
|
204
|
-
#
|
205
|
-
# The pipe will be opened in a non-blocking read mode. This
|
206
|
-
# sender will wait 'pipe_read_sleep' seconds between successive
|
207
|
-
# empty reads.
|
208
|
-
def process_pipe
|
209
|
-
# We want to open this pipe in non-blocking read mode b/c
|
210
|
-
# otherwise this process becomes hard to kill.
|
211
|
-
f = File.new(settings['pipe'], (File::RDONLY | File::NONBLOCK))
|
212
|
-
while true
|
213
|
-
process_file_handle(f)
|
214
|
-
# In non-blocking mode, an EOFError from f.readline doesn't mean
|
215
|
-
# there's no more data to read, just that there's no more data
|
216
|
-
# right *now*. If we sleep for a bit there might be more data
|
217
|
-
# coming down the pipe.
|
218
|
-
sleep settings['pipe_read_sleep']
|
219
|
-
end
|
220
|
-
f.close
|
221
|
-
end
|
222
|
-
|
223
|
-
# Process each line of a given file handle.
|
224
|
-
#
|
225
|
-
# @param [File] f the file to process
|
226
|
-
def process_file_handle f
|
227
|
-
begin
|
228
|
-
line = f.readline
|
229
|
-
rescue EOFError
|
230
|
-
line = nil
|
231
|
-
end
|
232
|
-
while line
|
233
|
-
process_line(line)
|
234
|
-
begin
|
235
|
-
# FIXME -- this call to File#readline blocks and doesn't let
|
236
|
-
# stuff like SIGINT (generated from Ctrl-C on a keyboard,
|
237
|
-
# say) take affect.
|
238
|
-
line = f.readline
|
239
|
-
rescue EOFError
|
240
|
-
line = nil
|
241
|
-
end
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
|
-
public
|
246
|
-
|
247
|
-
# Process a single line of text.
|
248
|
-
#
|
249
|
-
# @param [String] line
|
250
|
-
def process_line line
|
251
|
-
if looks_like_json?(line)
|
252
|
-
process_line_of_json_in_new_pipe(line)
|
253
|
-
else
|
254
|
-
process_line_of_tsv_in_this_pipe(line)
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
protected
|
259
|
-
|
260
|
-
# Parse and send a single +line+ of TSV input to the Zabbix server.
|
261
|
-
# The line will be split at tabs and expects either
|
262
|
-
#
|
263
|
-
# a) two columns: an item key and a value
|
264
|
-
# b) three columns: an item key, a value, and a timestamp
|
265
|
-
#
|
266
|
-
# Unexpected input will cause an error to be logged.
|
267
|
-
#
|
268
|
-
# @param [String] line a line of TSV data
|
269
|
-
def process_line_of_tsv_in_this_pipe line
|
270
|
-
parts = line.strip.split("\t")
|
271
|
-
case parts.size
|
272
|
-
when 2
|
273
|
-
timestamp = Time.now
|
274
|
-
key, value = parts
|
275
|
-
when 3
|
276
|
-
key, value = parts[0..1]
|
277
|
-
timestamp = Time.parse(parts.last)
|
278
|
-
else
|
279
|
-
error("Each line of input must be a tab separated row consisting of 2 columns (key, value) or 3 columns (timestamp, key, value)")
|
280
|
-
return
|
90
|
+
# @return [String]
|
91
|
+
def zabbix_sender_command
|
92
|
+
"timeout 3 zabbix_sender --zabbix-server #{server} --host #{host} --port #{port} --config #{config} --real-time --input-file - -vv".tap do |c|
|
93
|
+
c += " --with-timestamps" if timestamps?
|
281
94
|
end
|
282
|
-
send_data(key, value, timestamp)
|
283
95
|
end
|
284
96
|
|
285
|
-
#
|
286
|
-
# The JSON must have a key +data+ in order to be processed. The
|
287
|
-
# value of 'data' should be an Array of Hashes each with a +key+ and
|
288
|
-
# +value+.
|
289
|
-
#
|
290
|
-
# This ZabbixPipe's settings will be merged with the remainder of
|
291
|
-
# the JSON hash. This allows sending values for 'host2' to an
|
292
|
-
# instance of ZabbixPipe already set up to receive for 'host1'.
|
97
|
+
# Run a +zabbix_sender+ subprocess in the block.
|
293
98
|
#
|
294
|
-
#
|
295
|
-
|
296
|
-
# Example of expected input:
|
297
|
-
#
|
298
|
-
# {
|
299
|
-
# 'data': [
|
300
|
-
# {'key': 'foo.bar.baz', 'value': 10},
|
301
|
-
# {'key': 'snap.crackle.pop', 'value': 8 }
|
302
|
-
# ]
|
303
|
-
# }
|
304
|
-
#
|
305
|
-
# Or when sending for another host:
|
306
|
-
#
|
307
|
-
# {
|
308
|
-
# 'host': 'shazaam',
|
309
|
-
# 'applications': 'silly',
|
310
|
-
# 'data': [
|
311
|
-
# {'key': 'foo.bar.baz', 'value': 10},
|
312
|
-
# {'key': 'snap.crackle.pop', 'value': 8 }
|
313
|
-
# ]
|
314
|
-
# }
|
315
|
-
#
|
316
|
-
# @param [String] line a line of JSON data
|
317
|
-
def process_line_of_json_in_new_pipe line
|
99
|
+
# @yield [IO, IO, IO, Thread] Handle the subprocess.
|
100
|
+
def with_sender_subprocess &block
|
318
101
|
begin
|
319
|
-
|
320
|
-
rescue
|
321
|
-
|
322
|
-
return
|
323
|
-
end
|
324
|
-
|
325
|
-
data = json.delete('data')
|
326
|
-
unless data && data.is_a?(Array)
|
327
|
-
error("A line of JSON input must a have an Array key 'data'")
|
328
|
-
return
|
329
|
-
end
|
330
|
-
|
331
|
-
if json.empty?
|
332
|
-
# If there are no other settings then the daughter will be the
|
333
|
-
# same as the parent -- so just use 'self'.
|
334
|
-
daughter_pipe = self
|
335
|
-
else
|
336
|
-
# We merge the settings from 'self' with whatever else is
|
337
|
-
# present in the line.
|
338
|
-
begin
|
339
|
-
daughter_pipe = self.class.new(settings.stringify_keys.merge(json))
|
340
|
-
rescue Error => e
|
341
|
-
error(e.message)
|
342
|
-
return
|
343
|
-
end
|
344
|
-
end
|
345
|
-
|
346
|
-
data.each do |point|
|
347
|
-
key = point['key']
|
348
|
-
value = point['value']
|
349
|
-
unless key && value
|
350
|
-
warn("The elements of the 'data' Array must be Hashes with a 'key' and a 'value'")
|
351
|
-
next
|
352
|
-
end
|
353
|
-
|
354
|
-
tsv_line = [key, value].map(&:to_s).join("\t")
|
355
|
-
daughter_pipe.process_line(tsv_line)
|
102
|
+
Open3.popen3(zabbix_sender_env, zabbix_sender_command, &block)
|
103
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
104
|
+
warn(e.message)
|
356
105
|
end
|
357
106
|
end
|
358
107
|
|
359
|
-
#
|
108
|
+
# Convenience method for sending a block of text to
|
109
|
+
# +zabbix_sender+.
|
360
110
|
#
|
361
|
-
# @param [String]
|
362
|
-
|
363
|
-
|
364
|
-
|
111
|
+
# @param [String] text
|
112
|
+
def puts text
|
113
|
+
with_sender_subprocess do |stdin, stdout, stderr, wait_thr|
|
114
|
+
stdin.write(text)
|
115
|
+
stdin.close
|
116
|
+
output = [stdout.read.chomp, stderr.read.chomp].join("\n").strip
|
117
|
+
debug(output) if output.size > 0
|
118
|
+
end
|
365
119
|
end
|
366
120
|
|
367
|
-
#
|
368
|
-
|
369
|
-
|
370
|
-
# If the +key+ doesn't exist for this local agent's host, it will be
|
371
|
-
# added.
|
372
|
-
#
|
373
|
-
# FIXME passing +timestamp+ has no effect at present...
|
374
|
-
#
|
375
|
-
# @param [String] key
|
376
|
-
# @param [String, Fixnum, Float] value
|
377
|
-
# @param [Time] timestamp
|
378
|
-
def send_data key, value, timestamp
|
379
|
-
ensure_item_exists(key, value) unless fast?
|
380
|
-
command = "#{settings['sender']} --config #{settings['configuration_file']} --zabbix-server #{settings['server']} --host #{settings['host']} --key #{key} --value '#{value}'"
|
381
|
-
process_zabbix_sender_output(key, `#{command}`)
|
382
|
-
|
383
|
-
# command = "zabbix_sender --config #{configuration_file} --zabbix-server #{server} --input-file - --with-timestamps"
|
384
|
-
# open(command, 'w') do |zabbix_sender|
|
385
|
-
# zabbix_sender.write([settings['host'], key, timestamp.to_i, value].map(&:to_s).join("\t"))
|
386
|
-
# zabbix_sender.close_write
|
387
|
-
# process_zabbix_sender_output(zabbix_sender.read)
|
388
|
-
# end
|
121
|
+
# :nodoc:
|
122
|
+
def close
|
123
|
+
return
|
389
124
|
end
|
390
125
|
|
391
|
-
#
|
392
|
-
|
393
|
-
|
394
|
-
# @param [String, Fixnum, Float] value
|
395
|
-
def ensure_item_exists key, value
|
396
|
-
item = Item.find(:key => key, :host_id => host.id)
|
397
|
-
unless item
|
398
|
-
Item.new(:key => key, :host_id => host.id, :applications => applications, :value_type => Item.value_type_from_value(value)).save
|
399
|
-
|
400
|
-
# There is a time lag of about 15-30 seconds between (successfully)
|
401
|
-
# creating an item on the Zabbix server and having the Zabbix accept
|
402
|
-
# new data for that item.
|
403
|
-
#
|
404
|
-
# If it is crucial that *every single* data point be written, dial
|
405
|
-
# up this sleep period. The first data point for a new key will put
|
406
|
-
# the wrapper to sleep for this period of time, in hopes that the
|
407
|
-
# Zabbix server will catch up and be ready to accept new data
|
408
|
-
# points.
|
409
|
-
#
|
410
|
-
# If you don't care that you're going to lose the first few data
|
411
|
-
# points you send to Zabbix, then don't worry about it.
|
412
|
-
sleep settings['create_item_sleep']
|
413
|
-
end
|
126
|
+
# :nodoc:
|
127
|
+
def flush
|
128
|
+
return
|
414
129
|
end
|
415
130
|
|
416
|
-
# Parse the +text+ output by +zabbix_sender+.
|
417
|
-
#
|
418
|
-
# @param [String] key
|
419
|
-
# @param [String] text the output from +zabbix_sender+
|
420
|
-
# @return [Fixnum] the number of data points processed
|
421
|
-
def process_zabbix_sender_output key, text
|
422
|
-
return unless settings['verbose']
|
423
|
-
lines = text.strip.split("\n")
|
424
|
-
return if lines.size < 1
|
425
|
-
status_line = lines.first
|
426
|
-
status_line =~ /Processed +(\d+) +Failed +(\d+) +Total +(\d+)/
|
427
|
-
processed, failed, total = $1, $2, $3
|
428
|
-
warn("Failed to write #{failed} values to key '#{key}'") if failed.to_i != 0
|
429
|
-
processed
|
430
|
-
end
|
431
|
-
|
432
131
|
end
|
433
132
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Rubix::AutoSender do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@config_file = Tempfile.new('sender', '/tmp')
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "running in --fast mode" do
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "running in auto-vivify mode" do
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
@@ -140,5 +140,19 @@ describe Rubix::Monitor do
|
|
140
140
|
end
|
141
141
|
|
142
142
|
end
|
143
|
+
|
144
|
+
describe 'writing to a Sender' do
|
145
|
+
before do
|
146
|
+
@sender = mock("Rubix::Sender")
|
147
|
+
@sender.stub!(:close) ; @sender.stub!(:flush)
|
148
|
+
Rubix::Sender.should_receive(:new).with(kind_of(Hash)).and_return(@sender)
|
149
|
+
::ARGV.replace(['--send', '--host=foobar'])
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should write to the sender" do
|
153
|
+
@sender.should_receive(:puts).with('- key value')
|
154
|
+
@wrapper.run
|
155
|
+
end
|
156
|
+
end
|
143
157
|
|
144
158
|
end
|
data/spec/rubix/sender_spec.rb
CHANGED
@@ -6,18 +6,16 @@ describe Rubix::Sender do
|
|
6
6
|
@config_file = Tempfile.new('sender', '/tmp')
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
@sender.process_line("foo.bar.baz 123\n")
|
15
|
-
@sender.process_line({:host => 'newhost', :host_groups => 'foobar,baz', :data => [{:key => 'foo.bar.baz', :value => 123}]}.to_json)
|
16
|
-
end
|
9
|
+
it "has sensible defaults" do
|
10
|
+
sender = Rubix::Sender.new(:host => 'foobar')
|
11
|
+
sender.server.should == 'localhost'
|
12
|
+
sender.port.should == 10051
|
13
|
+
sender.config.should == '/etc/zabbix/zabbix_agentd.conf'
|
17
14
|
end
|
18
15
|
|
19
|
-
|
16
|
+
it "will raise an error without a host" do
|
17
|
+
lambda { Rubix::Sender.new }.should raise_error(Rubix::Error)
|
20
18
|
end
|
21
|
-
|
19
|
+
|
22
20
|
end
|
23
21
|
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 3
|
8
|
+
- 0
|
9
|
+
version: 0.3.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Dhruv Bansal
|
@@ -100,6 +100,7 @@ files:
|
|
100
100
|
- lib/rubix/examples/mongo_monitor.rb
|
101
101
|
- lib/rubix/examples/uptime_monitor.rb
|
102
102
|
- lib/rubix/examples/hbase_monitor.rb
|
103
|
+
- lib/rubix/auto_sender.rb
|
103
104
|
- lib/rubix/sender.rb
|
104
105
|
- lib/rubix/log.rb
|
105
106
|
- lib/rubix/models.rb
|
@@ -120,6 +121,7 @@ files:
|
|
120
121
|
- lib/rubix/connection.rb
|
121
122
|
- lib/rubix/associations.rb
|
122
123
|
- spec/test.yml
|
124
|
+
- spec/rubix/auto_sender_spec.rb
|
123
125
|
- spec/rubix/monitors/monitor_spec.rb
|
124
126
|
- spec/rubix/monitors/chef_monitor_spec.rb
|
125
127
|
- spec/rubix/monitors/cluster_monitor_spec.rb
|