stacktach-winchester/winchester/definition.py

285 lines
11 KiB
Python

# Copyright (c) 2014 Dark Secret Software Inc.
# Copyright (c) 2015 Rackspace
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import datetime
import fnmatch
import logging
import six
import timex
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, debug_group):
if self.trait_name not in event:
return debug_group.mismatch("not %s" % self.trait_name)
value = event[self.trait_name]
if self.op == '=':
return debug_group.check(value == self.value, "== failed")
elif self.op == '>':
return debug_group.check(value > self.value, "> failed")
elif self.op == '<':
return debug_group.check(value < self.value, "< failed")
return debug_group.mismatch("Criterion match() fall-thru")
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, debug_group):
if self.trait_name not in event:
return debug_group.mismatch("Time: not '%s'" % self.trait_name)
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 debug_group.mismatch("Time: no referenced trait")
return debug_group.check(value in timerange, "Not 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, debug_group):
if not self.match_type(event['event_type']):
return debug_group.mismatch("Wrong event type")
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 debug_group.mismatch("No timestamp trait")
if event['timestamp'] not in t:
return debug_group.mismatch("Not time yet.")
if not self.traits:
return debug_group.match()
return all(criterion.match(event, debug_group) for
criterion in self.traits.values())
class TriggerDefinition(object):
def __init__(self, config, debug_manager):
if 'name' not in config:
raise DefinitionError("Required field in trigger definition not "
"specified 'name'")
self.name = config['name']
self.debug_level = int(config.get('debug_level', 0))
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']]
if debug_manager:
self.set_debugger(debug_manager)
def set_debugger(self, debug_manager):
self.debugger = debug_manager.get_debugger(self)
def match(self, event):
# all distinguishing traits must exist to match.
group = self.debugger.get_group("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:
group.mismatch("not '%s'" % trait_name)
return None
for criteria in self.match_criteria:
if criteria.match(event, group):
group.match()
return criteria
group.mismatch("No matching 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):
group = self.debugger.get_group("Fire")
for criteria in self.fire_criteria:
matches = 0
for event in events:
if criteria.match(event, group):
matches += 1
if matches >= criteria.number:
break
if matches < criteria.number:
return group.mismatch("Not enough matching criteria")
return group.match()