rubix 0.2.1 → 0.3.0
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/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
|