##// END OF EJS Templates
rule engine: add support for NOT rule
ergo -
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,43 +1,47 b''
1 # appenlight README
1 # appenlight README
2
2
3
3
4 To run the app you need to have meet prerequsites:
4 To run the app you need to have meet prerequsites:
5
5
6 - running elasticsearch (2.3+ tested)
6 - running elasticsearch (2.3+ tested)
7 - running postgresql (9.5+ required)
7 - running postgresql (9.5+ required)
8 - running redis
8 - running redis
9
9
10 # Setup basics
10 # Setup basics
11
11
12 Set up the basic application database schema:
12 Set up the basic application database schema:
13
13
14 appenlight_initialize_db config.ini
14 appenlight_initialize_db config.ini
15
15
16 Set up basic elasticsearch schema:
16 Set up basic elasticsearch schema:
17
17
18 appenlight-reindex-elasticsearch -c config.ini -t all
18 appenlight-reindex-elasticsearch -c config.ini -t all
19
19
20 Installed the appenlight uptime plugin
20 Installed the appenlight uptime plugin
21
21
22 # Running
22 # Running
23
23
24 To run the application itself:
24 To run the application itself:
25
25
26 pserve --reload development.ini
26 pserve --reload development.ini
27
27
28 To run celery queue processing:
28 To run celery queue processing:
29
29
30 celery worker -A appenlight.celery -Q "reports,logs,metrics,default" --ini=development.ini
30 celery worker -A appenlight.celery -Q "reports,logs,metrics,default" --ini=development.ini
31
31
32 To run celery beats scheduling:
33
34 celery beat -A appenlight.celery --ini=development.ini
35
32 You should also run the channelstream websocket server for real-time notifications
36 You should also run the channelstream websocket server for real-time notifications
33
37
34 channelstream -i filename.ini
38 channelstream -i filename.ini
35
39
36 # Testing
40 # Testing
37
41
38 To run test suite:
42 To run test suite:
39
43
40 py.test appenlight/tests/tests.py --cov appenlight (this looks for testing.ini in repo root)
44 py.test appenlight/tests/tests.py --cov appenlight (this looks for testing.ini in repo root)
41
45
42 WARNING!!!
46 WARNING!!!
43 Some tests will insert data into elasticsearch or redis based on testing.ini
47 Some tests will insert data into elasticsearch or redis based on testing.ini
@@ -1,288 +1,303 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # AppEnlight Enterprise Edition, including its added features, Support
18 # AppEnlight Enterprise Edition, including its added features, Support
19 # services, and proprietary license terms, please see
19 # services, and proprietary license terms, please see
20 # https://rhodecode.com/licenses/
20 # https://rhodecode.com/licenses/
21
21
22 import logging
22 import logging
23 import operator
23 import operator
24
24
25 log = logging.getLogger(__name__)
25 log = logging.getLogger(__name__)
26
26
27
27
28 class RuleException(Exception):
28 class RuleException(Exception):
29 pass
29 pass
30
30
31
31
32 class KeyNotFoundException(RuleException):
32 class KeyNotFoundException(RuleException):
33 pass
33 pass
34
34
35
35
36 class UnknownTypeException(RuleException):
36 class UnknownTypeException(RuleException):
37 pass
37 pass
38
38
39
39
40 class BadConfigException(RuleException):
40 class BadConfigException(RuleException):
41 pass
41 pass
42
42
43
43
44 class InvalidValueException(RuleException):
44 class InvalidValueException(RuleException):
45 pass
45 pass
46
46
47
47
48 class RuleBase(object):
48 class RuleBase(object):
49 @classmethod
49 @classmethod
50 def default_dict_struct_getter(cls, struct, field_name):
50 def default_dict_struct_getter(cls, struct, field_name):
51 """
51 """
52 returns a key from dictionary based on field_name, if the name contains
52 returns a key from dictionary based on field_name, if the name contains
53 `:` then it means additional nesting levels should be checked for the
53 `:` then it means additional nesting levels should be checked for the
54 key so `a:b:c` means return struct['a']['b']['c']
54 key so `a:b:c` means return struct['a']['b']['c']
55
55
56 :param struct:
56 :param struct:
57 :param field_name:
57 :param field_name:
58 :return:
58 :return:
59 """
59 """
60 parts = field_name.split(':') if field_name else []
60 parts = field_name.split(':') if field_name else []
61 found = struct
61 found = struct
62 while parts:
62 while parts:
63 current_key = parts.pop(0)
63 current_key = parts.pop(0)
64 found = found.get(current_key)
64 found = found.get(current_key)
65 if not found and parts:
65 if not found and parts:
66 raise KeyNotFoundException('Key not found in structure')
66 raise KeyNotFoundException('Key not found in structure')
67 return found
67 return found
68
68
69 @classmethod
69 @classmethod
70 def default_obj_struct_getter(cls, struct, field_name):
70 def default_obj_struct_getter(cls, struct, field_name):
71 """
71 """
72 returns a key from instance based on field_name, if the name contains
72 returns a key from instance based on field_name, if the name contains
73 `:` then it means additional nesting levels should be checked for the
73 `:` then it means additional nesting levels should be checked for the
74 key so `a:b:c` means return struct.a.b.c
74 key so `a:b:c` means return struct.a.b.c
75
75
76 :param struct:
76 :param struct:
77 :param field_name:
77 :param field_name:
78 :return:
78 :return:
79 """
79 """
80 parts = field_name.split(':')
80 parts = field_name.split(':')
81 found = struct
81 found = struct
82 while parts:
82 while parts:
83 current_key = parts.pop(0)
83 current_key = parts.pop(0)
84 found = getattr(found, current_key, None)
84 found = getattr(found, current_key, None)
85 if not found and parts:
85 if not found and parts:
86 raise KeyNotFoundException('Key not found in structure')
86 raise KeyNotFoundException('Key not found in structure')
87 return found
87 return found
88
88
89 def normalized_type(self, field, value):
89 def normalized_type(self, field, value):
90 """
90 """
91 Converts text values from self.conf_value based on type_matrix below
91 Converts text values from self.conf_value based on type_matrix below
92 check_matrix defines what kind of checks we can perform on a field
92 check_matrix defines what kind of checks we can perform on a field
93 value based on field name
93 value based on field name
94 """
94 """
95 f_type = self.type_matrix.get(field)
95 f_type = self.type_matrix.get(field)
96 if f_type:
96 if f_type:
97 cast_to = f_type['type']
97 cast_to = f_type['type']
98 else:
98 else:
99 raise UnknownTypeException('Unknown type')
99 raise UnknownTypeException('Unknown type')
100
100
101 if value is None:
101 if value is None:
102 return None
102 return None
103
103
104 try:
104 try:
105 if cast_to == 'int':
105 if cast_to == 'int':
106 return int(value)
106 return int(value)
107 elif cast_to == 'float':
107 elif cast_to == 'float':
108 return float(value)
108 return float(value)
109 elif cast_to == 'unicode':
109 elif cast_to == 'unicode':
110 return str(value)
110 return str(value)
111 except ValueError as exc:
111 except ValueError as exc:
112 raise InvalidValueException(exc)
112 raise InvalidValueException(exc)
113
113
114
114
115 class Rule(RuleBase):
115 class Rule(RuleBase):
116 def __init__(self, config, type_matrix,
116 def __init__(self, config, type_matrix,
117 struct_getter=RuleBase.default_dict_struct_getter,
117 struct_getter=RuleBase.default_dict_struct_getter,
118 config_manipulator=None):
118 config_manipulator=None):
119 """
119 """
120
120
121 :param config: dict - contains rule configuration
121 :param config: dict - contains rule configuration
122 example::
122 example::
123 {
123 {
124 "field": "__OR__",
124 "field": "__OR__",
125 "rules": [
125 "rules": [
126 {
126 {
127 "field": "__AND__",
127 "field": "__AND__",
128 "rules": [
128 "rules": [
129 {
129 {
130 "op": "ge",
130 "op": "ge",
131 "field": "occurences",
131 "field": "occurences",
132 "value": "10"
132 "value": "10"
133 },
133 },
134 {
134 {
135 "op": "ge",
135 "op": "ge",
136 "field": "priority",
136 "field": "priority",
137 "value": "4"
137 "value": "4"
138 }
138 }
139 ]
139 ]
140 },
140 },
141 {
141 {
142 "op": "eq",
142 "op": "eq",
143 "field": "http_status",
143 "field": "http_status",
144 "value": "500"
144 "value": "500"
145 }
145 }
146 ]
146 ]
147 }
147 }
148 :param type_matrix: dict - contains map of type casts
148 :param type_matrix: dict - contains map of type casts
149 example::
149 example::
150 {
150 {
151 'http_status': 'int',
151 'http_status': 'int',
152 'priority': 'unicode',
152 'priority': 'unicode',
153 }
153 }
154 :param struct_getter: callable - used to grab the value of field from
154 :param struct_getter: callable - used to grab the value of field from
155 the structure passed to match() based
155 the structure passed to match() based
156 on key, default
156 on key, default
157
157
158 """
158 """
159 self.type_matrix = type_matrix
159 self.type_matrix = type_matrix
160 self.config = config
160 self.config = config
161 self.struct_getter = struct_getter
161 self.struct_getter = struct_getter
162 self.config_manipulator = config_manipulator
162 self.config_manipulator = config_manipulator
163 if config_manipulator:
163 if config_manipulator:
164 config_manipulator(self)
164 config_manipulator(self)
165
165
166 def subrule_check(self, rule_config, struct):
166 def subrule_check(self, rule_config, struct):
167 rule = Rule(rule_config, self.type_matrix,
167 rule = Rule(rule_config, self.type_matrix,
168 config_manipulator=self.config_manipulator)
168 config_manipulator=self.config_manipulator)
169 return rule.match(struct)
169 return rule.match(struct)
170
170
171 def match(self, struct):
171 def match(self, struct):
172 """
172 """
173 Check if rule matched for this specific report
173 Check if rule matched for this specific report
174 First tries report value, then tests tags in not found, then finally
174 First tries report value, then tests tags in not found, then finally
175 report group
175 report group
176 """
176 """
177 field_name = self.config.get('field')
177 field_name = self.config.get('field')
178 test_value = self.config.get('value')
178 test_value = self.config.get('value')
179
179
180 if not field_name:
180 if not field_name:
181 return False
181 return False
182
182
183 if field_name == '__AND__':
183 if field_name == '__AND__':
184 rule = AND(self.config['rules'], self.type_matrix,
184 rule = AND(self.config['rules'], self.type_matrix,
185 config_manipulator=self.config_manipulator)
185 config_manipulator=self.config_manipulator)
186 return rule.match(struct)
186 return rule.match(struct)
187 elif field_name == '__OR__':
187 elif field_name == '__OR__':
188 rule = OR(self.config['rules'], self.type_matrix,
188 rule = OR(self.config['rules'], self.type_matrix,
189 config_manipulator=self.config_manipulator)
189 config_manipulator=self.config_manipulator)
190 return rule.match(struct)
190 return rule.match(struct)
191 elif field_name == '__NOT__':
192 rule = NOT(self.config['rules'], self.type_matrix,
193 config_manipulator=self.config_manipulator)
194 return rule.match(struct)
191
195
192 if test_value is None:
196 if test_value is None:
193 return False
197 return False
194
198
195 try:
199 try:
196 struct_value = self.normalized_type(field_name,
200 struct_value = self.normalized_type(field_name,
197 self.struct_getter(struct,
201 self.struct_getter(struct,
198 field_name))
202 field_name))
199 except (UnknownTypeException, InvalidValueException) as exc:
203 except (UnknownTypeException, InvalidValueException) as exc:
200 log.error(str(exc))
204 log.error(str(exc))
201 return False
205 return False
202
206
203 try:
207 try:
204 test_value = self.normalized_type(field_name, test_value)
208 test_value = self.normalized_type(field_name, test_value)
205 except (UnknownTypeException, InvalidValueException) as exc:
209 except (UnknownTypeException, InvalidValueException) as exc:
206 log.error(str(exc))
210 log.error(str(exc))
207 return False
211 return False
208
212
209 if self.config['op'] not in ('startswith', 'endswith', 'contains'):
213 if self.config['op'] not in ('startswith', 'endswith', 'contains'):
210 try:
214 try:
211 return getattr(operator,
215 return getattr(operator,
212 self.config['op'])(struct_value, test_value)
216 self.config['op'])(struct_value, test_value)
213 except TypeError:
217 except TypeError:
214 return False
218 return False
215 elif self.config['op'] == 'startswith':
219 elif self.config['op'] == 'startswith':
216 return struct_value.startswith(test_value)
220 return struct_value.startswith(test_value)
217 elif self.config['op'] == 'endswith':
221 elif self.config['op'] == 'endswith':
218 return struct_value.endswith(test_value)
222 return struct_value.endswith(test_value)
219 elif self.config['op'] == 'contains':
223 elif self.config['op'] == 'contains':
220 return test_value in struct_value
224 return test_value in struct_value
221 raise BadConfigException('Invalid configuration, '
225 raise BadConfigException('Invalid configuration, '
222 'unknown operator: {}'.format(self.config))
226 'unknown operator: {}'.format(self.config))
223
227
224 def __repr__(self):
228 def __repr__(self):
225 return '<Rule {} {}>'.format(self.config.get('field'),
229 return '<Rule {} {}>'.format(self.config.get('field'),
226 self.config.get('value'))
230 self.config.get('value'))
227
231
228
232
229 class AND(Rule):
233 class AND(Rule):
230 def __init__(self, rules, *args, **kwargs):
234 def __init__(self, rules, *args, **kwargs):
231 super(AND, self).__init__({}, *args, **kwargs)
235 super(AND, self).__init__({}, *args, **kwargs)
232 self.rules = rules
236 self.rules = rules
233
237
234 def match(self, struct):
238 def match(self, struct):
235 return all([self.subrule_check(r_conf, struct) for r_conf
239 return all([self.subrule_check(r_conf, struct) for r_conf
236 in self.rules])
240 in self.rules])
237
241
238
242
243 class NOT(Rule):
244 def __init__(self, rules, *args, **kwargs):
245 super(NOT, self).__init__({}, *args, **kwargs)
246 self.rules = rules
247
248 def match(self, struct):
249 return all([not self.subrule_check(r_conf, struct) for r_conf
250 in self.rules])
251
252
239 class OR(Rule):
253 class OR(Rule):
240 def __init__(self, rules, *args, **kwargs):
254 def __init__(self, rules, *args, **kwargs):
241 super(OR, self).__init__({}, *args, **kwargs)
255 super(OR, self).__init__({}, *args, **kwargs)
242 self.rules = rules
256 self.rules = rules
243
257
244 def match(self, struct):
258 def match(self, struct):
245 return any([self.subrule_check(r_conf, struct) for r_conf
259 return any([self.subrule_check(r_conf, struct) for r_conf
246 in self.rules])
260 in self.rules])
247
261
248
262
249 class RuleService(object):
263 class RuleService(object):
250 @staticmethod
264 @staticmethod
251 def rule_from_config(config, field_mappings, labels_dict,
265 def rule_from_config(config, field_mappings, labels_dict,
252 manipulator_func=None):
266 manipulator_func=None):
253 """
267 """
254 Returns modified rule with manipulator function
268 Returns modified rule with manipulator function
255 By default manipulator function replaces field id from labels_dict
269 By default manipulator function replaces field id from labels_dict
256 with current field id proper for the rule from fields_mappings
270 with current field id proper for the rule from fields_mappings
257
271
258 because label X_X id might be pointing different value on next request
272 because label X_X id might be pointing different value on next request
259 when new term is returned from elasticsearch - this ensures things
273 when new term is returned from elasticsearch - this ensures things
260 are kept 1:1 all the time
274 are kept 1:1 all the time
261 """
275 """
262 rev_map = {}
276 rev_map = {}
263 for k, v in labels_dict.items():
277 for k, v in labels_dict.items():
264 rev_map[(v['agg'], v['key'],)] = k
278 rev_map[(v['agg'], v['key'],)] = k
265
279
266 if manipulator_func is None:
280 if manipulator_func is None:
267 def label_rewriter_func(rule):
281 def label_rewriter_func(rule):
268 field = rule.config.get('field')
282 field = rule.config.get('field')
269 if not field or rule.config['field'] in ['__OR__', '__AND__']:
283 if not field or rule.config['field'] in ['__OR__',
284 '__AND__', '__NOT__']:
270 return
285 return
271
286
272 to_map = field_mappings.get(rule.config['field'])
287 to_map = field_mappings.get(rule.config['field'])
273
288
274 # we need to replace series field with _AE_NOT_FOUND_ to not match
289 # we need to replace series field with _AE_NOT_FOUND_ to not match
275 # accidently some other field which happens to have the series that
290 # accidently some other field which happens to have the series that
276 # was used when the alert was created
291 # was used when the alert was created
277 if to_map:
292 if to_map:
278 to_replace = rev_map.get((to_map['agg'], to_map['key'],),
293 to_replace = rev_map.get((to_map['agg'], to_map['key'],),
279 '_AE_NOT_FOUND_')
294 '_AE_NOT_FOUND_')
280 else:
295 else:
281 to_replace = '_AE_NOT_FOUND_'
296 to_replace = '_AE_NOT_FOUND_'
282
297
283 rule.config['field'] = to_replace
298 rule.config['field'] = to_replace
284 rule.type_matrix[to_replace] = {"type": 'float'}
299 rule.type_matrix[to_replace] = {"type": 'float'}
285
300
286 manipulator_func = label_rewriter_func
301 manipulator_func = label_rewriter_func
287
302
288 return Rule(config, {}, config_manipulator=manipulator_func)
303 return Rule(config, {}, config_manipulator=manipulator_func)
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now