mirror of
https://github.com/valitydev/yandex-tank.git
synced 2024-11-06 18:35:18 +00:00
360 lines
12 KiB
Python
360 lines
12 KiB
Python
from Tank import Utils
|
|
from Tank.Core import AbstractPlugin
|
|
from Tank.Plugins.Aggregator import AggregatorPlugin, AggregateResultListener
|
|
from Tank.Plugins.ConsoleOnline import AbstractInfoWidget, ConsoleOnlinePlugin
|
|
import copy
|
|
import logging
|
|
import re
|
|
|
|
class AutostopPlugin(AbstractPlugin, AggregateResultListener):
|
|
SECTION = 'autostop'
|
|
|
|
def __init__(self, core):
|
|
AbstractPlugin.__init__(self, core)
|
|
self.cause_criteria = None
|
|
self.criterias = []
|
|
self.custom_criterias = []
|
|
self.counting = []
|
|
|
|
@staticmethod
|
|
def get_key():
|
|
return __file__;
|
|
|
|
def get_counting(self):
|
|
return self.counting
|
|
|
|
def add_counting(self, obj):
|
|
self.counting += [obj]
|
|
|
|
|
|
def add_criteria_class(self, criteria_class):
|
|
self.custom_criterias += [criteria_class]
|
|
|
|
def configure(self):
|
|
aggregator = self.core.get_plugin_of_type(AggregatorPlugin)
|
|
aggregator.add_result_listener(self)
|
|
self.criteria_str = " ".join(self.get_option("autostop", '').split("\n"))
|
|
self.add_criteria_class(AvgTimeCriteria)
|
|
self.add_criteria_class(NetCodesCriteria)
|
|
self.add_criteria_class(HTTPCodesCriteria)
|
|
self.add_criteria_class(QuantileCriteria)
|
|
|
|
def prepare_test(self):
|
|
for criteria_str in self.criteria_str.strip().split(")"):
|
|
if not criteria_str:
|
|
continue
|
|
self.log.debug("Criteria string: %s", criteria_str)
|
|
self.criterias.append(self.create_criteria(criteria_str))
|
|
|
|
self.log.debug("Criteria object: %s", self.criterias)
|
|
|
|
try:
|
|
console = self.core.get_plugin_of_type(ConsoleOnlinePlugin)
|
|
except Exception, ex:
|
|
self.log.debug("Console not found: %s", ex)
|
|
console = None
|
|
|
|
if console:
|
|
console.add_info_widget(AutostopWidget(self))
|
|
|
|
def is_test_finished(self):
|
|
if self.cause_criteria:
|
|
self.log.info("Autostop criteria requested test stop: %s", self.cause_criteria.explain())
|
|
return self.cause_criteria.get_rc()
|
|
else:
|
|
return -1
|
|
|
|
def create_criteria(self, criteria_str):
|
|
parsed = criteria_str.split("(")
|
|
type_str = parsed[0].strip().lower()
|
|
parsed[1] = parsed[1].split(")")[0].strip()
|
|
|
|
for criteria_class in self.custom_criterias:
|
|
if criteria_class.get_type_string() == type_str:
|
|
return criteria_class(self, parsed[1])
|
|
raise ValueError("Unsupported autostop criteria type: %s" % criteria_str)
|
|
|
|
def aggregate_second(self, second_aggregate_data):
|
|
self.counting = []
|
|
for criteria in self.criterias:
|
|
if criteria.notify(second_aggregate_data):
|
|
self.log.debug("Autostop criteria requested test stop: %s", criteria)
|
|
self.cause_criteria = criteria
|
|
|
|
class AutostopWidget(AbstractInfoWidget):
|
|
def __init__(self, sender):
|
|
AbstractInfoWidget.__init__(self)
|
|
self.owner = sender
|
|
|
|
def get_index(self):
|
|
return 25
|
|
|
|
def render(self, screen):
|
|
res = []
|
|
candidates = self.owner.get_counting()
|
|
for candidate in candidates:
|
|
text, perc = candidate.widget_explain()
|
|
if perc >= 0.95:
|
|
res += [screen.markup.RED_DARK + text + screen.markup.RESET]
|
|
elif perc >= 0.8:
|
|
res += [screen.markup.RED + text + screen.markup.RESET]
|
|
elif perc >= 0.5:
|
|
res += [screen.markup.YELLOW + text + screen.markup.RESET]
|
|
else:
|
|
res += [text]
|
|
|
|
if res:
|
|
return "Autostop:\n " + ("\n ".join(res))
|
|
else:
|
|
return ''
|
|
|
|
class AbstractCriteria:
|
|
RC_TIME = 21
|
|
RC_HTTP = 22
|
|
RC_NET = 23
|
|
RC_INST = 24
|
|
|
|
def __init__(self):
|
|
self.log = logging.getLogger(__name__)
|
|
self.cause_second = None
|
|
|
|
def count_matched_codes(self, codes_regex, codes_dict):
|
|
total = 0
|
|
for code, count in codes_dict.items():
|
|
if codes_regex.match(str(code)):
|
|
total += count
|
|
return total
|
|
|
|
def notify(self, aggregate_second):
|
|
raise RuntimeError("Abstract methods requires overriding")
|
|
|
|
def get_rc(self):
|
|
raise RuntimeError("Abstract methods requires overriding")
|
|
|
|
def explain(self):
|
|
raise RuntimeError("Abstract methods requires overriding")
|
|
|
|
def widget_explain(self):
|
|
return (self.explain(), 0)
|
|
|
|
@staticmethod
|
|
def get_type_string():
|
|
raise RuntimeError("Abstract methods requires overriding")
|
|
|
|
|
|
class AvgTimeCriteria(AbstractCriteria):
|
|
@staticmethod
|
|
def get_type_string():
|
|
return 'time'
|
|
|
|
def __init__(self, autostop, param_str):
|
|
AbstractCriteria.__init__(self)
|
|
self.seconds_count = 0
|
|
self.rt_limit = Utils.expand_to_milliseconds(param_str.split(',')[0])
|
|
self.seconds_limit = Utils.expand_to_seconds(param_str.split(',')[1])
|
|
self.autostop = autostop
|
|
|
|
def notify(self, aggregate_second):
|
|
if aggregate_second.overall.avg_response_time > self.rt_limit:
|
|
if not self.seconds_count:
|
|
self.cause_second = aggregate_second
|
|
|
|
self.log.debug(self.explain())
|
|
|
|
self.seconds_count += 1
|
|
self.autostop.add_counting(self)
|
|
if self.seconds_count >= self.seconds_limit:
|
|
return True
|
|
else:
|
|
self.seconds_count = 0
|
|
|
|
return False
|
|
|
|
def get_rc(self):
|
|
return self.RC_TIME
|
|
|
|
def explain(self):
|
|
items = (self.rt_limit, self.seconds_count, self.cause_second.time)
|
|
return "Average response time higher than %sms for %ss, started at: %s" % items
|
|
|
|
def widget_explain(self):
|
|
items = (self.rt_limit, self.seconds_count, self.seconds_limit)
|
|
return ("Avg Time >%sms for %s/%ss" % items, float(self.seconds_count) / self.seconds_limit)
|
|
|
|
|
|
class HTTPCodesCriteria(AbstractCriteria):
|
|
@staticmethod
|
|
def get_type_string():
|
|
return 'http'
|
|
|
|
def __init__(self, autostop, param_str):
|
|
AbstractCriteria.__init__(self)
|
|
self.seconds_count = 0
|
|
self.codes_mask = param_str.split(',')[0].lower()
|
|
self.codes_regex = re.compile(self.codes_mask.replace("x", '.'))
|
|
self.autostop = autostop
|
|
|
|
level_str = param_str.split(',')[1].strip()
|
|
if level_str[-1:] == '%':
|
|
self.level = float(level_str[:-1]) / 100
|
|
self.is_relative = True
|
|
else:
|
|
self.level = int(level_str)
|
|
self.is_relative = False
|
|
self.seconds_limit = Utils.expand_to_seconds(param_str.split(',')[2])
|
|
|
|
|
|
def notify(self, aggregate_second):
|
|
matched_responses = self.count_matched_codes(self.codes_regex, aggregate_second.overall.http_codes)
|
|
if self.is_relative:
|
|
if aggregate_second.overall.RPS:
|
|
matched_responses = float(matched_responses) / aggregate_second.overall.RPS
|
|
else:
|
|
matched_responses = 0
|
|
self.log.debug("HTTP codes matching mask %s: %s/%s", self.codes_mask, matched_responses, self.level)
|
|
|
|
if matched_responses >= self.level:
|
|
if not self.seconds_count:
|
|
self.cause_second = aggregate_second
|
|
|
|
self.log.debug(self.explain())
|
|
|
|
self.seconds_count += 1
|
|
self.autostop.add_counting(self)
|
|
if self.seconds_count >= self.seconds_limit:
|
|
return True
|
|
else:
|
|
self.seconds_count = 0
|
|
|
|
return False
|
|
|
|
def get_rc(self):
|
|
return self.RC_HTTP
|
|
|
|
def get_level_str(self):
|
|
if self.is_relative:
|
|
level_str = str(100 * self.level) + "%"
|
|
else:
|
|
level_str = self.level
|
|
return level_str
|
|
|
|
def explain(self):
|
|
items = (self.codes_mask, self.get_level_str(), self.seconds_count, self.cause_second.time)
|
|
return "%s codes count higher than %s for %ss, started at: %s" % items
|
|
|
|
def widget_explain(self):
|
|
items = (self.codes_mask, self.get_level_str(), self.seconds_count, self.seconds_limit)
|
|
return ("HTTP %s>%s for %s/%ss" % items, float(self.seconds_count) / self.seconds_limit)
|
|
|
|
|
|
class NetCodesCriteria(AbstractCriteria):
|
|
@staticmethod
|
|
def get_type_string():
|
|
return 'net'
|
|
|
|
def __init__(self, autostop, param_str):
|
|
AbstractCriteria.__init__(self)
|
|
self.seconds_count = 0
|
|
self.codes_mask = param_str.split(',')[0].lower()
|
|
self.codes_regex = re.compile(self.codes_mask.replace("x", '.'))
|
|
self.autostop = autostop
|
|
|
|
level_str = param_str.split(',')[1].strip()
|
|
if level_str[-1:] == '%':
|
|
self.level = float(level_str[:-1]) / 100
|
|
self.is_relative = True
|
|
else:
|
|
self.level = int(level_str)
|
|
self.is_relative = False
|
|
self.seconds_limit = Utils.expand_to_seconds(param_str.split(',')[2])
|
|
|
|
|
|
def notify(self, aggregate_second):
|
|
codes = copy.deepcopy(aggregate_second.overall.net_codes)
|
|
if '0' in codes.keys(): codes.pop('0')
|
|
matched_responses = self.count_matched_codes(self.codes_regex, codes)
|
|
if self.is_relative:
|
|
if aggregate_second.overall.RPS:
|
|
matched_responses = float(matched_responses) / aggregate_second.overall.RPS
|
|
else:
|
|
matched_responses = 0
|
|
self.log.debug("Net codes matching mask %s: %s/%s", self.codes_mask, matched_responses, self.level)
|
|
|
|
if matched_responses >= self.level:
|
|
if not self.seconds_count:
|
|
self.cause_second = aggregate_second
|
|
|
|
self.log.debug(self.explain())
|
|
|
|
self.seconds_count += 1
|
|
self.autostop.add_counting(self)
|
|
if self.seconds_count >= self.seconds_limit:
|
|
return True
|
|
else:
|
|
self.seconds_count = 0
|
|
|
|
return False
|
|
|
|
def get_rc(self):
|
|
return self.RC_NET
|
|
|
|
def get_level_str(self):
|
|
if self.is_relative:
|
|
level_str = str(100 * self.level) + "%"
|
|
else:
|
|
level_str = self.level
|
|
return level_str
|
|
|
|
def explain(self):
|
|
items = (self.codes_mask, self.get_level_str(), self.seconds_count, self.cause_second.time)
|
|
return "%s net codes count higher than %s for %ss, started at: %s" % items
|
|
|
|
def widget_explain(self):
|
|
items = (self.codes_mask, self.get_level_str(), self.seconds_count, self.seconds_limit)
|
|
return ("Net %s>%s for %s/%ss" % items, float(self.seconds_count) / self.seconds_limit)
|
|
|
|
|
|
class QuantileCriteria(AbstractCriteria):
|
|
@staticmethod
|
|
def get_type_string():
|
|
return 'quantile'
|
|
|
|
def __init__(self, autostop, param_str):
|
|
AbstractCriteria.__init__(self)
|
|
self.seconds_count = 0
|
|
self.quantile = float(param_str.split(',')[0])
|
|
self.rt_limit = Utils.expand_to_milliseconds(param_str.split(',')[1])
|
|
self.seconds_limit = Utils.expand_to_seconds(param_str.split(',')[2])
|
|
self.autostop = autostop
|
|
|
|
def notify(self, aggregate_second):
|
|
if not (self.quantile in aggregate_second.overall.quantiles.keys()):
|
|
self.log.warning("No qunatile %s in %s", self.quantile, aggregate_second.overall.quantiles)
|
|
if self.quantile in aggregate_second.overall.quantiles.keys() \
|
|
and aggregate_second.overall.quantiles[self.quantile] > self.rt_limit:
|
|
if not self.seconds_count:
|
|
self.cause_second = aggregate_second
|
|
|
|
self.log.debug(self.explain())
|
|
|
|
self.seconds_count += 1
|
|
self.autostop.add_counting(self)
|
|
if self.seconds_count >= self.seconds_limit:
|
|
return True
|
|
else:
|
|
self.seconds_count = 0
|
|
|
|
return False
|
|
|
|
def get_rc(self):
|
|
return self.RC_TIME
|
|
|
|
def explain(self):
|
|
items = (self.quantile, self.rt_limit, self.seconds_count, self.cause_second.time)
|
|
return "Percentile %s higher than %sms for %ss, started at: %s" % items
|
|
|
|
def widget_explain(self):
|
|
items = (self.quantile, self.rt_limit, self.seconds_count, self.seconds_limit)
|
|
return ("%s%% >%sms for %s/%ss" % items, float(self.seconds_count) / self.seconds_limit)
|
|
|