##// END OF EJS Templates
api: support permanent logs in sentry api
ergo -
Show More
@@ -1,1703 +1,1593 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 copy
22 import copy
23 import logging
23 import logging
24 import mock
24 import mock
25 import pyramid
25 import pyramid
26 import pytest
26 import pytest
27 import sqlalchemy as sa
27 import sqlalchemy as sa
28 import webob
28 import webob
29
29
30 from datetime import datetime
30 from datetime import datetime
31 from pyramid import testing
31 from pyramid import testing
32
32
33
33
34 from appenlight.models import DBSession
34 from appenlight.models import DBSession
35 from appenlight.lib.ext_json import json
35 from appenlight.lib.ext_json import json
36
36
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class DummyContext(object):
41 class DummyContext(object):
42 pass
42 pass
43
43
44
44
45 @pytest.mark.usefixtures('base_app')
45 @pytest.mark.usefixtures('base_app')
46 class BasicTest(object):
46 class BasicTest(object):
47 pass
47 pass
48
48
49
49
50 @pytest.mark.usefixtures('base_app')
50 @pytest.mark.usefixtures('base_app')
51 class TestMigration(object):
51 class TestMigration(object):
52 def test_migration(self):
52 def test_migration(self):
53 assert 1 == 1
53 assert 1 == 1
54
54
55
55
56 class TestAPIReports_0_4_Validation(object):
57 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
58 def test_no_payload(self, dummy_json):
59 import colander
60 from appenlight.validators import ReportListSchema_0_4
61 utcnow = datetime.utcnow()
62 schema = ReportListSchema_0_4().bind(utcnow=utcnow)
63 with pytest.raises(colander.Invalid):
64 schema.deserialize(dummy_json)
65
66 def test_minimal_payload(self, report_04_schema):
67 dummy_json = [{}]
68 import colander
69 from appenlight.validators import ReportListSchema_0_4
70 utcnow = datetime.utcnow()
71 schema = ReportListSchema_0_4().bind(utcnow=utcnow)
72 with pytest.raises(colander.Invalid):
73 schema.deserialize(dummy_json)
74
75 def test_minimal_payload(self):
76 from appenlight.validators import ReportListSchema_0_4
77 dummy_json = [{'report_details': [{}]}]
78 utcnow = datetime.utcnow()
79 schema = ReportListSchema_0_4().bind(utcnow=utcnow)
80 deserialized = schema.deserialize(dummy_json)
81
82 expected_deserialization = [
83 {'error_type': '',
84 'language': 'unknown',
85 'report_details': [
86 {'username': '',
87 'traceback': None,
88 'extra': None,
89 'frameinfo': None,
90 'url': '',
91 'ip': None,
92 'start_time': utcnow,
93 'group_string': None,
94 'request': {},
95 'request_stats': None,
96 'end_time': None,
97 'request_id': '',
98 'message': '',
99 'slow_calls': [],
100 'user_agent': ''}],
101 'server': 'unknown',
102 'occurences': 1,
103 'priority': 5,
104 'view_name': '',
105 'client': 'unknown',
106 'http_status': 200,
107 'error': '',
108 'tags': None}
109 ]
110 assert deserialized == expected_deserialization
111
112 def test_full_payload(self):
113 import appenlight.tests.payload_examples as payload_examples
114 from appenlight.validators import ReportListSchema_0_4
115 utcnow = datetime.utcnow()
116 schema = ReportListSchema_0_4().bind(utcnow=utcnow)
117 PYTHON_PAYLOAD = copy.deepcopy(payload_examples.PYTHON_PAYLOAD_0_4)
118 utcnow = datetime.utcnow()
119 PYTHON_PAYLOAD["tags"] = [("foo", 1), ("action", "test"), ("baz", 1.1),
120 ("date",
121 utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))]
122 dummy_json = [PYTHON_PAYLOAD]
123
124 deserialized = schema.deserialize(dummy_json)
125 assert deserialized[0]['error'] == PYTHON_PAYLOAD['error']
126 assert deserialized[0]['language'] == PYTHON_PAYLOAD['language']
127 assert deserialized[0]['server'] == PYTHON_PAYLOAD['server']
128 assert deserialized[0]['priority'] == PYTHON_PAYLOAD['priority']
129 assert deserialized[0]['view_name'] == PYTHON_PAYLOAD['view_name']
130 assert deserialized[0]['client'] == PYTHON_PAYLOAD['client']
131 assert deserialized[0]['http_status'] == PYTHON_PAYLOAD['http_status']
132 assert deserialized[0]['error'] == PYTHON_PAYLOAD['error']
133 assert deserialized[0]['occurences'] == PYTHON_PAYLOAD['occurences']
134 first_detail = deserialized[0]['report_details'][0]
135 payload_detail = PYTHON_PAYLOAD['report_details'][0]
136 assert first_detail['username'] == payload_detail['username']
137 assert first_detail['traceback'] == payload_detail['traceback']
138 assert first_detail['url'] == payload_detail['url']
139 assert first_detail['ip'] == payload_detail['ip']
140 assert first_detail['start_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \
141 payload_detail['start_time']
142 assert first_detail['ip'] == payload_detail['ip']
143 assert first_detail['group_string'] is None
144 assert first_detail['request_stats'] == payload_detail['request_stats']
145 assert first_detail['end_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \
146 payload_detail['end_time']
147 assert first_detail['request_id'] == payload_detail['request_id']
148 assert first_detail['message'] == payload_detail['message']
149 assert first_detail['user_agent'] == payload_detail['user_agent']
150 slow_call = first_detail['slow_calls'][0]
151 expected_slow_call = payload_detail['slow_calls'][0]
152 assert slow_call['start'].strftime('%Y-%m-%dT%H:%M:%S.0') == \
153 expected_slow_call['start']
154 assert slow_call['end'].strftime('%Y-%m-%dT%H:%M:%S.0') == \
155 expected_slow_call['end']
156 assert slow_call['statement'] == expected_slow_call['statement']
157 assert slow_call['parameters'] == expected_slow_call['parameters']
158 assert slow_call['type'] == expected_slow_call['type']
159 assert slow_call['subtype'] == expected_slow_call['subtype']
160 assert slow_call['location'] == ''
161 assert deserialized[0]['tags'] == [
162 ('foo', 1), ('action', 'test'),
163 ('baz', 1.1), ('date', utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))]
164
165
166 class TestSentryProto_7(object):
56 class TestSentryProto_7(object):
167 def test_log_payload(self):
57 def test_log_payload(self):
168 import appenlight.tests.payload_examples as payload_examples
58 import appenlight.tests.payload_examples as payload_examples
169 from appenlight.lib.enums import ParsedSentryEventType
59 from appenlight.lib.enums import ParsedSentryEventType
170 from appenlight.lib.utils.sentry import parse_sentry_event
60 from appenlight.lib.utils.sentry import parse_sentry_event
171 event_dict, event_type = parse_sentry_event(
61 event_dict, event_type = parse_sentry_event(
172 payload_examples.SENTRY_LOG_PAYLOAD_7)
62 payload_examples.SENTRY_LOG_PAYLOAD_7)
173 assert ParsedSentryEventType.LOG == event_type
63 assert ParsedSentryEventType.LOG == event_type
174 assert event_dict['log_level'] == 'CRITICAL'
64 assert event_dict['log_level'] == 'CRITICAL'
175 assert event_dict['message'] == 'TEST from django logging'
65 assert event_dict['message'] == 'TEST from django logging'
176 assert event_dict['namespace'] == 'testlogger'
66 assert event_dict['namespace'] == 'testlogger'
177 assert event_dict['request_id'] == '9a6172f2e6d2444582f83a6c333d9cfb'
67 assert event_dict['request_id'] == '9a6172f2e6d2444582f83a6c333d9cfb'
178 assert event_dict['server'] == 'ergo-virtual-machine'
68 assert event_dict['server'] == 'ergo-virtual-machine'
179 assert event_dict['date'] == datetime.utcnow().date().strftime(
69 assert event_dict['date'] == datetime.utcnow().date().strftime(
180 '%Y-%m-%dT%H:%M:%SZ')
70 '%Y-%m-%dT%H:%M:%SZ')
181 tags = [('site', 'example.com'),
71 tags = [('site', 'example.com'),
182 ('sys.argv', ["'manage.py'", "'runserver'"]),
72 ('sys.argv', ["'manage.py'", "'runserver'"]),
183 ('price', 6),
73 ('price', 6),
184 ('tag', "'extra'"),
74 ('tag', "'extra'"),
185 ('dupa', True),
75 ('dupa', True),
186 ('project', 'sentry'),
76 ('project', 'sentry'),
187 ('sentry_culprit', 'testlogger in index'),
77 ('sentry_culprit', 'testlogger in index'),
188 ('sentry_language', 'python'),
78 ('sentry_language', 'python'),
189 ('sentry_release', 'test')]
79 ('sentry_release', 'test')]
190 assert sorted(event_dict['tags']) == sorted(tags)
80 assert sorted(event_dict['tags']) == sorted(tags)
191
81
192 def test_report_payload(self):
82 def test_report_payload(self):
193 import appenlight.tests.payload_examples as payload_examples
83 import appenlight.tests.payload_examples as payload_examples
194 from appenlight.lib.enums import ParsedSentryEventType
84 from appenlight.lib.enums import ParsedSentryEventType
195 from appenlight.lib.utils.sentry import parse_sentry_event
85 from appenlight.lib.utils.sentry import parse_sentry_event
196 utcnow = datetime.utcnow().date().strftime('%Y-%m-%dT%H:%M:%SZ')
86 utcnow = datetime.utcnow().date().strftime('%Y-%m-%dT%H:%M:%SZ')
197 event_dict, event_type = parse_sentry_event(
87 event_dict, event_type = parse_sentry_event(
198 payload_examples.SENTRY_PYTHON_PAYLOAD_7)
88 payload_examples.SENTRY_PYTHON_PAYLOAD_7)
199 assert ParsedSentryEventType.ERROR_REPORT == event_type
89 assert ParsedSentryEventType.ERROR_REPORT == event_type
200 assert event_dict['client'] == 'sentry'
90 assert event_dict['client'] == 'sentry'
201 assert event_dict[
91 assert event_dict[
202 'error'] == 'Exception: test 500 ' \
92 'error'] == 'Exception: test 500 ' \
203 '\u0142\xf3\u201c\u0107\u201c\u0107\u017c\u0105'
93 '\u0142\xf3\u201c\u0107\u201c\u0107\u017c\u0105'
204 assert event_dict['language'] == 'python'
94 assert event_dict['language'] == 'python'
205 assert event_dict['ip'] == '127.0.0.1'
95 assert event_dict['ip'] == '127.0.0.1'
206 assert event_dict['request_id'] == '9fae652c8c1c4d6a8eee09260f613a98'
96 assert event_dict['request_id'] == '9fae652c8c1c4d6a8eee09260f613a98'
207 assert event_dict['server'] == 'ergo-virtual-machine'
97 assert event_dict['server'] == 'ergo-virtual-machine'
208 assert event_dict['start_time'] == utcnow
98 assert event_dict['start_time'] == utcnow
209 assert event_dict['url'] == 'http://127.0.0.1:8000/error'
99 assert event_dict['url'] == 'http://127.0.0.1:8000/error'
210 assert event_dict['user_agent'] == 'Mozilla/5.0 (X11; Linux x86_64) ' \
100 assert event_dict['user_agent'] == 'Mozilla/5.0 (X11; Linux x86_64) ' \
211 'AppleWebKit/537.36 (KHTML, ' \
101 'AppleWebKit/537.36 (KHTML, ' \
212 'like Gecko) Chrome/47.0.2526.106 ' \
102 'like Gecko) Chrome/47.0.2526.106 ' \
213 'Safari/537.36'
103 'Safari/537.36'
214 assert event_dict['view_name'] == 'djangoapp.views in error'
104 assert event_dict['view_name'] == 'djangoapp.views in error'
215 tags = [('site', 'example.com'), ('sentry_release', 'test')]
105 tags = [('site', 'example.com'), ('sentry_release', 'test')]
216 assert sorted(event_dict['tags']) == sorted(tags)
106 assert sorted(event_dict['tags']) == sorted(tags)
217 extra = [('sys.argv', ["'manage.py'", "'runserver'"]),
107 extra = [('sys.argv', ["'manage.py'", "'runserver'"]),
218 ('project', 'sentry')]
108 ('project', 'sentry')]
219 assert sorted(event_dict['extra']) == sorted(extra)
109 assert sorted(event_dict['extra']) == sorted(extra)
220 request = event_dict['request']
110 request = event_dict['request']
221 assert request['url'] == 'http://127.0.0.1:8000/error'
111 assert request['url'] == 'http://127.0.0.1:8000/error'
222 assert request['cookies'] == {'appenlight': 'X'}
112 assert request['cookies'] == {'appenlight': 'X'}
223 assert request['data'] is None
113 assert request['data'] is None
224 assert request['method'] == 'GET'
114 assert request['method'] == 'GET'
225 assert request['query_string'] == ''
115 assert request['query_string'] == ''
226 assert request['env'] == {'REMOTE_ADDR': '127.0.0.1',
116 assert request['env'] == {'REMOTE_ADDR': '127.0.0.1',
227 'SERVER_NAME': 'localhost',
117 'SERVER_NAME': 'localhost',
228 'SERVER_PORT': '8000'}
118 'SERVER_PORT': '8000'}
229 assert request['headers'] == {
119 assert request['headers'] == {
230 'Accept': 'text/html,application/xhtml+xml,'
120 'Accept': 'text/html,application/xhtml+xml,'
231 'application/xml;q=0.9,image/webp,*/*;q=0.8',
121 'application/xml;q=0.9,image/webp,*/*;q=0.8',
232 'Accept-Encoding': 'gzip, deflate, sdch',
122 'Accept-Encoding': 'gzip, deflate, sdch',
233 'Accept-Language': 'en-US,en;q=0.8,pl;q=0.6',
123 'Accept-Language': 'en-US,en;q=0.8,pl;q=0.6',
234 'Connection': 'keep-alive',
124 'Connection': 'keep-alive',
235 'Content-Length': '',
125 'Content-Length': '',
236 'Content-Type': 'text/plain',
126 'Content-Type': 'text/plain',
237 'Cookie': 'appenlight=X',
127 'Cookie': 'appenlight=X',
238 'Dnt': '1',
128 'Dnt': '1',
239 'Host': '127.0.0.1:8000',
129 'Host': '127.0.0.1:8000',
240 'Upgrade-Insecure-Requests': '1',
130 'Upgrade-Insecure-Requests': '1',
241 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) '
131 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) '
242 'AppleWebKit/537.36 (KHTML, like Gecko) '
132 'AppleWebKit/537.36 (KHTML, like Gecko) '
243 'Chrome/47.0.2526.106 Safari/537.36'}
133 'Chrome/47.0.2526.106 Safari/537.36'}
244 traceback = event_dict['traceback']
134 traceback = event_dict['traceback']
245 assert traceback[0]['cline'] == 'response = wrapped_callback(request, ' \
135 assert traceback[0]['cline'] == 'response = wrapped_callback(request, ' \
246 '*callback_args, **callback_kwargs)'
136 '*callback_args, **callback_kwargs)'
247 assert traceback[0]['file'] == 'django/core/handlers/base.py'
137 assert traceback[0]['file'] == 'django/core/handlers/base.py'
248 assert traceback[0]['fn'] == 'get_response'
138 assert traceback[0]['fn'] == 'get_response'
249 assert traceback[0]['line'] == 111
139 assert traceback[0]['line'] == 111
250 assert traceback[0]['module'] == 'django.core.handlers.base'
140 assert traceback[0]['module'] == 'django.core.handlers.base'
251
141
252 assert traceback[1]['cline'] == "raise Exception(u'test 500 " \
142 assert traceback[1]['cline'] == "raise Exception(u'test 500 " \
253 "\u0142\xf3\u201c\u0107\u201c\u0107" \
143 "\u0142\xf3\u201c\u0107\u201c\u0107" \
254 "\u017c\u0105')"
144 "\u017c\u0105')"
255 assert traceback[1]['file'] == 'djangoapp/views.py'
145 assert traceback[1]['file'] == 'djangoapp/views.py'
256 assert traceback[1]['fn'] == 'error'
146 assert traceback[1]['fn'] == 'error'
257 assert traceback[1]['line'] == 84
147 assert traceback[1]['line'] == 84
258 assert traceback[1]['module'] == 'djangoapp.views'
148 assert traceback[1]['module'] == 'djangoapp.views'
259 assert sorted(traceback[1]['vars']) == sorted([
149 assert sorted(traceback[1]['vars']) == sorted([
260 ('c',
150 ('c',
261 '<sqlite3.Cursor object at 0x7fe7c82af8f0>'),
151 '<sqlite3.Cursor object at 0x7fe7c82af8f0>'),
262 ('request',
152 ('request',
263 '<WSGIRequest at 0x140633490316304>'),
153 '<WSGIRequest at 0x140633490316304>'),
264 ('conn',
154 ('conn',
265 '<sqlite3.Connection object at 0x7fe7c8b23bf8>')])
155 '<sqlite3.Connection object at 0x7fe7c8b23bf8>')])
266
156
267
157
268 class TestAPIReports_0_5_Validation(object):
158 class TestAPIReports_0_5_Validation(object):
269 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
159 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
270 def test_no_payload(self, dummy_json):
160 def test_no_payload(self, dummy_json):
271 import colander
161 import colander
272 from appenlight.validators import ReportListSchema_0_5
162 from appenlight.validators import ReportListSchema_0_5
273 utcnow = datetime.utcnow()
163 utcnow = datetime.utcnow()
274 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
164 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
275 with pytest.raises(colander.Invalid):
165 with pytest.raises(colander.Invalid):
276 schema.deserialize(dummy_json)
166 schema.deserialize(dummy_json)
277
167
278 def test_minimal_payload(self):
168 def test_minimal_payload(self):
279 dummy_json = [{}]
169 dummy_json = [{}]
280 import colander
170 import colander
281 from appenlight.validators import ReportListSchema_0_5
171 from appenlight.validators import ReportListSchema_0_5
282 utcnow = datetime.utcnow()
172 utcnow = datetime.utcnow()
283 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
173 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
284 with pytest.raises(colander.Invalid):
174 with pytest.raises(colander.Invalid):
285 schema.deserialize(dummy_json)
175 schema.deserialize(dummy_json)
286
176
287 def test_minimal_payload(self):
177 def test_minimal_payload(self):
288 dummy_json = [{'report_details': [{}]}]
178 dummy_json = [{'report_details': [{}]}]
289 from appenlight.validators import ReportListSchema_0_5
179 from appenlight.validators import ReportListSchema_0_5
290 utcnow = datetime.utcnow()
180 utcnow = datetime.utcnow()
291 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
181 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
292
182
293 deserialized = schema.deserialize(dummy_json)
183 deserialized = schema.deserialize(dummy_json)
294
184
295 expected_deserialization = [
185 expected_deserialization = [
296 {'language': 'unknown',
186 {'language': 'unknown',
297 'server': 'unknown',
187 'server': 'unknown',
298 'occurences': 1,
188 'occurences': 1,
299 'priority': 5,
189 'priority': 5,
300 'view_name': '',
190 'view_name': '',
301 'client': 'unknown',
191 'client': 'unknown',
302 'http_status': 200,
192 'http_status': 200,
303 'error': '',
193 'error': '',
304 'tags': None,
194 'tags': None,
305 'username': '',
195 'username': '',
306 'traceback': None,
196 'traceback': None,
307 'extra': None,
197 'extra': None,
308 'url': '',
198 'url': '',
309 'ip': None,
199 'ip': None,
310 'start_time': utcnow,
200 'start_time': utcnow,
311 'group_string': None,
201 'group_string': None,
312 'request': {},
202 'request': {},
313 'request_stats': None,
203 'request_stats': None,
314 'end_time': None,
204 'end_time': None,
315 'request_id': '',
205 'request_id': '',
316 'message': '',
206 'message': '',
317 'slow_calls': [],
207 'slow_calls': [],
318 'user_agent': ''
208 'user_agent': ''
319 }
209 }
320 ]
210 ]
321 assert deserialized == expected_deserialization
211 assert deserialized == expected_deserialization
322
212
323 def test_full_payload(self):
213 def test_full_payload(self):
324 import appenlight.tests.payload_examples as payload_examples
214 import appenlight.tests.payload_examples as payload_examples
325 from appenlight.validators import ReportListSchema_0_5
215 from appenlight.validators import ReportListSchema_0_5
326 PYTHON_PAYLOAD = copy.deepcopy(payload_examples.PYTHON_PAYLOAD_0_5)
216 PYTHON_PAYLOAD = copy.deepcopy(payload_examples.PYTHON_PAYLOAD_0_5)
327 utcnow = datetime.utcnow()
217 utcnow = datetime.utcnow()
328 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
218 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
329 PYTHON_PAYLOAD["tags"] = [("foo", 1), ("action", "test"), ("baz", 1.1),
219 PYTHON_PAYLOAD["tags"] = [("foo", 1), ("action", "test"), ("baz", 1.1),
330 ("date",
220 ("date",
331 utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))]
221 utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))]
332 dummy_json = [PYTHON_PAYLOAD]
222 dummy_json = [PYTHON_PAYLOAD]
333 deserialized = schema.deserialize(dummy_json)[0]
223 deserialized = schema.deserialize(dummy_json)[0]
334 assert deserialized['error'] == PYTHON_PAYLOAD['error']
224 assert deserialized['error'] == PYTHON_PAYLOAD['error']
335 assert deserialized['language'] == PYTHON_PAYLOAD['language']
225 assert deserialized['language'] == PYTHON_PAYLOAD['language']
336 assert deserialized['server'] == PYTHON_PAYLOAD['server']
226 assert deserialized['server'] == PYTHON_PAYLOAD['server']
337 assert deserialized['priority'] == PYTHON_PAYLOAD['priority']
227 assert deserialized['priority'] == PYTHON_PAYLOAD['priority']
338 assert deserialized['view_name'] == PYTHON_PAYLOAD['view_name']
228 assert deserialized['view_name'] == PYTHON_PAYLOAD['view_name']
339 assert deserialized['client'] == PYTHON_PAYLOAD['client']
229 assert deserialized['client'] == PYTHON_PAYLOAD['client']
340 assert deserialized['http_status'] == PYTHON_PAYLOAD['http_status']
230 assert deserialized['http_status'] == PYTHON_PAYLOAD['http_status']
341 assert deserialized['error'] == PYTHON_PAYLOAD['error']
231 assert deserialized['error'] == PYTHON_PAYLOAD['error']
342 assert deserialized['occurences'] == PYTHON_PAYLOAD['occurences']
232 assert deserialized['occurences'] == PYTHON_PAYLOAD['occurences']
343 assert deserialized['username'] == PYTHON_PAYLOAD['username']
233 assert deserialized['username'] == PYTHON_PAYLOAD['username']
344 assert deserialized['traceback'] == PYTHON_PAYLOAD['traceback']
234 assert deserialized['traceback'] == PYTHON_PAYLOAD['traceback']
345 assert deserialized['url'] == PYTHON_PAYLOAD['url']
235 assert deserialized['url'] == PYTHON_PAYLOAD['url']
346 assert deserialized['ip'] == PYTHON_PAYLOAD['ip']
236 assert deserialized['ip'] == PYTHON_PAYLOAD['ip']
347 assert deserialized['start_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \
237 assert deserialized['start_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \
348 PYTHON_PAYLOAD['start_time']
238 PYTHON_PAYLOAD['start_time']
349 assert deserialized['ip'] == PYTHON_PAYLOAD['ip']
239 assert deserialized['ip'] == PYTHON_PAYLOAD['ip']
350 assert deserialized['group_string'] is None
240 assert deserialized['group_string'] is None
351 assert deserialized['request_stats'] == PYTHON_PAYLOAD['request_stats']
241 assert deserialized['request_stats'] == PYTHON_PAYLOAD['request_stats']
352 assert deserialized['end_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \
242 assert deserialized['end_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \
353 PYTHON_PAYLOAD['end_time']
243 PYTHON_PAYLOAD['end_time']
354 assert deserialized['request_id'] == PYTHON_PAYLOAD['request_id']
244 assert deserialized['request_id'] == PYTHON_PAYLOAD['request_id']
355 assert deserialized['message'] == PYTHON_PAYLOAD['message']
245 assert deserialized['message'] == PYTHON_PAYLOAD['message']
356 assert deserialized['user_agent'] == PYTHON_PAYLOAD['user_agent']
246 assert deserialized['user_agent'] == PYTHON_PAYLOAD['user_agent']
357 assert deserialized['slow_calls'][0]['start'].strftime(
247 assert deserialized['slow_calls'][0]['start'].strftime(
358 '%Y-%m-%dT%H:%M:%S.0') == PYTHON_PAYLOAD['slow_calls'][0][
248 '%Y-%m-%dT%H:%M:%S.0') == PYTHON_PAYLOAD['slow_calls'][0][
359 'start']
249 'start']
360 assert deserialized['slow_calls'][0]['end'].strftime(
250 assert deserialized['slow_calls'][0]['end'].strftime(
361 '%Y-%m-%dT%H:%M:%S.0') == PYTHON_PAYLOAD['slow_calls'][0][
251 '%Y-%m-%dT%H:%M:%S.0') == PYTHON_PAYLOAD['slow_calls'][0][
362 'end']
252 'end']
363 assert deserialized['slow_calls'][0]['statement'] == \
253 assert deserialized['slow_calls'][0]['statement'] == \
364 PYTHON_PAYLOAD['slow_calls'][0]['statement']
254 PYTHON_PAYLOAD['slow_calls'][0]['statement']
365 assert deserialized['slow_calls'][0]['parameters'] == \
255 assert deserialized['slow_calls'][0]['parameters'] == \
366 PYTHON_PAYLOAD['slow_calls'][0]['parameters']
256 PYTHON_PAYLOAD['slow_calls'][0]['parameters']
367 assert deserialized['slow_calls'][0]['type'] == \
257 assert deserialized['slow_calls'][0]['type'] == \
368 PYTHON_PAYLOAD['slow_calls'][0]['type']
258 PYTHON_PAYLOAD['slow_calls'][0]['type']
369 assert deserialized['slow_calls'][0]['subtype'] == \
259 assert deserialized['slow_calls'][0]['subtype'] == \
370 PYTHON_PAYLOAD['slow_calls'][0]['subtype']
260 PYTHON_PAYLOAD['slow_calls'][0]['subtype']
371 assert deserialized['slow_calls'][0]['location'] == ''
261 assert deserialized['slow_calls'][0]['location'] == ''
372 assert deserialized['tags'] == [
262 assert deserialized['tags'] == [
373 ('foo', 1), ('action', 'test'),
263 ('foo', 1), ('action', 'test'),
374 ('baz', 1.1), ('date', utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))]
264 ('baz', 1.1), ('date', utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))]
375
265
376
266
377 @pytest.mark.usefixtures('log_schema')
267 @pytest.mark.usefixtures('log_schema')
378 class TestAPILogsValidation(object):
268 class TestAPILogsValidation(object):
379 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
269 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
380 def test_no_payload(self, dummy_json, log_schema):
270 def test_no_payload(self, dummy_json, log_schema):
381 import colander
271 import colander
382
272
383 with pytest.raises(colander.Invalid):
273 with pytest.raises(colander.Invalid):
384 log_schema.deserialize(dummy_json)
274 log_schema.deserialize(dummy_json)
385
275
386 def test_minimal_payload(self, log_schema):
276 def test_minimal_payload(self, log_schema):
387 dummy_json = [{}]
277 dummy_json = [{}]
388 deserialized = log_schema.deserialize(dummy_json)[0]
278 deserialized = log_schema.deserialize(dummy_json)[0]
389 expected = {'log_level': 'UNKNOWN',
279 expected = {'log_level': 'UNKNOWN',
390 'namespace': '',
280 'namespace': '',
391 'server': 'unknown',
281 'server': 'unknown',
392 'request_id': '',
282 'request_id': '',
393 'primary_key': None,
283 'primary_key': None,
394 'date': datetime.utcnow(),
284 'date': datetime.utcnow(),
395 'message': '',
285 'message': '',
396 'tags': None}
286 'tags': None}
397 assert deserialized['log_level'] == expected['log_level']
287 assert deserialized['log_level'] == expected['log_level']
398 assert deserialized['message'] == expected['message']
288 assert deserialized['message'] == expected['message']
399 assert deserialized['namespace'] == expected['namespace']
289 assert deserialized['namespace'] == expected['namespace']
400 assert deserialized['request_id'] == expected['request_id']
290 assert deserialized['request_id'] == expected['request_id']
401 assert deserialized['server'] == expected['server']
291 assert deserialized['server'] == expected['server']
402 assert deserialized['tags'] == expected['tags']
292 assert deserialized['tags'] == expected['tags']
403 assert deserialized['primary_key'] == expected['primary_key']
293 assert deserialized['primary_key'] == expected['primary_key']
404
294
405 def test_normal_payload(self, log_schema):
295 def test_normal_payload(self, log_schema):
406 import appenlight.tests.payload_examples as payload_examples
296 import appenlight.tests.payload_examples as payload_examples
407 deserialized = log_schema.deserialize(payload_examples.LOG_EXAMPLES)[0]
297 deserialized = log_schema.deserialize(payload_examples.LOG_EXAMPLES)[0]
408 expected = payload_examples.LOG_EXAMPLES[0]
298 expected = payload_examples.LOG_EXAMPLES[0]
409 assert deserialized['log_level'] == expected['log_level']
299 assert deserialized['log_level'] == expected['log_level']
410 assert deserialized['message'] == expected['message']
300 assert deserialized['message'] == expected['message']
411 assert deserialized['namespace'] == expected['namespace']
301 assert deserialized['namespace'] == expected['namespace']
412 assert deserialized['request_id'] == expected['request_id']
302 assert deserialized['request_id'] == expected['request_id']
413 assert deserialized['server'] == expected['server']
303 assert deserialized['server'] == expected['server']
414 assert deserialized['date'].strftime('%Y-%m-%dT%H:%M:%S.%f') == \
304 assert deserialized['date'].strftime('%Y-%m-%dT%H:%M:%S.%f') == \
415 expected['date']
305 expected['date']
416 assert deserialized['tags'][0][0] == "tag_name"
306 assert deserialized['tags'][0][0] == "tag_name"
417 assert deserialized['tags'][0][1] == "tag_value"
307 assert deserialized['tags'][0][1] == "tag_value"
418 assert deserialized['tags'][1][0] == "tag_name2"
308 assert deserialized['tags'][1][0] == "tag_name2"
419 assert deserialized['tags'][1][1] == 2
309 assert deserialized['tags'][1][1] == 2
420
310
421 def test_normal_payload_date_without_microseconds(self, log_schema):
311 def test_normal_payload_date_without_microseconds(self, log_schema):
422 import appenlight.tests.payload_examples as payload_examples
312 import appenlight.tests.payload_examples as payload_examples
423 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
313 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
424 LOG_EXAMPLE[0]['date'] = datetime.utcnow().strftime(
314 LOG_EXAMPLE[0]['date'] = datetime.utcnow().strftime(
425 '%Y-%m-%dT%H:%M:%S')
315 '%Y-%m-%dT%H:%M:%S')
426 deserialized = log_schema.deserialize(LOG_EXAMPLE)
316 deserialized = log_schema.deserialize(LOG_EXAMPLE)
427 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M:%S') == \
317 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M:%S') == \
428 LOG_EXAMPLE[0]['date']
318 LOG_EXAMPLE[0]['date']
429
319
430 def test_normal_payload_date_without_seconds(self, log_schema):
320 def test_normal_payload_date_without_seconds(self, log_schema):
431 import appenlight.tests.payload_examples as payload_examples
321 import appenlight.tests.payload_examples as payload_examples
432 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
322 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
433 LOG_EXAMPLE[0]['date'] = datetime.utcnow().date().strftime(
323 LOG_EXAMPLE[0]['date'] = datetime.utcnow().date().strftime(
434 '%Y-%m-%dT%H:%M')
324 '%Y-%m-%dT%H:%M')
435 deserialized = log_schema.deserialize(LOG_EXAMPLE)
325 deserialized = log_schema.deserialize(LOG_EXAMPLE)
436 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') == \
326 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') == \
437 LOG_EXAMPLE[0]['date']
327 LOG_EXAMPLE[0]['date']
438
328
439 def test_payload_empty_date(self, log_schema):
329 def test_payload_empty_date(self, log_schema):
440 import appenlight.tests.payload_examples as payload_examples
330 import appenlight.tests.payload_examples as payload_examples
441 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
331 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
442 LOG_EXAMPLE[0]['date'] = None
332 LOG_EXAMPLE[0]['date'] = None
443 deserialized = log_schema.deserialize(LOG_EXAMPLE)
333 deserialized = log_schema.deserialize(LOG_EXAMPLE)
444 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') is not None
334 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') is not None
445
335
446 def test_payload_no_date(self, log_schema):
336 def test_payload_no_date(self, log_schema):
447 import appenlight.tests.payload_examples as payload_examples
337 import appenlight.tests.payload_examples as payload_examples
448 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
338 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
449 LOG_EXAMPLE[0].pop('date', None)
339 LOG_EXAMPLE[0].pop('date', None)
450 deserialized = log_schema.deserialize(LOG_EXAMPLE)
340 deserialized = log_schema.deserialize(LOG_EXAMPLE)
451 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') is not None
341 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') is not None
452
342
453
343
454 @pytest.mark.usefixtures('general_metrics_schema')
344 @pytest.mark.usefixtures('general_metrics_schema')
455 class TestAPIGeneralMetricsValidation(object):
345 class TestAPIGeneralMetricsValidation(object):
456 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
346 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
457 def test_no_payload(self, dummy_json, general_metrics_schema):
347 def test_no_payload(self, dummy_json, general_metrics_schema):
458 import colander
348 import colander
459
349
460 with pytest.raises(colander.Invalid):
350 with pytest.raises(colander.Invalid):
461 general_metrics_schema.deserialize(dummy_json)
351 general_metrics_schema.deserialize(dummy_json)
462
352
463 def test_minimal_payload(self, general_metrics_schema):
353 def test_minimal_payload(self, general_metrics_schema):
464 dummy_json = [{}]
354 dummy_json = [{'tags': [['counter_a', 15.5], ['counter_b', 63]]}]
465 deserialized = general_metrics_schema.deserialize(dummy_json)[0]
355 deserialized = general_metrics_schema.deserialize(dummy_json)[0]
466 expected = {'namespace': '',
356 expected = {'namespace': '',
467 'server_name': 'unknown',
357 'server_name': 'unknown',
468 'tags': None,
358 'tags': [('counter_a', 15.5), ('counter_b', 63)],
469 'timestamp': datetime.utcnow()}
359 'timestamp': datetime.utcnow()}
470 assert deserialized['namespace'] == expected['namespace']
360 assert deserialized['namespace'] == expected['namespace']
471 assert deserialized['server_name'] == expected['server_name']
361 assert deserialized['server_name'] == expected['server_name']
472 assert deserialized['tags'] == expected['tags']
362 assert deserialized['tags'] == expected['tags']
473
363
474 def test_normal_payload(self, general_metrics_schema):
364 def test_normal_payload(self, general_metrics_schema):
475 import appenlight.tests.payload_examples as payload_examples
365 import appenlight.tests.payload_examples as payload_examples
476 dummy_json = [payload_examples.METRICS_PAYLOAD]
366 dummy_json = [payload_examples.METRICS_PAYLOAD]
477 deserialized = general_metrics_schema.deserialize(dummy_json)[0]
367 deserialized = general_metrics_schema.deserialize(dummy_json)[0]
478 expected = {'namespace': 'some.monitor',
368 expected = {'namespace': 'some.monitor',
479 'server_name': 'server.name',
369 'server_name': 'server.name',
480 'tags': [('usage_foo', 15.5), ('usage_bar', 63)],
370 'tags': [('usage_foo', 15.5), ('usage_bar', 63)],
481 'timestamp': datetime.utcnow()}
371 'timestamp': datetime.utcnow()}
482 assert deserialized['namespace'] == expected['namespace']
372 assert deserialized['namespace'] == expected['namespace']
483 assert deserialized['server_name'] == expected['server_name']
373 assert deserialized['server_name'] == expected['server_name']
484 assert deserialized['tags'] == expected['tags']
374 assert deserialized['tags'] == expected['tags']
485
375
486
376
487 @pytest.mark.usefixtures('request_metrics_schema')
377 @pytest.mark.usefixtures('request_metrics_schema')
488 class TestAPIRequestMetricsValidation(object):
378 class TestAPIRequestMetricsValidation(object):
489 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
379 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
490 def test_no_payload(self, dummy_json, request_metrics_schema):
380 def test_no_payload(self, dummy_json, request_metrics_schema):
491 import colander
381 import colander
492
382
493 with pytest.raises(colander.Invalid):
383 with pytest.raises(colander.Invalid):
494 print(request_metrics_schema.deserialize(dummy_json))
384 print(request_metrics_schema.deserialize(dummy_json))
495
385
496 def test_normal_payload(self, request_metrics_schema):
386 def test_normal_payload(self, request_metrics_schema):
497 import appenlight.tests.payload_examples as payload_examples
387 import appenlight.tests.payload_examples as payload_examples
498 dummy_json = payload_examples.REQUEST_METRICS_EXAMPLES
388 dummy_json = payload_examples.REQUEST_METRICS_EXAMPLES
499 deserialized = request_metrics_schema.deserialize(dummy_json)[0]
389 deserialized = request_metrics_schema.deserialize(dummy_json)[0]
500 expected = {'metrics': [('dir/module:func',
390 expected = {'metrics': [('dir/module:func',
501 {'custom': 0.0,
391 {'custom': 0.0,
502 'custom_calls': 0.0,
392 'custom_calls': 0.0,
503 'main': 0.01664,
393 'main': 0.01664,
504 'nosql': 0.00061,
394 'nosql': 0.00061,
505 'nosql_calls': 23.0,
395 'nosql_calls': 23.0,
506 'remote': 0.0,
396 'remote': 0.0,
507 'remote_calls': 0.0,
397 'remote_calls': 0.0,
508 'requests': 1,
398 'requests': 1,
509 'sql': 0.00105,
399 'sql': 0.00105,
510 'sql_calls': 2.0,
400 'sql_calls': 2.0,
511 'tmpl': 0.0,
401 'tmpl': 0.0,
512 'tmpl_calls': 0.0}),
402 'tmpl_calls': 0.0}),
513 ('SomeView.function',
403 ('SomeView.function',
514 {'custom': 0.0,
404 {'custom': 0.0,
515 'custom_calls': 0.0,
405 'custom_calls': 0.0,
516 'main': 0.647261,
406 'main': 0.647261,
517 'nosql': 0.306554,
407 'nosql': 0.306554,
518 'nosql_calls': 140.0,
408 'nosql_calls': 140.0,
519 'remote': 0.0,
409 'remote': 0.0,
520 'remote_calls': 0.0,
410 'remote_calls': 0.0,
521 'requests': 28,
411 'requests': 28,
522 'sql': 0.0,
412 'sql': 0.0,
523 'sql_calls': 0.0,
413 'sql_calls': 0.0,
524 'tmpl': 0.0,
414 'tmpl': 0.0,
525 'tmpl_calls': 0.0})],
415 'tmpl_calls': 0.0})],
526 'server': 'some.server.hostname',
416 'server': 'some.server.hostname',
527 'timestamp': datetime.utcnow()}
417 'timestamp': datetime.utcnow()}
528 assert deserialized['server'] == expected['server']
418 assert deserialized['server'] == expected['server']
529 metric = deserialized['metrics'][0]
419 metric = deserialized['metrics'][0]
530 expected_metric = expected['metrics'][0]
420 expected_metric = expected['metrics'][0]
531 assert metric[0] == expected_metric[0]
421 assert metric[0] == expected_metric[0]
532 assert sorted(metric[1].items()) == sorted(expected_metric[1].items())
422 assert sorted(metric[1].items()) == sorted(expected_metric[1].items())
533
423
534
424
535 @pytest.mark.usefixtures('default_application')
425 @pytest.mark.usefixtures('default_application')
536 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
426 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
537 class TestAPIReportsView(object):
427 class TestAPIReportsView(object):
538 def test_no_json_payload(self, default_application):
428 def test_no_json_payload(self, default_application):
539 import colander
429 import colander
540 from appenlight.models.services.application import ApplicationService
430 from appenlight.models.services.application import ApplicationService
541 from appenlight.views.api import reports_create
431 from appenlight.views.api import reports_create
542
432
543 context = DummyContext()
433 context = DummyContext()
544 context.resource = ApplicationService.by_id(1)
434 context.resource = ApplicationService.by_id(1)
545 request = testing.DummyRequest(
435 request = testing.DummyRequest(
546 headers={'Content-Type': 'application/json'})
436 headers={'Content-Type': 'application/json'})
547 request.unsafe_json_body = ''
437 request.unsafe_json_body = ''
548 request.context = context
438 request.context = context
549 route = mock.Mock()
439 route = mock.Mock()
550 route.name = 'api_reports'
440 route.name = 'api_reports'
551 request.matched_route = route
441 request.matched_route = route
552 with pytest.raises(colander.Invalid):
442 with pytest.raises(colander.Invalid):
553 response = reports_create(request)
443 response = reports_create(request)
554
444
555 def test_single_proper_json_0_5_payload(self):
445 def test_single_proper_json_0_5_payload(self):
556 import appenlight.tests.payload_examples as payload_examples
446 import appenlight.tests.payload_examples as payload_examples
557 from appenlight.views.api import reports_create
447 from appenlight.views.api import reports_create
558 from appenlight.models.services.application import ApplicationService
448 from appenlight.models.services.application import ApplicationService
559 from appenlight.models.report_group import ReportGroup
449 from appenlight.models.report_group import ReportGroup
560 route = mock.Mock()
450 route = mock.Mock()
561 route.name = 'api_reports'
451 route.name = 'api_reports'
562 request = pyramid.threadlocal.get_current_request()
452 request = pyramid.threadlocal.get_current_request()
563 context = DummyContext()
453 context = DummyContext()
564 context.resource = ApplicationService.by_id(1)
454 context.resource = ApplicationService.by_id(1)
565 request.context = context
455 request.context = context
566 request.matched_route = route
456 request.matched_route = route
567 PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5
457 PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5
568 request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD)]
458 request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD)]
569 reports_create(request)
459 reports_create(request)
570 query = DBSession.query(ReportGroup)
460 query = DBSession.query(ReportGroup)
571 report = query.first()
461 report = query.first()
572 assert query.count() == 1
462 assert query.count() == 1
573 assert report.total_reports == 1
463 assert report.total_reports == 1
574
464
575 def test_grouping_0_5(self):
465 def test_grouping_0_5(self):
576 import appenlight.tests.payload_examples as payload_examples
466 import appenlight.tests.payload_examples as payload_examples
577 from appenlight.views.api import reports_create
467 from appenlight.views.api import reports_create
578 from appenlight.models.services.application import ApplicationService
468 from appenlight.models.services.application import ApplicationService
579 from appenlight.models.report_group import ReportGroup
469 from appenlight.models.report_group import ReportGroup
580 route = mock.Mock()
470 route = mock.Mock()
581 route.name = 'api_reports'
471 route.name = 'api_reports'
582 request = pyramid.threadlocal.get_current_request()
472 request = pyramid.threadlocal.get_current_request()
583 context = DummyContext()
473 context = DummyContext()
584 context.resource = ApplicationService.by_id(1)
474 context.resource = ApplicationService.by_id(1)
585 request.context = context
475 request.context = context
586 request.matched_route = route
476 request.matched_route = route
587 PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5
477 PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5
588 request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD),
478 request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD),
589 copy.deepcopy(PYTHON_PAYLOAD)]
479 copy.deepcopy(PYTHON_PAYLOAD)]
590 reports_create(request)
480 reports_create(request)
591 query = DBSession.query(ReportGroup)
481 query = DBSession.query(ReportGroup)
592 report = query.first()
482 report = query.first()
593 assert query.count() == 1
483 assert query.count() == 1
594 assert report.total_reports == 2
484 assert report.total_reports == 2
595
485
596 def test_grouping_different_reports_0_5(self):
486 def test_grouping_different_reports_0_5(self):
597 import appenlight.tests.payload_examples as payload_examples
487 import appenlight.tests.payload_examples as payload_examples
598 from appenlight.views.api import reports_create
488 from appenlight.views.api import reports_create
599 from appenlight.models.services.application import ApplicationService
489 from appenlight.models.services.application import ApplicationService
600 from appenlight.models.report_group import ReportGroup
490 from appenlight.models.report_group import ReportGroup
601 route = mock.Mock()
491 route = mock.Mock()
602 route.name = 'api_reports'
492 route.name = 'api_reports'
603 request = pyramid.threadlocal.get_current_request()
493 request = pyramid.threadlocal.get_current_request()
604 context = DummyContext()
494 context = DummyContext()
605 context.resource = ApplicationService.by_id(1)
495 context.resource = ApplicationService.by_id(1)
606 request.context = context
496 request.context = context
607 request.matched_route = route
497 request.matched_route = route
608 PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5
498 PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5
609 PARSED_REPORT_404 = payload_examples.PARSED_REPORT_404
499 PARSED_REPORT_404 = payload_examples.PARSED_REPORT_404
610 request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD),
500 request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD),
611 copy.deepcopy(PARSED_REPORT_404)]
501 copy.deepcopy(PARSED_REPORT_404)]
612 reports_create(request)
502 reports_create(request)
613 query = DBSession.query(ReportGroup)
503 query = DBSession.query(ReportGroup)
614 report = query.first()
504 report = query.first()
615 assert query.count() == 2
505 assert query.count() == 2
616 assert report.total_reports == 1
506 assert report.total_reports == 1
617
507
618
508
619 @pytest.mark.usefixtures('default_application')
509 @pytest.mark.usefixtures('default_application')
620 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
510 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
621 class TestAirbrakeXMLView(object):
511 class TestAirbrakeXMLView(object):
622
512
623 def test_normal_payload_parsing(self):
513 def test_normal_payload_parsing(self):
624 import datetime
514 import datetime
625 import defusedxml.ElementTree as ElementTree
515 import defusedxml.ElementTree as ElementTree
626 import appenlight.tests.payload_examples as payload_examples
516 import appenlight.tests.payload_examples as payload_examples
627 from appenlight.lib.utils.airbrake import parse_airbrake_xml
517 from appenlight.lib.utils.airbrake import parse_airbrake_xml
628 from appenlight.validators import ReportListSchema_0_5
518 from appenlight.validators import ReportListSchema_0_5
629
519
630 context = DummyContext()
520 context = DummyContext()
631 request = testing.DummyRequest(
521 request = testing.DummyRequest(
632 headers={'Content-Type': 'application/xml'})
522 headers={'Content-Type': 'application/xml'})
633 request.context = context
523 request.context = context
634 request.context.possibly_public = False
524 request.context.possibly_public = False
635 root = ElementTree.fromstring(payload_examples.AIRBRAKE_RUBY_EXAMPLE)
525 root = ElementTree.fromstring(payload_examples.AIRBRAKE_RUBY_EXAMPLE)
636 request.context.airbrake_xml_etree = root
526 request.context.airbrake_xml_etree = root
637 error_dict = parse_airbrake_xml(request)
527 error_dict = parse_airbrake_xml(request)
638 schema = ReportListSchema_0_5().bind(utcnow=datetime.datetime.utcnow())
528 schema = ReportListSchema_0_5().bind(utcnow=datetime.datetime.utcnow())
639 deserialized_report = schema.deserialize([error_dict])[0]
529 deserialized_report = schema.deserialize([error_dict])[0]
640 assert deserialized_report['client'] == 'Airbrake Notifier'
530 assert deserialized_report['client'] == 'Airbrake Notifier'
641 assert deserialized_report['error'] == 'NameError: undefined local variable or method `sdfdfdf\' for #<#<Class:0x000000039a8b90>:0x00000002c53df0>'
531 assert deserialized_report['error'] == 'NameError: undefined local variable or method `sdfdfdf\' for #<#<Class:0x000000039a8b90>:0x00000002c53df0>'
642 assert deserialized_report['http_status'] == 500
532 assert deserialized_report['http_status'] == 500
643 assert deserialized_report['language'] == 'unknown'
533 assert deserialized_report['language'] == 'unknown'
644 assert deserialized_report['message'] == ''
534 assert deserialized_report['message'] == ''
645 assert deserialized_report['occurences'] == 1
535 assert deserialized_report['occurences'] == 1
646 assert deserialized_report['priority'] == 5
536 assert deserialized_report['priority'] == 5
647 d_request = deserialized_report['request']
537 d_request = deserialized_report['request']
648 assert d_request['GET'] == {'test': '1234'}
538 assert d_request['GET'] == {'test': '1234'}
649 assert d_request['action_dispatch.request.parameters'] == {
539 assert d_request['action_dispatch.request.parameters'] == {
650 'action': 'index',
540 'action': 'index',
651 'controller': 'welcome',
541 'controller': 'welcome',
652 'test': '1234'}
542 'test': '1234'}
653 assert deserialized_report['request_id'] == 'c11b2267f3ad8b00a1768cae35559fa1'
543 assert deserialized_report['request_id'] == 'c11b2267f3ad8b00a1768cae35559fa1'
654 assert deserialized_report['server'] == 'ergo-desktop'
544 assert deserialized_report['server'] == 'ergo-desktop'
655 assert deserialized_report['traceback'][0] == {
545 assert deserialized_report['traceback'][0] == {
656 'cline': 'block in start_thread',
546 'cline': 'block in start_thread',
657 'file': '/home/ergo/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/webrick/server.rb',
547 'file': '/home/ergo/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/webrick/server.rb',
658 'fn': 'block in start_thread',
548 'fn': 'block in start_thread',
659 'line': '191',
549 'line': '191',
660 'module': '',
550 'module': '',
661 'vars': {}}
551 'vars': {}}
662 assert deserialized_report['traceback'][-1] == {
552 assert deserialized_report['traceback'][-1] == {
663 'cline': '_app_views_welcome_index_html_erb___2570061166873166679_31748940',
553 'cline': '_app_views_welcome_index_html_erb___2570061166873166679_31748940',
664 'file': '[PROJECT_ROOT]/app/views/welcome/index.html.erb',
554 'file': '[PROJECT_ROOT]/app/views/welcome/index.html.erb',
665 'fn': '_app_views_welcome_index_html_erb___2570061166873166679_31748940',
555 'fn': '_app_views_welcome_index_html_erb___2570061166873166679_31748940',
666 'line': '3',
556 'line': '3',
667 'module': '',
557 'module': '',
668 'vars': {}}
558 'vars': {}}
669 assert deserialized_report['url'] == 'http://0.0.0.0:3000/welcome/index?test=1234'
559 assert deserialized_report['url'] == 'http://0.0.0.0:3000/welcome/index?test=1234'
670 assert deserialized_report['view_name'] == 'welcome:index'
560 assert deserialized_report['view_name'] == 'welcome:index'
671
561
672 def test_normal_payload_view(self):
562 def test_normal_payload_view(self):
673 import defusedxml.ElementTree as ElementTree
563 import defusedxml.ElementTree as ElementTree
674 import appenlight.tests.payload_examples as payload_examples
564 import appenlight.tests.payload_examples as payload_examples
675
565
676 from appenlight.models.services.application import ApplicationService
566 from appenlight.models.services.application import ApplicationService
677 from appenlight.views.api import airbrake_xml_compat
567 from appenlight.views.api import airbrake_xml_compat
678
568
679 context = DummyContext()
569 context = DummyContext()
680 context.resource = ApplicationService.by_id(1)
570 context.resource = ApplicationService.by_id(1)
681 request = testing.DummyRequest(
571 request = testing.DummyRequest(
682 headers={'Content-Type': 'application/xml'})
572 headers={'Content-Type': 'application/xml'})
683 request.context = context
573 request.context = context
684 request.context.possibly_public = False
574 request.context.possibly_public = False
685 root = ElementTree.fromstring(payload_examples.AIRBRAKE_RUBY_EXAMPLE)
575 root = ElementTree.fromstring(payload_examples.AIRBRAKE_RUBY_EXAMPLE)
686 request.context.airbrake_xml_etree = root
576 request.context.airbrake_xml_etree = root
687 route = mock.Mock()
577 route = mock.Mock()
688 route.name = 'api_airbrake'
578 route.name = 'api_airbrake'
689 request.matched_route = route
579 request.matched_route = route
690 result = airbrake_xml_compat(request)
580 result = airbrake_xml_compat(request)
691 assert '<notice><id>' in result
581 assert '<notice><id>' in result
692
582
693
583
694 @pytest.mark.usefixtures('default_application')
584 @pytest.mark.usefixtures('default_application')
695 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
585 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
696 class TestAPILogView(object):
586 class TestAPILogView(object):
697 def test_no_json_payload(self, base_app):
587 def test_no_json_payload(self, base_app):
698 import colander
588 import colander
699 from appenlight.models.services.application import ApplicationService
589 from appenlight.models.services.application import ApplicationService
700 from appenlight.views.api import logs_create
590 from appenlight.views.api import logs_create
701
591
702 context = DummyContext()
592 context = DummyContext()
703 context.resource = ApplicationService.by_id(1)
593 context.resource = ApplicationService.by_id(1)
704 request = testing.DummyRequest(
594 request = testing.DummyRequest(
705 headers={'Content-Type': 'application/json'})
595 headers={'Content-Type': 'application/json'})
706 request.context = context
596 request.context = context
707 request.registry = base_app.registry
597 request.registry = base_app.registry
708 request.unsafe_json_body = ''
598 request.unsafe_json_body = ''
709 route = mock.Mock()
599 route = mock.Mock()
710 route.name = 'api_logs'
600 route.name = 'api_logs'
711 request.matched_route = route
601 request.matched_route = route
712 with pytest.raises(colander.Invalid):
602 with pytest.raises(colander.Invalid):
713 response = logs_create(request)
603 response = logs_create(request)
714
604
715 def test_single_json_payload(self):
605 def test_single_json_payload(self):
716 import appenlight.tests.payload_examples as payload_examples
606 import appenlight.tests.payload_examples as payload_examples
717 from appenlight.models.log import Log
607 from appenlight.models.log import Log
718 from appenlight.views.api import logs_create
608 from appenlight.views.api import logs_create
719 from appenlight.models.services.application import ApplicationService
609 from appenlight.models.services.application import ApplicationService
720 route = mock.Mock()
610 route = mock.Mock()
721 route.name = 'api_logs'
611 route.name = 'api_logs'
722 request = pyramid.threadlocal.get_current_request()
612 request = pyramid.threadlocal.get_current_request()
723 context = DummyContext()
613 context = DummyContext()
724 context.resource = ApplicationService.by_id(1)
614 context.resource = ApplicationService.by_id(1)
725 request.context = context
615 request.context = context
726 request.matched_route = route
616 request.matched_route = route
727 request.unsafe_json_body = [copy.deepcopy(
617 request.unsafe_json_body = [copy.deepcopy(
728 payload_examples.LOG_EXAMPLES[0])]
618 payload_examples.LOG_EXAMPLES[0])]
729 logs_create(request)
619 logs_create(request)
730 query = DBSession.query(Log)
620 query = DBSession.query(Log)
731 log = query.first()
621 log = query.first()
732 assert query.count() == 1
622 assert query.count() == 1
733 assert log.message == "OMG ValueError happened"
623 assert log.message == "OMG ValueError happened"
734
624
735 def test_multiple_json_payload(self):
625 def test_multiple_json_payload(self):
736 import appenlight.tests.payload_examples as payload_examples
626 import appenlight.tests.payload_examples as payload_examples
737 from appenlight.models.log import Log
627 from appenlight.models.log import Log
738 from appenlight.views.api import logs_create
628 from appenlight.views.api import logs_create
739 from appenlight.models.services.application import ApplicationService
629 from appenlight.models.services.application import ApplicationService
740 route = mock.Mock()
630 route = mock.Mock()
741 route.name = 'api_logs'
631 route.name = 'api_logs'
742 request = pyramid.threadlocal.get_current_request()
632 request = pyramid.threadlocal.get_current_request()
743 context = DummyContext()
633 context = DummyContext()
744 context.resource = ApplicationService.by_id(1)
634 context.resource = ApplicationService.by_id(1)
745 request.context = context
635 request.context = context
746 request.matched_route = route
636 request.matched_route = route
747 LOG_PAYLOAD = payload_examples.LOG_EXAMPLES[0]
637 LOG_PAYLOAD = payload_examples.LOG_EXAMPLES[0]
748 LOG_PAYLOAD2 = payload_examples.LOG_EXAMPLES[1]
638 LOG_PAYLOAD2 = payload_examples.LOG_EXAMPLES[1]
749 request.unsafe_json_body = copy.deepcopy([LOG_PAYLOAD, LOG_PAYLOAD2])
639 request.unsafe_json_body = copy.deepcopy([LOG_PAYLOAD, LOG_PAYLOAD2])
750 logs_create(request)
640 logs_create(request)
751 query = DBSession.query(Log).order_by(sa.asc(Log.log_id))
641 query = DBSession.query(Log).order_by(sa.asc(Log.log_id))
752 assert query.count() == 2
642 assert query.count() == 2
753 assert query[0].message == "OMG ValueError happened"
643 assert query[0].message == "OMG ValueError happened"
754 assert query[1].message == "OMG ValueError happened2"
644 assert query[1].message == "OMG ValueError happened2"
755
645
756 def test_public_key_rewriting(self):
646 def test_public_key_rewriting(self):
757 import appenlight.tests.payload_examples as payload_examples
647 import appenlight.tests.payload_examples as payload_examples
758 from appenlight.models.log import Log
648 from appenlight.models.log import Log
759 from appenlight.views.api import logs_create
649 from appenlight.views.api import logs_create
760 from appenlight.models.services.application import ApplicationService
650 from appenlight.models.services.application import ApplicationService
761 route = mock.Mock()
651 route = mock.Mock()
762 route.name = 'api_logs'
652 route.name = 'api_logs'
763 request = pyramid.threadlocal.get_current_request()
653 request = pyramid.threadlocal.get_current_request()
764 context = DummyContext()
654 context = DummyContext()
765 context.resource = ApplicationService.by_id(1)
655 context.resource = ApplicationService.by_id(1)
766 request.context = context
656 request.context = context
767 request.matched_route = route
657 request.matched_route = route
768
658
769 LOG_PAYLOAD = copy.deepcopy(payload_examples.LOG_EXAMPLES[0])
659 LOG_PAYLOAD = copy.deepcopy(payload_examples.LOG_EXAMPLES[0])
770 LOG_PAYLOAD2 = copy.deepcopy(payload_examples.LOG_EXAMPLES[1])
660 LOG_PAYLOAD2 = copy.deepcopy(payload_examples.LOG_EXAMPLES[1])
771 LOG_PAYLOAD['primary_key'] = 'X2'
661 LOG_PAYLOAD['primary_key'] = 'X2'
772 LOG_PAYLOAD2['primary_key'] = 'X2'
662 LOG_PAYLOAD2['primary_key'] = 'X2'
773 request.unsafe_json_body = [LOG_PAYLOAD, LOG_PAYLOAD2]
663 request.unsafe_json_body = [LOG_PAYLOAD, LOG_PAYLOAD2]
774 logs_create(request)
664 logs_create(request)
775
665
776 query = DBSession.query(Log).order_by(sa.asc(Log.log_id))
666 query = DBSession.query(Log).order_by(sa.asc(Log.log_id))
777 assert query.count() == 1
667 assert query.count() == 1
778 assert query[0].message == "OMG ValueError happened2"
668 assert query[0].message == "OMG ValueError happened2"
779
669
780 @pytest.mark.usefixtures('default_application')
670 @pytest.mark.usefixtures('default_application')
781 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
671 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
782 class TestAPIGeneralMetricsView(object):
672 class TestAPIGeneralMetricsView(object):
783 def test_no_json_payload(self, base_app):
673 def test_no_json_payload(self, base_app):
784 import colander
674 import colander
785 from appenlight.models.services.application import ApplicationService
675 from appenlight.models.services.application import ApplicationService
786 from appenlight.views.api import general_metrics_create
676 from appenlight.views.api import general_metrics_create
787 route = mock.Mock()
677 route = mock.Mock()
788 route.name = 'api_general_metrics'
678 route.name = 'api_general_metrics'
789 context = DummyContext()
679 context = DummyContext()
790 context.resource = ApplicationService.by_id(1)
680 context.resource = ApplicationService.by_id(1)
791 request = testing.DummyRequest(
681 request = testing.DummyRequest(
792 headers={'Content-Type': 'application/json'})
682 headers={'Content-Type': 'application/json'})
793 request.context = context
683 request.context = context
794 request.registry = base_app.registry
684 request.registry = base_app.registry
795 request.unsafe_json_body = ''
685 request.unsafe_json_body = ''
796 request.matched_route = route
686 request.matched_route = route
797 with pytest.raises(colander.Invalid):
687 with pytest.raises(colander.Invalid):
798 general_metrics_create(request)
688 general_metrics_create(request)
799
689
800 def test_single_json_payload(self):
690 def test_single_json_payload(self):
801 import appenlight.tests.payload_examples as payload_examples
691 import appenlight.tests.payload_examples as payload_examples
802 from appenlight.models.metric import Metric
692 from appenlight.models.metric import Metric
803 from appenlight.views.api import general_metrics_create
693 from appenlight.views.api import general_metrics_create
804 from appenlight.models.services.application import ApplicationService
694 from appenlight.models.services.application import ApplicationService
805 route = mock.Mock()
695 route = mock.Mock()
806 route.name = 'api_general_metric'
696 route.name = 'api_general_metric'
807 request = pyramid.threadlocal.get_current_request()
697 request = pyramid.threadlocal.get_current_request()
808 request.matched_route = route
698 request.matched_route = route
809 context = DummyContext()
699 context = DummyContext()
810 context.resource = ApplicationService.by_id(1)
700 context.resource = ApplicationService.by_id(1)
811 request.context = context
701 request.context = context
812 request.unsafe_json_body = payload_examples.METRICS_PAYLOAD
702 request.unsafe_json_body = payload_examples.METRICS_PAYLOAD
813 general_metrics_create(request)
703 general_metrics_create(request)
814 query = DBSession.query(Metric)
704 query = DBSession.query(Metric)
815 metric = query.first()
705 metric = query.first()
816 assert query.count() == 1
706 assert query.count() == 1
817 assert metric.namespace == 'some.monitor'
707 assert metric.namespace == 'some.monitor'
818
708
819 def test_multiple_json_payload(self):
709 def test_multiple_json_payload(self):
820 import appenlight.tests.payload_examples as payload_examples
710 import appenlight.tests.payload_examples as payload_examples
821 from appenlight.models.metric import Metric
711 from appenlight.models.metric import Metric
822 from appenlight.views.api import general_metrics_create
712 from appenlight.views.api import general_metrics_create
823 from appenlight.models.services.application import ApplicationService
713 from appenlight.models.services.application import ApplicationService
824 route = mock.Mock()
714 route = mock.Mock()
825 route.name = 'api_general_metrics'
715 route.name = 'api_general_metrics'
826 request = pyramid.threadlocal.get_current_request()
716 request = pyramid.threadlocal.get_current_request()
827 request.matched_route = route
717 request.matched_route = route
828 context = DummyContext()
718 context = DummyContext()
829 context.resource = ApplicationService.by_id(1)
719 context.resource = ApplicationService.by_id(1)
830 request.context = context
720 request.context = context
831 request.unsafe_json_body = [
721 request.unsafe_json_body = [
832 copy.deepcopy(payload_examples.METRICS_PAYLOAD),
722 copy.deepcopy(payload_examples.METRICS_PAYLOAD),
833 copy.deepcopy(payload_examples.METRICS_PAYLOAD),
723 copy.deepcopy(payload_examples.METRICS_PAYLOAD),
834 ]
724 ]
835 general_metrics_create(request)
725 general_metrics_create(request)
836 query = DBSession.query(Metric)
726 query = DBSession.query(Metric)
837 metric = query.first()
727 metric = query.first()
838 assert query.count() == 2
728 assert query.count() == 2
839 assert metric.namespace == 'some.monitor'
729 assert metric.namespace == 'some.monitor'
840
730
841
731
842 class TestGroupingMessageReplacements(object):
732 class TestGroupingMessageReplacements(object):
843 def replace_default_repr_python(self):
733 def replace_default_repr_python(self):
844 test_str = '''
734 test_str = '''
845 ConnectionError: ConnectionError((<urllib3.connection.HTTPConnection object at 0x7f87a0ba9fd0>, 'Connection to domain.gr timed out. (connect timeout=10)')) caused by: ConnectTimeoutError((<urllib3.connection.HTTPConnection object at 0x7f87a0ba9fd0>, 'Connection to domain.gr timed out. (connect timeout=10)'))
735 ConnectionError: ConnectionError((<urllib3.connection.HTTPConnection object at 0x7f87a0ba9fd0>, 'Connection to domain.gr timed out. (connect timeout=10)')) caused by: ConnectTimeoutError((<urllib3.connection.HTTPConnection object at 0x7f87a0ba9fd0>, 'Connection to domain.gr timed out. (connect timeout=10)'))
846 '''
736 '''
847 regex = r'<(.*?) object at (.*?)>'
737 regex = r'<(.*?) object at (.*?)>'
848
738
849
739
850 class TestRulesKeyGetter(object):
740 class TestRulesKeyGetter(object):
851 def test_default_dict_getter_top_key(self):
741 def test_default_dict_getter_top_key(self):
852 from appenlight.lib.rule import Rule
742 from appenlight.lib.rule import Rule
853 struct = {
743 struct = {
854 "a": {
744 "a": {
855 "b": 'b',
745 "b": 'b',
856 "c": {
746 "c": {
857 "d": 'd',
747 "d": 'd',
858 "g": {
748 "g": {
859 "h": 'h'
749 "h": 'h'
860 }
750 }
861 },
751 },
862 "e": 'e'
752 "e": 'e'
863 },
753 },
864 "f": 'f'
754 "f": 'f'
865 }
755 }
866 result = Rule.default_dict_struct_getter(struct, "a")
756 result = Rule.default_dict_struct_getter(struct, "a")
867 assert result == struct['a']
757 assert result == struct['a']
868
758
869 def test_default_dict_getter_sub_key(self):
759 def test_default_dict_getter_sub_key(self):
870 from appenlight.lib.rule import Rule
760 from appenlight.lib.rule import Rule
871 struct = {
761 struct = {
872 "a": {
762 "a": {
873 "b": 'b',
763 "b": 'b',
874 "c": {
764 "c": {
875 "d": 'd',
765 "d": 'd',
876 "g": {
766 "g": {
877 "h": 'h'
767 "h": 'h'
878 }
768 }
879 },
769 },
880 "e": 'e'
770 "e": 'e'
881 },
771 },
882 "f": 'f'
772 "f": 'f'
883 }
773 }
884 result = Rule.default_dict_struct_getter(struct, 'a:b')
774 result = Rule.default_dict_struct_getter(struct, 'a:b')
885 assert result == struct['a']['b']
775 assert result == struct['a']['b']
886 result = Rule.default_dict_struct_getter(struct, 'a:c:d')
776 result = Rule.default_dict_struct_getter(struct, 'a:c:d')
887 assert result == struct['a']['c']['d']
777 assert result == struct['a']['c']['d']
888
778
889 def test_default_obj_getter_top_key(self):
779 def test_default_obj_getter_top_key(self):
890 from appenlight.lib.rule import Rule
780 from appenlight.lib.rule import Rule
891 class TestStruct(object):
781 class TestStruct(object):
892 def __init__(self, a, b):
782 def __init__(self, a, b):
893 self.a = a
783 self.a = a
894 self.b = b
784 self.b = b
895
785
896 struct = TestStruct(a='a',
786 struct = TestStruct(a='a',
897 b=TestStruct(a='x', b='y'))
787 b=TestStruct(a='x', b='y'))
898 result = Rule.default_obj_struct_getter(struct, "a")
788 result = Rule.default_obj_struct_getter(struct, "a")
899 assert result == struct.a
789 assert result == struct.a
900
790
901 def test_default_obj_getter_sub_key(self):
791 def test_default_obj_getter_sub_key(self):
902 from appenlight.lib.rule import Rule
792 from appenlight.lib.rule import Rule
903 class TestStruct(object):
793 class TestStruct(object):
904 def __init__(self, name, a, b):
794 def __init__(self, name, a, b):
905 self.name = name
795 self.name = name
906 self.a = a
796 self.a = a
907 self.b = b
797 self.b = b
908
798
909 def __repr__(self):
799 def __repr__(self):
910 return '<obj {}>'.format(self.name)
800 return '<obj {}>'.format(self.name)
911
801
912 c = TestStruct('c', a=5, b='z')
802 c = TestStruct('c', a=5, b='z')
913 b = TestStruct('b', a=c, b='y')
803 b = TestStruct('b', a=c, b='y')
914 struct = TestStruct('a', a='a', b=b)
804 struct = TestStruct('a', a='a', b=b)
915 result = Rule.default_obj_struct_getter(struct, 'b:b')
805 result = Rule.default_obj_struct_getter(struct, 'b:b')
916 assert result == struct.b.b
806 assert result == struct.b.b
917 result = Rule.default_obj_struct_getter(struct, 'b:a:b')
807 result = Rule.default_obj_struct_getter(struct, 'b:a:b')
918 assert result == struct.b.a.b
808 assert result == struct.b.a.b
919
809
920
810
921 @pytest.mark.usefixtures('report_type_matrix')
811 @pytest.mark.usefixtures('report_type_matrix')
922 class TestRulesParsing():
812 class TestRulesParsing():
923 @pytest.mark.parametrize("op, struct_value, test_value, match_result", [
813 @pytest.mark.parametrize("op, struct_value, test_value, match_result", [
924 ('eq', 500, 500, True),
814 ('eq', 500, 500, True),
925 ('eq', 600, 500, False),
815 ('eq', 600, 500, False),
926 ('eq', 300, 500, False),
816 ('eq', 300, 500, False),
927 ('eq', "300", 500, False),
817 ('eq', "300", 500, False),
928 ('eq', "600", 500, False),
818 ('eq', "600", 500, False),
929 ('eq', "500", 500, True),
819 ('eq', "500", 500, True),
930 ('ne', 500, 500, False),
820 ('ne', 500, 500, False),
931 ('ne', 600, 500, True),
821 ('ne', 600, 500, True),
932 ('ne', 300, 500, True),
822 ('ne', 300, 500, True),
933 ('ne', "300", 500, True),
823 ('ne', "300", 500, True),
934 ('ne', "600", 500, True),
824 ('ne', "600", 500, True),
935 ('ne', "500", 500, False),
825 ('ne', "500", 500, False),
936 ('ge', 500, 500, True),
826 ('ge', 500, 500, True),
937 ('ge', 600, 500, True),
827 ('ge', 600, 500, True),
938 ('ge', 499, 500, False),
828 ('ge', 499, 500, False),
939 ('gt', 499, 500, False),
829 ('gt', 499, 500, False),
940 ('gt', 500, 500, False),
830 ('gt', 500, 500, False),
941 ('gt', 501, 500, True),
831 ('gt', 501, 500, True),
942 ('le', 499, 500, True),
832 ('le', 499, 500, True),
943 ('le', 500, 500, True),
833 ('le', 500, 500, True),
944 ('le', 501, 500, False),
834 ('le', 501, 500, False),
945 ('lt', 499, 500, True),
835 ('lt', 499, 500, True),
946 ('lt', 500, 500, False),
836 ('lt', 500, 500, False),
947 ('lt', 501, 500, False),
837 ('lt', 501, 500, False),
948 ])
838 ])
949 def test_single_op_int(self, op, struct_value, test_value, match_result,
839 def test_single_op_int(self, op, struct_value, test_value, match_result,
950 report_type_matrix):
840 report_type_matrix):
951 from appenlight.lib.rule import Rule
841 from appenlight.lib.rule import Rule
952 rule_config = {
842 rule_config = {
953 "op": op,
843 "op": op,
954 "field": "http_status",
844 "field": "http_status",
955 "value": test_value
845 "value": test_value
956 }
846 }
957 rule = Rule(rule_config, report_type_matrix)
847 rule = Rule(rule_config, report_type_matrix)
958
848
959 data = {
849 data = {
960 "http_status": struct_value
850 "http_status": struct_value
961 }
851 }
962 assert rule.match(data) is match_result
852 assert rule.match(data) is match_result
963
853
964 @pytest.mark.parametrize("op, struct_value, test_value, match_result", [
854 @pytest.mark.parametrize("op, struct_value, test_value, match_result", [
965 ('ge', "500.01", 500, True),
855 ('ge', "500.01", 500, True),
966 ('ge', "500.01", 500.02, False),
856 ('ge', "500.01", 500.02, False),
967 ('le', "500.01", 500.02, True)
857 ('le', "500.01", 500.02, True)
968 ])
858 ])
969 def test_single_op_float(self, op, struct_value, test_value, match_result,
859 def test_single_op_float(self, op, struct_value, test_value, match_result,
970 report_type_matrix):
860 report_type_matrix):
971 from appenlight.lib.rule import Rule
861 from appenlight.lib.rule import Rule
972 rule_config = {
862 rule_config = {
973 "op": op,
863 "op": op,
974 "field": "duration",
864 "field": "duration",
975 "value": test_value
865 "value": test_value
976 }
866 }
977 rule = Rule(rule_config, report_type_matrix)
867 rule = Rule(rule_config, report_type_matrix)
978
868
979 data = {
869 data = {
980 "duration": struct_value
870 "duration": struct_value
981 }
871 }
982 assert rule.match(data) is match_result
872 assert rule.match(data) is match_result
983
873
984 @pytest.mark.parametrize("op, struct_value, test_value, match_result", [
874 @pytest.mark.parametrize("op, struct_value, test_value, match_result", [
985 ('contains', 'foo bar baz', 'foo', True),
875 ('contains', 'foo bar baz', 'foo', True),
986 ('contains', 'foo bar baz', 'bar', True),
876 ('contains', 'foo bar baz', 'bar', True),
987 ('contains', 'foo bar baz', 'dupa', False),
877 ('contains', 'foo bar baz', 'dupa', False),
988 ('startswith', 'foo bar baz', 'foo', True),
878 ('startswith', 'foo bar baz', 'foo', True),
989 ('startswith', 'foo bar baz', 'bar', False),
879 ('startswith', 'foo bar baz', 'bar', False),
990 ('endswith', 'foo bar baz', 'baz', True),
880 ('endswith', 'foo bar baz', 'baz', True),
991 ('endswith', 'foo bar baz', 'bar', False),
881 ('endswith', 'foo bar baz', 'bar', False),
992 ])
882 ])
993 def test_single_op_string(self, op, struct_value, test_value,
883 def test_single_op_string(self, op, struct_value, test_value,
994 match_result, report_type_matrix):
884 match_result, report_type_matrix):
995 from appenlight.lib.rule import Rule
885 from appenlight.lib.rule import Rule
996 rule_config = {
886 rule_config = {
997 "op": op,
887 "op": op,
998 "field": "error",
888 "field": "error",
999 "value": test_value
889 "value": test_value
1000 }
890 }
1001 rule = Rule(rule_config, report_type_matrix)
891 rule = Rule(rule_config, report_type_matrix)
1002
892
1003 data = {
893 data = {
1004 "error": struct_value
894 "error": struct_value
1005 }
895 }
1006 assert rule.match(data) is match_result
896 assert rule.match(data) is match_result
1007
897
1008 @pytest.mark.parametrize("field, value, s_type", [
898 @pytest.mark.parametrize("field, value, s_type", [
1009 ('field_unicode', 500, str),
899 ('field_unicode', 500, str),
1010 ('field_unicode', 500.0, str),
900 ('field_unicode', 500.0, str),
1011 ('field_unicode', "500", str),
901 ('field_unicode', "500", str),
1012 ('field_int', "500", int),
902 ('field_int', "500", int),
1013 ('field_int', 500, int),
903 ('field_int', 500, int),
1014 ('field_int', 500.0, int),
904 ('field_int', 500.0, int),
1015 ('field_float', "500", float),
905 ('field_float', "500", float),
1016 ('field_float', 500, float),
906 ('field_float', 500, float),
1017 ('field_float', 500.0, float),
907 ('field_float', 500.0, float),
1018 ])
908 ])
1019 def test_type_normalization(self, field, value, s_type):
909 def test_type_normalization(self, field, value, s_type):
1020 from appenlight.lib.rule import Rule
910 from appenlight.lib.rule import Rule
1021 type_matrix = {
911 type_matrix = {
1022 'field_unicode': {"type": 'unicode'},
912 'field_unicode': {"type": 'unicode'},
1023 'field_float': {"type": 'float'},
913 'field_float': {"type": 'float'},
1024 'field_int': {"type": 'int'},
914 'field_int': {"type": 'int'},
1025 }
915 }
1026
916
1027 rule = Rule({}, type_matrix)
917 rule = Rule({}, type_matrix)
1028 n_value = rule.normalized_type(field, value)
918 n_value = rule.normalized_type(field, value)
1029 assert isinstance(n_value, s_type) is True
919 assert isinstance(n_value, s_type) is True
1030
920
1031
921
1032 @pytest.mark.usefixtures('report_type_matrix')
922 @pytest.mark.usefixtures('report_type_matrix')
1033 class TestNestedRuleParsing():
923 class TestNestedRuleParsing():
1034
924
1035 @pytest.mark.parametrize("data, result", [
925 @pytest.mark.parametrize("data, result", [
1036 ({"http_status": 501, "group": {"priority": 7, "occurences": 11}},
926 ({"http_status": 501, "group": {"priority": 7, "occurences": 11}},
1037 False),
927 False),
1038 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
928 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
1039 False),
929 False),
1040 ({"http_status": 500, "group": {"priority": 1, "occurences": 11}},
930 ({"http_status": 500, "group": {"priority": 1, "occurences": 11}},
1041 False),
931 False),
1042 ({"http_status": 101, "group": {"priority": 3, "occurences": 5}},
932 ({"http_status": 101, "group": {"priority": 3, "occurences": 5}},
1043 True),
933 True),
1044 ])
934 ])
1045 def test_NOT_rule(self, data, result, report_type_matrix):
935 def test_NOT_rule(self, data, result, report_type_matrix):
1046 from appenlight.lib.rule import Rule
936 from appenlight.lib.rule import Rule
1047 rule_config = {
937 rule_config = {
1048 "field": "__NOT__",
938 "field": "__NOT__",
1049 "rules": [
939 "rules": [
1050 {
940 {
1051 "op": "ge",
941 "op": "ge",
1052 "field": "group:occurences",
942 "field": "group:occurences",
1053 "value": "10"
943 "value": "10"
1054 },
944 },
1055 {
945 {
1056 "op": "ge",
946 "op": "ge",
1057 "field": "group:priority",
947 "field": "group:priority",
1058 "value": "4"
948 "value": "4"
1059 }
949 }
1060 ]
950 ]
1061 }
951 }
1062
952
1063 rule = Rule(rule_config, report_type_matrix)
953 rule = Rule(rule_config, report_type_matrix)
1064 assert rule.match(data) is result
954 assert rule.match(data) is result
1065
955
1066 @pytest.mark.parametrize("data, result", [
956 @pytest.mark.parametrize("data, result", [
1067 ({"http_status": 501, "group": {"priority": 7, "occurences": 11}},
957 ({"http_status": 501, "group": {"priority": 7, "occurences": 11}},
1068 True),
958 True),
1069 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
959 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
1070 True),
960 True),
1071 ({"http_status": 500, "group": {"priority": 1, "occurences": 1}},
961 ({"http_status": 500, "group": {"priority": 1, "occurences": 1}},
1072 True),
962 True),
1073 ({"http_status": 101, "group": {"priority": 3, "occurences": 11}},
963 ({"http_status": 101, "group": {"priority": 3, "occurences": 11}},
1074 False),
964 False),
1075 ])
965 ])
1076 def test_nested_OR_AND_rule(self, data, result, report_type_matrix):
966 def test_nested_OR_AND_rule(self, data, result, report_type_matrix):
1077 from appenlight.lib.rule import Rule
967 from appenlight.lib.rule import Rule
1078 rule_config = {
968 rule_config = {
1079 "field": "__OR__",
969 "field": "__OR__",
1080 "rules": [
970 "rules": [
1081 {
971 {
1082 "field": "__AND__",
972 "field": "__AND__",
1083 "rules": [
973 "rules": [
1084 {
974 {
1085 "op": "ge",
975 "op": "ge",
1086 "field": "group:occurences",
976 "field": "group:occurences",
1087 "value": "10"
977 "value": "10"
1088 },
978 },
1089 {
979 {
1090 "op": "ge",
980 "op": "ge",
1091 "field": "group:priority",
981 "field": "group:priority",
1092 "value": "4"
982 "value": "4"
1093 }
983 }
1094 ]
984 ]
1095 },
985 },
1096 {
986 {
1097 "op": "eq",
987 "op": "eq",
1098 "field": "http_status",
988 "field": "http_status",
1099 "value": "500"
989 "value": "500"
1100 }
990 }
1101 ]
991 ]
1102 }
992 }
1103
993
1104 rule = Rule(rule_config, report_type_matrix)
994 rule = Rule(rule_config, report_type_matrix)
1105 assert rule.match(data) is result
995 assert rule.match(data) is result
1106
996
1107 @pytest.mark.parametrize("data, result", [
997 @pytest.mark.parametrize("data, result", [
1108 ({"http_status": 501, "group": {"priority": 7, "occurences": 11}},
998 ({"http_status": 501, "group": {"priority": 7, "occurences": 11}},
1109 True),
999 True),
1110 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
1000 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
1111 True),
1001 True),
1112 ({"http_status": 500, "group": {"priority": 1, "occurences": 1}},
1002 ({"http_status": 500, "group": {"priority": 1, "occurences": 1}},
1113 True),
1003 True),
1114 ({"http_status": 101, "group": {"priority": 3, "occurences": 1}},
1004 ({"http_status": 101, "group": {"priority": 3, "occurences": 1}},
1115 False),
1005 False),
1116 ])
1006 ])
1117 def test_nested_OR_OR_rule(self, data, result, report_type_matrix):
1007 def test_nested_OR_OR_rule(self, data, result, report_type_matrix):
1118 from appenlight.lib.rule import Rule
1008 from appenlight.lib.rule import Rule
1119 rule_config = {
1009 rule_config = {
1120 "field": "__OR__",
1010 "field": "__OR__",
1121 "rules": [
1011 "rules": [
1122 {"field": "__OR__",
1012 {"field": "__OR__",
1123 "rules": [
1013 "rules": [
1124 {"op": "ge",
1014 {"op": "ge",
1125 "field": "group:occurences",
1015 "field": "group:occurences",
1126 "value": "10"
1016 "value": "10"
1127 },
1017 },
1128 {"op": "ge",
1018 {"op": "ge",
1129 "field": "group:priority",
1019 "field": "group:priority",
1130 "value": "4"
1020 "value": "4"
1131 }
1021 }
1132 ]
1022 ]
1133 },
1023 },
1134 {"op": "eq",
1024 {"op": "eq",
1135 "field": "http_status",
1025 "field": "http_status",
1136 "value": "500"
1026 "value": "500"
1137 }
1027 }
1138 ]
1028 ]
1139 }
1029 }
1140
1030
1141 rule = Rule(rule_config, report_type_matrix)
1031 rule = Rule(rule_config, report_type_matrix)
1142 assert rule.match(data) is result
1032 assert rule.match(data) is result
1143
1033
1144 @pytest.mark.parametrize("data, result", [
1034 @pytest.mark.parametrize("data, result", [
1145 ({"http_status": 500, "group": {"priority": 7, "occurences": 11}},
1035 ({"http_status": 500, "group": {"priority": 7, "occurences": 11}},
1146 True),
1036 True),
1147 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
1037 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
1148 False),
1038 False),
1149 ({"http_status": 500, "group": {"priority": 1, "occurences": 1}},
1039 ({"http_status": 500, "group": {"priority": 1, "occurences": 1}},
1150 False),
1040 False),
1151 ({"http_status": 101, "group": {"priority": 3, "occurences": 1}},
1041 ({"http_status": 101, "group": {"priority": 3, "occurences": 1}},
1152 False),
1042 False),
1153 ])
1043 ])
1154 def test_nested_AND_AND_rule(self, data, result, report_type_matrix):
1044 def test_nested_AND_AND_rule(self, data, result, report_type_matrix):
1155 from appenlight.lib.rule import Rule
1045 from appenlight.lib.rule import Rule
1156 rule_config = {
1046 rule_config = {
1157 "field": "__AND__",
1047 "field": "__AND__",
1158 "rules": [
1048 "rules": [
1159 {"field": "__AND__",
1049 {"field": "__AND__",
1160 "rules": [
1050 "rules": [
1161 {"op": "ge",
1051 {"op": "ge",
1162 "field": "group:occurences",
1052 "field": "group:occurences",
1163 "value": "10"
1053 "value": "10"
1164 },
1054 },
1165 {"op": "ge",
1055 {"op": "ge",
1166 "field": "group:priority",
1056 "field": "group:priority",
1167 "value": "4"
1057 "value": "4"
1168 }]
1058 }]
1169 },
1059 },
1170 {"op": "eq",
1060 {"op": "eq",
1171 "field": "http_status",
1061 "field": "http_status",
1172 "value": "500"
1062 "value": "500"
1173 }
1063 }
1174 ]
1064 ]
1175 }
1065 }
1176
1066
1177 rule = Rule(rule_config, report_type_matrix)
1067 rule = Rule(rule_config, report_type_matrix)
1178 assert rule.match(data) is result
1068 assert rule.match(data) is result
1179
1069
1180 @pytest.mark.parametrize("data, result", [
1070 @pytest.mark.parametrize("data, result", [
1181 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1071 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1182 "url_path": '/test/register', "error": "foo test bar"}, True),
1072 "url_path": '/test/register', "error": "foo test bar"}, True),
1183 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1073 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1184 "url_path": '/test/register', "error": "foo INVALID bar"}, False),
1074 "url_path": '/test/register', "error": "foo INVALID bar"}, False),
1185 ])
1075 ])
1186 def test_nested_AND_AND_AND_rule(self, data, result, report_type_matrix):
1076 def test_nested_AND_AND_AND_rule(self, data, result, report_type_matrix):
1187 from appenlight.lib.rule import Rule
1077 from appenlight.lib.rule import Rule
1188 rule_config = {
1078 rule_config = {
1189 "field": "__AND__",
1079 "field": "__AND__",
1190 "rules": [
1080 "rules": [
1191 {"field": "__AND__",
1081 {"field": "__AND__",
1192 "rules": [
1082 "rules": [
1193 {"op": "ge",
1083 {"op": "ge",
1194 "field": "group:occurences",
1084 "field": "group:occurences",
1195 "value": "10"
1085 "value": "10"
1196 },
1086 },
1197 {"field": "__AND__",
1087 {"field": "__AND__",
1198 "rules": [
1088 "rules": [
1199 {"op": "endswith",
1089 {"op": "endswith",
1200 "field": "url_path",
1090 "field": "url_path",
1201 "value": "register"},
1091 "value": "register"},
1202 {"op": "contains",
1092 {"op": "contains",
1203 "field": "error",
1093 "field": "error",
1204 "value": "test"}]}]
1094 "value": "test"}]}]
1205 },
1095 },
1206 {"op": "eq",
1096 {"op": "eq",
1207 "field": "http_status",
1097 "field": "http_status",
1208 "value": "500"
1098 "value": "500"
1209 }
1099 }
1210 ]
1100 ]
1211 }
1101 }
1212
1102
1213 rule = Rule(rule_config, report_type_matrix)
1103 rule = Rule(rule_config, report_type_matrix)
1214 assert rule.match(data) is result
1104 assert rule.match(data) is result
1215
1105
1216 @pytest.mark.parametrize("data, result", [
1106 @pytest.mark.parametrize("data, result", [
1217 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1107 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1218 "url_path": 6, "error": 3}, False),
1108 "url_path": 6, "error": 3}, False),
1219 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1109 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1220 "url_path": '/test/register', "error": "foo INVALID bar"}, True),
1110 "url_path": '/test/register', "error": "foo INVALID bar"}, True),
1221 ])
1111 ])
1222 def test_nested_AND_AND_OR_rule(self, data, result, report_type_matrix):
1112 def test_nested_AND_AND_OR_rule(self, data, result, report_type_matrix):
1223 from appenlight.lib.rule import Rule
1113 from appenlight.lib.rule import Rule
1224 rule_config = {
1114 rule_config = {
1225 "field": "__AND__",
1115 "field": "__AND__",
1226 "rules": [
1116 "rules": [
1227 {"field": "__AND__",
1117 {"field": "__AND__",
1228 "rules": [
1118 "rules": [
1229 {"op": "ge",
1119 {"op": "ge",
1230 "field": "group:occurences",
1120 "field": "group:occurences",
1231 "value": "10"
1121 "value": "10"
1232 },
1122 },
1233 {"field": "__OR__",
1123 {"field": "__OR__",
1234 "rules": [
1124 "rules": [
1235 {"op": "endswith",
1125 {"op": "endswith",
1236 "field": "url_path",
1126 "field": "url_path",
1237 "value": "register"
1127 "value": "register"
1238 },
1128 },
1239 {"op": "contains",
1129 {"op": "contains",
1240 "field": "error",
1130 "field": "error",
1241 "value": "test"
1131 "value": "test"
1242 }]}]
1132 }]}]
1243 },
1133 },
1244 {"op": "eq",
1134 {"op": "eq",
1245 "field": "http_status",
1135 "field": "http_status",
1246 "value": "500"
1136 "value": "500"
1247 }
1137 }
1248 ]
1138 ]
1249 }
1139 }
1250
1140
1251 rule = Rule(rule_config, report_type_matrix)
1141 rule = Rule(rule_config, report_type_matrix)
1252 assert rule.match(data) is result
1142 assert rule.match(data) is result
1253
1143
1254 @pytest.mark.parametrize("op, field, value, should_fail", [
1144 @pytest.mark.parametrize("op, field, value, should_fail", [
1255 ('eq', 'http_status', "1", False),
1145 ('eq', 'http_status', "1", False),
1256 ('ne', 'http_status', "1", False),
1146 ('ne', 'http_status', "1", False),
1257 ('ne', 'http_status', "foo", True),
1147 ('ne', 'http_status', "foo", True),
1258 ('startswith', 'http_status', "1", True),
1148 ('startswith', 'http_status', "1", True),
1259 ('eq', 'group:priority', "1", False),
1149 ('eq', 'group:priority', "1", False),
1260 ('ne', 'group:priority', "1", False),
1150 ('ne', 'group:priority', "1", False),
1261 ('ge', 'group:priority', "1", False),
1151 ('ge', 'group:priority', "1", False),
1262 ('le', 'group:priority', "1", False),
1152 ('le', 'group:priority', "1", False),
1263 ('startswith', 'group:priority', "1", True),
1153 ('startswith', 'group:priority', "1", True),
1264 ('eq', 'url_domain', "1", False),
1154 ('eq', 'url_domain', "1", False),
1265 ('ne', 'url_domain', "1", False),
1155 ('ne', 'url_domain', "1", False),
1266 ('startswith', 'url_domain', "1", False),
1156 ('startswith', 'url_domain', "1", False),
1267 ('endswith', 'url_domain', "1", False),
1157 ('endswith', 'url_domain', "1", False),
1268 ('contains', 'url_domain', "1", False),
1158 ('contains', 'url_domain', "1", False),
1269 ('ge', 'url_domain', "1", True),
1159 ('ge', 'url_domain', "1", True),
1270 ('eq', 'url_path', "1", False),
1160 ('eq', 'url_path', "1", False),
1271 ('ne', 'url_path', "1", False),
1161 ('ne', 'url_path', "1", False),
1272 ('startswith', 'url_path', "1", False),
1162 ('startswith', 'url_path', "1", False),
1273 ('endswith', 'url_path', "1", False),
1163 ('endswith', 'url_path', "1", False),
1274 ('contains', 'url_path', "1", False),
1164 ('contains', 'url_path', "1", False),
1275 ('ge', 'url_path', "1", True),
1165 ('ge', 'url_path', "1", True),
1276 ('eq', 'error', "1", False),
1166 ('eq', 'error', "1", False),
1277 ('ne', 'error', "1", False),
1167 ('ne', 'error', "1", False),
1278 ('startswith', 'error', "1", False),
1168 ('startswith', 'error', "1", False),
1279 ('endswith', 'error', "1", False),
1169 ('endswith', 'error', "1", False),
1280 ('contains', 'error', "1", False),
1170 ('contains', 'error', "1", False),
1281 ('ge', 'error', "1", True),
1171 ('ge', 'error', "1", True),
1282 ('ge', 'url_path', "1", True),
1172 ('ge', 'url_path', "1", True),
1283 ('eq', 'tags:server_name', "1", False),
1173 ('eq', 'tags:server_name', "1", False),
1284 ('ne', 'tags:server_name', "1", False),
1174 ('ne', 'tags:server_name', "1", False),
1285 ('startswith', 'tags:server_name', "1", False),
1175 ('startswith', 'tags:server_name', "1", False),
1286 ('endswith', 'tags:server_name', "1", False),
1176 ('endswith', 'tags:server_name', "1", False),
1287 ('contains', 'tags:server_name', "1", False),
1177 ('contains', 'tags:server_name', "1", False),
1288 ('ge', 'tags:server_name', "1", True),
1178 ('ge', 'tags:server_name', "1", True),
1289 ('contains', 'traceback', "1", False),
1179 ('contains', 'traceback', "1", False),
1290 ('ge', 'traceback', "1", True),
1180 ('ge', 'traceback', "1", True),
1291 ('eq', 'group:occurences', "1", False),
1181 ('eq', 'group:occurences', "1", False),
1292 ('ne', 'group:occurences', "1", False),
1182 ('ne', 'group:occurences', "1", False),
1293 ('ge', 'group:occurences', "1", False),
1183 ('ge', 'group:occurences', "1", False),
1294 ('le', 'group:occurences', "1", False),
1184 ('le', 'group:occurences', "1", False),
1295 ('contains', 'group:occurences', "1", True),
1185 ('contains', 'group:occurences', "1", True),
1296 ])
1186 ])
1297 def test_rule_validation(self, op, field, value, should_fail,
1187 def test_rule_validation(self, op, field, value, should_fail,
1298 report_type_matrix):
1188 report_type_matrix):
1299 import colander
1189 import colander
1300 from appenlight.validators import build_rule_schema
1190 from appenlight.validators import build_rule_schema
1301 rule_config = {
1191 rule_config = {
1302 "op": op,
1192 "op": op,
1303 "field": field,
1193 "field": field,
1304 "value": value
1194 "value": value
1305 }
1195 }
1306
1196
1307 schema = build_rule_schema(rule_config, report_type_matrix)
1197 schema = build_rule_schema(rule_config, report_type_matrix)
1308 if should_fail:
1198 if should_fail:
1309 with pytest.raises(colander.Invalid):
1199 with pytest.raises(colander.Invalid):
1310 schema.deserialize(rule_config)
1200 schema.deserialize(rule_config)
1311 else:
1201 else:
1312 schema.deserialize(rule_config)
1202 schema.deserialize(rule_config)
1313
1203
1314 def test_nested_proper_rule_validation(self, report_type_matrix):
1204 def test_nested_proper_rule_validation(self, report_type_matrix):
1315 from appenlight.validators import build_rule_schema
1205 from appenlight.validators import build_rule_schema
1316 rule_config = {
1206 rule_config = {
1317 "field": "__AND__",
1207 "field": "__AND__",
1318 "rules": [
1208 "rules": [
1319 {
1209 {
1320 "field": "__AND__",
1210 "field": "__AND__",
1321 "rules": [
1211 "rules": [
1322 {
1212 {
1323 "op": "ge",
1213 "op": "ge",
1324 "field": "group:occurences",
1214 "field": "group:occurences",
1325 "value": "10"
1215 "value": "10"
1326 },
1216 },
1327 {
1217 {
1328 "field": "__OR__",
1218 "field": "__OR__",
1329 "rules": [
1219 "rules": [
1330 {
1220 {
1331 "op": "endswith",
1221 "op": "endswith",
1332 "field": "url_path",
1222 "field": "url_path",
1333 "value": "register"
1223 "value": "register"
1334 },
1224 },
1335 {
1225 {
1336 "op": "contains",
1226 "op": "contains",
1337 "field": "error",
1227 "field": "error",
1338 "value": "test"
1228 "value": "test"
1339 }
1229 }
1340 ]
1230 ]
1341 }
1231 }
1342 ]
1232 ]
1343 },
1233 },
1344 {
1234 {
1345 "op": "eq",
1235 "op": "eq",
1346 "field": "http_status",
1236 "field": "http_status",
1347 "value": "500"
1237 "value": "500"
1348 }
1238 }
1349 ]
1239 ]
1350 }
1240 }
1351
1241
1352 schema = build_rule_schema(rule_config, report_type_matrix)
1242 schema = build_rule_schema(rule_config, report_type_matrix)
1353 deserialized = schema.deserialize(rule_config)
1243 deserialized = schema.deserialize(rule_config)
1354
1244
1355 def test_nested_bad_rule_validation(self, report_type_matrix):
1245 def test_nested_bad_rule_validation(self, report_type_matrix):
1356 import colander
1246 import colander
1357 from appenlight.validators import build_rule_schema
1247 from appenlight.validators import build_rule_schema
1358 rule_config = {
1248 rule_config = {
1359 "field": "__AND__",
1249 "field": "__AND__",
1360 "rules": [
1250 "rules": [
1361 {
1251 {
1362 "field": "__AND__",
1252 "field": "__AND__",
1363 "rules": [
1253 "rules": [
1364 {
1254 {
1365 "op": "ge",
1255 "op": "ge",
1366 "field": "group:occurences",
1256 "field": "group:occurences",
1367 "value": "10"
1257 "value": "10"
1368 },
1258 },
1369 {
1259 {
1370 "field": "__OR__",
1260 "field": "__OR__",
1371 "rules": [
1261 "rules": [
1372 {
1262 {
1373 "op": "gt",
1263 "op": "gt",
1374 "field": "url_path",
1264 "field": "url_path",
1375 "value": "register"
1265 "value": "register"
1376 },
1266 },
1377 {
1267 {
1378 "op": "contains",
1268 "op": "contains",
1379 "field": "error",
1269 "field": "error",
1380 "value": "test"
1270 "value": "test"
1381 }
1271 }
1382 ]
1272 ]
1383 }
1273 }
1384 ]
1274 ]
1385 },
1275 },
1386 {
1276 {
1387 "op": "eq",
1277 "op": "eq",
1388 "field": "http_status",
1278 "field": "http_status",
1389 "value": "500"
1279 "value": "500"
1390 }
1280 }
1391 ]
1281 ]
1392 }
1282 }
1393
1283
1394 schema = build_rule_schema(rule_config, report_type_matrix)
1284 schema = build_rule_schema(rule_config, report_type_matrix)
1395 with pytest.raises(colander.Invalid):
1285 with pytest.raises(colander.Invalid):
1396 deserialized = schema.deserialize(rule_config)
1286 deserialized = schema.deserialize(rule_config)
1397
1287
1398 def test_config_manipulator(self):
1288 def test_config_manipulator(self):
1399 from appenlight.lib.rule import Rule
1289 from appenlight.lib.rule import Rule
1400 type_matrix = {
1290 type_matrix = {
1401 'a': {"type": 'int',
1291 'a': {"type": 'int',
1402 "ops": ('eq', 'ne', 'ge', 'le',)},
1292 "ops": ('eq', 'ne', 'ge', 'le',)},
1403 'b': {"type": 'int',
1293 'b': {"type": 'int',
1404 "ops": ('eq', 'ne', 'ge', 'le',)},
1294 "ops": ('eq', 'ne', 'ge', 'le',)},
1405 }
1295 }
1406 rule_config = {
1296 rule_config = {
1407 "field": "__OR__",
1297 "field": "__OR__",
1408 "rules": [
1298 "rules": [
1409 {
1299 {
1410 "field": "__OR__",
1300 "field": "__OR__",
1411 "rules": [
1301 "rules": [
1412 {
1302 {
1413 "op": "ge",
1303 "op": "ge",
1414 "field": "a",
1304 "field": "a",
1415 "value": "10"
1305 "value": "10"
1416 }
1306 }
1417 ]
1307 ]
1418 },
1308 },
1419 {
1309 {
1420 "op": "eq",
1310 "op": "eq",
1421 "field": "b",
1311 "field": "b",
1422 "value": "500"
1312 "value": "500"
1423 }
1313 }
1424 ]
1314 ]
1425 }
1315 }
1426
1316
1427 def rule_manipulator(rule):
1317 def rule_manipulator(rule):
1428 if 'value' in rule.config:
1318 if 'value' in rule.config:
1429 rule.config['value'] = "1"
1319 rule.config['value'] = "1"
1430
1320
1431 rule = Rule(rule_config, type_matrix,
1321 rule = Rule(rule_config, type_matrix,
1432 config_manipulator=rule_manipulator)
1322 config_manipulator=rule_manipulator)
1433 rule.match({"a": 1,
1323 rule.match({"a": 1,
1434 "b": "2"})
1324 "b": "2"})
1435 assert rule.config['rules'][0]['rules'][0]['value'] == "1"
1325 assert rule.config['rules'][0]['rules'][0]['value'] == "1"
1436 assert rule.config['rules'][1]['value'] == "1"
1326 assert rule.config['rules'][1]['value'] == "1"
1437 assert rule.type_matrix["b"]['type'] == "int"
1327 assert rule.type_matrix["b"]['type'] == "int"
1438
1328
1439 def test_dynamic_config_manipulator(self):
1329 def test_dynamic_config_manipulator(self):
1440 from appenlight.lib.rule import Rule
1330 from appenlight.lib.rule import Rule
1441 rule_config = {
1331 rule_config = {
1442 "field": "__OR__",
1332 "field": "__OR__",
1443 "rules": [
1333 "rules": [
1444 {
1334 {
1445 "field": "__OR__",
1335 "field": "__OR__",
1446 "rules": [
1336 "rules": [
1447 {
1337 {
1448 "op": "ge",
1338 "op": "ge",
1449 "field": "a",
1339 "field": "a",
1450 "value": "10"
1340 "value": "10"
1451 }
1341 }
1452 ]
1342 ]
1453 },
1343 },
1454 {
1344 {
1455 "op": "eq",
1345 "op": "eq",
1456 "field": "b",
1346 "field": "b",
1457 "value": "500"
1347 "value": "500"
1458 }
1348 }
1459 ]
1349 ]
1460 }
1350 }
1461
1351
1462 def rule_manipulator(rule):
1352 def rule_manipulator(rule):
1463 rule.type_matrix = {
1353 rule.type_matrix = {
1464 'a': {"type": 'int',
1354 'a': {"type": 'int',
1465 "ops": ('eq', 'ne', 'ge', 'le',)},
1355 "ops": ('eq', 'ne', 'ge', 'le',)},
1466 'b': {"type": 'unicode',
1356 'b': {"type": 'unicode',
1467 "ops": ('eq', 'ne', 'ge', 'le',)},
1357 "ops": ('eq', 'ne', 'ge', 'le',)},
1468 }
1358 }
1469
1359
1470 if 'value' in rule.config:
1360 if 'value' in rule.config:
1471 if rule.config['field'] == 'a':
1361 if rule.config['field'] == 'a':
1472 rule.config['value'] = "1"
1362 rule.config['value'] = "1"
1473 elif rule.config['field'] == 'b':
1363 elif rule.config['field'] == 'b':
1474 rule.config['value'] = "2"
1364 rule.config['value'] = "2"
1475
1365
1476 rule = Rule(rule_config, {},
1366 rule = Rule(rule_config, {},
1477 config_manipulator=rule_manipulator)
1367 config_manipulator=rule_manipulator)
1478 rule.match({"a": 11,
1368 rule.match({"a": 11,
1479 "b": "55"})
1369 "b": "55"})
1480 assert rule.config['rules'][0]['rules'][0]['value'] == "1"
1370 assert rule.config['rules'][0]['rules'][0]['value'] == "1"
1481 assert rule.config['rules'][1]['value'] == "2"
1371 assert rule.config['rules'][1]['value'] == "2"
1482 assert rule.type_matrix["b"]['type'] == "unicode"
1372 assert rule.type_matrix["b"]['type'] == "unicode"
1483
1373
1484
1374
1485 @pytest.mark.usefixtures('base_app', 'with_migrations')
1375 @pytest.mark.usefixtures('base_app', 'with_migrations')
1486 class TestViewsWithForms(object):
1376 class TestViewsWithForms(object):
1487 def test_bad_csrf(self):
1377 def test_bad_csrf(self):
1488 from appenlight.forms import CSRFException
1378 from appenlight.forms import CSRFException
1489 from appenlight.views.index import register
1379 from appenlight.views.index import register
1490 post_data = {'dupa': 'dupa'}
1380 post_data = {'dupa': 'dupa'}
1491 request = testing.DummyRequest(post=post_data)
1381 request = testing.DummyRequest(post=post_data)
1492 request.POST = webob.multidict.MultiDict(request.POST)
1382 request.POST = webob.multidict.MultiDict(request.POST)
1493 with pytest.raises(CSRFException):
1383 with pytest.raises(CSRFException):
1494 register(request)
1384 register(request)
1495
1385
1496 def test_proper_csrf(self):
1386 def test_proper_csrf(self):
1497 from appenlight.views.index import register
1387 from appenlight.views.index import register
1498 request = pyramid.threadlocal.get_current_request()
1388 request = pyramid.threadlocal.get_current_request()
1499 post_data = {'dupa': 'dupa',
1389 post_data = {'dupa': 'dupa',
1500 'csrf_token': request.session.get_csrf_token()}
1390 'csrf_token': request.session.get_csrf_token()}
1501 request = testing.DummyRequest(post=post_data)
1391 request = testing.DummyRequest(post=post_data)
1502 request.POST = webob.multidict.MultiDict(request.POST)
1392 request.POST = webob.multidict.MultiDict(request.POST)
1503 result = register(request)
1393 result = register(request)
1504 assert result['form'].errors['email'][0] == 'This field is required.'
1394 assert result['form'].errors['email'][0] == 'This field is required.'
1505
1395
1506
1396
1507 @pytest.mark.usefixtures('base_app', 'with_migrations', 'default_data')
1397 @pytest.mark.usefixtures('base_app', 'with_migrations', 'default_data')
1508 class TestRegistration(object):
1398 class TestRegistration(object):
1509 def test_invalid_form(self):
1399 def test_invalid_form(self):
1510 from appenlight.views.index import register
1400 from appenlight.views.index import register
1511 request = pyramid.threadlocal.get_current_request()
1401 request = pyramid.threadlocal.get_current_request()
1512 post_data = {'user_name': '',
1402 post_data = {'user_name': '',
1513 'user_password': '',
1403 'user_password': '',
1514 'email': '',
1404 'email': '',
1515 'csrf_token': request.session.get_csrf_token()}
1405 'csrf_token': request.session.get_csrf_token()}
1516 request = testing.DummyRequest(post=post_data)
1406 request = testing.DummyRequest(post=post_data)
1517 request.POST = webob.multidict.MultiDict(request.POST)
1407 request.POST = webob.multidict.MultiDict(request.POST)
1518 result = register(request)
1408 result = register(request)
1519 assert result['form'].errors['user_name'][0] == \
1409 assert result['form'].errors['user_name'][0] == \
1520 'This field is required.'
1410 'This field is required.'
1521
1411
1522 def test_valid_form(self):
1412 def test_valid_form(self):
1523 from appenlight.views.index import register
1413 from appenlight.views.index import register
1524 from ziggurat_foundations.models.services.user import UserService
1414 from ziggurat_foundations.models.services.user import UserService
1525 request = pyramid.threadlocal.get_current_request()
1415 request = pyramid.threadlocal.get_current_request()
1526 post_data = {'user_name': 'foo',
1416 post_data = {'user_name': 'foo',
1527 'user_password': 'barr',
1417 'user_password': 'barr',
1528 'email': 'test@test.foo',
1418 'email': 'test@test.foo',
1529 'csrf_token': request.session.get_csrf_token()}
1419 'csrf_token': request.session.get_csrf_token()}
1530 request = testing.DummyRequest(post=post_data)
1420 request = testing.DummyRequest(post=post_data)
1531 request.add_flash_to_headers = mock.Mock()
1421 request.add_flash_to_headers = mock.Mock()
1532 request.POST = webob.multidict.MultiDict(request.POST)
1422 request.POST = webob.multidict.MultiDict(request.POST)
1533 assert UserService.by_user_name('foo') is None
1423 assert UserService.by_user_name('foo') is None
1534 register(request)
1424 register(request)
1535 user = UserService.by_user_name('foo')
1425 user = UserService.by_user_name('foo')
1536 assert user.user_name == 'foo'
1426 assert user.user_name == 'foo'
1537 assert len(user.user_password) == 60
1427 assert len(user.user_password) == 60
1538
1428
1539
1429
1540 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables',
1430 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables',
1541 'default_user')
1431 'default_user')
1542 class TestApplicationCreation(object):
1432 class TestApplicationCreation(object):
1543 def test_wrong_data(self):
1433 def test_wrong_data(self):
1544 import appenlight.views.applications as applications
1434 import appenlight.views.applications as applications
1545 from ziggurat_foundations.models.services.user import UserService
1435 from ziggurat_foundations.models.services.user import UserService
1546 request = pyramid.threadlocal.get_current_request()
1436 request = pyramid.threadlocal.get_current_request()
1547 request.user = UserService.by_user_name('testuser')
1437 request.user = UserService.by_user_name('testuser')
1548 request.unsafe_json_body = {}
1438 request.unsafe_json_body = {}
1549 request.headers['X-XSRF-TOKEN'] = request.session.get_csrf_token()
1439 request.headers['X-XSRF-TOKEN'] = request.session.get_csrf_token()
1550 response = applications.application_create(request)
1440 response = applications.application_create(request)
1551 assert response.code == 422
1441 assert response.code == 422
1552
1442
1553 def test_proper_data(self):
1443 def test_proper_data(self):
1554 import appenlight.views.applications as applications
1444 import appenlight.views.applications as applications
1555 from ziggurat_foundations.models.services.user import UserService
1445 from ziggurat_foundations.models.services.user import UserService
1556
1446
1557 request = pyramid.threadlocal.get_current_request()
1447 request = pyramid.threadlocal.get_current_request()
1558 request.user = UserService.by_user_name('testuser')
1448 request.user = UserService.by_user_name('testuser')
1559 request.unsafe_json_body = {"resource_name": "app name",
1449 request.unsafe_json_body = {"resource_name": "app name",
1560 "domains": "foo"}
1450 "domains": "foo"}
1561 request.headers['X-XSRF-TOKEN'] = request.session.get_csrf_token()
1451 request.headers['X-XSRF-TOKEN'] = request.session.get_csrf_token()
1562 app_dict = applications.application_create(request)
1452 app_dict = applications.application_create(request)
1563 assert app_dict['public_key'] is not None
1453 assert app_dict['public_key'] is not None
1564 assert app_dict['api_key'] is not None
1454 assert app_dict['api_key'] is not None
1565 assert app_dict['resource_name'] == 'app name'
1455 assert app_dict['resource_name'] == 'app name'
1566 assert app_dict['owner_group_id'] is None
1456 assert app_dict['owner_group_id'] is None
1567 assert app_dict['resource_id'] is not None
1457 assert app_dict['resource_id'] is not None
1568 assert app_dict['default_grouping'] == 'url_traceback'
1458 assert app_dict['default_grouping'] == 'url_traceback'
1569 assert app_dict['possible_permissions'] == ('view', 'update_reports')
1459 assert app_dict['possible_permissions'] == ('view', 'update_reports')
1570 assert app_dict['slow_report_threshold'] == 10
1460 assert app_dict['slow_report_threshold'] == 10
1571 assert app_dict['owner_user_name'] == 'testuser'
1461 assert app_dict['owner_user_name'] == 'testuser'
1572 assert app_dict['owner_user_id'] == request.user.id
1462 assert app_dict['owner_user_id'] == request.user.id
1573 assert app_dict['domains'] is 'foo'
1463 assert app_dict['domains'] is 'foo'
1574 assert app_dict['postprocessing_rules'] == []
1464 assert app_dict['postprocessing_rules'] == []
1575 assert app_dict['error_report_threshold'] == 10
1465 assert app_dict['error_report_threshold'] == 10
1576 assert app_dict['allow_permanent_storage'] is False
1466 assert app_dict['allow_permanent_storage'] is False
1577 assert app_dict['resource_type'] == 'application'
1467 assert app_dict['resource_type'] == 'application'
1578 assert app_dict['current_permissions'] == []
1468 assert app_dict['current_permissions'] == []
1579
1469
1580
1470
1581 @pytest.mark.usefixtures('default_application')
1471 @pytest.mark.usefixtures('default_application')
1582 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
1472 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
1583 class TestAPISentryView(object):
1473 class TestAPISentryView(object):
1584 def test_no_payload(self, default_application):
1474 def test_no_payload(self, default_application):
1585 import colander
1475 import colander
1586 from appenlight.models.services.application import ApplicationService
1476 from appenlight.models.services.application import ApplicationService
1587 from appenlight.views.api import sentry_compat
1477 from appenlight.views.api import sentry_compat
1588 from appenlight.lib.request import JSONException
1478 from appenlight.lib.request import JSONException
1589
1479
1590 context = DummyContext()
1480 context = DummyContext()
1591 context.resource = ApplicationService.by_id(1)
1481 context.resource = ApplicationService.by_id(1)
1592 request = testing.DummyRequest(
1482 request = testing.DummyRequest(
1593 headers={'Content-Type': 'application/json'})
1483 headers={'Content-Type': 'application/json'})
1594 request.unsafe_json_body = ''
1484 request.unsafe_json_body = ''
1595 request.context = context
1485 request.context = context
1596 route = mock.Mock()
1486 route = mock.Mock()
1597 route.name = 'api_sentry'
1487 route.name = 'api_sentry'
1598 request.matched_route = route
1488 request.matched_route = route
1599 with pytest.raises(JSONException):
1489 with pytest.raises(JSONException):
1600 sentry_compat(request)
1490 sentry_compat(request)
1601
1491
1602 def test_java_client_payload(self):
1492 def test_java_client_payload(self):
1603 from appenlight.views.api import sentry_compat
1493 from appenlight.views.api import sentry_compat
1604 from appenlight.models.services.application import ApplicationService
1494 from appenlight.models.services.application import ApplicationService
1605 from appenlight.models.report_group import ReportGroup
1495 from appenlight.models.report_group import ReportGroup
1606 route = mock.Mock()
1496 route = mock.Mock()
1607 route.name = 'api_sentry'
1497 route.name = 'api_sentry'
1608 request = pyramid.threadlocal.get_current_request()
1498 request = pyramid.threadlocal.get_current_request()
1609 context = DummyContext()
1499 context = DummyContext()
1610 context.resource = ApplicationService.by_id(1)
1500 context.resource = ApplicationService.by_id(1)
1611 request.context = context
1501 request.context = context
1612 request.matched_route = route
1502 request.matched_route = route
1613 request.body = b'eJy1UmFr2zAQ/S0T+7BCLOzYThp/C6xjG6SDLd/GCBf57Ki' \
1503 request.body = b'eJy1UmFr2zAQ/S0T+7BCLOzYThp/C6xjG6SDLd/GCBf57Ki' \
1614 b'RJSHJJiXkv+/UlC7p2kAZA33Ru6f33t1pz3BAHVayZhWr87' \
1504 b'RJSHJJiXkv+/UlC7p2kAZA33Ru6f33t1pz3BAHVayZhWr87' \
1615 b'JMs+I6q3MsrifFep2vc1iXM1HMpgBTNmIdeg8tEvlmJ9AGa' \
1505 b'JMs+I6q3MsrifFep2vc1iXM1HMpgBTNmIdeg8tEvlmJ9AGa' \
1616 b'fQ7goOkQoDOUmGcZpMkLZO0WGZFRadMiaHIR1EVnTMu3k3b' \
1506 b'fQ7goOkQoDOUmGcZpMkLZO0WGZFRadMiaHIR1EVnTMu3k3b' \
1617 b'oiMgqJrXpgOpOVjLLTiPkWAVhMa4jih3MAAholfWyUDAksz' \
1507 b'oiMgqJrXpgOpOVjLLTiPkWAVhMa4jih3MAAholfWyUDAksz' \
1618 b'm1iopICbg8fWH52B8VWXZVYwHrWfV/jBipD2gW2no8CFMa5' \
1508 b'm1iopICbg8fWH52B8VWXZVYwHrWfV/jBipD2gW2no8CFMa5' \
1619 b'JButCDSjoQG6mR6LgLDojPPn/7sbydL25ep34HGl+y3DiE+' \
1509 b'JButCDSjoQG6mR6LgLDojPPn/7sbydL25ep34HGl+y3DiE+' \
1620 b'lH0xXBXjMzFBsXW99SS7pWKYXRw91zqgK4BgZ4/DZVVP/cs' \
1510 b'lH0xXBXjMzFBsXW99SS7pWKYXRw91zqgK4BgZ4/DZVVP/cs' \
1621 b'3NuzSZPfAKqP2Cdj4tw7U/cKH0fEFeiWQFqE2FIHAmMPjaN' \
1511 b'3NuzSZPfAKqP2Cdj4tw7U/cKH0fEFeiWQFqE2FIHAmMPjaN' \
1622 b'Y/kHvbzY/JqdHUq9o/KxqQHkcsabX4piDuT4aK+pXG1ZNi/' \
1512 b'Y/kHvbzY/JqdHUq9o/KxqQHkcsabX4piDuT4aK+pXG1ZNi/' \
1623 b'IwOpEyruXC1LiB3vPO3BmOOxTUCIqv5LIg5H12oh9cf0l+P' \
1513 b'IwOpEyruXC1LiB3vPO3BmOOxTUCIqv5LIg5H12oh9cf0l+P' \
1624 b'MvP5P8kddgoFIEvMGzM5cRSD2aLJ6qTdHKm6nv9pPcRFba0' \
1514 b'MvP5P8kddgoFIEvMGzM5cRSD2aLJ6qTdHKm6nv9pPcRFba0' \
1625 b'Kd0eleeCFuGN+9JZ9TaXIn/V5JYMBvxXg3L6PwzSE4dkfOb' \
1515 b'Kd0eleeCFuGN+9JZ9TaXIn/V5JYMBvxXg3L6PwzSE4dkfOb' \
1626 b'w7CtfWmP85SdCs8OvA53fUV19cg=='
1516 b'w7CtfWmP85SdCs8OvA53fUV19cg=='
1627 sentry_compat(request)
1517 sentry_compat(request)
1628 query = DBSession.query(ReportGroup)
1518 query = DBSession.query(ReportGroup)
1629 report = query.first()
1519 report = query.first()
1630 assert query.count() == 1
1520 assert query.count() == 1
1631 assert report.total_reports == 1
1521 assert report.total_reports == 1
1632
1522
1633 def test_ruby_client_payload(self):
1523 def test_ruby_client_payload(self):
1634 from appenlight.views.api import sentry_compat
1524 from appenlight.views.api import sentry_compat
1635 from appenlight.models.services.application import ApplicationService
1525 from appenlight.models.services.application import ApplicationService
1636 from appenlight.models.report_group import ReportGroup
1526 from appenlight.models.report_group import ReportGroup
1637 from appenlight.tests.payload_examples import SENTRY_RUBY_ENCODED
1527 from appenlight.tests.payload_examples import SENTRY_RUBY_ENCODED
1638 route = mock.Mock()
1528 route = mock.Mock()
1639 route.name = 'api_sentry'
1529 route.name = 'api_sentry'
1640 request = testing.DummyRequest(
1530 request = testing.DummyRequest(
1641 headers={'Content-Type': 'application/octet-stream',
1531 headers={'Content-Type': 'application/octet-stream',
1642 'User-Agent': 'sentry-ruby/1.0.0',
1532 'User-Agent': 'sentry-ruby/1.0.0',
1643 'X-Sentry-Auth': 'Sentry sentry_version=5, '
1533 'X-Sentry-Auth': 'Sentry sentry_version=5, '
1644 'sentry_client=raven-ruby/1.0.0, '
1534 'sentry_client=raven-ruby/1.0.0, '
1645 'sentry_timestamp=1462378483, '
1535 'sentry_timestamp=1462378483, '
1646 'sentry_key=xxx, sentry_secret=xxx'
1536 'sentry_key=xxx, sentry_secret=xxx'
1647 })
1537 })
1648 context = DummyContext()
1538 context = DummyContext()
1649 context.resource = ApplicationService.by_id(1)
1539 context.resource = ApplicationService.by_id(1)
1650 request.context = context
1540 request.context = context
1651 request.matched_route = route
1541 request.matched_route = route
1652 request.body = SENTRY_RUBY_ENCODED
1542 request.body = SENTRY_RUBY_ENCODED
1653 sentry_compat(request)
1543 sentry_compat(request)
1654 query = DBSession.query(ReportGroup)
1544 query = DBSession.query(ReportGroup)
1655 report = query.first()
1545 report = query.first()
1656 assert query.count() == 1
1546 assert query.count() == 1
1657 assert report.total_reports == 1
1547 assert report.total_reports == 1
1658
1548
1659 def test_python_client_decoded_payload(self):
1549 def test_python_client_decoded_payload(self):
1660 from appenlight.views.api import sentry_compat
1550 from appenlight.views.api import sentry_compat
1661 from appenlight.models.services.application import ApplicationService
1551 from appenlight.models.services.application import ApplicationService
1662 from appenlight.models.report_group import ReportGroup
1552 from appenlight.models.report_group import ReportGroup
1663 from appenlight.tests.payload_examples import SENTRY_PYTHON_PAYLOAD_7
1553 from appenlight.tests.payload_examples import SENTRY_PYTHON_PAYLOAD_7
1664 route = mock.Mock()
1554 route = mock.Mock()
1665 route.name = 'api_sentry'
1555 route.name = 'api_sentry'
1666 request = pyramid.threadlocal.get_current_request()
1556 request = pyramid.threadlocal.get_current_request()
1667 context = DummyContext()
1557 context = DummyContext()
1668 context.resource = ApplicationService.by_id(1)
1558 context.resource = ApplicationService.by_id(1)
1669 request.context = context
1559 request.context = context
1670 request.matched_route = route
1560 request.matched_route = route
1671 request.body = json.dumps(SENTRY_PYTHON_PAYLOAD_7).encode('utf8')
1561 request.body = json.dumps(SENTRY_PYTHON_PAYLOAD_7).encode('utf8')
1672 sentry_compat(request)
1562 sentry_compat(request)
1673 query = DBSession.query(ReportGroup)
1563 query = DBSession.query(ReportGroup)
1674 report = query.first()
1564 report = query.first()
1675 assert query.count() == 1
1565 assert query.count() == 1
1676 assert report.total_reports == 1
1566 assert report.total_reports == 1
1677
1567
1678 def test_python_client_encoded_payload(self):
1568 def test_python_client_encoded_payload(self):
1679 from appenlight.views.api import sentry_compat
1569 from appenlight.views.api import sentry_compat
1680 from appenlight.models.services.application import ApplicationService
1570 from appenlight.models.services.application import ApplicationService
1681 from appenlight.models.report_group import ReportGroup
1571 from appenlight.models.report_group import ReportGroup
1682 from appenlight.tests.payload_examples import SENTRY_PYTHON_ENCODED
1572 from appenlight.tests.payload_examples import SENTRY_PYTHON_ENCODED
1683 route = mock.Mock()
1573 route = mock.Mock()
1684 route.name = 'api_sentry'
1574 route.name = 'api_sentry'
1685 request = testing.DummyRequest(
1575 request = testing.DummyRequest(
1686 headers={'Content-Type': 'application/octet-stream',
1576 headers={'Content-Type': 'application/octet-stream',
1687 'Content-Encoding': 'deflate',
1577 'Content-Encoding': 'deflate',
1688 'User-Agent': 'sentry-ruby/1.0.0',
1578 'User-Agent': 'sentry-ruby/1.0.0',
1689 'X-Sentry-Auth': 'Sentry sentry_version=5, '
1579 'X-Sentry-Auth': 'Sentry sentry_version=5, '
1690 'sentry_client=raven-ruby/1.0.0, '
1580 'sentry_client=raven-ruby/1.0.0, '
1691 'sentry_timestamp=1462378483, '
1581 'sentry_timestamp=1462378483, '
1692 'sentry_key=xxx, sentry_secret=xxx'
1582 'sentry_key=xxx, sentry_secret=xxx'
1693 })
1583 })
1694 context = DummyContext()
1584 context = DummyContext()
1695 context.resource = ApplicationService.by_id(1)
1585 context.resource = ApplicationService.by_id(1)
1696 request.context = context
1586 request.context = context
1697 request.matched_route = route
1587 request.matched_route = route
1698 request.body = SENTRY_PYTHON_ENCODED
1588 request.body = SENTRY_PYTHON_ENCODED
1699 sentry_compat(request)
1589 sentry_compat(request)
1700 query = DBSession.query(ReportGroup)
1590 query = DBSession.query(ReportGroup)
1701 report = query.first()
1591 report = query.first()
1702 assert query.count() == 1
1592 assert query.count() == 1
1703 assert report.total_reports == 1
1593 assert report.total_reports == 1
@@ -1,743 +1,752 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 datetime
22 import datetime
23
23
24 import colander
24 import colander
25 from colander import null
25 from colander import null
26
26
27 # those keywords are here so we can distingush between searching for tags and
27 # those keywords are here so we can distingush between searching for tags and
28 # normal properties of reports/logs
28 # normal properties of reports/logs
29 accepted_search_params = ['resource',
29 accepted_search_params = ['resource',
30 'request_id',
30 'request_id',
31 'start_date',
31 'start_date',
32 'end_date',
32 'end_date',
33 'page',
33 'page',
34 'min_occurences',
34 'min_occurences',
35 'http_status',
35 'http_status',
36 'priority',
36 'priority',
37 'error',
37 'error',
38 'url_path',
38 'url_path',
39 'url_domain',
39 'url_domain',
40 'report_status',
40 'report_status',
41 'min_duration',
41 'min_duration',
42 'max_duration',
42 'max_duration',
43 'message',
43 'message',
44 'level',
44 'level',
45 'namespace']
45 'namespace']
46
46
47
47
48 @colander.deferred
48 @colander.deferred
49 def deferred_utcnow(node, kw):
49 def deferred_utcnow(node, kw):
50 return kw['utcnow']
50 return kw['utcnow']
51
51
52
52
53 def lowercase_preparer(input_data):
53 def lowercase_preparer(input_data):
54 """
54 """
55 Transforms a list of string entries to lowercase
55 Transforms a list of string entries to lowercase
56 Used in search query validation
56 Used in search query validation
57 """
57 """
58 if not input_data:
58 if not input_data:
59 return input_data
59 return input_data
60 return [x.lower() for x in input_data]
60 return [x.lower() for x in input_data]
61
61
62
62
63 def shortener_factory(cutoff_size=32):
63 def shortener_factory(cutoff_size=32):
64 """
64 """
65 Limits the input data to specific character count
65 Limits the input data to specific character count
66 :arg cutoff_cutoff_size How much characters to store
66 :arg cutoff_cutoff_size How much characters to store
67
67
68 """
68 """
69
69
70 def shortener(input_data):
70 def shortener(input_data):
71 if not input_data:
71 if not input_data:
72 return input_data
72 return input_data
73 else:
73 else:
74 if isinstance(input_data, str):
74 if isinstance(input_data, str):
75 return input_data[:cutoff_size]
75 return input_data[:cutoff_size]
76 else:
76 else:
77 return input_data
77 return input_data
78
78
79 return shortener
79 return shortener
80
80
81
81
82 def cast_to_unicode_or_null(value):
82 def cast_to_unicode_or_null(value):
83 if value is not colander.null:
83 if value is not colander.null:
84 return str(value)
84 return str(value)
85 return None
85 return None
86
86
87
87
88 class NonTZDate(colander.DateTime):
88 class NonTZDate(colander.DateTime):
89 """ Returns null for incorrect date format - also removes tz info"""
89 """ Returns null for incorrect date format - also removes tz info"""
90
90
91 def deserialize(self, node, cstruct):
91 def deserialize(self, node, cstruct):
92 # disabled for now
92 # disabled for now
93 # if cstruct and isinstance(cstruct, str):
93 # if cstruct and isinstance(cstruct, str):
94 # if ':' not in cstruct:
94 # if ':' not in cstruct:
95 # cstruct += ':0.0'
95 # cstruct += ':0.0'
96 # if '.' not in cstruct:
96 # if '.' not in cstruct:
97 # cstruct += '.0'
97 # cstruct += '.0'
98 value = super(NonTZDate, self).deserialize(node, cstruct)
98 value = super(NonTZDate, self).deserialize(node, cstruct)
99 if value:
99 if value:
100 return value.replace(tzinfo=None)
100 return value.replace(tzinfo=None)
101 return value
101 return value
102
102
103
103
104 class UnknownType(object):
104 class UnknownType(object):
105 """
105 """
106 Universal type that will accept a deserialized JSON object and store it unaltered
106 Universal type that will accept a deserialized JSON object and store it unaltered
107 """
107 """
108
108
109 def serialize(self, node, appstruct):
109 def serialize(self, node, appstruct):
110 if appstruct is null:
110 if appstruct is null:
111 return null
111 return null
112 return appstruct
112 return appstruct
113
113
114 def deserialize(self, node, cstruct):
114 def deserialize(self, node, cstruct):
115 if cstruct is null:
115 if cstruct is null:
116 return null
116 return null
117 return cstruct
117 return cstruct
118
118
119 def cstruct_children(self):
119 def cstruct_children(self):
120 return []
120 return []
121
121
122
122
123 # SLOW REPORT SCHEMA
123 # SLOW REPORT SCHEMA
124
124
125 def rewrite_type(input_data):
125 def rewrite_type(input_data):
126 """
126 """
127 Fix for legacy appenlight clients
127 Fix for legacy appenlight clients
128 """
128 """
129 if input_data == 'remote_call':
129 if input_data == 'remote_call':
130 return 'remote'
130 return 'remote'
131 return input_data
131 return input_data
132
132
133
133
134 class ExtraTupleSchema(colander.TupleSchema):
134 class ExtraTupleSchema(colander.TupleSchema):
135 name = colander.SchemaNode(colander.String(),
135 name = colander.SchemaNode(colander.String(),
136 validator=colander.Length(1, 64))
136 validator=colander.Length(1, 64))
137 value = colander.SchemaNode(UnknownType(),
137 value = colander.SchemaNode(UnknownType(),
138 preparer=shortener_factory(512),
138 preparer=shortener_factory(512),
139 missing=None)
139 missing=None)
140
140
141
141
142 class ExtraSchemaList(colander.SequenceSchema):
142 class ExtraSchemaList(colander.SequenceSchema):
143 tag = ExtraTupleSchema()
143 tag = ExtraTupleSchema()
144 missing = None
144 missing = None
145
145
146
146
147 class TagsTupleSchema(colander.TupleSchema):
147 class TagsTupleSchema(colander.TupleSchema):
148 name = colander.SchemaNode(colander.String(),
148 name = colander.SchemaNode(colander.String(),
149 validator=colander.Length(1, 128))
149 validator=colander.Length(1, 128))
150 value = colander.SchemaNode(UnknownType(),
150 value = colander.SchemaNode(UnknownType(),
151 preparer=shortener_factory(128),
151 preparer=shortener_factory(128),
152 missing=None)
152 missing=None)
153
153
154
154
155 class TagSchemaList(colander.SequenceSchema):
155 class TagSchemaList(colander.SequenceSchema):
156 tag = TagsTupleSchema()
156 tag = TagsTupleSchema()
157 missing = None
157 missing = None
158
158
159
159
160 class NumericTagsTupleSchema(colander.TupleSchema):
160 class NumericTagsTupleSchema(colander.TupleSchema):
161 name = colander.SchemaNode(colander.String(),
161 name = colander.SchemaNode(colander.String(),
162 validator=colander.Length(1, 128))
162 validator=colander.Length(1, 128))
163 value = colander.SchemaNode(colander.Float(), missing=0)
163 value = colander.SchemaNode(colander.Float(), missing=0)
164
164
165
165
166 class NumericTagSchemaList(colander.SequenceSchema):
166 class NumericTagSchemaList(colander.SequenceSchema):
167 tag = NumericTagsTupleSchema()
167 tag = NumericTagsTupleSchema()
168 missing = None
168 missing = None
169
169
170
170
171 class SlowCallSchema(colander.MappingSchema):
171 class SlowCallSchema(colander.MappingSchema):
172 """
172 """
173 Validates slow call format in slow call list
173 Validates slow call format in slow call list
174 """
174 """
175 start = colander.SchemaNode(NonTZDate())
175 start = colander.SchemaNode(NonTZDate())
176 end = colander.SchemaNode(NonTZDate())
176 end = colander.SchemaNode(NonTZDate())
177 statement = colander.SchemaNode(colander.String(), missing='')
177 statement = colander.SchemaNode(colander.String(), missing='')
178 parameters = colander.SchemaNode(UnknownType(), missing=None)
178 parameters = colander.SchemaNode(UnknownType(), missing=None)
179 type = colander.SchemaNode(
179 type = colander.SchemaNode(
180 colander.String(),
180 colander.String(),
181 preparer=rewrite_type,
181 preparer=rewrite_type,
182 validator=colander.OneOf(
182 validator=colander.OneOf(
183 ['tmpl', 'sql', 'nosql', 'remote', 'unknown', 'custom']),
183 ['tmpl', 'sql', 'nosql', 'remote', 'unknown', 'custom']),
184 missing='unknown')
184 missing='unknown')
185 subtype = colander.SchemaNode(colander.String(),
185 subtype = colander.SchemaNode(colander.String(),
186 validator=colander.Length(1, 16),
186 validator=colander.Length(1, 16),
187 missing='unknown')
187 missing='unknown')
188 location = colander.SchemaNode(colander.String(),
188 location = colander.SchemaNode(colander.String(),
189 validator=colander.Length(1, 255),
189 validator=colander.Length(1, 255),
190 missing='')
190 missing='')
191
191
192
192
193 def limited_date(node, value):
193 def limited_date(node, value):
194 """ checks to make sure that the value is not older/newer than 2h """
194 """ checks to make sure that the value is not older/newer than 2h """
195 hours = 2
195 past_hours = 72
196 min_time = datetime.datetime.utcnow() - datetime.timedelta(hours=72)
196 future_hours = 2
197 max_time = datetime.datetime.utcnow() + datetime.timedelta(hours=2)
197 min_time = datetime.datetime.utcnow() - datetime.timedelta(
198 hours=past_hours)
199 max_time = datetime.datetime.utcnow() + datetime.timedelta(
200 hours=future_hours)
198 if min_time > value:
201 if min_time > value:
199 msg = '%r is older from current UTC time by ' + str(hours) + ' hours.'
202 msg = '%r is older from current UTC time by ' + str(past_hours)
200 msg += ' Ask administrator to enable permanent logging for ' \
203 msg += ' hours. Ask administrator to enable permanent logging for ' \
201 'your application to store logs with dates in past.'
204 'your application to store logs with dates in past.'
202 raise colander.Invalid(node, msg % value)
205 raise colander.Invalid(node, msg % value)
203 if max_time < value:
206 if max_time < value:
204 msg = '%r is newer from current UTC time by ' + str(hours) + ' hours'
207 msg = '%r is newer from current UTC time by ' + str(future_hours)
205 msg += ' Ask administrator to enable permanent logging for ' \
208 msg += ' hours. Ask administrator to enable permanent logging for ' \
206 'your application to store logs with dates in future.'
209 'your application to store logs with dates in future.'
207 raise colander.Invalid(node, msg % value)
210 raise colander.Invalid(node, msg % value)
208
211
209
212
210 class SlowCallListSchema(colander.SequenceSchema):
213 class SlowCallListSchema(colander.SequenceSchema):
211 """
214 """
212 Validates list of individual slow calls
215 Validates list of individual slow calls
213 """
216 """
214 slow_call = SlowCallSchema()
217 slow_call = SlowCallSchema()
215
218
216
219
217 class RequestStatsSchema(colander.MappingSchema):
220 class RequestStatsSchema(colander.MappingSchema):
218 """
221 """
219 Validates format of requests statistics dictionary
222 Validates format of requests statistics dictionary
220 """
223 """
221 main = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
224 main = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
222 missing=0)
225 missing=0)
223 sql = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
226 sql = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
224 missing=0)
227 missing=0)
225 nosql = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
228 nosql = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
226 missing=0)
229 missing=0)
227 remote = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
230 remote = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
228 missing=0)
231 missing=0)
229 tmpl = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
232 tmpl = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
230 missing=0)
233 missing=0)
231 custom = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
234 custom = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
232 missing=0)
235 missing=0)
233 sql_calls = colander.SchemaNode(colander.Float(),
236 sql_calls = colander.SchemaNode(colander.Float(),
234 validator=colander.Range(0),
237 validator=colander.Range(0),
235 missing=0)
238 missing=0)
236 nosql_calls = colander.SchemaNode(colander.Float(),
239 nosql_calls = colander.SchemaNode(colander.Float(),
237 validator=colander.Range(0),
240 validator=colander.Range(0),
238 missing=0)
241 missing=0)
239 remote_calls = colander.SchemaNode(colander.Float(),
242 remote_calls = colander.SchemaNode(colander.Float(),
240 validator=colander.Range(0),
243 validator=colander.Range(0),
241 missing=0)
244 missing=0)
242 tmpl_calls = colander.SchemaNode(colander.Float(),
245 tmpl_calls = colander.SchemaNode(colander.Float(),
243 validator=colander.Range(0),
246 validator=colander.Range(0),
244 missing=0)
247 missing=0)
245 custom_calls = colander.SchemaNode(colander.Float(),
248 custom_calls = colander.SchemaNode(colander.Float(),
246 validator=colander.Range(0),
249 validator=colander.Range(0),
247 missing=0)
250 missing=0)
248
251
249
252
250 class FrameInfoVarSchema(colander.SequenceSchema):
253 class FrameInfoVarSchema(colander.SequenceSchema):
251 """
254 """
252 Validates format of frame variables of a traceback
255 Validates format of frame variables of a traceback
253 """
256 """
254 vars = colander.SchemaNode(UnknownType(),
257 vars = colander.SchemaNode(UnknownType(),
255 validator=colander.Length(2, 2))
258 validator=colander.Length(2, 2))
256
259
257
260
258 class FrameInfoSchema(colander.MappingSchema):
261 class FrameInfoSchema(colander.MappingSchema):
259 """
262 """
260 Validates format of a traceback line
263 Validates format of a traceback line
261 """
264 """
262 cline = colander.SchemaNode(colander.String(), missing='')
265 cline = colander.SchemaNode(colander.String(), missing='')
263 module = colander.SchemaNode(colander.String(), missing='')
266 module = colander.SchemaNode(colander.String(), missing='')
264 line = colander.SchemaNode(colander.String(), missing='')
267 line = colander.SchemaNode(colander.String(), missing='')
265 file = colander.SchemaNode(colander.String(), missing='')
268 file = colander.SchemaNode(colander.String(), missing='')
266 fn = colander.SchemaNode(colander.String(), missing='')
269 fn = colander.SchemaNode(colander.String(), missing='')
267 vars = FrameInfoVarSchema()
270 vars = FrameInfoVarSchema()
268
271
269
272
270 class FrameInfoListSchema(colander.SequenceSchema):
273 class FrameInfoListSchema(colander.SequenceSchema):
271 """
274 """
272 Validates format of list of traceback lines
275 Validates format of list of traceback lines
273 """
276 """
274 frame = colander.SchemaNode(UnknownType())
277 frame = colander.SchemaNode(UnknownType())
275
278
276
279
277 class ReportDetailBaseSchema(colander.MappingSchema):
280 class ReportDetailBaseSchema(colander.MappingSchema):
278 """
281 """
279 Validates format of report - ie. request parameters and stats for a request in report group
282 Validates format of report - ie. request parameters and stats for a request in report group
280 """
283 """
281 username = colander.SchemaNode(colander.String(),
284 username = colander.SchemaNode(colander.String(),
282 preparer=[shortener_factory(255),
285 preparer=[shortener_factory(255),
283 lambda x: x or ''],
286 lambda x: x or ''],
284 missing='')
287 missing='')
285 request_id = colander.SchemaNode(colander.String(),
288 request_id = colander.SchemaNode(colander.String(),
286 preparer=shortener_factory(40),
289 preparer=shortener_factory(40),
287 missing='')
290 missing='')
288 url = colander.SchemaNode(colander.String(),
291 url = colander.SchemaNode(colander.String(),
289 preparer=shortener_factory(1024), missing='')
292 preparer=shortener_factory(1024), missing='')
290 ip = colander.SchemaNode(colander.String(), preparer=shortener_factory(39),
293 ip = colander.SchemaNode(colander.String(), preparer=shortener_factory(39),
291 missing=None)
294 missing=None)
292 start_time = colander.SchemaNode(NonTZDate(), validator=limited_date,
295 start_time = colander.SchemaNode(NonTZDate(), validator=limited_date,
293 missing=deferred_utcnow)
296 missing=deferred_utcnow)
294 end_time = colander.SchemaNode(NonTZDate(), validator=limited_date,
297 end_time = colander.SchemaNode(NonTZDate(), validator=limited_date,
295 missing=None)
298 missing=None)
296 user_agent = colander.SchemaNode(colander.String(),
299 user_agent = colander.SchemaNode(colander.String(),
297 preparer=[shortener_factory(512),
300 preparer=[shortener_factory(512),
298 lambda x: x or ''],
301 lambda x: x or ''],
299 missing='')
302 missing='')
300 message = colander.SchemaNode(colander.String(),
303 message = colander.SchemaNode(colander.String(),
301 preparer=shortener_factory(2048),
304 preparer=shortener_factory(2048),
302 missing='')
305 missing='')
303 group_string = colander.SchemaNode(colander.String(),
306 group_string = colander.SchemaNode(colander.String(),
304 validator=colander.Length(1, 512),
307 validator=colander.Length(1, 512),
305 missing=None)
308 missing=None)
306 request_stats = RequestStatsSchema(missing=None)
309 request_stats = RequestStatsSchema(missing=None)
307 request = colander.SchemaNode(colander.Mapping(unknown='preserve'),
310 request = colander.SchemaNode(colander.Mapping(unknown='preserve'),
308 missing={})
311 missing={})
309 traceback = FrameInfoListSchema(missing=None)
312 traceback = FrameInfoListSchema(missing=None)
310 slow_calls = SlowCallListSchema(missing=[])
313 slow_calls = SlowCallListSchema(missing=[])
311 extra = ExtraSchemaList()
314 extra = ExtraSchemaList()
312
315
313
316
314 class ReportDetailSchema_0_4(ReportDetailBaseSchema):
315 frameinfo = FrameInfoListSchema(missing=None)
316
317
318 class ReportDetailSchema_0_5(ReportDetailBaseSchema):
317 class ReportDetailSchema_0_5(ReportDetailBaseSchema):
319 pass
318 pass
320
319
321
320
322 class ReportDetailListSchema(colander.SequenceSchema):
321 class ReportDetailSchemaPermissiveDate_0_5(ReportDetailSchema_0_5):
323 """
322 start_time = colander.SchemaNode(NonTZDate(), missing=deferred_utcnow)
324 Validates format of list of reports
323 end_time = colander.SchemaNode(NonTZDate(), missing=None)
325 """
326 report_detail = ReportDetailSchema_0_4()
327 validator = colander.Length(1)
328
324
329
325
330 class ReportSchemaBase(colander.MappingSchema):
326 class ReportSchemaBase(colander.MappingSchema):
331 """
327 """
332 Validates format of report group
328 Validates format of report group
333 """
329 """
334 client = colander.SchemaNode(colander.String(),
330 client = colander.SchemaNode(colander.String(),
335 preparer=lambda x: x or 'unknown')
331 preparer=lambda x: x or 'unknown')
336 server = colander.SchemaNode(
332 server = colander.SchemaNode(
337 colander.String(),
333 colander.String(),
338 preparer=[
334 preparer=[
339 lambda x: x.lower() if x else 'unknown', shortener_factory(128)],
335 lambda x: x.lower() if x else 'unknown', shortener_factory(128)],
340 missing='unknown')
336 missing='unknown')
341 priority = colander.SchemaNode(colander.Int(),
337 priority = colander.SchemaNode(colander.Int(),
342 preparer=[lambda x: x or 5],
338 preparer=[lambda x: x or 5],
343 validator=colander.Range(1, 10),
339 validator=colander.Range(1, 10),
344 missing=5)
340 missing=5)
345 language = colander.SchemaNode(colander.String(), missing='unknown')
341 language = colander.SchemaNode(colander.String(), missing='unknown')
346 error = colander.SchemaNode(colander.String(),
342 error = colander.SchemaNode(colander.String(),
347 preparer=shortener_factory(512),
343 preparer=shortener_factory(512),
348 missing='')
344 missing='')
349 view_name = colander.SchemaNode(colander.String(),
345 view_name = colander.SchemaNode(colander.String(),
350 preparer=[shortener_factory(128),
346 preparer=[shortener_factory(128),
351 lambda x: x or ''],
347 lambda x: x or ''],
352 missing='')
348 missing='')
353 http_status = colander.SchemaNode(colander.Int(),
349 http_status = colander.SchemaNode(colander.Int(),
354 preparer=[lambda x: x or 200],
350 preparer=[lambda x: x or 200],
355 validator=colander.Range(1))
351 validator=colander.Range(1))
356
352
357 occurences = colander.SchemaNode(colander.Int(),
353 occurences = colander.SchemaNode(colander.Int(),
358 validator=colander.Range(1, 99999999999),
354 validator=colander.Range(1, 99999999999),
359 missing=1)
355 missing=1)
360 tags = TagSchemaList()
356 tags = TagSchemaList()
361
357
362
358
363 class ReportSchema_0_5(ReportSchemaBase, ReportDetailSchema_0_5):
359 class ReportSchema_0_5(ReportSchemaBase, ReportDetailSchema_0_5):
364 pass
360 pass
365
361
366
362
363 class ReportSchemaPermissiveDate_0_5(ReportSchemaBase,
364 ReportDetailSchemaPermissiveDate_0_5):
365 pass
366
367
367 class ReportListSchema_0_5(colander.SequenceSchema):
368 class ReportListSchema_0_5(colander.SequenceSchema):
368 """
369 """
369 Validates format of list of report groups
370 Validates format of list of report groups
370 """
371 """
371 report = ReportSchema_0_5()
372 report = ReportSchema_0_5()
372 validator = colander.Length(1)
373 validator = colander.Length(1)
373
374
374
375
376 class ReportListPermissiveDateSchema_0_5(colander.SequenceSchema):
377 """
378 Validates format of list of report groups
379 """
380 report = ReportSchemaPermissiveDate_0_5()
381 validator = colander.Length(1)
382
383
375 class LogSchema(colander.MappingSchema):
384 class LogSchema(colander.MappingSchema):
376 """
385 """
377 Validates format if individual log entry
386 Validates format if individual log entry
378 """
387 """
379 primary_key = colander.SchemaNode(UnknownType(),
388 primary_key = colander.SchemaNode(UnknownType(),
380 preparer=[cast_to_unicode_or_null,
389 preparer=[cast_to_unicode_or_null,
381 shortener_factory(128)],
390 shortener_factory(128)],
382 missing=None)
391 missing=None)
383 log_level = colander.SchemaNode(colander.String(),
392 log_level = colander.SchemaNode(colander.String(),
384 preparer=shortener_factory(10),
393 preparer=shortener_factory(10),
385 missing='UNKNOWN')
394 missing='UNKNOWN')
386 message = colander.SchemaNode(colander.String(),
395 message = colander.SchemaNode(colander.String(),
387 preparer=shortener_factory(4096),
396 preparer=shortener_factory(4096),
388 missing='')
397 missing='')
389 namespace = colander.SchemaNode(colander.String(),
398 namespace = colander.SchemaNode(colander.String(),
390 preparer=shortener_factory(128),
399 preparer=shortener_factory(128),
391 missing='')
400 missing='')
392 request_id = colander.SchemaNode(colander.String(),
401 request_id = colander.SchemaNode(colander.String(),
393 preparer=shortener_factory(40),
402 preparer=shortener_factory(40),
394 missing='')
403 missing='')
395 server = colander.SchemaNode(colander.String(),
404 server = colander.SchemaNode(colander.String(),
396 preparer=shortener_factory(128),
405 preparer=shortener_factory(128),
397 missing='unknown')
406 missing='unknown')
398 date = colander.SchemaNode(NonTZDate(),
407 date = colander.SchemaNode(NonTZDate(),
399 validator=limited_date,
408 validator=limited_date,
400 missing=deferred_utcnow)
409 missing=deferred_utcnow)
401 tags = TagSchemaList()
410 tags = TagSchemaList()
402
411
403
412
404 class LogSchemaPermanent(LogSchema):
413 class LogSchemaPermanent(LogSchema):
405 date = colander.SchemaNode(NonTZDate(),
414 date = colander.SchemaNode(NonTZDate(),
406 missing=deferred_utcnow)
415 missing=deferred_utcnow)
407 permanent = colander.SchemaNode(colander.Boolean(), missing=False)
416 permanent = colander.SchemaNode(colander.Boolean(), missing=False)
408
417
409
418
410 class LogListSchema(colander.SequenceSchema):
419 class LogListSchema(colander.SequenceSchema):
411 """
420 """
412 Validates format of list of log entries
421 Validates format of list of log entries
413 """
422 """
414 log = LogSchema()
423 log = LogSchema()
415 validator = colander.Length(1)
424 validator = colander.Length(1)
416
425
417
426
418 class LogListPermanentSchema(colander.SequenceSchema):
427 class LogListPermanentSchema(colander.SequenceSchema):
419 """
428 """
420 Validates format of list of log entries
429 Validates format of list of log entries
421 """
430 """
422 log = LogSchemaPermanent()
431 log = LogSchemaPermanent()
423 validator = colander.Length(1)
432 validator = colander.Length(1)
424
433
425
434
426 class ViewRequestStatsSchema(RequestStatsSchema):
435 class ViewRequestStatsSchema(RequestStatsSchema):
427 requests = colander.SchemaNode(colander.Integer(),
436 requests = colander.SchemaNode(colander.Integer(),
428 validator=colander.Range(0),
437 validator=colander.Range(0),
429 missing=0)
438 missing=0)
430
439
431
440
432 class ViewMetricTupleSchema(colander.TupleSchema):
441 class ViewMetricTupleSchema(colander.TupleSchema):
433 """
442 """
434 Validates list of views and their corresponding request stats object ie:
443 Validates list of views and their corresponding request stats object ie:
435 ["dir/module:func",{"custom": 0.0..}]
444 ["dir/module:func",{"custom": 0.0..}]
436 """
445 """
437 view_name = colander.SchemaNode(colander.String(),
446 view_name = colander.SchemaNode(colander.String(),
438 preparer=[shortener_factory(128),
447 preparer=[shortener_factory(128),
439 lambda x: x or 'unknown'],
448 lambda x: x or 'unknown'],
440 missing='unknown')
449 missing='unknown')
441 metrics = ViewRequestStatsSchema()
450 metrics = ViewRequestStatsSchema()
442
451
443
452
444 class ViewMetricListSchema(colander.SequenceSchema):
453 class ViewMetricListSchema(colander.SequenceSchema):
445 """
454 """
446 Validates view breakdown stats objects list
455 Validates view breakdown stats objects list
447 {metrics key of server/time object}
456 {metrics key of server/time object}
448 """
457 """
449 view_tuple = ViewMetricTupleSchema()
458 view_tuple = ViewMetricTupleSchema()
450 validator = colander.Length(1)
459 validator = colander.Length(1)
451
460
452
461
453 class ViewMetricSchema(colander.MappingSchema):
462 class ViewMetricSchema(colander.MappingSchema):
454 """
463 """
455 Validates server/timeinterval object, ie:
464 Validates server/timeinterval object, ie:
456 {server/time object}
465 {server/time object}
457
466
458 """
467 """
459 timestamp = colander.SchemaNode(NonTZDate(),
468 timestamp = colander.SchemaNode(NonTZDate(),
460 validator=limited_date,
469 validator=limited_date,
461 missing=None)
470 missing=None)
462 server = colander.SchemaNode(colander.String(),
471 server = colander.SchemaNode(colander.String(),
463 preparer=[shortener_factory(128),
472 preparer=[shortener_factory(128),
464 lambda x: x or 'unknown'],
473 lambda x: x or 'unknown'],
465 missing='unknown')
474 missing='unknown')
466 metrics = ViewMetricListSchema()
475 metrics = ViewMetricListSchema()
467
476
468
477
469 class GeneralMetricSchema(colander.MappingSchema):
478 class GeneralMetricSchema(colander.MappingSchema):
470 """
479 """
471 Validates universal metric schema
480 Validates universal metric schema
472
481
473 """
482 """
474 namespace = colander.SchemaNode(colander.String(), missing='',
483 namespace = colander.SchemaNode(colander.String(), missing='',
475 preparer=shortener_factory(128))
484 preparer=shortener_factory(128))
476
485
477 server_name = colander.SchemaNode(colander.String(),
486 server_name = colander.SchemaNode(colander.String(),
478 preparer=[shortener_factory(128),
487 preparer=[shortener_factory(128),
479 lambda x: x or 'unknown'],
488 lambda x: x or 'unknown'],
480 missing='unknown')
489 missing='unknown')
481 timestamp = colander.SchemaNode(NonTZDate(), validator=limited_date,
490 timestamp = colander.SchemaNode(NonTZDate(), validator=limited_date,
482 missing=deferred_utcnow)
491 missing=deferred_utcnow)
483 tags = TagSchemaList(missing=colander.required)
492 tags = TagSchemaList(missing=colander.required)
484
493
485
494
486 class GeneralMetricsListSchema(colander.SequenceSchema):
495 class GeneralMetricsListSchema(colander.SequenceSchema):
487 metric = GeneralMetricSchema()
496 metric = GeneralMetricSchema()
488 validator = colander.Length(1)
497 validator = colander.Length(1)
489
498
490
499
491 class MetricsListSchema(colander.SequenceSchema):
500 class MetricsListSchema(colander.SequenceSchema):
492 """
501 """
493 Validates list of metrics objects ie:
502 Validates list of metrics objects ie:
494 [{server/time object}, ] part
503 [{server/time object}, ] part
495
504
496
505
497 """
506 """
498 metric = ViewMetricSchema()
507 metric = ViewMetricSchema()
499 validator = colander.Length(1)
508 validator = colander.Length(1)
500
509
501
510
502 class StringToAppList(object):
511 class StringToAppList(object):
503 """
512 """
504 Returns validated list of application ids from user query and
513 Returns validated list of application ids from user query and
505 set of applications user is allowed to look at
514 set of applications user is allowed to look at
506 transform string to list containing single integer
515 transform string to list containing single integer
507 """
516 """
508
517
509 def serialize(self, node, appstruct):
518 def serialize(self, node, appstruct):
510 if appstruct is null:
519 if appstruct is null:
511 return null
520 return null
512 return appstruct
521 return appstruct
513
522
514 def deserialize(self, node, cstruct):
523 def deserialize(self, node, cstruct):
515 if cstruct is null:
524 if cstruct is null:
516 return null
525 return null
517
526
518 apps = set([int(a) for a in node.bindings['resources']])
527 apps = set([int(a) for a in node.bindings['resources']])
519
528
520 if isinstance(cstruct, str):
529 if isinstance(cstruct, str):
521 cstruct = [cstruct]
530 cstruct = [cstruct]
522
531
523 cstruct = [int(a) for a in cstruct]
532 cstruct = [int(a) for a in cstruct]
524
533
525 valid_apps = list(apps.intersection(set(cstruct)))
534 valid_apps = list(apps.intersection(set(cstruct)))
526 if valid_apps:
535 if valid_apps:
527 return valid_apps
536 return valid_apps
528 return null
537 return null
529
538
530 def cstruct_children(self):
539 def cstruct_children(self):
531 return []
540 return []
532
541
533
542
534 @colander.deferred
543 @colander.deferred
535 def possible_applications_validator(node, kw):
544 def possible_applications_validator(node, kw):
536 possible_apps = [int(a) for a in kw['resources']]
545 possible_apps = [int(a) for a in kw['resources']]
537 return colander.All(colander.ContainsOnly(possible_apps),
546 return colander.All(colander.ContainsOnly(possible_apps),
538 colander.Length(1))
547 colander.Length(1))
539
548
540
549
541 @colander.deferred
550 @colander.deferred
542 def possible_applications(node, kw):
551 def possible_applications(node, kw):
543 return [int(a) for a in kw['resources']]
552 return [int(a) for a in kw['resources']]
544
553
545
554
546 @colander.deferred
555 @colander.deferred
547 def today_start(node, kw):
556 def today_start(node, kw):
548 return datetime.datetime.utcnow().replace(second=0, microsecond=0,
557 return datetime.datetime.utcnow().replace(second=0, microsecond=0,
549 minute=0,
558 minute=0,
550 hour=0)
559 hour=0)
551
560
552
561
553 @colander.deferred
562 @colander.deferred
554 def today_end(node, kw):
563 def today_end(node, kw):
555 return datetime.datetime.utcnow().replace(second=0, microsecond=0,
564 return datetime.datetime.utcnow().replace(second=0, microsecond=0,
556 minute=59, hour=23)
565 minute=59, hour=23)
557
566
558
567
559 @colander.deferred
568 @colander.deferred
560 def old_start(node, kw):
569 def old_start(node, kw):
561 t_delta = datetime.timedelta(days=90)
570 t_delta = datetime.timedelta(days=90)
562 return datetime.datetime.utcnow().replace(second=0, microsecond=0,
571 return datetime.datetime.utcnow().replace(second=0, microsecond=0,
563 minute=0,
572 minute=0,
564 hour=0) - t_delta
573 hour=0) - t_delta
565
574
566
575
567 @colander.deferred
576 @colander.deferred
568 def today_end(node, kw):
577 def today_end(node, kw):
569 return datetime.datetime.utcnow().replace(second=0, microsecond=0,
578 return datetime.datetime.utcnow().replace(second=0, microsecond=0,
570 minute=59, hour=23)
579 minute=59, hour=23)
571
580
572
581
573 class PermissiveDate(colander.DateTime):
582 class PermissiveDate(colander.DateTime):
574 """ Returns null for incorrect date format - also removes tz info"""
583 """ Returns null for incorrect date format - also removes tz info"""
575
584
576 def deserialize(self, node, cstruct):
585 def deserialize(self, node, cstruct):
577 if not cstruct:
586 if not cstruct:
578 return null
587 return null
579
588
580 try:
589 try:
581 result = colander.iso8601.parse_date(
590 result = colander.iso8601.parse_date(
582 cstruct, default_timezone=self.default_tzinfo)
591 cstruct, default_timezone=self.default_tzinfo)
583 except colander.iso8601.ParseError:
592 except colander.iso8601.ParseError:
584 return null
593 return null
585 return result.replace(tzinfo=None)
594 return result.replace(tzinfo=None)
586
595
587
596
588 class LogSearchSchema(colander.MappingSchema):
597 class LogSearchSchema(colander.MappingSchema):
589 def schema_type(self, **kw):
598 def schema_type(self, **kw):
590 return colander.Mapping(unknown='preserve')
599 return colander.Mapping(unknown='preserve')
591
600
592 resource = colander.SchemaNode(StringToAppList(),
601 resource = colander.SchemaNode(StringToAppList(),
593 validator=possible_applications_validator,
602 validator=possible_applications_validator,
594 missing=possible_applications)
603 missing=possible_applications)
595
604
596 message = colander.SchemaNode(colander.Sequence(accept_scalar=True),
605 message = colander.SchemaNode(colander.Sequence(accept_scalar=True),
597 colander.SchemaNode(colander.String()),
606 colander.SchemaNode(colander.String()),
598 missing=None)
607 missing=None)
599 level = colander.SchemaNode(colander.Sequence(accept_scalar=True),
608 level = colander.SchemaNode(colander.Sequence(accept_scalar=True),
600 colander.SchemaNode(colander.String()),
609 colander.SchemaNode(colander.String()),
601 preparer=lowercase_preparer,
610 preparer=lowercase_preparer,
602 missing=None)
611 missing=None)
603 namespace = colander.SchemaNode(colander.Sequence(accept_scalar=True),
612 namespace = colander.SchemaNode(colander.Sequence(accept_scalar=True),
604 colander.SchemaNode(colander.String()),
613 colander.SchemaNode(colander.String()),
605 preparer=lowercase_preparer,
614 preparer=lowercase_preparer,
606 missing=None)
615 missing=None)
607 request_id = colander.SchemaNode(colander.Sequence(accept_scalar=True),
616 request_id = colander.SchemaNode(colander.Sequence(accept_scalar=True),
608 colander.SchemaNode(colander.String()),
617 colander.SchemaNode(colander.String()),
609 preparer=lowercase_preparer,
618 preparer=lowercase_preparer,
610 missing=None)
619 missing=None)
611 start_date = colander.SchemaNode(PermissiveDate(),
620 start_date = colander.SchemaNode(PermissiveDate(),
612 missing=None)
621 missing=None)
613 end_date = colander.SchemaNode(PermissiveDate(),
622 end_date = colander.SchemaNode(PermissiveDate(),
614 missing=None)
623 missing=None)
615 page = colander.SchemaNode(colander.Integer(),
624 page = colander.SchemaNode(colander.Integer(),
616 validator=colander.Range(min=1),
625 validator=colander.Range(min=1),
617 missing=1)
626 missing=1)
618
627
619
628
620 class ReportSearchSchema(colander.MappingSchema):
629 class ReportSearchSchema(colander.MappingSchema):
621 def schema_type(self, **kw):
630 def schema_type(self, **kw):
622 return colander.Mapping(unknown='preserve')
631 return colander.Mapping(unknown='preserve')
623
632
624 resource = colander.SchemaNode(StringToAppList(),
633 resource = colander.SchemaNode(StringToAppList(),
625 validator=possible_applications_validator,
634 validator=possible_applications_validator,
626 missing=possible_applications)
635 missing=possible_applications)
627 request_id = colander.SchemaNode(colander.Sequence(accept_scalar=True),
636 request_id = colander.SchemaNode(colander.Sequence(accept_scalar=True),
628 colander.SchemaNode(colander.String()),
637 colander.SchemaNode(colander.String()),
629 missing=None)
638 missing=None)
630 start_date = colander.SchemaNode(PermissiveDate(),
639 start_date = colander.SchemaNode(PermissiveDate(),
631 missing=None)
640 missing=None)
632 end_date = colander.SchemaNode(PermissiveDate(),
641 end_date = colander.SchemaNode(PermissiveDate(),
633 missing=None)
642 missing=None)
634 page = colander.SchemaNode(colander.Integer(),
643 page = colander.SchemaNode(colander.Integer(),
635 validator=colander.Range(min=1),
644 validator=colander.Range(min=1),
636 missing=1)
645 missing=1)
637
646
638 min_occurences = colander.SchemaNode(
647 min_occurences = colander.SchemaNode(
639 colander.Sequence(accept_scalar=True),
648 colander.Sequence(accept_scalar=True),
640 colander.SchemaNode(colander.Integer()),
649 colander.SchemaNode(colander.Integer()),
641 missing=None)
650 missing=None)
642
651
643 http_status = colander.SchemaNode(colander.Sequence(accept_scalar=True),
652 http_status = colander.SchemaNode(colander.Sequence(accept_scalar=True),
644 colander.SchemaNode(colander.Integer()),
653 colander.SchemaNode(colander.Integer()),
645 missing=None)
654 missing=None)
646 priority = colander.SchemaNode(colander.Sequence(accept_scalar=True),
655 priority = colander.SchemaNode(colander.Sequence(accept_scalar=True),
647 colander.SchemaNode(colander.Integer()),
656 colander.SchemaNode(colander.Integer()),
648 missing=None)
657 missing=None)
649 error = colander.SchemaNode(colander.Sequence(accept_scalar=True),
658 error = colander.SchemaNode(colander.Sequence(accept_scalar=True),
650 colander.SchemaNode(colander.String()),
659 colander.SchemaNode(colander.String()),
651 missing=None)
660 missing=None)
652 url_path = colander.SchemaNode(colander.Sequence(accept_scalar=True),
661 url_path = colander.SchemaNode(colander.Sequence(accept_scalar=True),
653 colander.SchemaNode(colander.String()),
662 colander.SchemaNode(colander.String()),
654 missing=None)
663 missing=None)
655 url_domain = colander.SchemaNode(colander.Sequence(accept_scalar=True),
664 url_domain = colander.SchemaNode(colander.Sequence(accept_scalar=True),
656 colander.SchemaNode(colander.String()),
665 colander.SchemaNode(colander.String()),
657 missing=None)
666 missing=None)
658 report_status = colander.SchemaNode(colander.Sequence(accept_scalar=True),
667 report_status = colander.SchemaNode(colander.Sequence(accept_scalar=True),
659 colander.SchemaNode(colander.String()),
668 colander.SchemaNode(colander.String()),
660 missing=None)
669 missing=None)
661 min_duration = colander.SchemaNode(colander.Sequence(accept_scalar=True),
670 min_duration = colander.SchemaNode(colander.Sequence(accept_scalar=True),
662 colander.SchemaNode(colander.Float()),
671 colander.SchemaNode(colander.Float()),
663 missing=None)
672 missing=None)
664 max_duration = colander.SchemaNode(colander.Sequence(accept_scalar=True),
673 max_duration = colander.SchemaNode(colander.Sequence(accept_scalar=True),
665 colander.SchemaNode(colander.Float()),
674 colander.SchemaNode(colander.Float()),
666 missing=None)
675 missing=None)
667
676
668
677
669 class TagSchema(colander.MappingSchema):
678 class TagSchema(colander.MappingSchema):
670 """
679 """
671 Used in log search
680 Used in log search
672 """
681 """
673 name = colander.SchemaNode(colander.String(),
682 name = colander.SchemaNode(colander.String(),
674 validator=colander.Length(1, 32))
683 validator=colander.Length(1, 32))
675 value = colander.SchemaNode(colander.Sequence(accept_scalar=True),
684 value = colander.SchemaNode(colander.Sequence(accept_scalar=True),
676 colander.SchemaNode(colander.String(),
685 colander.SchemaNode(colander.String(),
677 validator=colander.Length(
686 validator=colander.Length(
678 1, 128)),
687 1, 128)),
679 missing=None)
688 missing=None)
680 op = colander.SchemaNode(colander.String(),
689 op = colander.SchemaNode(colander.String(),
681 validator=colander.Length(1, 128),
690 validator=colander.Length(1, 128),
682 missing=None)
691 missing=None)
683
692
684
693
685 class TagListSchema(colander.SequenceSchema):
694 class TagListSchema(colander.SequenceSchema):
686 tag = TagSchema()
695 tag = TagSchema()
687
696
688
697
689 class RuleFieldType(object):
698 class RuleFieldType(object):
690 """ Validator which succeeds if the value passed to it is one of
699 """ Validator which succeeds if the value passed to it is one of
691 a fixed set of values """
700 a fixed set of values """
692
701
693 def __init__(self, cast_to):
702 def __init__(self, cast_to):
694 self.cast_to = cast_to
703 self.cast_to = cast_to
695
704
696 def __call__(self, node, value):
705 def __call__(self, node, value):
697 try:
706 try:
698 if self.cast_to == 'int':
707 if self.cast_to == 'int':
699 int(value)
708 int(value)
700 elif self.cast_to == 'float':
709 elif self.cast_to == 'float':
701 float(value)
710 float(value)
702 elif self.cast_to == 'unicode':
711 elif self.cast_to == 'unicode':
703 str(value)
712 str(value)
704 except:
713 except:
705 raise colander.Invalid(node,
714 raise colander.Invalid(node,
706 "Can't cast {} to {}".format(
715 "Can't cast {} to {}".format(
707 value, self.cast_to))
716 value, self.cast_to))
708
717
709
718
710 def build_rule_schema(ruleset, check_matrix):
719 def build_rule_schema(ruleset, check_matrix):
711 """
720 """
712 Accepts ruleset and a map of fields/possible operations and builds
721 Accepts ruleset and a map of fields/possible operations and builds
713 validation class
722 validation class
714 """
723 """
715
724
716 schema = colander.SchemaNode(colander.Mapping())
725 schema = colander.SchemaNode(colander.Mapping())
717 schema.add(colander.SchemaNode(colander.String(), name='field'))
726 schema.add(colander.SchemaNode(colander.String(), name='field'))
718
727
719 if ruleset['field'] in ['__AND__', '__OR__', '__NOT__']:
728 if ruleset['field'] in ['__AND__', '__OR__', '__NOT__']:
720 subrules = colander.SchemaNode(colander.Tuple(), name='rules')
729 subrules = colander.SchemaNode(colander.Tuple(), name='rules')
721 for rule in ruleset['rules']:
730 for rule in ruleset['rules']:
722 subrules.add(build_rule_schema(rule, check_matrix))
731 subrules.add(build_rule_schema(rule, check_matrix))
723 schema.add(subrules)
732 schema.add(subrules)
724 else:
733 else:
725 op_choices = check_matrix[ruleset['field']]['ops']
734 op_choices = check_matrix[ruleset['field']]['ops']
726 cast_to = check_matrix[ruleset['field']]['type']
735 cast_to = check_matrix[ruleset['field']]['type']
727 schema.add(colander.SchemaNode(colander.String(),
736 schema.add(colander.SchemaNode(colander.String(),
728 validator=colander.OneOf(op_choices),
737 validator=colander.OneOf(op_choices),
729 name='op'))
738 name='op'))
730
739
731 schema.add(colander.SchemaNode(colander.String(),
740 schema.add(colander.SchemaNode(colander.String(),
732 name='value',
741 name='value',
733 validator=RuleFieldType(cast_to)))
742 validator=RuleFieldType(cast_to)))
734 return schema
743 return schema
735
744
736
745
737 class ConfigTypeSchema(colander.MappingSchema):
746 class ConfigTypeSchema(colander.MappingSchema):
738 type = colander.SchemaNode(colander.String(), missing=None)
747 type = colander.SchemaNode(colander.String(), missing=None)
739 config = colander.SchemaNode(UnknownType(), missing=None)
748 config = colander.SchemaNode(UnknownType(), missing=None)
740
749
741
750
742 class MappingListSchema(colander.SequenceSchema):
751 class MappingListSchema(colander.SequenceSchema):
743 config = colander.SchemaNode(UnknownType())
752 config = colander.SchemaNode(UnknownType())
@@ -1,422 +1,427 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 base64
22 import base64
23 import io
23 import io
24 import datetime
24 import datetime
25 import json
25 import json
26 import logging
26 import logging
27 import urllib.request, urllib.parse, urllib.error
27 import urllib.request, urllib.parse, urllib.error
28 import zlib
28 import zlib
29
29
30 from gzip import GzipFile
30 from gzip import GzipFile
31 from pyramid.view import view_config
31 from pyramid.view import view_config
32 from pyramid.httpexceptions import HTTPBadRequest
32 from pyramid.httpexceptions import HTTPBadRequest
33
33
34 import appenlight.celery.tasks as tasks
34 import appenlight.celery.tasks as tasks
35 from appenlight.lib.api import rate_limiting, check_cors
35 from appenlight.lib.api import rate_limiting, check_cors
36 from appenlight.lib.enums import ParsedSentryEventType
36 from appenlight.lib.enums import ParsedSentryEventType
37 from appenlight.lib.utils import parse_proto
37 from appenlight.lib.utils import parse_proto
38 from appenlight.lib.utils.airbrake import parse_airbrake_xml
38 from appenlight.lib.utils.airbrake import parse_airbrake_xml
39 from appenlight.lib.utils.date_utils import convert_date
39 from appenlight.lib.utils.date_utils import convert_date
40 from appenlight.lib.utils.sentry import parse_sentry_event
40 from appenlight.lib.utils.sentry import parse_sentry_event
41 from appenlight.lib.request import JSONException
41 from appenlight.lib.request import JSONException
42 from appenlight.validators import (LogListSchema,
42 from appenlight.validators import (LogListSchema,
43 MetricsListSchema,
43 MetricsListSchema,
44 GeneralMetricsListSchema,
44 GeneralMetricsListSchema,
45 GeneralMetricSchema,
45 GeneralMetricSchema,
46 LogListPermanentSchema,
46 LogListPermanentSchema,
47 ReportListSchema_0_5,
47 ReportListSchema_0_5,
48 LogSchema,
48 LogSchema,
49 LogSchemaPermanent,
49 LogSchemaPermanent,
50 ReportSchema_0_5)
50 ReportSchema_0_5)
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 @view_config(route_name='api_logs', renderer='string', permission='create',
55 @view_config(route_name='api_logs', renderer='string', permission='create',
56 require_csrf=False)
56 require_csrf=False)
57 @view_config(route_name='api_log', renderer='string', permission='create',
57 @view_config(route_name='api_log', renderer='string', permission='create',
58 require_csrf=False)
58 require_csrf=False)
59 def logs_create(request):
59 def logs_create(request):
60 """
60 """
61 Endpoint for log aggregation
61 Endpoint for log aggregation
62 """
62 """
63 application = request.context.resource
63 application = request.context.resource
64 if request.method.upper() == 'OPTIONS':
64 if request.method.upper() == 'OPTIONS':
65 return check_cors(request, application)
65 return check_cors(request, application)
66 else:
66 else:
67 check_cors(request, application, should_return=False)
67 check_cors(request, application, should_return=False)
68
68
69 params = dict(request.params.copy())
69 params = dict(request.params.copy())
70 proto_version = parse_proto(params.get('protocol_version', ''))
70 proto_version = parse_proto(params.get('protocol_version', ''))
71 payload = request.unsafe_json_body
71 payload = request.unsafe_json_body
72 sequence_accepted = request.matched_route.name == 'api_logs'
72 sequence_accepted = request.matched_route.name == 'api_logs'
73
73
74 if sequence_accepted:
74 if sequence_accepted:
75 if application.allow_permanent_storage:
75 if application.allow_permanent_storage:
76 schema = LogListPermanentSchema().bind(
76 schema = LogListPermanentSchema().bind(
77 utcnow=datetime.datetime.utcnow())
77 utcnow=datetime.datetime.utcnow())
78 else:
78 else:
79 schema = LogListSchema().bind(
79 schema = LogListSchema().bind(
80 utcnow=datetime.datetime.utcnow())
80 utcnow=datetime.datetime.utcnow())
81 else:
81 else:
82 if application.allow_permanent_storage:
82 if application.allow_permanent_storage:
83 schema = LogSchemaPermanent().bind(
83 schema = LogSchemaPermanent().bind(
84 utcnow=datetime.datetime.utcnow())
84 utcnow=datetime.datetime.utcnow())
85 else:
85 else:
86 schema = LogSchema().bind(
86 schema = LogSchema().bind(
87 utcnow=datetime.datetime.utcnow())
87 utcnow=datetime.datetime.utcnow())
88
88
89 deserialized_logs = schema.deserialize(payload)
89 deserialized_logs = schema.deserialize(payload)
90 if sequence_accepted is False:
90 if sequence_accepted is False:
91 deserialized_logs = [deserialized_logs]
91 deserialized_logs = [deserialized_logs]
92
92
93 rate_limiting(request, application, 'per_application_logs_rate_limit',
93 rate_limiting(request, application, 'per_application_logs_rate_limit',
94 len(deserialized_logs))
94 len(deserialized_logs))
95
95
96 # pprint.pprint(deserialized_logs)
96 # pprint.pprint(deserialized_logs)
97
97
98 # we need to split those out so we can process the pkey ones one by one
98 # we need to split those out so we can process the pkey ones one by one
99 non_pkey_logs = [log_dict for log_dict in deserialized_logs
99 non_pkey_logs = [log_dict for log_dict in deserialized_logs
100 if not log_dict['primary_key']]
100 if not log_dict['primary_key']]
101 pkey_dict = {}
101 pkey_dict = {}
102 # try to process the logs as best as we can and group together to reduce
102 # try to process the logs as best as we can and group together to reduce
103 # the amount of
103 # the amount of
104 for log_dict in deserialized_logs:
104 for log_dict in deserialized_logs:
105 if log_dict['primary_key']:
105 if log_dict['primary_key']:
106 key = (log_dict['primary_key'], log_dict['namespace'],)
106 key = (log_dict['primary_key'], log_dict['namespace'],)
107 if not key in pkey_dict:
107 if not key in pkey_dict:
108 pkey_dict[key] = []
108 pkey_dict[key] = []
109 pkey_dict[key].append(log_dict)
109 pkey_dict[key].append(log_dict)
110
110
111 if non_pkey_logs:
111 if non_pkey_logs:
112 log.debug('%s non-pkey logs received: %s' % (application,
112 log.debug('%s non-pkey logs received: %s' % (application,
113 len(non_pkey_logs)))
113 len(non_pkey_logs)))
114 tasks.add_logs.delay(application.resource_id, params, non_pkey_logs)
114 tasks.add_logs.delay(application.resource_id, params, non_pkey_logs)
115 if pkey_dict:
115 if pkey_dict:
116 logs_to_insert = []
116 logs_to_insert = []
117 for primary_key_tuple, payload in pkey_dict.items():
117 for primary_key_tuple, payload in pkey_dict.items():
118 sorted_logs = sorted(payload, key=lambda x: x['date'])
118 sorted_logs = sorted(payload, key=lambda x: x['date'])
119 logs_to_insert.append(sorted_logs[-1])
119 logs_to_insert.append(sorted_logs[-1])
120 log.debug('%s pkey logs received: %s' % (application,
120 log.debug('%s pkey logs received: %s' % (application,
121 len(logs_to_insert)))
121 len(logs_to_insert)))
122 tasks.add_logs.delay(application.resource_id, params, logs_to_insert)
122 tasks.add_logs.delay(application.resource_id, params, logs_to_insert)
123
123
124 log.info('LOG call %s %s client:%s' % (
124 log.info('LOG call %s %s client:%s' % (
125 application, proto_version, request.headers.get('user_agent')))
125 application, proto_version, request.headers.get('user_agent')))
126 return 'OK: Logs accepted'
126 return 'OK: Logs accepted'
127
127
128
128
129 @view_config(route_name='api_request_stats', renderer='string',
129 @view_config(route_name='api_request_stats', renderer='string',
130 permission='create', require_csrf=False)
130 permission='create', require_csrf=False)
131 @view_config(route_name='api_metrics', renderer='string',
131 @view_config(route_name='api_metrics', renderer='string',
132 permission='create', require_csrf=False)
132 permission='create', require_csrf=False)
133 def request_metrics_create(request):
133 def request_metrics_create(request):
134 """
134 """
135 Endpoint for performance metrics, aggregates view performance stats
135 Endpoint for performance metrics, aggregates view performance stats
136 and converts them to general metric row
136 and converts them to general metric row
137 """
137 """
138 application = request.context.resource
138 application = request.context.resource
139 if request.method.upper() == 'OPTIONS':
139 if request.method.upper() == 'OPTIONS':
140 return check_cors(request, application)
140 return check_cors(request, application)
141 else:
141 else:
142 check_cors(request, application, should_return=False)
142 check_cors(request, application, should_return=False)
143
143
144 params = dict(request.params.copy())
144 params = dict(request.params.copy())
145 proto_version = parse_proto(params.get('protocol_version', ''))
145 proto_version = parse_proto(params.get('protocol_version', ''))
146
146
147 payload = request.unsafe_json_body
147 payload = request.unsafe_json_body
148 schema = MetricsListSchema()
148 schema = MetricsListSchema()
149 dataset = schema.deserialize(payload)
149 dataset = schema.deserialize(payload)
150
150
151 rate_limiting(request, application, 'per_application_metrics_rate_limit',
151 rate_limiting(request, application, 'per_application_metrics_rate_limit',
152 len(dataset))
152 len(dataset))
153
153
154 # looping report data
154 # looping report data
155 metrics = {}
155 metrics = {}
156 for metric in dataset:
156 for metric in dataset:
157 server_name = metric.get('server', '').lower() or 'unknown'
157 server_name = metric.get('server', '').lower() or 'unknown'
158 start_interval = convert_date(metric['timestamp'])
158 start_interval = convert_date(metric['timestamp'])
159 start_interval = start_interval.replace(second=0, microsecond=0)
159 start_interval = start_interval.replace(second=0, microsecond=0)
160
160
161 for view_name, view_metrics in metric['metrics']:
161 for view_name, view_metrics in metric['metrics']:
162 key = '%s%s%s' % (metric['server'], start_interval, view_name)
162 key = '%s%s%s' % (metric['server'], start_interval, view_name)
163 if start_interval not in metrics:
163 if start_interval not in metrics:
164 metrics[key] = {"requests": 0, "main": 0, "sql": 0,
164 metrics[key] = {"requests": 0, "main": 0, "sql": 0,
165 "nosql": 0, "remote": 0, "tmpl": 0,
165 "nosql": 0, "remote": 0, "tmpl": 0,
166 "custom": 0, 'sql_calls': 0,
166 "custom": 0, 'sql_calls': 0,
167 'nosql_calls': 0,
167 'nosql_calls': 0,
168 'remote_calls': 0, 'tmpl_calls': 0,
168 'remote_calls': 0, 'tmpl_calls': 0,
169 'custom_calls': 0,
169 'custom_calls': 0,
170 "start_interval": start_interval,
170 "start_interval": start_interval,
171 "server_name": server_name,
171 "server_name": server_name,
172 "view_name": view_name
172 "view_name": view_name
173 }
173 }
174 metrics[key]["requests"] += int(view_metrics['requests'])
174 metrics[key]["requests"] += int(view_metrics['requests'])
175 metrics[key]["main"] += round(view_metrics['main'], 5)
175 metrics[key]["main"] += round(view_metrics['main'], 5)
176 metrics[key]["sql"] += round(view_metrics['sql'], 5)
176 metrics[key]["sql"] += round(view_metrics['sql'], 5)
177 metrics[key]["nosql"] += round(view_metrics['nosql'], 5)
177 metrics[key]["nosql"] += round(view_metrics['nosql'], 5)
178 metrics[key]["remote"] += round(view_metrics['remote'], 5)
178 metrics[key]["remote"] += round(view_metrics['remote'], 5)
179 metrics[key]["tmpl"] += round(view_metrics['tmpl'], 5)
179 metrics[key]["tmpl"] += round(view_metrics['tmpl'], 5)
180 metrics[key]["custom"] += round(view_metrics.get('custom', 0.0),
180 metrics[key]["custom"] += round(view_metrics.get('custom', 0.0),
181 5)
181 5)
182 metrics[key]["sql_calls"] += int(
182 metrics[key]["sql_calls"] += int(
183 view_metrics.get('sql_calls', 0))
183 view_metrics.get('sql_calls', 0))
184 metrics[key]["nosql_calls"] += int(
184 metrics[key]["nosql_calls"] += int(
185 view_metrics.get('nosql_calls', 0))
185 view_metrics.get('nosql_calls', 0))
186 metrics[key]["remote_calls"] += int(
186 metrics[key]["remote_calls"] += int(
187 view_metrics.get('remote_calls', 0))
187 view_metrics.get('remote_calls', 0))
188 metrics[key]["tmpl_calls"] += int(
188 metrics[key]["tmpl_calls"] += int(
189 view_metrics.get('tmpl_calls', 0))
189 view_metrics.get('tmpl_calls', 0))
190 metrics[key]["custom_calls"] += int(
190 metrics[key]["custom_calls"] += int(
191 view_metrics.get('custom_calls', 0))
191 view_metrics.get('custom_calls', 0))
192
192
193 if not metrics[key]["requests"]:
193 if not metrics[key]["requests"]:
194 # fix this here because validator can't
194 # fix this here because validator can't
195 metrics[key]["requests"] = 1
195 metrics[key]["requests"] = 1
196 # metrics dict is being built to minimize
196 # metrics dict is being built to minimize
197 # the amount of queries used
197 # the amount of queries used
198 # in case we get multiple rows from same minute
198 # in case we get multiple rows from same minute
199
199
200 normalized_metrics = []
200 normalized_metrics = []
201 for metric in metrics.values():
201 for metric in metrics.values():
202 new_metric = {
202 new_metric = {
203 'namespace': 'appenlight.request_metric',
203 'namespace': 'appenlight.request_metric',
204 'timestamp': metric.pop('start_interval'),
204 'timestamp': metric.pop('start_interval'),
205 'server_name': metric['server_name'],
205 'server_name': metric['server_name'],
206 'tags': list(metric.items())
206 'tags': list(metric.items())
207 }
207 }
208 normalized_metrics.append(new_metric)
208 normalized_metrics.append(new_metric)
209
209
210 tasks.add_metrics.delay(application.resource_id, params,
210 tasks.add_metrics.delay(application.resource_id, params,
211 normalized_metrics, proto_version)
211 normalized_metrics, proto_version)
212
212
213 log.info('REQUEST METRICS call {} {} client:{}'.format(
213 log.info('REQUEST METRICS call {} {} client:{}'.format(
214 application.resource_name, proto_version,
214 application.resource_name, proto_version,
215 request.headers.get('user_agent')))
215 request.headers.get('user_agent')))
216 return 'OK: request metrics accepted'
216 return 'OK: request metrics accepted'
217
217
218
218
219 @view_config(route_name='api_general_metrics', renderer='string',
219 @view_config(route_name='api_general_metrics', renderer='string',
220 permission='create', require_csrf=False)
220 permission='create', require_csrf=False)
221 @view_config(route_name='api_general_metric', renderer='string',
221 @view_config(route_name='api_general_metric', renderer='string',
222 permission='create', require_csrf=False)
222 permission='create', require_csrf=False)
223 def general_metrics_create(request):
223 def general_metrics_create(request):
224 """
224 """
225 Endpoint for general metrics aggregation
225 Endpoint for general metrics aggregation
226 """
226 """
227 application = request.context.resource
227 application = request.context.resource
228 if request.method.upper() == 'OPTIONS':
228 if request.method.upper() == 'OPTIONS':
229 return check_cors(request, application)
229 return check_cors(request, application)
230 else:
230 else:
231 check_cors(request, application, should_return=False)
231 check_cors(request, application, should_return=False)
232
232
233 params = dict(request.params.copy())
233 params = dict(request.params.copy())
234 proto_version = parse_proto(params.get('protocol_version', ''))
234 proto_version = parse_proto(params.get('protocol_version', ''))
235 payload = request.unsafe_json_body
235 payload = request.unsafe_json_body
236 sequence_accepted = request.matched_route.name == 'api_general_metrics'
236 sequence_accepted = request.matched_route.name == 'api_general_metrics'
237 if sequence_accepted:
237 if sequence_accepted:
238 schema = GeneralMetricsListSchema().bind(
238 schema = GeneralMetricsListSchema().bind(
239 utcnow=datetime.datetime.utcnow())
239 utcnow=datetime.datetime.utcnow())
240 else:
240 else:
241 schema = GeneralMetricSchema().bind(utcnow=datetime.datetime.utcnow())
241 schema = GeneralMetricSchema().bind(utcnow=datetime.datetime.utcnow())
242
242
243 deserialized_metrics = schema.deserialize(payload)
243 deserialized_metrics = schema.deserialize(payload)
244 if sequence_accepted is False:
244 if sequence_accepted is False:
245 deserialized_metrics = [deserialized_metrics]
245 deserialized_metrics = [deserialized_metrics]
246
246
247 rate_limiting(request, application, 'per_application_metrics_rate_limit',
247 rate_limiting(request, application, 'per_application_metrics_rate_limit',
248 len(deserialized_metrics))
248 len(deserialized_metrics))
249
249
250 tasks.add_metrics.delay(application.resource_id, params,
250 tasks.add_metrics.delay(application.resource_id, params,
251 deserialized_metrics, proto_version)
251 deserialized_metrics, proto_version)
252
252
253 log.info('METRICS call {} {} client:{}'.format(
253 log.info('METRICS call {} {} client:{}'.format(
254 application.resource_name, proto_version,
254 application.resource_name, proto_version,
255 request.headers.get('user_agent')))
255 request.headers.get('user_agent')))
256 return 'OK: Metrics accepted'
256 return 'OK: Metrics accepted'
257
257
258
258
259 @view_config(route_name='api_reports', renderer='string', permission='create',
259 @view_config(route_name='api_reports', renderer='string', permission='create',
260 require_csrf=False)
260 require_csrf=False)
261 @view_config(route_name='api_slow_reports', renderer='string',
261 @view_config(route_name='api_slow_reports', renderer='string',
262 permission='create', require_csrf=False)
262 permission='create', require_csrf=False)
263 @view_config(route_name='api_report', renderer='string', permission='create',
263 @view_config(route_name='api_report', renderer='string', permission='create',
264 require_csrf=False)
264 require_csrf=False)
265 def reports_create(request):
265 def reports_create(request):
266 """
266 """
267 Endpoint for exception and slowness reports
267 Endpoint for exception and slowness reports
268 """
268 """
269 # route_url('reports')
269 # route_url('reports')
270 application = request.context.resource
270 application = request.context.resource
271 if request.method.upper() == 'OPTIONS':
271 if request.method.upper() == 'OPTIONS':
272 return check_cors(request, application)
272 return check_cors(request, application)
273 else:
273 else:
274 check_cors(request, application, should_return=False)
274 check_cors(request, application, should_return=False)
275 params = dict(request.params.copy())
275 params = dict(request.params.copy())
276 proto_version = parse_proto(params.get('protocol_version', ''))
276 proto_version = parse_proto(params.get('protocol_version', ''))
277 payload = request.unsafe_json_body
277 payload = request.unsafe_json_body
278 sequence_accepted = request.matched_route.name == 'api_reports'
278 sequence_accepted = request.matched_route.name == 'api_reports'
279
279
280 if sequence_accepted:
280 if sequence_accepted:
281 schema = ReportListSchema_0_5().bind(
281 schema = ReportListSchema_0_5().bind(
282 utcnow=datetime.datetime.utcnow())
282 utcnow=datetime.datetime.utcnow())
283 else:
283 else:
284 schema = ReportSchema_0_5().bind(
284 schema = ReportSchema_0_5().bind(
285 utcnow=datetime.datetime.utcnow())
285 utcnow=datetime.datetime.utcnow())
286
286
287 deserialized_reports = schema.deserialize(payload)
287 deserialized_reports = schema.deserialize(payload)
288 if sequence_accepted is False:
288 if sequence_accepted is False:
289 deserialized_reports = [deserialized_reports]
289 deserialized_reports = [deserialized_reports]
290 if deserialized_reports:
290 if deserialized_reports:
291 rate_limiting(request, application,
291 rate_limiting(request, application,
292 'per_application_reports_rate_limit',
292 'per_application_reports_rate_limit',
293 len(deserialized_reports))
293 len(deserialized_reports))
294
294
295 # pprint.pprint(deserialized_reports)
295 # pprint.pprint(deserialized_reports)
296 tasks.add_reports.delay(application.resource_id, params,
296 tasks.add_reports.delay(application.resource_id, params,
297 deserialized_reports)
297 deserialized_reports)
298 log.info('REPORT call %s, %s client:%s' % (
298 log.info('REPORT call %s, %s client:%s' % (
299 application,
299 application,
300 proto_version,
300 proto_version,
301 request.headers.get('user_agent'))
301 request.headers.get('user_agent'))
302 )
302 )
303 return 'OK: Reports accepted'
303 return 'OK: Reports accepted'
304
304
305
305
306 @view_config(route_name='api_airbrake', renderer='string', permission='create',
306 @view_config(route_name='api_airbrake', renderer='string', permission='create',
307 require_csrf=False)
307 require_csrf=False)
308 def airbrake_xml_compat(request):
308 def airbrake_xml_compat(request):
309 """
309 """
310 Airbrake compatible endpoint for XML reports
310 Airbrake compatible endpoint for XML reports
311 """
311 """
312 application = request.context.resource
312 application = request.context.resource
313 if request.method.upper() == 'OPTIONS':
313 if request.method.upper() == 'OPTIONS':
314 return check_cors(request, application)
314 return check_cors(request, application)
315 else:
315 else:
316 check_cors(request, application, should_return=False)
316 check_cors(request, application, should_return=False)
317
317
318 params = request.params.copy()
318 params = request.params.copy()
319
319
320 error_dict = parse_airbrake_xml(request)
320 error_dict = parse_airbrake_xml(request)
321 schema = ReportListSchema_0_5().bind(utcnow=datetime.datetime.utcnow())
321 schema = ReportListSchema_0_5().bind(utcnow=datetime.datetime.utcnow())
322 deserialized_reports = schema.deserialize([error_dict])
322 deserialized_reports = schema.deserialize([error_dict])
323 rate_limiting(request, application, 'per_application_reports_rate_limit',
323 rate_limiting(request, application, 'per_application_reports_rate_limit',
324 len(deserialized_reports))
324 len(deserialized_reports))
325
325
326 tasks.add_reports.delay(application.resource_id, params,
326 tasks.add_reports.delay(application.resource_id, params,
327 deserialized_reports)
327 deserialized_reports)
328 log.info('%s AIRBRAKE call for application %s, api_ver:%s client:%s' % (
328 log.info('%s AIRBRAKE call for application %s, api_ver:%s client:%s' % (
329 500, application.resource_name,
329 500, application.resource_name,
330 request.params.get('protocol_version', 'unknown'),
330 request.params.get('protocol_version', 'unknown'),
331 request.headers.get('user_agent'))
331 request.headers.get('user_agent'))
332 )
332 )
333 return '<notice><id>no-id</id><url>%s</url></notice>' % \
333 return '<notice><id>no-id</id><url>%s</url></notice>' % \
334 request.registry.settings['mailing.app_url']
334 request.registry.settings['mailing.app_url']
335
335
336
336
337 def decompress_gzip(data):
337 def decompress_gzip(data):
338 try:
338 try:
339 fp = io.StringIO(data)
339 fp = io.StringIO(data)
340 with GzipFile(fileobj=fp) as f:
340 with GzipFile(fileobj=fp) as f:
341 return f.read()
341 return f.read()
342 except Exception as exc:
342 except Exception as exc:
343 raise
343 raise
344 log.error(exc)
344 log.error(exc)
345 raise HTTPBadRequest()
345 raise HTTPBadRequest()
346
346
347
347
348 def decompress_zlib(data):
348 def decompress_zlib(data):
349 try:
349 try:
350 return zlib.decompress(data)
350 return zlib.decompress(data)
351 except Exception as exc:
351 except Exception as exc:
352 raise
352 raise
353 log.error(exc)
353 log.error(exc)
354 raise HTTPBadRequest()
354 raise HTTPBadRequest()
355
355
356
356
357 def decode_b64(data):
357 def decode_b64(data):
358 try:
358 try:
359 return base64.b64decode(data)
359 return base64.b64decode(data)
360 except Exception as exc:
360 except Exception as exc:
361 raise
361 raise
362 log.error(exc)
362 log.error(exc)
363 raise HTTPBadRequest()
363 raise HTTPBadRequest()
364
364
365
365
366 @view_config(route_name='api_sentry', renderer='string', permission='create',
366 @view_config(route_name='api_sentry', renderer='string', permission='create',
367 require_csrf=False)
367 require_csrf=False)
368 @view_config(route_name='api_sentry_slash', renderer='string',
368 @view_config(route_name='api_sentry_slash', renderer='string',
369 permission='create', require_csrf=False)
369 permission='create', require_csrf=False)
370 def sentry_compat(request):
370 def sentry_compat(request):
371 """
371 """
372 Sentry compatible endpoint
372 Sentry compatible endpoint
373 """
373 """
374 application = request.context.resource
374 application = request.context.resource
375 if request.method.upper() == 'OPTIONS':
375 if request.method.upper() == 'OPTIONS':
376 return check_cors(request, application)
376 return check_cors(request, application)
377 else:
377 else:
378 check_cors(request, application, should_return=False)
378 check_cors(request, application, should_return=False)
379
379
380 # handle various report encoding
380 # handle various report encoding
381 content_encoding = request.headers.get('Content-Encoding')
381 content_encoding = request.headers.get('Content-Encoding')
382 content_type = request.headers.get('Content-Type')
382 content_type = request.headers.get('Content-Type')
383 if content_encoding == 'gzip':
383 if content_encoding == 'gzip':
384 body = decompress_gzip(request.body)
384 body = decompress_gzip(request.body)
385 elif content_encoding == 'deflate':
385 elif content_encoding == 'deflate':
386 body = decompress_zlib(request.body)
386 body = decompress_zlib(request.body)
387 else:
387 else:
388 body = request.body
388 body = request.body
389 # attempt to fix string before decoding for stupid clients
389 # attempt to fix string before decoding for stupid clients
390 if content_type == 'application/x-www-form-urlencoded':
390 if content_type == 'application/x-www-form-urlencoded':
391 body = urllib.parse.unquote(body.decode('utf8'))
391 body = urllib.parse.unquote(body.decode('utf8'))
392 check_char = '{' if isinstance(body, str) else b'{'
392 check_char = '{' if isinstance(body, str) else b'{'
393 if not body.startswith(check_char):
393 if not body.startswith(check_char):
394 try:
394 try:
395 body = decode_b64(body)
395 body = decode_b64(body)
396 body = decompress_zlib(body)
396 body = decompress_zlib(body)
397 except Exception as exc:
397 except Exception as exc:
398 log.info(exc)
398 log.info(exc)
399
399
400 try:
400 try:
401 json_body = json.loads(body.decode('utf8'))
401 json_body = json.loads(body.decode('utf8'))
402 except ValueError:
402 except ValueError:
403 raise JSONException("Incorrect JSON")
403 raise JSONException("Incorrect JSON")
404
404
405 event, event_type = parse_sentry_event(json_body)
405 event, event_type = parse_sentry_event(json_body)
406
406
407 if event_type == ParsedSentryEventType.LOG:
407 if event_type == ParsedSentryEventType.LOG:
408 schema = LogSchema().bind(utcnow=datetime.datetime.utcnow())
408 if application.allow_permanent_storage:
409 schema = LogSchemaPermanent().bind(
410 utcnow=datetime.datetime.utcnow())
411 else:
412 schema = LogSchema().bind(
413 utcnow=datetime.datetime.utcnow())
409 deserialized_logs = schema.deserialize(event)
414 deserialized_logs = schema.deserialize(event)
410 non_pkey_logs = [deserialized_logs]
415 non_pkey_logs = [deserialized_logs]
411 log.debug('%s non-pkey logs received: %s' % (application,
416 log.debug('%s non-pkey logs received: %s' % (application,
412 len(non_pkey_logs)))
417 len(non_pkey_logs)))
413 tasks.add_logs.delay(application.resource_id, {}, non_pkey_logs)
418 tasks.add_logs.delay(application.resource_id, {}, non_pkey_logs)
414 if event_type == ParsedSentryEventType.ERROR_REPORT:
419 if event_type == ParsedSentryEventType.ERROR_REPORT:
415 schema = ReportSchema_0_5().bind(utcnow=datetime.datetime.utcnow())
420 schema = ReportSchema_0_5().bind(utcnow=datetime.datetime.utcnow())
416 deserialized_reports = [schema.deserialize(event)]
421 deserialized_reports = [schema.deserialize(event)]
417 rate_limiting(request, application,
422 rate_limiting(request, application,
418 'per_application_reports_rate_limit',
423 'per_application_reports_rate_limit',
419 len(deserialized_reports))
424 len(deserialized_reports))
420 tasks.add_reports.delay(application.resource_id, {},
425 tasks.add_reports.delay(application.resource_id, {},
421 deserialized_reports)
426 deserialized_reports)
422 return 'OK: Events accepted'
427 return 'OK: Events accepted'
General Comments 0
You need to be logged in to leave comments. Login now