Source code for performance.driver.core.eventfilters

import re
import logging
from threading import Timer

from performance.driver.core.events import isEventMatching
from performance.driver.core.utils import parseTimeExpr

DSL_TOKENS = re.compile(r'(\*|\w+)(?:\[(.*?)\])?(\:(?:\w[\:\w\(\),]*))?')
DSL_ATTRIB = re.compile(r'(?:^|,)([\w\.\']+)([=~!><]+)([^,]+)')
DSL_FLAGS = re.compile(r'\:([^\:]+)')

REPLACE_DICT = re.compile(r"\.\'(.*?)\'")

global_single_events = {}


def tokenizeExpression(expression):
  """
  Tokenize expression
  """

  # The test boolean value all test conditions should be test against
  # (This helps us implement the negation opeartion below)
  testTrue = True

  # In case the expression is a negation pop it now
  if expression.startswith(':not('):
    if not expression.endswith(')'):
      raise ValueError(
          'Unable to find closing parenthesis on "{}"'.format(expression)
        )
    testTrue = False
    expression = expression[5:-1]

  # Find all the events to match against
  matches = DSL_TOKENS.findall(expression)
  if not matches:
    raise ValueError(
        'The given expression "{}" is not a valid event filter DSL'.format(
            expression))

  # Process event matches
  events = []
  for (event, exprAttrib, flags) in matches:

    # Calculate event id
    eid = event + ':' + exprAttrib + ':' + flags

    # Process sub-tokens
    flags = list(map(lambda x: x.lower(), DSL_FLAGS.findall(str(flags))))

    # Flag parameters
    flagParameters = {}
    for i in range(0, len(flags)):
      flag = flags[i]
      if '(' in flag:
        (flag, params) = flag.split('(')
        if not params.endswith(')'):
          raise ValueError(
              'Mismatched closing parenthesis in flag {}'.format(flags[i]))
        flags[i] = flag
        flagParameters[flag] = params[:-1]

    # Compile attribute selectors
    attrib = []
    if exprAttrib:
      for (left, op, right) in DSL_ATTRIB.findall(exprAttrib):

        # Expand .'xx' to ['xxx']
        left = REPLACE_DICT.sub(lambda x: '["{}"]'.format(x.group(1)), left)

        # Shorthand some ops
        if op == "=":
          op = "=="

        # Handle loose regex match
        if op == "~=":
          attrib.append(
              eval('lambda event: not regex.search(str(event.{})) is None'.
                   format(left), {'regex': re.compile(right)}))

        # Handle exact regex match
        elif op == "~==":
          attrib.append(
              eval('lambda event: not regex.match(str(event.{})) is None'.
                   format(left), {'regex': re.compile(right)}))

        # Handle `in` operator
        elif op == "<~":
          attrib.append(lambda event: right in list(getattr(event, left)))

        # Handle operator match
        else:
          if not right.isnumeric() and not right[0] in ('"', "'"):
            right = '"{}"'.format(right.replace('"', '\\"'))
          attrib.append(
              eval('lambda event: event.{} {} {}'.format(left, op, right)))

    # Collect flags
    events.append((event, attrib, flags, flagParameters, eid, testTrue))

  # Return events
  return events


class EventFilterSession:
  """
  An event filter session
  """

  def __init__(self, filter, traceids, callback):
    self.foundEvent = None
    self.triggerAtExit = False
    self.filter = filter
    self.traceids = traceids
    self.callback = callback
    self.timer = None
    self.counterGroups = {}
    self.logger = logging.getLogger('EventFilter<{}>'.format(filter.expression))

    # Immediately call-back to matched filters
    for (eventSpec, attribChecks, flags, flagParameters,
         eid, testTrue) in self.filter.events:
      if 'single' in flags:
        if eid in global_single_events:
          callback(global_single_events[eid])

  def afterTimerCallback(self):
    """
    Callback for the :after(x) selector
    """
    self.timer = None
    self.callback(self.foundEvent)

  def handle(self, event):
    """
    Handle the incoming event
    """
    for (eventSpec, attribChecks, flags, flagParameters,
         eid, TRUE) in self.filter.events:

      # (NOTE: `TRUE` is True when the filter is positive, or False if it's
      # negated using a :not(..) expression)

      # Handle all events or matching events
      if TRUE == (eventSpec != "*" and not isEventMatching(event, eventSpec)):
        continue

      # Handle attributes
      attribCheckFailed = False
      for attribCheckFn in attribChecks:
        try:
          if not attribCheckFn(event):
            attribCheckFailed = True
            break
        except KeyError:
          attribCheckFailed = True
          break
      if TRUE == attribCheckFailed:
        continue

      # Handle trace ID
      if TRUE == (not self.traceids is None and not 'notrace' in flags and not
          event.hasTraces(self.traceids)):
        continue

      # Handle order
      if 'first' in flags:
        if self.foundEvent is None:
          self.foundEvent = event
          self.callback(event)
        break
      if 'last' in flags:
        self.foundEvent = event
        self.triggerAtExit = True
        break
      if 'single' in flags:
        if eid in global_single_events:
          break
        self.foundEvent = event
        self.callback(event)
        global_single_events[eid] = event
        break
      if 'after' in flags:
        time = parseTimeExpr(flagParameters['after'])
        if time is None:
          raise ValueError(
              'Event selector `:after({})` contains an invalid time expression'.
              format(flagParameters['after']))

        # Restart timer to call the callback after the given delay
        if self.timer:
          self.timer.cancel()
        self.logger.debug('(Re)Starting timer after {} sec'.format(time))
        self.foundEvent = event
        self.timer = Timer(time, self.afterTimerCallback)
        self.timer.start()
        break
      if 'nth' in flags:
        parts = flagParameters['nth'].split(',')
        nth = int(parts[0])
        grp = eid
        if len(parts) > 1:
          grp = parts[1]
        if not grp in self.counterGroups:
          self.counterGroups[grp] = 0
        self.counterGroups[grp] += 1
        self.logger.debug('Found {} hits on group {} ({} requested)'.format(
          self.counterGroups[grp], grp, nth))
        if nth == self.counterGroups[grp]:
          self.logger.debug('Found all {} hits found'.format(nth))
          self.foundEvent = event
          self.callback(event)
        break

      # Fire callback
      self.callback(event)
      break

  def finalize(self):
    """
    Called when a tracking session is finalised
    """

    # If we have an active :after() timer, flush it now
    if self.timer:
      self.afterTimerCallback()

    # Submit the last event
    if self.triggerAtExit and self.foundEvent:
      self.callback(self.foundEvent)

  def __str__(self):
    return '<Session[{}], traceid={}>'.format(self.filter.expression,
                                              self.traceids)

  def __repr__(self):
    return '<Session[{}], traceid={}>'.format(self.filter.expression,
                                              self.traceids)


[docs]class EventFilter: """ Various trackers in *DC/OS Performance Test Driver* are operating purely on events. Therefore it's some times needed to use a more elaborate selector in order to filter the correct events. The following filter expression is currently supported and are closely modeled around the CSS syntax: .. code-block:: css EventName[attrib1=value,attrib2=value,...]:selector1:selector2:... Where: * _Event Name_ is the name of the event or ``*`` if you want to match any event. * _Attributes_ is a comma-separated list of ``<attrib> <operator> <value>`` values. For example: ``method==post``. The following table summarises the different operators you can use for the attributes. +-----------------+----------------------------------------------------+ | Operator | Description | +=================+====================================================+ | ``=`` or ``==`` | Equal (case sensitive for strings) | +-----------------+----------------------------------------------------+ | ``!=`` | Not equal | +-----------------+----------------------------------------------------+ | ``>``, ``>=`` | Grater than / Grater than or equal | +-----------------+----------------------------------------------------+ | ``<``, ``<=`` | Less than / Less than or equal | +-----------------+----------------------------------------------------+ | ``~=`` | Partial regular expression match | +-----------------+----------------------------------------------------+ | ``~==`` | Exact regular expression match | +-----------------+----------------------------------------------------+ | ``<~`` | Value in list or key in dictionary (like ``in``) | +-----------------+----------------------------------------------------+ * _Selector_ specifies which event out of many similar to chose. Valid selectors are: +-----------------+----------------------------------------------------+ | Selector | Description | +=================+====================================================+ | ``:first`` | Match the first event in the tracking session | +-----------------+----------------------------------------------------+ | ``:last`` | Match the last event in the tracking session | +-----------------+----------------------------------------------------+ | ``:nth(n)`` | Match the n-th event in the tracking session. If | | ``:nth(n,grp)`` | a ``grp`` parameter is specified, the counter will | | | be groupped with the given indicator. | +-----------------+----------------------------------------------------+ | ``:single`` | Match a single event, globally. After the first | | | match all other usages accept by default. | +-----------------+----------------------------------------------------+ | ``:after(Xs)`` | Trigger after X seconds after the last event | +-----------------+----------------------------------------------------+ | ``:notrace`` | Ignore the trace ID matching and accept any event, | | | even if they do not belong in the trace session. | +-----------------+----------------------------------------------------+ For example, to match every ``HTTPRequestEvent``: .. code-block:: css HTTPRequestEvent Or, to match every POST ``HTTPRequestEvent``: .. code-block:: css HTTPRequestEvent[method=post] Or, to match the last ``HTTPResponseEndEvent`` .. code-block:: css HTTPResponseEndEvent:last Or, to match the ``HTTPRequestStartEvent`` that contains the string "foo": .. code-block:: css HTTPResponseEndEvent[body~=foo] Or match any first event: .. code-block:: css *:first """ def __init__(self, expression): self.expression = expression self.events = tokenizeExpression(expression) def start(self, traceids, callback): """ Start a tracking session with the given trace ID """ # Make sure trace IDs is always a list if (traceids != None) and (type(traceids) not in (list, set, tuple)): traceids = set([traceids]) return EventFilterSession(self, traceids, callback) def __str__(self): return '<Filter[{}]>'.format(self.expression) def __repr__(self): return '<Filter[{}]>'.format(self.expression)