
Full trigger logic now works. Added pipeline workers, and test handler. Added example configs Lots of unittests.
251 lines
8.9 KiB
Python
251 lines
8.9 KiB
Python
import logging
|
|
import collections
|
|
import datetime
|
|
import six
|
|
import timex
|
|
import fnmatch
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DefinitionError(Exception):
|
|
pass
|
|
|
|
|
|
def filter_event_timestamps(event):
|
|
return dict((trait, value) for trait, value in event.items()
|
|
if isinstance(value, datetime.datetime))
|
|
|
|
|
|
class Criterion(object):
|
|
|
|
@classmethod
|
|
def get_from_expression(cls, expression, trait_name):
|
|
if isinstance(expression, collections.Mapping):
|
|
if len(expression) != 1:
|
|
raise DefinitionError("Only exactly one type of match is allowed per criterion expression")
|
|
ctype = expression.keys()[0]
|
|
expr = expression[ctype]
|
|
if ctype == 'int':
|
|
return NumericCriterion(expr, trait_name)
|
|
elif ctype =='float':
|
|
return FloatCriterion(expr, trait_name)
|
|
elif ctype == 'datetime':
|
|
return TimeCriterion(expr, trait_name)
|
|
elif ctype == 'string' or ctype == 'text':
|
|
return Criterion(expr, trait_name)
|
|
else:
|
|
# A constant. -mdragon
|
|
return Criterion(expression, trait_name)
|
|
|
|
def __init__(self, expr, trait_name):
|
|
self.trait_name = trait_name
|
|
#match a constant
|
|
self.op = '='
|
|
self.value = expr
|
|
|
|
def match(self, event):
|
|
if self.trait_name not in event:
|
|
return False
|
|
value = event[self.trait_name]
|
|
if self.op == '=':
|
|
return value == self.value
|
|
elif self.op == '>':
|
|
return value > self.value
|
|
elif self.op == '<':
|
|
return value < self.value
|
|
|
|
|
|
class NumericCriterion(Criterion):
|
|
|
|
def __init__(self, expr, trait_name):
|
|
self.trait_name = trait_name
|
|
if not isinstance(expr, six.string_types):
|
|
self.op = '='
|
|
self.value = expr
|
|
else:
|
|
expr = expr.strip().split(None, 1)
|
|
if len(expr) == 2:
|
|
self.op = expr[0]
|
|
value = expr[1].strip()
|
|
elif len(expr) == 1:
|
|
self.op = '='
|
|
value = expr[0]
|
|
else:
|
|
raise DefinitionError('Invalid numeric criterion.')
|
|
try:
|
|
self.value = self._convert(value)
|
|
except ValueError:
|
|
raise DefinitionError('Invalid numeric criterion.')
|
|
|
|
def _convert(self, value):
|
|
return int(value)
|
|
|
|
|
|
class FloatCriterion(NumericCriterion):
|
|
|
|
def _convert(self, value):
|
|
return float(value)
|
|
|
|
|
|
class TimeCriterion(Criterion):
|
|
|
|
def __init__(self, expression, trait_name):
|
|
self.trait_name = trait_name
|
|
self.time_expr = timex.parse(expression)
|
|
|
|
def match(self, event):
|
|
if self.trait_name not in event:
|
|
return False
|
|
value = event[self.trait_name]
|
|
try:
|
|
timerange = self.time_expr(**filter_event_timestamps(event))
|
|
except timex.TimexExpressionError:
|
|
# the event doesn't contain a trait referenced in the expression.
|
|
return False
|
|
return value in timerange
|
|
|
|
|
|
class Criteria(object):
|
|
def __init__(self, config):
|
|
self.included_types = []
|
|
self.excluded_types = []
|
|
if 'event_type' in config:
|
|
event_types = config['event_type']
|
|
if isinstance(event_types, six.string_types):
|
|
event_types = [event_types]
|
|
for t in event_types:
|
|
if t.startswith('!'):
|
|
self.excluded_types.append(t[1:])
|
|
else:
|
|
self.included_types.append(t)
|
|
else:
|
|
self.included_types.append('*')
|
|
if self.excluded_types and not self.included_types:
|
|
self.included_types.append('*')
|
|
if 'number' in config:
|
|
self.number = config['number']
|
|
else:
|
|
self.number = 1
|
|
if 'timestamp' in config:
|
|
self.timestamp = timex.parse(config['timestamp'])
|
|
else:
|
|
self.timestamp = None
|
|
self.map_distinguished_by = dict()
|
|
if 'map_distinguished_by' in config:
|
|
self.map_distinguished_by = config['map_distinguished_by']
|
|
self.traits = dict()
|
|
if 'traits' in config:
|
|
for trait, criterion in config['traits'].items():
|
|
self.traits[trait] = Criterion.get_from_expression(criterion, trait)
|
|
|
|
def included_type(self, event_type):
|
|
return any(fnmatch.fnmatch(event_type, t) for t in self.included_types)
|
|
|
|
def excluded_type(self, event_type):
|
|
return any(fnmatch.fnmatch(event_type, t) for t in self.excluded_types)
|
|
|
|
def match_type(self, event_type):
|
|
return (self.included_type(event_type)
|
|
and not self.excluded_type(event_type))
|
|
|
|
def match(self, event):
|
|
if not self.match_type(event['event_type']):
|
|
return False
|
|
if self.timestamp:
|
|
try:
|
|
t = self.timestamp(**filter_event_timestamps(event))
|
|
except timex.TimexExpressionError:
|
|
# the event doesn't contain a trait referenced in the expression.
|
|
return False
|
|
if event['timestamp'] not in t:
|
|
return False
|
|
if not self.traits:
|
|
return True
|
|
return all(criterion.match(event) for
|
|
criterion in self.traits.values())
|
|
|
|
|
|
class TriggerDefinition(object):
|
|
|
|
def __init__(self, config):
|
|
if 'name' not in config:
|
|
raise DefinitionError("Required field in trigger definition not "
|
|
"specified 'name'")
|
|
self.name = config['name']
|
|
self.distinguished_by = config.get('distinguished_by', [])
|
|
for dt in self.distinguished_by:
|
|
if isinstance(dt, collections.Mapping):
|
|
if len(dt) > 1:
|
|
raise DefinitionError("Invalid distinguising expression "
|
|
"%s. Only one trait allowed in an expression" % str(dt))
|
|
self.fire_delay = config.get('fire_delay', 0)
|
|
if 'expiration' not in config:
|
|
raise DefinitionError("Required field in trigger definition not "
|
|
"specified 'expiration'")
|
|
self.expiration = timex.parse(config['expiration'])
|
|
self.fire_pipeline = config.get('fire_pipeline')
|
|
self.expire_pipeline = config.get('expire_pipeline')
|
|
if not self.fire_pipeline and not self.expire_pipeline:
|
|
raise DefinitionError("At least one of: 'fire_pipeline' or "
|
|
"'expire_pipeline' must be specified in a "
|
|
"trigger definition.")
|
|
if 'fire_criteria' not in config:
|
|
raise DefinitionError("Required criteria in trigger definition not "
|
|
"specified 'fire_criteria'")
|
|
self.fire_criteria = [Criteria(c) for c in config['fire_criteria']]
|
|
if 'match_criteria' not in config:
|
|
raise DefinitionError("Required criteria in trigger definition not "
|
|
"specified 'match_criteria'")
|
|
self.match_criteria = [Criteria(c) for c in config['match_criteria']]
|
|
self.load_criteria = []
|
|
if 'load_criteria' in config:
|
|
self.load_criteria = [Criteria(c) for c in config['load_criteria']]
|
|
|
|
def match(self, event):
|
|
# all distinguishing traits must exist to match.
|
|
for dt in self.distinguished_by:
|
|
if isinstance(dt, collections.Mapping):
|
|
trait_name = dt.keys()[0]
|
|
else:
|
|
trait_name = dt
|
|
if trait_name not in event:
|
|
return None
|
|
for criteria in self.match_criteria:
|
|
if criteria.match(event):
|
|
return criteria
|
|
return None
|
|
|
|
def get_distinguishing_traits(self, event, matching_criteria):
|
|
dist_traits = dict()
|
|
for dt in self.distinguished_by:
|
|
d_expr = None
|
|
if isinstance(dt, collections.Mapping):
|
|
trait_name = dt.keys()[0]
|
|
d_expr = timex.parse(dt[trait_name])
|
|
else:
|
|
trait_name = dt
|
|
event_trait_name = matching_criteria.map_distinguished_by.get(trait_name, trait_name)
|
|
if d_expr is not None:
|
|
dist_traits[trait_name] = d_expr(timestamp=event[event_trait_name])
|
|
else:
|
|
dist_traits[trait_name] = event[event_trait_name]
|
|
return dist_traits
|
|
|
|
def get_fire_timestamp(self, timestamp):
|
|
return timestamp + datetime.timedelta(seconds=self.fire_delay)
|
|
|
|
def should_fire(self, events):
|
|
for criteria in self.fire_criteria:
|
|
matches = 0
|
|
for event in events:
|
|
if criteria.match(event):
|
|
matches += 1
|
|
if matches >= criteria.number:
|
|
break
|
|
if matches < criteria.number:
|
|
return False
|
|
return True
|
|
|