#! /usr/local/bin/ruby # Copyright (c) 2006 Frédéric Senault. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of Frédéric Senault or any contributors may be # used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. $LOAD_PATH << File.dirname($0) require 'syslog' require 'common' # The part that extracts the contents of the performance data and stores it # in rrd databases. class TMain < Main # Finds all the collecters plugins, and store them in the global config # (tester only). def loadcollecters() Dir.glob($conf[:collpath]) do |f| begin require "#{f}" rescue Exception => e puts e end end end # Finds all the tester plugins, instanciate then, sort them by order, and # store the objects in the global config (tester only). def loadtesters() Dir.glob($conf[:testpath]) do |f| begin require "#{f}" rescue Exception => e puts e end end $conf[:tests] = [] Tester.globtests.sort! { |a, b| a.order <=> b.order } Tester.globtests.each { |t| $conf[:tests].push(t.new) } end # The main loop ; parses data sources, and present data to the appropriate # plugins (tester only). def parsefile() allrrds = {} allperf = [] Collecter.globcolls.each do |oc| c = oc.new c.each do |nd| ok = false $conf[:tests].each do |tst| begin tst.register(nd) do |id| name = tst.rrdname(nd, id) rrd = allrrds[name] if(rrd.nil?) then rrd = RRDStruct.new(name) rrd.attachtest(tst, id) allrrds[name] = rrd end rrd.attachdata(nd) dolog(:info, tst.class.to_s + ' took ' + \ nd.short) ok = true end break if(ok) rescue Exception => e dolog(:error, e) end end dolog(:info, 'Nobody took ' + \ nd.short) unless(ok) end end begin allrrds.each_value do |rrd| rrd.initrrdstruct() rrd.matchdata() dolog(:notice, 'Inserting data : ' + rrd.name + ', ' + \ rrd.counttimes.to_s + 't, ' + \ rrd.countds.to_s + 's, ' + \ rrd.countdp.to_s + 'd') rrd.writerrd() rrd.clear() end rescue Exception => e dolog(:error, e) end end end # The main collecter class ; must be inherited by the child plugins to work. class Collecter include Enumerable @@gcolls = [] # This is triggered when the class is inherited ; it allows the class to maintain # a class variable to store it's children. def Collecter.inherited(c) @@gcolls.push(c) end # Gives the list of plugins classes. def Collecter.globcolls @@gcolls end # The plugin must be Enumerable. def each raise "Child must implement each." end end # This class implements the basic recommended data for the collecter plugins. # Of course, it can be extended by the child plugins and the testers if needed. class NGraphData # Unix timestamp for the performance data attr_reader :time # Hostname. attr_reader :host # The performance data itself ; its format is free - as long as the # tester plugins can make something out of it. attr_reader :perfdata # Transforms the timestamp into a Time class. def ptime ptime = Time.at(@time) end # Sort id of the data, used for the logs. def short() @host end end # The main tester class ; it has to be inherited by all the plugins. The flow is : # 1. The order class property is queried to order the plugins. # 2. The object is instanciated. # 3. The data is offered to the plugins through the register method. # 4. If the register method yielded one or more identifiers, the rrdname method # is called with the data and the id. # 5. The initrrdstruct is then called with a new RRDStruct object to populate and the id. # 6. The getdata is called with the RRDStruct, the data, and the id. class Tester @@gtests = [] # This is triggered when the class is inherited ; it allows the class to maintain # a class variable to store it's children. def Tester.inherited(c) @@gtests.push(c) unless(c.to_s == 'ServiceTest') end # The list of tester plugins. def Tester.globtests @@gtests end # The order in which the plugin will be interrogated (lowest order first). def self.order 999 end # This is called with the data to examine. Must be implemented, and must # yield one or more identifiers if it wants the. The ndata object is # totally opaque for the tester system. def register(ndata) raise "register must be implemented" end # This is called to generate a filename for the rrd files. By default, # the name is generated based on the id of the source and the hostname. def rrdname(ndata, id) ndata.host.normalize + '_' + id.to_s.normalize end # Called to populate the RRD file and datastructure. The rrd parameter # is a RRDStruct object. This function must be implemented. def initrrdstruct(rrd, id) raise "inittrdstruct must be implemented" end # The last called function, to store the data itself. The rdd parameter # is the RRDStruct object initialized before, and it also takes the identifier. def getdata(rrd, ndata, id) raise "getdata must be implemented" end end # Compatibilty shim. class ServiceTest < Tester end # The main datastructure of the tester system. It represents one rrd file, but # usually multiple values at diverse measure times. # The method useable from the plugins are : # 1. addds : from the initrrdstruct method in the plugin. # 2. addtime : from the getdata method in the plugin. # 3. data[]= : from the getdata method, after addtime. class RRDStruct include Enumerable # A hash table of the attached RRDData objects. attr_accessor :data # The basic interval for the highest grained precision - usually defined # at a system level with the $conf[:step] value. attr_accessor :step # The name if the rrd ; will be expanded to $conf[:rrdspath] + '/' + name + '.rrd'. attr_accessor :name # The odest stored data. attr_reader :inittime # The list of RRA objects linked to this file. Usually defined at a system # level with the $conf[:rras] value. Can be overridden via the addrra function. attr_reader :rras # The RRDStruct is created empty with only a name. It will be filled by the # different methods of the plugins. def initialize(name) @name = name @fname = $conf[:rrdspath] + '/' + name + '.rrd' @tst = {} @rras = nil @data = {} @values = {} @nd = nil end # Adds a data source to the file ; returns a RRDData object. It must be called # in the Tester#initrrdstruct method. See the RRDData#initialize method for # the parameters. def addds(name, type = nil, heartbeat = nil, min = nil, max = nil) @data[name] = RRDData.new(name, type, heartbeat, min, max) end # Called internally to link a line of data to the RRDStruct. def attachdata(nd) @nd = [] if(@nd.nil?) @nd.push(nd) end # Must be called when a new timestamp is received. Returns a hash table with # the keys of the datasources as a key, and 'U' (undefined) for a balue. def addtime(time) if(@values[time].nil?) then @values[time] = {} @data.each do |k, v| @values[time][k] = 'U' end end @values[time] end # Adds a customized rra ; see RRA#new for the parameters ; if this method # is called once, the default $conf[:rras] is not used anymore. def addrra(funct = :AVERAGE, xff = 0.5, steps = nil, \ rows = nil, title = nil, start = nil) @rras ||= [] @rras.push(RRA.new(funct, xff, steps, rows, title, start)) end # Clears all data from this measure. def clear @values = {} end # Attaches a plugin to the RRDStruct when it takes the data. def attachtest(tst, id) @tst[id] = tst end # Called internally to instanciate the rrdstructs. def initrrdstruct @tst.each do |id, t| t.initrrdstruct(self, id) end end # Simple counter of the different data measure timestamps. Used for logging. def counttimes @values.keys.length end # Counter of the datasources. Used for logging. def countds @data.keys.length end # Counter of all the non unknown points. Used for logging. def countdp m = 0 @values.each_key do |k| @values[k].each_value do |v| m += 1 unless(v.nil? || v == 'U') end end m end # The data points can be enumerated. def each @data.each_key.sort do |t| yield(t, @data[t]) end end # Called internally to collect the data. def matchdata @nd.each do |s| @tst.each do |id, t| t.getdata(self, s, id) end end end # Called internally to create the rrd files themselves. def createfile unless(File.exists?(@fname) || @values.keys.length == 0) then @inittime = @values.keys.sort[0] - 1 @step = $conf[:step] @rras = $conf[:rras] if(@rras.nil?) system_d($conf[:rrdtool] + ' ' + to_rra) if($?.exitstatus != 0) then puts "Création #{@name} : #{$?.exitstatus}" end end end # Called internally to update the datapoints. def writerrd createfile if(@data.keys.length > 0 && @values.keys.length > 0) then c = to_rru system_d($conf[:rrdtool] + ' ' + c) if($?.exitstatus != 0) then puts "Update #{@name} : #{$?.exitstatus}" end end end # Called internally to generate the rrdtool create command line. def to_rra() c = [] c.push("create #{@fname}") c.push("-b #{@inittime}") c.push("-s #{@step}") @data.each_value do |v| c.push(v.to_rra) end @rras.each do |v| c.push(v.to_rra) end c.join(' ') end # Called internally to generate the rrdtool update command line. def to_rru() c = [] c.push("update #{@fname}") tm = [] @data.keys.sort.each do |k| tm.push(k) end c.push("-t #{tm.join(':')}") @values.keys.sort.each do |t| tv = t.to_s @values[t].keys.sort.each do |k| tv += ':' + @values[t][k].to_s end c.push(tv) end c.join(' ') end end # Object returned from the addds method, defines the different data to store in the rrd. class RRDData # The name of the datasource. attr_reader :name # The type of the datasource, one of :GAUGE, :COUNTER, # :ABSOLUTE or :DERIVE. attr_reader :type # A new datasource. The name is the key of the datasource in the file, the type # is one of the fundamental rrdtools types (by default, :GAUGE). # The heartbeat is the minimum interval at which the data must be updated. The # min and max values define the validity of the inserted data. If unspecified, # any values will be allowed. def initialize(name, type = nil, heartbeat = nil, min = nil, max = nil) @name = name @type = ( type.nil? ? :GAUGE : type ) @value = 'U' @heartbeat = ( heartbeat.nil? ? $conf[:heartbeat] : heartbeat ) @min = ( min.nil? ? 'U' : min ) @max = ( max.nil? ? 'U' : max ) end # Used internally to generate the command line for rrdtool create. def to_rra [ 'DS', @name, @type.to_s, @heartbeat.to_s, @min.to_s, @max.to_s ].join(':') end end main = TMain.new() main.readparam() main.loadcollecters() main.loadtesters() main.parsefile()