require 'configliere'
module Rubix
# A generic monitor class for constructing Zabbix monitors.
#
# This class handles the low-level logic of sleeping, waking up, and
# sending data to Zabbix.
#
# It's up to a subclass to determine how to make a measurement.
#
# Here's an example of a script which measures the uptime of the
# current machine.
#
# #!/usr/bin/env ruby
# # in uptime_monitor
# class UptimeMonitor < Rubix::Monitor
#
# def measure
# return unless `uptime`.chomp =~ /(\d+) days.*(\d+) users.*load average: ([\d\.]+), ([\d\.]+), ([\d\.]+)/
#
# # can write one value at a time
# write ['uptime', $1.to_i]
#
# # or can use a block
# write do |data|
# # values can be arrays
# data << ['users', $2.to_i]
# # or hashes
# data << { :key => 'load15', :value => $3.to_i }
# data << { :key => 'load5', :value => $4.to_i }
# # can even pass a different host
# data << { :key => 'load1', :value => $5.to_i, :host => 'foobar-host' }
# end
# end
# end
#
# UptimeMonitor.run if $0 == __FILE__
#
# See what the script measures by running it directly.
#
# $ ./uptime_monitor
#
# Or have it send its output to another file or FIFO
#
# $ ./uptime_monitor /path/to/some/file
#
# Or have it loop every 30 seconds
#
# $ ./uptime_monitor --loop=30 /path/to/some/file &
class Monitor
#
# Class-level settings and a function to run a monito
#
def self.default_settings
@default_settings ||= Configliere::Param.new.tap do |s|
s.use :commandline
s.define :loop, :description => "Run every this many seconds", :required => false, :type => Integer
# The following options are only used when sending directly
# with zabbix_sender
s.define :server, :description => "IP of a Zabbix server", :required => false, :default => 'localhost'
s.define :port, :description => "Port of a Zabbix server", :required => false, :default => 10051, :type => Integer
s.define :host, :description => "Name of a Zabbix host", :required => false, :default => ENV["HOSTNAME"]
s.define :config, :description => "Local Zabbix agentd configuration file", :required => false, :default => "/etc/zabbix/zabbix_agentd.conf"
s.define :send, :description => "Send data directlyt to Zabbix using 'zabbix_sender'", :required => false, :default => false, :type => :boolean
end
end
def self.run
settings = default_settings
begin
settings.resolve!
rescue => e
puts e.message
exit(1)
end
new(settings).run
end
#
# Instance-level settings that provide logic for running once or
# looping.
#
attr_reader :settings
def initialize settings
@settings = settings
end
def loop?
loop_period > 0
end
def loop_period
settings[:loop].to_i
end
def run
begin
if loop?
while true
measure
output.flush if output
sleep loop_period
end
else
measure
end
ensure
close
end
end
def measure
raise NotImplementedError.new("Override the 'measure' method in a subclass to conduct a measurement.")
end
#
# Methods for writing data to Zabbix.
#
def write measurement=nil, &block
return unless output
return unless measurement || block_given?
data = [measurement]
block.call(data) if block_given?
text = data.compact.map { |measurement| format_measurement(measurement) }.compact.join("\n")
begin
output.puts(text)
rescue Errno::ENXIO
# FIFO's reader isn't alive...
end
end
def format_measurement measurement
# key value
[].tap do |vals|
case measurement
when Hash
vals << (measurement[:host].nil? ? '-' : measurement[:host])
vals << measurement[:key]
vals << measurement[:timestamp] if measurement[:timestamp]
value = measurement[:value].to_s
if value.include?(' ')
value.insert(0, "'")
value.insert(-1, "'")
end
vals << value
when Array
if measurement.length == 2
vals << '-'
vals.concat(measurement)
else
vals.concat(measurement)
end
else
return
end
end.map(&:to_s).join(' ')
end
def output_path
settings.rest && settings.rest.first
end
def stdout?
output_path.nil?
end
def file?
!stdout? && (!File.exist?(output_path) || File.ftype(output_path) == 'file')
end
def fifo?
!stdout? && File.exist?(output_path) && File.ftype(output_path) == 'fifo'
end
def sender?
settings[:send] == true
end
def output
return @output if @output
case
when sender?
@output = Sender.new(:host => settings[:host], :server => settings[:server], :port => settings[:port], :config => settings[:config])
when stdout?
@output = $stdout
when fifo?
begin
@output = open(output_path, (File::WRONLY | File::NONBLOCK))
rescue Errno::ENXIO
nil
# FIFO's reader isn't alive...
end
else
@output = File.open(output_path, 'a')
end
end
def close
return unless output
output.flush
case
when stdout?
return
else
output.close
end
end
end
end