Package Products :: Package ZenModel :: Module MinMaxThreshold |
[hide private]
[frames] | no frames]

# Source Code for Module Products.ZenModel.MinMaxThreshold

      1  ###########################################################################
      2  #
      3  # This program is part of Zenoss Core, an open source monitoring platform.
      4  # Copyright (C) 2007, Zenoss Inc.
      5  #
      6  # This program is free software; you can redistribute it and/or modify it
      7  # under the terms of the GNU General Public License version 2 or (at your
      8  # option) any later version as published by the Free Software Foundation.
      9  #
     10  # For complete information please visit: http://www.zenoss.com/oss/
     11  #
     12  ###########################################################################
     13
     14  __doc__= """MinMaxThreshold
     15  Make threshold comparisons dynamic by using TALES expresssions,
     16  rather than just number bounds checking.
     17  """
     18
     19  import os
     20  import rrdtool
     21  from AccessControl import Permissions
     22
     23  from Globals import InitializeClass
     24  from ThresholdClass import ThresholdClass
     25  from ThresholdInstance import ThresholdInstance, ThresholdContext
     26  from Products.ZenEvents import Event
     27  from Products.ZenEvents.ZenEventClasses import Perf_Snmp
     28  from Products.ZenUtils.ZenTales import talesEval, talesEvalStr
     29  from Products.ZenUtils.Utils import zenPath, rrd_daemon_running
     30  from Products.ZenEvents.Exceptions import pythonThresholdException, \
     31          rpnThresholdException
     32
     33  import logging
     34  log = logging.getLogger('zen.MinMaxCheck')
     35
     36  from Products.ZenUtils.Utils import unused, nanToNone
     37
     38  # Note:  this import is for backwards compatibility.
     39  # Import Products.ZenRRD.utils.rpneval directy.
     40  from Products.ZenRRD.utils import rpneval
     41
     42  NaN = float('nan')
     43
     44 -class MinMaxThreshold(ThresholdClass):
     45      """
     46      Threshold class that can evaluate RPNs and Python expressions
     47      """
     48
     49      minval = ""
     50      maxval = ""
     51      eventClass = Perf_Snmp
     52      severity = 3
     53      escalateCount = 0
     54
     55      _properties = ThresholdClass._properties + (
     56          {'id':'minval',        'type':'string',  'mode':'w'},
     57          {'id':'maxval',        'type':'string',  'mode':'w'},
     58          {'id':'eventClass',    'type':'string',  'mode':'w'},
     59          {'id':'severity',      'type':'int',     'mode':'w'},
     60          {'id':'escalateCount', 'type':'int',     'mode':'w'}
     61          )
     62
     63      factory_type_information = (
     64          {
     65          'immediate_view' : 'editRRDThreshold',
     66          'actions'        :
     67          (
     68          { 'id'            : 'edit'
     69            , 'name'          : 'Min/Max Threshold'
     70            , 'action'        : 'editRRDThreshold'
     71            , 'permissions'   : ( Permissions.view, )
     72            },
     73          )
     74          },
     75          )
     76
     77 -    def createThresholdInstance(self, context):
     78          """Return the config used by the collector to process min/max
     79          thresholds. (id, minval, maxval, severity, escalateCount)
     80          """
     81          mmt = MinMaxThresholdInstance(self.id,
     82                                        ThresholdContext(context),
     83                                        self.dsnames,
     84                                        minval=self.getMinval(context),
     85                                        maxval=self.getMaxval(context),
     86                                        eventClass=self.eventClass,
     87                                        severity=self.severity,
     88                                        escalateCount=self.escalateCount)
     89          return mmt
     90
     91 -    def getMinval(self, context):
     92          """Build the min value for this threshold.
     93          """
     94          minval = None
     95          if self.minval:
     96              try:
     97                  express = "python:%s" % self.minval
     98                  minval = talesEval(express, context)
     99              except:
    100                  msg= "User-supplied Python expression (%s) for minimum value caused error: %s" % \
    101                             ( self.minval,  self.dsnames )
    102                  log.error( msg )
    103                  raise pythonThresholdException(msg)
    104                  minval = None
    105          return nanToNone(minval)
    106
    107
    108 -    def getMaxval(self, context):
    109          """Build the max value for this threshold.
    110          """
    111          maxval = None
    112          if self.maxval:
    113              try:
    114                  express = "python:%s" % self.maxval
    115                  maxval = talesEval(express, context)
    116              except:
    117                  msg= "User-supplied Python expression (%s) for maximum value caused error: %s" % \
    118                             ( self.maxval,  self.dsnames )
    119                  log.error( msg )
    120                  raise pythonThresholdException(msg)
    121                  maxval = None
    122          return nanToNone(maxval)
    123
    124  InitializeClass(MinMaxThreshold)
    125  MinMaxThresholdClass = MinMaxThreshold
    126
    127
    128
    129 -class MinMaxThresholdInstance(ThresholdInstance):
    130      # Not strictly necessary, but helps when restoring instances from
    131      # pickle files that were not constructed with a count member.
    132      count = {}
    133
    134 -    def __init__(self, id, context, dpNames,
    135                   minval, maxval, eventClass, severity, escalateCount):
    136          self.count = {}
    137          self._context = context
    138          self.id = id
    139          self.minimum = minval
    140          self.maximum = maxval
    141          self.eventClass = eventClass
    142          self.severity = severity
    143          self.escalateCount = escalateCount
    144          self.dataPointNames = dpNames
    145          self._rrdInfoCache = {}
    146
    147 -    def name(self):
    148          "return the name of this threshold (from the ThresholdClass)"
    149          return self.id
    150
    151 -    def context(self):
    152          "Return an identifying context (device, or device and component)"
    153          return self._context
    154
    155 -    def dataPoints(self):
    156          "Returns the names of the datapoints used to compute the threshold"
    157          return self.dataPointNames
    158
    159 -    def rrdInfoCache(self, dp):
    160          if dp in self._rrdInfoCache:
    161              return self._rrdInfoCache[dp]
    162
    163          daemon_args = ()
    164          daemon = rrd_daemon_running()
    165          if daemon:
    166              daemon_args = ('--daemon', daemon)
    167
    168          data = rrdtool.info(self.context().path(dp), *daemon_args)
    169          # handle both old and new style RRD versions
    170          try:
    171              # old style 1.2.x
    172              value = data['step'], data['ds']['ds0']['type']
    173          except KeyError:
    174              # new style 1.3.x
    175              value = data['step'], data['ds[ds0].type']
    176          self._rrdInfoCache[dp] = value
    177          return value
    178
    179 -    def countKey(self, dp):
    180          return ':'.join(self.context().key()) + ':' + dp
    181
    182 -    def getCount(self, dp):
    183          countKey = self.countKey(dp)
    184          if not countKey in self.count:
    185              return None
    186          return self.count[countKey]
    187
    188 -    def incrementCount(self, dp):
    189          countKey = self.countKey(dp)
    190          if not countKey in self.count:
    191              self.resetCount(dp)
    192          self.count[countKey] += 1
    193          return self.count[countKey]
    194
    195 -    def resetCount(self, dp):
    196          self.count[self.countKey(dp)] = 0
    197
    198 -    def fetchLastValue(self, dp, cycleTime):
    199          """
    200          Fetch the most recent value for a data point from the RRD file.
    201          """
    202          startStop, names, values = rrdtool.fetch(self.context().path(dp),
    203              'AVERAGE', '-s', 'now-%d' % (cycleTime*2), '-e', 'now')
    204          values = [ v[0] for v in values if v[0] is not None ]
    205          if values: return values[-1]
    206
    207 -    def check(self, dataPoints):
    208          """The given datapoints have been updated, so re-evaluate.
    209          returns events or an empty sequence"""
    210          unused(dataPoints)
    211          result = []
    212          for dp in self.dataPointNames:
    213              cycleTime, rrdType = self.rrdInfoCache(dp)
    214              result.extend(self.checkRange(
    215                  dp, self.fetchLastValue(dp, cycleTime)))
    216          return result
    217
    218 -    def checkRaw(self, dataPoint, timeOf, value):
    219          """A new datapoint has been collected, use the given _raw_
    220          value to re-evalue the threshold."""
    221          unused(timeOf)
    222          result = []
    223          if value is None: return result
    224          try:
    225              cycleTime, rrdType = self.rrdInfoCache(dataPoint)
    226          except Exception:
    227              log.exception('Unable to read RRD file for %s' % dataPoint)
    228              return result
    229          if rrdType != 'GAUGE' and value is None:
    230              value = self.fetchLastValue(dataPoint, cycleTime)
    231          result.extend(self.checkRange(dataPoint, value))
    232          return result
    233
    234 -    def checkRange(self, dp, value):
    235          'Check the value for min/max thresholds'
    236          log.debug("Checking %s %s against min %s and max %s",
    237                    dp, value, self.minimum, self.maximum)
    238          if value is None:
    239              return []
    240          if isinstance(value, basestring):
    241              value = float(value)
    242          thresh = None
    243
    244          # Handle all cases where both minimum and maximum are set.
    245          if self.maximum is not None and self.minimum is not None:
    246              if self.maximum >= self.minimum and value > self.maximum:
    247                  thresh = self.maximum
    248                  how = 'exceeded'
    249              if self.maximum >= self.minimum and value < self.minimum:
    250                  thresh = self.maximum
    251                  how = 'not met'
    252              elif self.maximum < self.minimum \
    253                  and (value < self.minimum and value > self.maximum):
    254                  thresh = self.maximum
    255                  how = 'violated'
    256
    257          # Handle simple cases where only minimum or maximum is set.
    258          elif self.maximum is not None and value > self.maximum:
    259              thresh = self.maximum
    260              how = 'exceeded'
    261          elif self.minimum is not None and value < self.minimum:
    262              thresh = self.minimum
    263              how = 'not met'
    264
    265          if thresh is not None:
    266              severity = self.severity
    267              count = self.incrementCount(dp)
    268              if self.escalateCount and count >= self.escalateCount:
    269                  severity = min(severity + 1, 5)
    270              summary = 'threshold of %s %s: current value %f' % (
    271                  self.name(), how, float(value))
    272              return self.processEvent(dict(
    273                           device=self.context().deviceName,
    274                           summary=summary,
    275                           eventKey=self.id,
    276                           eventClass=self.eventClass,
    277                           component=self.context().componentName,
    278                           how=how,
    279                           min=self.minimum,
    280                           max=self.maximum,
    281                           current=value,
    282                           severity=severity))
    283          else:
    284              count = self.getCount(dp)
    285              if count is None or count > 0:
    286                  summary = 'threshold of %s restored: current value %f' % (
    287                      self.name(), value)
    288                  self.resetCount(dp)
    289                  return self.processClearEvent(dict(
    290                               device=self.context().deviceName,
    291                               summary=summary,
    292                               eventKey=self.id,
    293                               eventClass=self.eventClass,
    294                               component=self.context().componentName,
    295                               min=self.minimum,
    296                               max=self.maximum,
    297                               current=value,
    298                               severity=Event.Clear))
    299          return []
    300
    301 -    def processEvent(self, evt):
    302          """
    303          When a threshold condition is violated, pre-process it for (possibly) nicer
    304          formatting or more complicated logic.
    305
    306          @paramater evt: event
    307          @type evt: dictionary
    308          @rtype: list of dictionaries
    309          """
    310          return [evt]
    311
    312 -    def processClearEvent(self, evt):
    313          """
    314          When a threshold condition is restored, pre-process it for (possibly) nicer
    315          formatting or more complicated logic.
    316
    317          @paramater evt: event
    318          @type evt: dictionary
    319          @rtype: list of dictionaries
    320          """
    321          return [evt]
    322
    323 -    def raiseRPNExc( self ):
    324          """
    325          Raise an RPN exception, taking care to log all details.
    326          """
    327          msg= "The following RPN exception is from user-supplied code."
    328          log.exception( msg )
    329          raise rpnThresholdException(msg)
    330
    331
    332 -    def getGraphElements(self, template, context, gopts, namespace, color,
    333                           legend, relatedGps):
    334          """Produce a visual indication on the graph of where the
    335          threshold applies."""
    336          unused(template, namespace)
    337          if not color.startswith('#'):
    338              color = '#%s' % color
    339          minval = self.minimum
    340          if minval is None:
    341              minval = NaN
    342          maxval = self.maximum
    343          if maxval is None:
    344              maxval = NaN
    345          if not self.dataPointNames:
    346              return gopts
    347          gp = relatedGps[self.dataPointNames[0]]
    348
    349          # Attempt any RPN expressions
    350          rpn = getattr(gp, 'rpn', None)
    351          if rpn:
    352              try:
    353                  rpn = talesEvalStr(rpn, context)
    354              except:
    355                  self.raiseRPNExc()
    356                  return gopts
    357
    358              try:
    359                  minval = rpneval(minval, rpn)
    360              except:
    361                  minval= 0
    362                  self.raiseRPNExc()
    363
    364              try:
    365                  maxval = rpneval(maxval, rpn)
    366              except:
    367                  maxval= 0
    368                  self.raiseRPNExc()
    369
    370          minstr = self.setPower(minval)
    371          maxstr = self.setPower(maxval)
    372
    373          minval = nanToNone(minval)
    374          maxval = nanToNone(maxval)
    375          if legend:
    376              gopts.append(
    377                  "HRULE:%s%s:%s\\j" % (minval or maxval, color, legend))
    378          elif minval is not None and maxval is not None:
    379              if minval == maxval:
    380                  gopts.append(
    381                      "HRULE:%s%s:%s not equal to %s\\j" % (minval, color,
    382                          self.getNames(relatedGps), minstr))
    383              elif minval < maxval:
    384                  gopts.append(
    385                      "HRULE:%s%s:%s not within %s and %s\\j" % (minval, color,
    386                          self.getNames(relatedGps), minstr, maxstr))
    387                  gopts.append("HRULE:%s%s" % (maxval, color))
    388              elif minval > maxval:
    389                  gopts.append(
    390                      "HRULE:%s%s:%s between %s and %s\\j" % (minval, color,
    391                          self.getNames(relatedGps), maxstr, minstr))
    392                  gopts.append("HRULE:%s%s" % (maxval, color))
    393          elif minval is not None :
    394              gopts.append(
    395                  "HRULE:%s%s:%s less than %s\\j" % (minval, color,
    396                      self.getNames(relatedGps), minstr))
    397          elif maxval is not None:
    398              gopts.append(
    399                  "HRULE:%s%s:%s greater than %s\\j" % (maxval, color,
    400                      self.getNames(relatedGps), maxstr))
    401
    402          return gopts
    403
    404
    405 -    def getNames(self, relatedGps):
    406          names = list(set(x.split('_', 1)[1] for x in self.dataPointNames))
    407          names.sort()
    408          return ', '.join(names)
    409
    410 -    def setPower(self, number):
    411          powers = ("k", "M", "G")
    412          if number < 1000: return number
    413          for power in powers:
    414              number = number / 1000.0
    415              if number < 1000:
    416                  return "%0.2f%s" % (number, power)
    417          return "%.2f%s" % (number, powers[-1])
    418
    419  from twisted.spread import pb
    420  pb.setUnjellyableForClass(MinMaxThresholdInstance, MinMaxThresholdInstance)
    421