text: # coding=utf-8
  
  """
  Diamond handler that check if values are too high or too low, if so send an
  alert to a Sentry server
  
  This handler requires the Python module Raven:
  http://raven.readthedocs.org/en/latest/index.html
  
  To work this handler need a similar configuration:
  
  [[SentryHandler]]
  
  # Create a new project in Sentry and copy the DSN here:
  dsn = http://user:pass@hostname/id
  
  [[[load]]]
  
  name = Load Average
  # check for load average of the last 15 minutes
  path = loadavg.15
  max = 8.5
  
  [[[free_memory]]]
  
  name = Free Memory
  path = memory.MemFree
  min = 66020000
  """
  
  __author__ = 'Bruno Clermont'
  __email__ = 'bruno.clermont@gmail.com'
  
  import logging
  import re
  
  from Handler import Handler
  from diamond.collector import get_hostname
  from configobj import Section
  
  try:
      import raven.handlers.logging
  except ImportError:
      raven = None
  
  
  class InvalidRule(ValueError):
      """
      invalid rule
      """
      pass
  
  
  class BaseResult(object):
      """
      Base class for a Rule minimum/maximum check result
      """
      adjective = None
  
      def __init__(self, value, threshold):
          """
          @type value: float
          @param value: metric value
          @type threshold: float
          @param threshold: value that trigger a warning
          """
          self.value = value
          self.threshold = threshold
  
          if not raven:
              self.log.error('raven.handlers.logging import failed. '
                             'Handler disabled')
              self.enabled = False
              return
  
      @property
      def verbose_message(self):
          """return more complete message"""
          if self.threshold is None:
              return 'No threshold'
          return '%.1f is %s than %.1f' % (self.value,
                                           self.adjective,
                                           self.threshold)
  
      @property
      def _is_error(self):
          raise NotImplementedError('_is_error')
  
      @property
      def is_error(self):
          """
          for some reason python do this:
          >>> 1.0 > None
          True
          >>> 1.0 < None
          False
          so we just check if min/max is not None before return _is_error
          """
          if self.threshold is None:
              return False
          return self._is_error
  
      def __str__(self):
          name = self.__class__.__name__.lower()
          if self.threshold is None:
              return '%s: %.1f no threshold' % (name, self.value)
          return '%.1f (%s: %.1f)' % (self.value, name, self.threshold)
  
  
  class Minimum(BaseResult):
      """
      Minimum result
      """
      adjective = 'lower'
  
      @property
      def _is_error(self):
          """if it's too low"""
          return self.value < self.threshold
  
  
  class Maximum(BaseResult):
      """
      Maximum result
      """
      adjective = 'higher'
  
      @property
      def _is_error(self):
          """if it's too high"""
          return self.value > self.threshold
  
  
  class Rule(object):
      """
      Alert rule
      """
  
      def __init__(self, name, path, min=None, max=None):
          """
          @type name: string
          @param name: rule name, used to identify this rule in Sentry
          @type path: string
          @param path: un-compiled regular expression of the path of the rule
          @type min: string of float/int, int or float. will be convert to float
          @param min: optional minimal value that if value goes below it send
              an alert to Sentry
          @type max: string of float/int, int or float. will be convert to float
          @param max: optional maximal value that if value goes over it send
              an alert to Sentry
          """
          self.name = name
          # counters that can be used to debug rule
          self.counter_errors = 0
          self.counter_pass = 0
  
          # force min and max to be float
          try:
              self.min = float(min)
          except TypeError:
              self.min = None
          try:
              self.max = float(max)
          except TypeError:
              self.max = None
  
          if self.min is None and self.max is None:
              raise InvalidRule("%s: %s: both min and max are unset or invalid"
                                % (name, path))
  
          if self.min is not None and self.max is not None:
              if self.min > self.max:
                  raise InvalidRule("min %.1f is larger than max %.1f" % (
                      self.min, self.max))
  
          # compile path regular expression
          self.regexp = re.compile(r'(?P<prefix>.*)\.(?P<path>%s)$' % path)
  
      def process(self, metric, handler):
          """
          process a single diamond metric
          @type metric: diamond.metric.Metric
          @param metric: metric to process
          @type handler: diamond.handler.sentry.SentryHandler
          @param handler: configured Sentry graphite handler
          @rtype None
          """
          match = self.regexp.match(metric.path)
          if match:
              minimum = Minimum(metric.value, self.min)
              maximum = Maximum(metric.value, self.max)
  
              if minimum.is_error or maximum.is_error:
                  self.counter_errors += 1
                  message = "%s Warning on %s: %.1f" % (self.name,
                                                        handler.hostname,
                                                        metric.value)
                  culprit = "%s %s" % (handler.hostname, match.group('path'))
                  handler.raven_logger.error(message, extra={
                      'culprit': culprit,
                      'data': {
                          'metric prefix': match.group('prefix'),
                          'metric path': match.group('path'),
                          'minimum check': minimum.verbose_message,
                          'maximum check': maximum.verbose_message,
                          'metric original path': metric.path,
                          'metric value': metric.value,
                          'metric precision': metric.precision,
                          'metric timestamp': metric.timestamp,
                          'minimum threshold': self.min,
                          'maximum threshold': self.max,
                          'path regular expression': self.regexp.pattern,
                          'total errors': self.counter_errors,
                          'total pass': self.counter_pass,
                          'hostname': handler.hostname
                      }
                  }
                  )
              else:
                  self.counter_pass += 1
  
      def __repr__(self):
          return '%s: min:%s max:%s %s' % (self.name, self.min, self.max,
                                           self.regexp.pattern)
  
  
  class SentryHandler(Handler):
      """
      Diamond handler that check if a metric goes too low or too high
      """
  
      # valid key name in rules sub-section
      VALID_RULES_KEYS = ('name', 'path', 'min', 'max')
  
      def __init__(self, config=None):
          """
          @type config: configobj.ConfigObj
          """
          Handler.__init__(self, config)
          if not raven:
              return
          # init sentry/raven
          self.sentry_log_handler = raven.handlers.logging.SentryHandler(
              self.config['dsn'])
          self.raven_logger = logging.getLogger(self.__class__.__name__)
          self.raven_logger.addHandler(self.sentry_log_handler)
          self.configure_sentry_errors()
          self.rules = self.compile_rules()
          self.hostname = get_hostname(self.config)
          if not len(self.rules):
              self.log.warning("No rules, this graphite handler is unused")
  
      def get_default_config_help(self):
          """
          Returns the help text for the configuration options for this handler
          """
          config = super(SentryHandler, self).get_default_config_help()
  
          config.update({
              'dsn': '',
          })
  
          return config
  
      def get_default_config(self):
          """
          Return the default config for the handler
          """
          config = super(SentryHandler, self).get_default_config()
  
          config.update({
              'dsn': '',
          })
  
          return config
  
      def compile_rules(self):
          """
          Compile alert rules
          @rtype list of Rules
          """
          output = []
          # validate configuration, skip invalid section
          for key_name, section in self.config.items():
              rule = self.compile_section(section)
              if rule is not None:
                  output.append(rule)
          return output
  
      def compile_section(self, section):
          """
          Validate if a section is a valid rule
          @type section: configobj.Section
          @param section: section to validate
          @rtype Rule or None
          @return None if invalid
          """
          if section.__class__ != Section:
              # not a section, just skip
              return
  
          # name and path are mandatory
          keys = section.keys()
          for key in ('name', 'path'):
              if key not in keys:
                  self.log.warning("section %s miss key '%s' ignore", key,
                                   section.name)
                  return
  
          # just warn if invalid key in section
          for key in keys:
              if key not in self.VALID_RULES_KEYS:
                  self.log.warning("invalid key %s in section %s",
                                   key, section.name)
  
          # need at least a min or a max
          if 'min' not in keys and 'max' not in keys:
              self.log.warning("either 'min' or 'max' is defined in %s",
                               section.name)
              return
  
          # add rule to the list
          kwargs = {
              'name': section['name'],
              'path': section['path']
          }
          for argument in ('min', 'max'):
              try:
                  kwargs[argument] = section[argument]
              except KeyError:
                  pass
  
          # init rule
          try:
              return Rule(**kwargs)
          except InvalidRule, err:
              self.log.error(str(err))
  
      def configure_sentry_errors(self):
          """
          Configure sentry.errors to use the same loggers as the root handler
          @rtype: None
          """
          sentry_errors_logger = logging.getLogger('sentry.errors')
          root_logger = logging.getLogger()
          for handler in root_logger.handlers:
              sentry_errors_logger.addHandler(handler)
  
      def process(self, metric):
          """
          process a single metric
          @type metric: diamond.metric.Metric
          @param metric: metric to process
          @rtype None
          """
          for rule in self.rules:
              rule.process(metric, self)
  
      def __repr__(self):
          return "SentryHandler '%s' %d rules" % (
              self.sentry_log_handler.client.servers, len(self.rules))
  