#!/usr/bin/python # -*- coding: utf-8 -*- # (C) Copyright 2015-2017 Hewlett Packard Enterprise LP # # 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 sys import pyparsing import six _DETERMINISTIC_ASSIGNMENT_LEN = 3 _DETERMINISTIC_ASSIGNMENT_SHORT_LEN = 1 _DETERMINISTIC_ASSIGNMENT_VALUE_INDEX = 2 _DEFAULT_PERIOD = 60 _DEFAULT_PERIODS = 1 class SubExpr(object): def __init__(self, tokens): if not tokens.func: if tokens.relational_op.lower() in ['gte', 'gt', '>=', '>']: self._func = "max" else: self._func = "min" else: self._func = tokens.func self._metric_name = tokens.metric_name self._dimensions = tokens.dimensions_list self._operator = tokens.relational_op self._threshold = float(tokens.threshold) if tokens.period: self._period = int(tokens.period) else: self._period = _DEFAULT_PERIOD if tokens.periods: self._periods = int(tokens.periods) else: self._periods = _DEFAULT_PERIODS self._deterministic = tokens.deterministic self._id = None @property def fmtd_sub_expr_str(self): """Get the entire sub expressions as a string with spaces.""" result = u"{}({}".format(self.normalized_func, self._metric_name) if self._dimensions is not None: result += "{" + self.dimensions_str + "}" if self._period != _DEFAULT_PERIOD: result += ", {}".format(self._period) result += ")" result += " {} {}".format(self._operator, self._threshold) if self._periods != _DEFAULT_PERIODS: result += " times {}".format(self._periods) return result @property def dimensions_str(self): """Get all the dimensions as a single comma delimited string.""" return u",".join(self._dimensions) @property def operands_list(self): """Get this sub expression as a list.""" return [self] @property def func(self): """Get the function as it appears in the orig expression.""" return self._func @property def normalized_func(self): """Get the function upper-cased.""" return self._func.upper() @property def metric_name(self): """Get the metric name as it appears in the orig expression.""" return self._metric_name @property def normalized_metric_name(self): """Get the metric name lower-cased.""" return self._metric_name.lower() @property def dimensions(self): """Get the dimensions.""" return u",".join(self._dimensions) @property def dimensions_as_list(self): """Get the dimensions as a list.""" if self._dimensions: return self._dimensions else: return [] @property def operator(self): """Get the operator.""" return self._operator @property def threshold(self): """Get the threshold value.""" return self._threshold @property def period(self): """Get the period. Default is 60 seconds.""" if self._period: return self._period else: return u'60' @property def periods(self): """Get the periods. Default is 1.""" if self._periods: return self._periods else: return u'1' @property def deterministic(self): return True if self._deterministic else False @property def normalized_operator(self): """Get the operator as one of LT, GT, LTE, or GTE.""" if self._operator.lower() == "lt" or self._operator == "<": return u"LT" elif self._operator.lower() == "gt" or self._operator == ">": return u"GT" elif self._operator.lower() == "lte" or self._operator == "<=": return u"LTE" elif self._operator.lower() == "gte" or self._operator == ">=": return u"GTE" @property def id(self): """Get the id used to identify this sub expression in the repo.""" return self._id @id.setter def id(self, id): """Set the d used to identify this sub expression in the repo.""" self._id = id class BinaryOp(object): def __init__(self, tokens): self.op = tokens[0][1] self.operands = tokens[0][0::2] @property def operands_list(self): return ([sub_operand for operand in self.operands for sub_operand in operand.operands_list]) class AndSubExpr(BinaryOp): """Expand later as needed.""" pass class OrSubExpr(BinaryOp): """Expand later as needed.""" pass COMMA = pyparsing.Suppress(pyparsing.Literal(",")) LPAREN = pyparsing.Suppress(pyparsing.Literal("(")) RPAREN = pyparsing.Suppress(pyparsing.Literal(")")) EQUAL = pyparsing.Literal("=") LBRACE = pyparsing.Suppress(pyparsing.Literal("{")) RBRACE = pyparsing.Suppress(pyparsing.Literal("}")) def periodValidation(instr, loc, tokens): period = int(tokens[0]) if period == 0: raise pyparsing.ParseFatalException(instr, loc, "Period must not be 0") if (period % 60) != 0: raise pyparsing.ParseFatalException(instr, loc, "Period {} must be a multiple of 60" .format(period)) # Must return the string return tokens[0] def periodsValidation(instr, loc, tokens): periods = int(tokens[0]) if periods < 1: raise pyparsing.ParseFatalException(instr, loc, "Periods {} must be 1 or greater" .format(periods)) # Must return the string return tokens[0] # Initialize non-ascii unicode code points in the Basic Multilingual Plane. unicode_printables = u''.join( six.unichr(c) for c in range(128, 65536) if not six.unichr(c).isspace()) # Does not like comma. No Literals from above allowed. valid_identifier_chars = ( (unicode_printables + pyparsing.alphanums + ".-_#!$%&'*+/:;?@[\\]^`|~")) metric_name = ( pyparsing.Word(valid_identifier_chars, min=1, max=255)("metric_name")) dimension_name = pyparsing.Word(valid_identifier_chars + ' ', min=1, max=255) dimension_value = pyparsing.Word(valid_identifier_chars + ' ', min=1, max=255) MINUS = pyparsing.Literal('-') integer_number = pyparsing.Word(pyparsing.nums) decimal_number = (pyparsing.Optional(MINUS) + integer_number + pyparsing.Optional("." + integer_number)) decimal_number.setParseAction(lambda tokens: "".join(tokens)) max = pyparsing.CaselessLiteral("max") min = pyparsing.CaselessLiteral("min") avg = pyparsing.CaselessLiteral("avg") count = pyparsing.CaselessLiteral("count") sum = pyparsing.CaselessLiteral("sum") last = pyparsing.CaselessLiteral("last") func = (max | min | avg | count | sum | last)("func") less_than_op = ( (pyparsing.CaselessLiteral("<") | pyparsing.CaselessLiteral("lt"))) less_than_eq_op = ( (pyparsing.CaselessLiteral("<=") | pyparsing.CaselessLiteral("lte"))) greater_than_op = ( (pyparsing.CaselessLiteral(">") | pyparsing.CaselessLiteral("gt"))) greater_than_eq_op = ( (pyparsing.CaselessLiteral(">=") | pyparsing.CaselessLiteral("gte"))) # Order is important. Put longer prefix first. relational_op = ( less_than_eq_op | less_than_op | greater_than_eq_op | greater_than_op)( "relational_op") AND = pyparsing.CaselessLiteral("and") | pyparsing.CaselessLiteral("&&") OR = pyparsing.CaselessLiteral("or") | pyparsing.CaselessLiteral("||") logical_op = (AND | OR)("logical_op") times = pyparsing.CaselessLiteral("times") dimension = dimension_name + EQUAL + dimension_value dimension.setParseAction(lambda tokens: "".join(tokens)) dimension_list = pyparsing.Group((LBRACE + pyparsing.Optional( pyparsing.delimitedList(dimension)) + RBRACE))("dimensions_list") metric = metric_name + pyparsing.Optional(dimension_list) period = integer_number.copy().addParseAction(periodValidation)("period") threshold = decimal_number("threshold") periods = integer_number.copy().addParseAction(periodsValidation)("periods") deterministic = ( pyparsing.CaselessLiteral('deterministic') )('deterministic') function_and_metric = ( func + LPAREN + metric + pyparsing.Optional(COMMA + deterministic) + pyparsing.Optional(COMMA + period) + RPAREN ) expression = pyparsing.Forward() sub_expression = ((function_and_metric | metric) + relational_op + threshold + pyparsing.Optional(times + periods) | LPAREN + expression + RPAREN) sub_expression.setParseAction(SubExpr) expression = ( pyparsing.operatorPrecedence(sub_expression, [(AND, 2, pyparsing.opAssoc.LEFT, AndSubExpr), (OR, 2, pyparsing.opAssoc.LEFT, OrSubExpr)])) class AlarmExprParser(object): def __init__(self, expr): self._expr = expr @property def sub_expr_list(self): # Remove all spaces before parsing. Simple, quick fix for whitespace # issue with dimension list not allowing whitespace after comma. parse_result = (expression + pyparsing.stringEnd).parseString( self._expr) sub_expr_list = parse_result[0].operands_list return sub_expr_list def main(): """Used for development and testing.""" expr_list = [ "max(-_.千幸福的笑脸{घोड़ा=馬, " "dn2=dv2,千幸福的笑脸घ=千幸福的笑脸घ}) gte 100 " "times 3 && " "(min(ເຮືອນ{dn3=dv3,家=дом}) < 10 or sum(biz{dn5=dv5}) >99 and " "count(fizzle) lt 0or count(baz) > 1)".decode('utf8'), "max(foo{hostname=mini-mon,千=千}, 120) > 100 and (max(bar)>100 " " or max(biz)>100)".decode('utf8'), "max(foo)>=100", "test_metric{this=that, that = this} < 1", "max ( 3test_metric5 { this = that }) lt 5 times 3", "3test_metric5 lt 3", "ntp.offset > 1 or ntp.offset < -5", "max(3test_metric5{it's this=that's it}) lt 5 times 3", "count(log.error{test=1}, deterministic) > 1.0", "count(log.error{test=1}, deterministic, 120) > 1.0", "last(test_metric{hold=here}) < 13", "count(log.error{test=1}, deterministic, 130) > 1.0", "count(log.error{test=1}, deterministic) > 1.0 times 0", ] for expr in expr_list: print('orig expr: {}'.format(expr.encode('utf8'))) sub_exprs = [] try: alarm_expr_parser = AlarmExprParser(expr) sub_exprs = alarm_expr_parser.sub_expr_list except Exception as ex: print("Parse failed: {}".format(ex)) for sub_expr in sub_exprs: print('sub expr: {}'.format( sub_expr.fmtd_sub_expr_str.encode('utf8'))) print('sub_expr dimensions: {}'.format( sub_expr.dimensions_str.encode('utf8'))) print('sub_expr deterministic: {}'.format( sub_expr.deterministic)) print('sub_expr period: {}'.format( sub_expr.period)) print("") print("") if __name__ == "__main__": sys.exit(main())