module Zabbix require 'socket' require 'json' ## # AgentConfiguration holds data that's scraped from a zabbix_agentd config file. It's # initialized when the gem is required. If it finds configuration you can # access it with class methods. This is not meant to be instantiated. class AgentConfiguration def self.initialize @agentConfPaths = [ '/etc/zabbix/zabbix_agentd.conf', '/usr/local/etc/zabbix/zabbix_agentd.conf', '/opt/zabbix/etc/zabbix_agentd.conf', '/opt/zabbix/conf/zabbix_agentd.conf' ] @proxy = nil @hostname = nil @agentConfPaths.each { |path| if File.exist?(path) File.new(path).each_line { |line| if not @proxy match = /^Server=?(.*)/.match(line) if match @proxy = match[1].strip.split(',').pop end end if not @hostname match = /^Hostname=?(.*)/.match(line) if match @hostname = match[1].strip end end break if @proxy and @hostname } end break if @proxy and @host } if not @host @host = Socket.gethostname end end ## # Returns the value of the Server= assignment in zabbix_agentd.conf (if any) # def self.zabbixProxy @proxy end ## # Returns the value of the Hostname= asignment in zabbix_agentd.conf (if any) # def self.zabbixHostname @hostname end end module Sender require 'set' ## # Pipe instances encapsulate communication to a running instance of zabbix_sender # via a pipe to STDIN. If you want your program to send data itself (as opposed # to say printing stdin lines that you can then pipe to zabbix_sender yourself), # you'll want to make an instance of this # class Pipe ## # pipe file handle object attr_reader :pipe ## # target host (server or proxy) name or ip attr_reader :targetHost ## # path to the zabbix_sender executable attr_reader :exePath ## # Initialize a new Sender object. Both proxy: and path: are optional. # # An attempt is made to provide sane default values. If you have a zabbix_agentd.conf # file in one of the usual places and zabbix_sender is on your path, it'll probably # just work # def initialize(proxy: Zabbix::AgentConfiguration.zabbixProxy, path: 'zabbix_sender') @targetHost = proxy @exePath = path @pipe = nil end ## # Opens a pipe to the zabbix_sender command with appropriate options specified. # If the pipe is already opened when this command is called, it is first closed # via a call to flush def open self.flush @pipe = IO.popen(%Q(#{@exePath} -z #{@targetHost} -vv -T -i-),'w') end ## # Closes the zabbix_sender pipe if it's open def flush if @pipe and not @pipe.closed? @pipe.close end end ## # Send a Batch instance to zabbix (via zabbix_sender). This assumes that # the pipe is already open. def sendBatch(aBatch) # Assumes that the pipe is open @pipe.puts(aBatch.to_senderline) end ## # Send a Batch instance to zabbix (via zabbix_sender). This opens the pipe, # writes the data to the pipe, and closes the pipe all in one go. def sendBatchAtomic(aBatch) self.open self.sendBatch(aBatch) self.flush end end ## # ItemData instances hold the k-v pair of a value and its timestamp # along with the hostname to which the data belongs. It handles # formatting that data appropriately as input to zabbix_sender class ItemData ## # The item key that'll get the new value attr_accessor:key ## # The value that the item will get attr_accessor :value ## # The name of the zabbix host that owns the item key attr_accessor :hostname ## # The timestamp for this datapoint attr_accessor :timestamp ## # All values must be provided. def initialize(key: nil,value: nil, timestamp: nil, hostname: nil) @key = key @value = value @timestamp = timestamp @hostname = hostname end ## # Render the ItemData instance as a line of text that can be piped into zabbix_sender def to_senderline if @timestamp.to_i == 0 puts %Q("#{@hostname}" #{@key} #{@timestamp.to_i} #{@value}\n) abort("Attempt was made to render a timestamp of zero. You DO NOT want this - it can kill db performance. Fix it.") end return %Q("#{@hostname}" #{@key} #{@timestamp.to_i} #{@value}\n) end end ## # Discovery instances are a special type of ItemData that you will typically # create and hang on to as you accumulate (discover) related entities. You # then pass the discover instance into a Batch via addDiscovery(), which # includes it in the batch of data just like an ordinary ItemData instance. class Discovery < ItemData attr_reader :entities ## # The only required parameter is key:, which is the discovery rule key. # def initialize(key: nil,value: nil, timestamp: nil, hostname: nil) super @entities = Set.new end ## # This is how you pass data to zabbix that you use to construct items from item templates. Pass in # as many key-value pairs as you need. You'll reference these in the item prototype like {#MYKEY} # # Note that the keys (which you can pass as symbols if you want) are forced to uppercase. This is # here because the author once spent way too much time trying to figure out why discovery wasn't # working right one day. All caps seems to fix the issue. # def add_entity(aHash) # just send in key value pairs - these will be the variables you can use in the discovery item prototypes zabbified = Hash.new aHash.each_pair { |key,value| zabbified[%Q({##{key.to_s.upcase}})] = value } @entities.add(zabbified) end ## # Render this discovery instance as a zabbix_sender line. # def to_senderline disco = { 'data'=>Array.new } disco['data'] = @entities.to_a @value = disco.to_json super end end ## # Batch instances hold all the data and discovery that you collect as your program # does its thing with source data. Once you've done all your data collection, you can: # # * Send the batch instance to an instance of Pipe to have it transmitted to zabbix # * puts mybatch.to_senderline to output the zabbix_sender input text # * Use it to help feed data into zabbix by whatever other mechanism you might be # using, e.g. a ruby implementation of the zabbix sender protocol # class Batch ## # This is an array of all the ItemData and Discovery instances that have been added # to this discovery since instantiation. # attr_reader :data ## # Both parameters are optional - reasonable defaults are provided. # # Bear in mind that the hostname and timestamp values you provide here will be applied # to all the ItemData and Discovery objects you add via the addItemData() and # addDiscovery() methods by default (unless you override them when you add them) def initialize(timestamp: Time.now, hostname: Zabbix::AgentConfiguration.zabbixHostname) @time = timestamp @hostname = hostname @data = Array.new end ## # Create a new instance of ItemData and add that instance to the list of data that this # batch contains. You must provide a key and a value. You *can* provide a timestamp and # a hostname. If you do not provide a timestamp or hostname, they will be given the # timestamp and hostname associated with the instance of Batch that you're working with # def addItemData(key: nil,value: nil,timestamp: @time, hostname: @hostname) @data.push(ItemData.new(key: key,value: value,timestamp: timestamp,hostname: hostname)) end ## # Add a discovery object to this batch of data. The object will be added to the # top of the item list. # # If you did not specifically provide a hostname or a timestamp when you # instantiated the Discovery, they'll given the ones provided when this instance # of Batch was constructed. # def addDiscovery(aDiscovery) # It doesn't matter right now really as zabbix has to digest the disco # and won't do it before it tries to process the data, but it makes logical # sense to put discos first. aDiscovery.timestamp = @time if not aDiscovery.timestamp aDiscovery.hostname = @hostname if not aDiscovery.hostname @data.unshift(aDiscovery) end ## # Render this batch of data as a sequence of lines of text appropriate # for sending into zabbix_sender # def to_senderline @data.collect {|line| line.to_senderline}.join end end end # module Sender end # module Zabbix