##// END OF EJS Templates
metrics: allow indexing items with dates in past if permanent storage is enabled
ergo -
Show More
@@ -1,752 +1,765 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # AppEnlight Enterprise Edition, including its added features, Support
19 19 # services, and proprietary license terms, please see
20 20 # https://rhodecode.com/licenses/
21 21
22 22 import datetime
23 23
24 24 import colander
25 25 from colander import null
26 26
27 27 # those keywords are here so we can distingush between searching for tags and
28 28 # normal properties of reports/logs
29 29 accepted_search_params = ['resource',
30 30 'request_id',
31 31 'start_date',
32 32 'end_date',
33 33 'page',
34 34 'min_occurences',
35 35 'http_status',
36 36 'priority',
37 37 'error',
38 38 'url_path',
39 39 'url_domain',
40 40 'report_status',
41 41 'min_duration',
42 42 'max_duration',
43 43 'message',
44 44 'level',
45 45 'namespace']
46 46
47 47
48 48 @colander.deferred
49 49 def deferred_utcnow(node, kw):
50 50 return kw['utcnow']
51 51
52 52
53 53 def lowercase_preparer(input_data):
54 54 """
55 55 Transforms a list of string entries to lowercase
56 56 Used in search query validation
57 57 """
58 58 if not input_data:
59 59 return input_data
60 60 return [x.lower() for x in input_data]
61 61
62 62
63 63 def shortener_factory(cutoff_size=32):
64 64 """
65 65 Limits the input data to specific character count
66 66 :arg cutoff_cutoff_size How much characters to store
67 67
68 68 """
69 69
70 70 def shortener(input_data):
71 71 if not input_data:
72 72 return input_data
73 73 else:
74 74 if isinstance(input_data, str):
75 75 return input_data[:cutoff_size]
76 76 else:
77 77 return input_data
78 78
79 79 return shortener
80 80
81 81
82 82 def cast_to_unicode_or_null(value):
83 83 if value is not colander.null:
84 84 return str(value)
85 85 return None
86 86
87 87
88 88 class NonTZDate(colander.DateTime):
89 89 """ Returns null for incorrect date format - also removes tz info"""
90 90
91 91 def deserialize(self, node, cstruct):
92 92 # disabled for now
93 93 # if cstruct and isinstance(cstruct, str):
94 94 # if ':' not in cstruct:
95 95 # cstruct += ':0.0'
96 96 # if '.' not in cstruct:
97 97 # cstruct += '.0'
98 98 value = super(NonTZDate, self).deserialize(node, cstruct)
99 99 if value:
100 100 return value.replace(tzinfo=None)
101 101 return value
102 102
103 103
104 104 class UnknownType(object):
105 105 """
106 106 Universal type that will accept a deserialized JSON object and store it unaltered
107 107 """
108 108
109 109 def serialize(self, node, appstruct):
110 110 if appstruct is null:
111 111 return null
112 112 return appstruct
113 113
114 114 def deserialize(self, node, cstruct):
115 115 if cstruct is null:
116 116 return null
117 117 return cstruct
118 118
119 119 def cstruct_children(self):
120 120 return []
121 121
122 122
123 123 # SLOW REPORT SCHEMA
124 124
125 125 def rewrite_type(input_data):
126 126 """
127 127 Fix for legacy appenlight clients
128 128 """
129 129 if input_data == 'remote_call':
130 130 return 'remote'
131 131 return input_data
132 132
133 133
134 134 class ExtraTupleSchema(colander.TupleSchema):
135 135 name = colander.SchemaNode(colander.String(),
136 136 validator=colander.Length(1, 64))
137 137 value = colander.SchemaNode(UnknownType(),
138 138 preparer=shortener_factory(512),
139 139 missing=None)
140 140
141 141
142 142 class ExtraSchemaList(colander.SequenceSchema):
143 143 tag = ExtraTupleSchema()
144 144 missing = None
145 145
146 146
147 147 class TagsTupleSchema(colander.TupleSchema):
148 148 name = colander.SchemaNode(colander.String(),
149 149 validator=colander.Length(1, 128))
150 150 value = colander.SchemaNode(UnknownType(),
151 151 preparer=shortener_factory(128),
152 152 missing=None)
153 153
154 154
155 155 class TagSchemaList(colander.SequenceSchema):
156 156 tag = TagsTupleSchema()
157 157 missing = None
158 158
159 159
160 160 class NumericTagsTupleSchema(colander.TupleSchema):
161 161 name = colander.SchemaNode(colander.String(),
162 162 validator=colander.Length(1, 128))
163 163 value = colander.SchemaNode(colander.Float(), missing=0)
164 164
165 165
166 166 class NumericTagSchemaList(colander.SequenceSchema):
167 167 tag = NumericTagsTupleSchema()
168 168 missing = None
169 169
170 170
171 171 class SlowCallSchema(colander.MappingSchema):
172 172 """
173 173 Validates slow call format in slow call list
174 174 """
175 175 start = colander.SchemaNode(NonTZDate())
176 176 end = colander.SchemaNode(NonTZDate())
177 177 statement = colander.SchemaNode(colander.String(), missing='')
178 178 parameters = colander.SchemaNode(UnknownType(), missing=None)
179 179 type = colander.SchemaNode(
180 180 colander.String(),
181 181 preparer=rewrite_type,
182 182 validator=colander.OneOf(
183 183 ['tmpl', 'sql', 'nosql', 'remote', 'unknown', 'custom']),
184 184 missing='unknown')
185 185 subtype = colander.SchemaNode(colander.String(),
186 186 validator=colander.Length(1, 16),
187 187 missing='unknown')
188 188 location = colander.SchemaNode(colander.String(),
189 189 validator=colander.Length(1, 255),
190 190 missing='')
191 191
192 192
193 193 def limited_date(node, value):
194 194 """ checks to make sure that the value is not older/newer than 2h """
195 195 past_hours = 72
196 196 future_hours = 2
197 197 min_time = datetime.datetime.utcnow() - datetime.timedelta(
198 198 hours=past_hours)
199 199 max_time = datetime.datetime.utcnow() + datetime.timedelta(
200 200 hours=future_hours)
201 201 if min_time > value:
202 202 msg = '%r is older from current UTC time by ' + str(past_hours)
203 203 msg += ' hours. Ask administrator to enable permanent logging for ' \
204 204 'your application to store logs with dates in past.'
205 205 raise colander.Invalid(node, msg % value)
206 206 if max_time < value:
207 207 msg = '%r is newer from current UTC time by ' + str(future_hours)
208 208 msg += ' hours. Ask administrator to enable permanent logging for ' \
209 209 'your application to store logs with dates in future.'
210 210 raise colander.Invalid(node, msg % value)
211 211
212 212
213 213 class SlowCallListSchema(colander.SequenceSchema):
214 214 """
215 215 Validates list of individual slow calls
216 216 """
217 217 slow_call = SlowCallSchema()
218 218
219 219
220 220 class RequestStatsSchema(colander.MappingSchema):
221 221 """
222 222 Validates format of requests statistics dictionary
223 223 """
224 224 main = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
225 225 missing=0)
226 226 sql = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
227 227 missing=0)
228 228 nosql = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
229 229 missing=0)
230 230 remote = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
231 231 missing=0)
232 232 tmpl = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
233 233 missing=0)
234 234 custom = colander.SchemaNode(colander.Float(), validator=colander.Range(0),
235 235 missing=0)
236 236 sql_calls = colander.SchemaNode(colander.Float(),
237 237 validator=colander.Range(0),
238 238 missing=0)
239 239 nosql_calls = colander.SchemaNode(colander.Float(),
240 240 validator=colander.Range(0),
241 241 missing=0)
242 242 remote_calls = colander.SchemaNode(colander.Float(),
243 243 validator=colander.Range(0),
244 244 missing=0)
245 245 tmpl_calls = colander.SchemaNode(colander.Float(),
246 246 validator=colander.Range(0),
247 247 missing=0)
248 248 custom_calls = colander.SchemaNode(colander.Float(),
249 249 validator=colander.Range(0),
250 250 missing=0)
251 251
252 252
253 253 class FrameInfoVarSchema(colander.SequenceSchema):
254 254 """
255 255 Validates format of frame variables of a traceback
256 256 """
257 257 vars = colander.SchemaNode(UnknownType(),
258 258 validator=colander.Length(2, 2))
259 259
260 260
261 261 class FrameInfoSchema(colander.MappingSchema):
262 262 """
263 263 Validates format of a traceback line
264 264 """
265 265 cline = colander.SchemaNode(colander.String(), missing='')
266 266 module = colander.SchemaNode(colander.String(), missing='')
267 267 line = colander.SchemaNode(colander.String(), missing='')
268 268 file = colander.SchemaNode(colander.String(), missing='')
269 269 fn = colander.SchemaNode(colander.String(), missing='')
270 270 vars = FrameInfoVarSchema()
271 271
272 272
273 273 class FrameInfoListSchema(colander.SequenceSchema):
274 274 """
275 275 Validates format of list of traceback lines
276 276 """
277 277 frame = colander.SchemaNode(UnknownType())
278 278
279 279
280 280 class ReportDetailBaseSchema(colander.MappingSchema):
281 281 """
282 282 Validates format of report - ie. request parameters and stats for a request in report group
283 283 """
284 284 username = colander.SchemaNode(colander.String(),
285 285 preparer=[shortener_factory(255),
286 286 lambda x: x or ''],
287 287 missing='')
288 288 request_id = colander.SchemaNode(colander.String(),
289 289 preparer=shortener_factory(40),
290 290 missing='')
291 291 url = colander.SchemaNode(colander.String(),
292 292 preparer=shortener_factory(1024), missing='')
293 293 ip = colander.SchemaNode(colander.String(), preparer=shortener_factory(39),
294 294 missing=None)
295 295 start_time = colander.SchemaNode(NonTZDate(), validator=limited_date,
296 296 missing=deferred_utcnow)
297 297 end_time = colander.SchemaNode(NonTZDate(), validator=limited_date,
298 298 missing=None)
299 299 user_agent = colander.SchemaNode(colander.String(),
300 300 preparer=[shortener_factory(512),
301 301 lambda x: x or ''],
302 302 missing='')
303 303 message = colander.SchemaNode(colander.String(),
304 304 preparer=shortener_factory(2048),
305 305 missing='')
306 306 group_string = colander.SchemaNode(colander.String(),
307 307 validator=colander.Length(1, 512),
308 308 missing=None)
309 309 request_stats = RequestStatsSchema(missing=None)
310 310 request = colander.SchemaNode(colander.Mapping(unknown='preserve'),
311 311 missing={})
312 312 traceback = FrameInfoListSchema(missing=None)
313 313 slow_calls = SlowCallListSchema(missing=[])
314 314 extra = ExtraSchemaList()
315 315
316 316
317 317 class ReportDetailSchema_0_5(ReportDetailBaseSchema):
318 318 pass
319 319
320 320
321 321 class ReportDetailSchemaPermissiveDate_0_5(ReportDetailSchema_0_5):
322 322 start_time = colander.SchemaNode(NonTZDate(), missing=deferred_utcnow)
323 323 end_time = colander.SchemaNode(NonTZDate(), missing=None)
324 324
325 325
326 326 class ReportSchemaBase(colander.MappingSchema):
327 327 """
328 328 Validates format of report group
329 329 """
330 330 client = colander.SchemaNode(colander.String(),
331 331 preparer=lambda x: x or 'unknown')
332 332 server = colander.SchemaNode(
333 333 colander.String(),
334 334 preparer=[
335 335 lambda x: x.lower() if x else 'unknown', shortener_factory(128)],
336 336 missing='unknown')
337 337 priority = colander.SchemaNode(colander.Int(),
338 338 preparer=[lambda x: x or 5],
339 339 validator=colander.Range(1, 10),
340 340 missing=5)
341 341 language = colander.SchemaNode(colander.String(), missing='unknown')
342 342 error = colander.SchemaNode(colander.String(),
343 343 preparer=shortener_factory(512),
344 344 missing='')
345 345 view_name = colander.SchemaNode(colander.String(),
346 346 preparer=[shortener_factory(128),
347 347 lambda x: x or ''],
348 348 missing='')
349 349 http_status = colander.SchemaNode(colander.Int(),
350 350 preparer=[lambda x: x or 200],
351 351 validator=colander.Range(1))
352 352
353 353 occurences = colander.SchemaNode(colander.Int(),
354 354 validator=colander.Range(1, 99999999999),
355 355 missing=1)
356 356 tags = TagSchemaList()
357 357
358 358
359 359 class ReportSchema_0_5(ReportSchemaBase, ReportDetailSchema_0_5):
360 360 pass
361 361
362 362
363 363 class ReportSchemaPermissiveDate_0_5(ReportSchemaBase,
364 364 ReportDetailSchemaPermissiveDate_0_5):
365 365 pass
366 366
367 367
368 368 class ReportListSchema_0_5(colander.SequenceSchema):
369 369 """
370 370 Validates format of list of report groups
371 371 """
372 372 report = ReportSchema_0_5()
373 373 validator = colander.Length(1)
374 374
375 375
376 376 class ReportListPermissiveDateSchema_0_5(colander.SequenceSchema):
377 377 """
378 378 Validates format of list of report groups
379 379 """
380 380 report = ReportSchemaPermissiveDate_0_5()
381 381 validator = colander.Length(1)
382 382
383 383
384 384 class LogSchema(colander.MappingSchema):
385 385 """
386 386 Validates format if individual log entry
387 387 """
388 388 primary_key = colander.SchemaNode(UnknownType(),
389 389 preparer=[cast_to_unicode_or_null,
390 390 shortener_factory(128)],
391 391 missing=None)
392 392 log_level = colander.SchemaNode(colander.String(),
393 393 preparer=shortener_factory(10),
394 394 missing='UNKNOWN')
395 395 message = colander.SchemaNode(colander.String(),
396 396 preparer=shortener_factory(4096),
397 397 missing='')
398 398 namespace = colander.SchemaNode(colander.String(),
399 399 preparer=shortener_factory(128),
400 400 missing='')
401 401 request_id = colander.SchemaNode(colander.String(),
402 402 preparer=shortener_factory(40),
403 403 missing='')
404 404 server = colander.SchemaNode(colander.String(),
405 405 preparer=shortener_factory(128),
406 406 missing='unknown')
407 407 date = colander.SchemaNode(NonTZDate(),
408 408 validator=limited_date,
409 409 missing=deferred_utcnow)
410 410 tags = TagSchemaList()
411 411
412 412
413 413 class LogSchemaPermanent(LogSchema):
414 414 date = colander.SchemaNode(NonTZDate(),
415 415 missing=deferred_utcnow)
416 416 permanent = colander.SchemaNode(colander.Boolean(), missing=False)
417 417
418 418
419 419 class LogListSchema(colander.SequenceSchema):
420 420 """
421 421 Validates format of list of log entries
422 422 """
423 423 log = LogSchema()
424 424 validator = colander.Length(1)
425 425
426 426
427 427 class LogListPermanentSchema(colander.SequenceSchema):
428 428 """
429 429 Validates format of list of log entries
430 430 """
431 431 log = LogSchemaPermanent()
432 432 validator = colander.Length(1)
433 433
434 434
435 435 class ViewRequestStatsSchema(RequestStatsSchema):
436 436 requests = colander.SchemaNode(colander.Integer(),
437 437 validator=colander.Range(0),
438 438 missing=0)
439 439
440 440
441 441 class ViewMetricTupleSchema(colander.TupleSchema):
442 442 """
443 443 Validates list of views and their corresponding request stats object ie:
444 444 ["dir/module:func",{"custom": 0.0..}]
445 445 """
446 446 view_name = colander.SchemaNode(colander.String(),
447 447 preparer=[shortener_factory(128),
448 448 lambda x: x or 'unknown'],
449 449 missing='unknown')
450 450 metrics = ViewRequestStatsSchema()
451 451
452 452
453 453 class ViewMetricListSchema(colander.SequenceSchema):
454 454 """
455 455 Validates view breakdown stats objects list
456 456 {metrics key of server/time object}
457 457 """
458 458 view_tuple = ViewMetricTupleSchema()
459 459 validator = colander.Length(1)
460 460
461 461
462 462 class ViewMetricSchema(colander.MappingSchema):
463 463 """
464 464 Validates server/timeinterval object, ie:
465 465 {server/time object}
466 466
467 467 """
468 468 timestamp = colander.SchemaNode(NonTZDate(),
469 469 validator=limited_date,
470 470 missing=None)
471 471 server = colander.SchemaNode(colander.String(),
472 472 preparer=[shortener_factory(128),
473 473 lambda x: x or 'unknown'],
474 474 missing='unknown')
475 475 metrics = ViewMetricListSchema()
476 476
477 477
478 478 class GeneralMetricSchema(colander.MappingSchema):
479 479 """
480 480 Validates universal metric schema
481 481
482 482 """
483 483 namespace = colander.SchemaNode(colander.String(), missing='',
484 484 preparer=shortener_factory(128))
485 485
486 486 server_name = colander.SchemaNode(colander.String(),
487 487 preparer=[shortener_factory(128),
488 488 lambda x: x or 'unknown'],
489 489 missing='unknown')
490 490 timestamp = colander.SchemaNode(NonTZDate(), validator=limited_date,
491 491 missing=deferred_utcnow)
492 492 tags = TagSchemaList(missing=colander.required)
493 493
494 494
495 class GeneralMetricPermanentSchema(GeneralMetricSchema):
496 """
497 Validates universal metric schema
498
499 """
500 timestamp = colander.SchemaNode(NonTZDate(), missing=deferred_utcnow)
501
502
495 503 class GeneralMetricsListSchema(colander.SequenceSchema):
496 504 metric = GeneralMetricSchema()
497 505 validator = colander.Length(1)
498 506
499 507
508 class GeneralMetricsPermanentListSchema(colander.SequenceSchema):
509 metric = GeneralMetricPermanentSchema()
510 validator = colander.Length(1)
511
512
500 513 class MetricsListSchema(colander.SequenceSchema):
501 514 """
502 515 Validates list of metrics objects ie:
503 516 [{server/time object}, ] part
504 517
505 518
506 519 """
507 520 metric = ViewMetricSchema()
508 521 validator = colander.Length(1)
509 522
510 523
511 524 class StringToAppList(object):
512 525 """
513 526 Returns validated list of application ids from user query and
514 527 set of applications user is allowed to look at
515 528 transform string to list containing single integer
516 529 """
517 530
518 531 def serialize(self, node, appstruct):
519 532 if appstruct is null:
520 533 return null
521 534 return appstruct
522 535
523 536 def deserialize(self, node, cstruct):
524 537 if cstruct is null:
525 538 return null
526 539
527 540 apps = set([int(a) for a in node.bindings['resources']])
528 541
529 542 if isinstance(cstruct, str):
530 543 cstruct = [cstruct]
531 544
532 545 cstruct = [int(a) for a in cstruct]
533 546
534 547 valid_apps = list(apps.intersection(set(cstruct)))
535 548 if valid_apps:
536 549 return valid_apps
537 550 return null
538 551
539 552 def cstruct_children(self):
540 553 return []
541 554
542 555
543 556 @colander.deferred
544 557 def possible_applications_validator(node, kw):
545 558 possible_apps = [int(a) for a in kw['resources']]
546 559 return colander.All(colander.ContainsOnly(possible_apps),
547 560 colander.Length(1))
548 561
549 562
550 563 @colander.deferred
551 564 def possible_applications(node, kw):
552 565 return [int(a) for a in kw['resources']]
553 566
554 567
555 568 @colander.deferred
556 569 def today_start(node, kw):
557 570 return datetime.datetime.utcnow().replace(second=0, microsecond=0,
558 571 minute=0,
559 572 hour=0)
560 573
561 574
562 575 @colander.deferred
563 576 def today_end(node, kw):
564 577 return datetime.datetime.utcnow().replace(second=0, microsecond=0,
565 578 minute=59, hour=23)
566 579
567 580
568 581 @colander.deferred
569 582 def old_start(node, kw):
570 583 t_delta = datetime.timedelta(days=90)
571 584 return datetime.datetime.utcnow().replace(second=0, microsecond=0,
572 585 minute=0,
573 586 hour=0) - t_delta
574 587
575 588
576 589 @colander.deferred
577 590 def today_end(node, kw):
578 591 return datetime.datetime.utcnow().replace(second=0, microsecond=0,
579 592 minute=59, hour=23)
580 593
581 594
582 595 class PermissiveDate(colander.DateTime):
583 596 """ Returns null for incorrect date format - also removes tz info"""
584 597
585 598 def deserialize(self, node, cstruct):
586 599 if not cstruct:
587 600 return null
588 601
589 602 try:
590 603 result = colander.iso8601.parse_date(
591 604 cstruct, default_timezone=self.default_tzinfo)
592 605 except colander.iso8601.ParseError:
593 606 return null
594 607 return result.replace(tzinfo=None)
595 608
596 609
597 610 class LogSearchSchema(colander.MappingSchema):
598 611 def schema_type(self, **kw):
599 612 return colander.Mapping(unknown='preserve')
600 613
601 614 resource = colander.SchemaNode(StringToAppList(),
602 615 validator=possible_applications_validator,
603 616 missing=possible_applications)
604 617
605 618 message = colander.SchemaNode(colander.Sequence(accept_scalar=True),
606 619 colander.SchemaNode(colander.String()),
607 620 missing=None)
608 621 level = colander.SchemaNode(colander.Sequence(accept_scalar=True),
609 622 colander.SchemaNode(colander.String()),
610 623 preparer=lowercase_preparer,
611 624 missing=None)
612 625 namespace = colander.SchemaNode(colander.Sequence(accept_scalar=True),
613 626 colander.SchemaNode(colander.String()),
614 627 preparer=lowercase_preparer,
615 628 missing=None)
616 629 request_id = colander.SchemaNode(colander.Sequence(accept_scalar=True),
617 630 colander.SchemaNode(colander.String()),
618 631 preparer=lowercase_preparer,
619 632 missing=None)
620 633 start_date = colander.SchemaNode(PermissiveDate(),
621 634 missing=None)
622 635 end_date = colander.SchemaNode(PermissiveDate(),
623 636 missing=None)
624 637 page = colander.SchemaNode(colander.Integer(),
625 638 validator=colander.Range(min=1),
626 639 missing=1)
627 640
628 641
629 642 class ReportSearchSchema(colander.MappingSchema):
630 643 def schema_type(self, **kw):
631 644 return colander.Mapping(unknown='preserve')
632 645
633 646 resource = colander.SchemaNode(StringToAppList(),
634 647 validator=possible_applications_validator,
635 648 missing=possible_applications)
636 649 request_id = colander.SchemaNode(colander.Sequence(accept_scalar=True),
637 650 colander.SchemaNode(colander.String()),
638 651 missing=None)
639 652 start_date = colander.SchemaNode(PermissiveDate(),
640 653 missing=None)
641 654 end_date = colander.SchemaNode(PermissiveDate(),
642 655 missing=None)
643 656 page = colander.SchemaNode(colander.Integer(),
644 657 validator=colander.Range(min=1),
645 658 missing=1)
646 659
647 660 min_occurences = colander.SchemaNode(
648 661 colander.Sequence(accept_scalar=True),
649 662 colander.SchemaNode(colander.Integer()),
650 663 missing=None)
651 664
652 665 http_status = colander.SchemaNode(colander.Sequence(accept_scalar=True),
653 666 colander.SchemaNode(colander.Integer()),
654 667 missing=None)
655 668 priority = colander.SchemaNode(colander.Sequence(accept_scalar=True),
656 669 colander.SchemaNode(colander.Integer()),
657 670 missing=None)
658 671 error = colander.SchemaNode(colander.Sequence(accept_scalar=True),
659 672 colander.SchemaNode(colander.String()),
660 673 missing=None)
661 674 url_path = colander.SchemaNode(colander.Sequence(accept_scalar=True),
662 675 colander.SchemaNode(colander.String()),
663 676 missing=None)
664 677 url_domain = colander.SchemaNode(colander.Sequence(accept_scalar=True),
665 678 colander.SchemaNode(colander.String()),
666 679 missing=None)
667 680 report_status = colander.SchemaNode(colander.Sequence(accept_scalar=True),
668 681 colander.SchemaNode(colander.String()),
669 682 missing=None)
670 683 min_duration = colander.SchemaNode(colander.Sequence(accept_scalar=True),
671 684 colander.SchemaNode(colander.Float()),
672 685 missing=None)
673 686 max_duration = colander.SchemaNode(colander.Sequence(accept_scalar=True),
674 687 colander.SchemaNode(colander.Float()),
675 688 missing=None)
676 689
677 690
678 691 class TagSchema(colander.MappingSchema):
679 692 """
680 693 Used in log search
681 694 """
682 695 name = colander.SchemaNode(colander.String(),
683 696 validator=colander.Length(1, 32))
684 697 value = colander.SchemaNode(colander.Sequence(accept_scalar=True),
685 698 colander.SchemaNode(colander.String(),
686 699 validator=colander.Length(
687 700 1, 128)),
688 701 missing=None)
689 702 op = colander.SchemaNode(colander.String(),
690 703 validator=colander.Length(1, 128),
691 704 missing=None)
692 705
693 706
694 707 class TagListSchema(colander.SequenceSchema):
695 708 tag = TagSchema()
696 709
697 710
698 711 class RuleFieldType(object):
699 712 """ Validator which succeeds if the value passed to it is one of
700 713 a fixed set of values """
701 714
702 715 def __init__(self, cast_to):
703 716 self.cast_to = cast_to
704 717
705 718 def __call__(self, node, value):
706 719 try:
707 720 if self.cast_to == 'int':
708 721 int(value)
709 722 elif self.cast_to == 'float':
710 723 float(value)
711 724 elif self.cast_to == 'unicode':
712 725 str(value)
713 726 except:
714 727 raise colander.Invalid(node,
715 728 "Can't cast {} to {}".format(
716 729 value, self.cast_to))
717 730
718 731
719 732 def build_rule_schema(ruleset, check_matrix):
720 733 """
721 734 Accepts ruleset and a map of fields/possible operations and builds
722 735 validation class
723 736 """
724 737
725 738 schema = colander.SchemaNode(colander.Mapping())
726 739 schema.add(colander.SchemaNode(colander.String(), name='field'))
727 740
728 741 if ruleset['field'] in ['__AND__', '__OR__', '__NOT__']:
729 742 subrules = colander.SchemaNode(colander.Tuple(), name='rules')
730 743 for rule in ruleset['rules']:
731 744 subrules.add(build_rule_schema(rule, check_matrix))
732 745 schema.add(subrules)
733 746 else:
734 747 op_choices = check_matrix[ruleset['field']]['ops']
735 748 cast_to = check_matrix[ruleset['field']]['type']
736 749 schema.add(colander.SchemaNode(colander.String(),
737 750 validator=colander.OneOf(op_choices),
738 751 name='op'))
739 752
740 753 schema.add(colander.SchemaNode(colander.String(),
741 754 name='value',
742 755 validator=RuleFieldType(cast_to)))
743 756 return schema
744 757
745 758
746 759 class ConfigTypeSchema(colander.MappingSchema):
747 760 type = colander.SchemaNode(colander.String(), missing=None)
748 761 config = colander.SchemaNode(UnknownType(), missing=None)
749 762
750 763
751 764 class MappingListSchema(colander.SequenceSchema):
752 765 config = colander.SchemaNode(UnknownType())
@@ -1,427 +1,438 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # AppEnlight Enterprise Edition, including its added features, Support
19 19 # services, and proprietary license terms, please see
20 20 # https://rhodecode.com/licenses/
21 21
22 22 import base64
23 23 import io
24 24 import datetime
25 25 import json
26 26 import logging
27 27 import urllib.request, urllib.parse, urllib.error
28 28 import zlib
29 29
30 30 from gzip import GzipFile
31 31 from pyramid.view import view_config
32 32 from pyramid.httpexceptions import HTTPBadRequest
33 33
34 34 import appenlight.celery.tasks as tasks
35 35 from appenlight.lib.api import rate_limiting, check_cors
36 36 from appenlight.lib.enums import ParsedSentryEventType
37 37 from appenlight.lib.utils import parse_proto
38 38 from appenlight.lib.utils.airbrake import parse_airbrake_xml
39 39 from appenlight.lib.utils.date_utils import convert_date
40 40 from appenlight.lib.utils.sentry import parse_sentry_event
41 41 from appenlight.lib.request import JSONException
42 42 from appenlight.validators import (LogListSchema,
43 43 MetricsListSchema,
44 44 GeneralMetricsListSchema,
45 GeneralMetricsPermanentListSchema,
45 46 GeneralMetricSchema,
47 GeneralMetricPermanentSchema,
46 48 LogListPermanentSchema,
47 49 ReportListSchema_0_5,
48 50 LogSchema,
49 51 LogSchemaPermanent,
50 52 ReportSchema_0_5)
51 53
52 54 log = logging.getLogger(__name__)
53 55
54 56
55 57 @view_config(route_name='api_logs', renderer='string', permission='create',
56 58 require_csrf=False)
57 59 @view_config(route_name='api_log', renderer='string', permission='create',
58 60 require_csrf=False)
59 61 def logs_create(request):
60 62 """
61 63 Endpoint for log aggregation
62 64 """
63 65 application = request.context.resource
64 66 if request.method.upper() == 'OPTIONS':
65 67 return check_cors(request, application)
66 68 else:
67 69 check_cors(request, application, should_return=False)
68 70
69 71 params = dict(request.params.copy())
70 72 proto_version = parse_proto(params.get('protocol_version', ''))
71 73 payload = request.unsafe_json_body
72 74 sequence_accepted = request.matched_route.name == 'api_logs'
73 75
74 76 if sequence_accepted:
75 77 if application.allow_permanent_storage:
76 78 schema = LogListPermanentSchema().bind(
77 79 utcnow=datetime.datetime.utcnow())
78 80 else:
79 81 schema = LogListSchema().bind(
80 82 utcnow=datetime.datetime.utcnow())
81 83 else:
82 84 if application.allow_permanent_storage:
83 85 schema = LogSchemaPermanent().bind(
84 86 utcnow=datetime.datetime.utcnow())
85 87 else:
86 88 schema = LogSchema().bind(
87 89 utcnow=datetime.datetime.utcnow())
88 90
89 91 deserialized_logs = schema.deserialize(payload)
90 92 if sequence_accepted is False:
91 93 deserialized_logs = [deserialized_logs]
92 94
93 95 rate_limiting(request, application, 'per_application_logs_rate_limit',
94 96 len(deserialized_logs))
95 97
96 98 # pprint.pprint(deserialized_logs)
97 99
98 100 # we need to split those out so we can process the pkey ones one by one
99 101 non_pkey_logs = [log_dict for log_dict in deserialized_logs
100 102 if not log_dict['primary_key']]
101 103 pkey_dict = {}
102 104 # try to process the logs as best as we can and group together to reduce
103 105 # the amount of
104 106 for log_dict in deserialized_logs:
105 107 if log_dict['primary_key']:
106 108 key = (log_dict['primary_key'], log_dict['namespace'],)
107 109 if not key in pkey_dict:
108 110 pkey_dict[key] = []
109 111 pkey_dict[key].append(log_dict)
110 112
111 113 if non_pkey_logs:
112 114 log.debug('%s non-pkey logs received: %s' % (application,
113 115 len(non_pkey_logs)))
114 116 tasks.add_logs.delay(application.resource_id, params, non_pkey_logs)
115 117 if pkey_dict:
116 118 logs_to_insert = []
117 119 for primary_key_tuple, payload in pkey_dict.items():
118 120 sorted_logs = sorted(payload, key=lambda x: x['date'])
119 121 logs_to_insert.append(sorted_logs[-1])
120 122 log.debug('%s pkey logs received: %s' % (application,
121 123 len(logs_to_insert)))
122 124 tasks.add_logs.delay(application.resource_id, params, logs_to_insert)
123 125
124 126 log.info('LOG call %s %s client:%s' % (
125 127 application, proto_version, request.headers.get('user_agent')))
126 128 return 'OK: Logs accepted'
127 129
128 130
129 131 @view_config(route_name='api_request_stats', renderer='string',
130 132 permission='create', require_csrf=False)
131 133 @view_config(route_name='api_metrics', renderer='string',
132 134 permission='create', require_csrf=False)
133 135 def request_metrics_create(request):
134 136 """
135 137 Endpoint for performance metrics, aggregates view performance stats
136 138 and converts them to general metric row
137 139 """
138 140 application = request.context.resource
139 141 if request.method.upper() == 'OPTIONS':
140 142 return check_cors(request, application)
141 143 else:
142 144 check_cors(request, application, should_return=False)
143 145
144 146 params = dict(request.params.copy())
145 147 proto_version = parse_proto(params.get('protocol_version', ''))
146 148
147 149 payload = request.unsafe_json_body
148 150 schema = MetricsListSchema()
149 151 dataset = schema.deserialize(payload)
150 152
151 153 rate_limiting(request, application, 'per_application_metrics_rate_limit',
152 154 len(dataset))
153 155
154 156 # looping report data
155 157 metrics = {}
156 158 for metric in dataset:
157 159 server_name = metric.get('server', '').lower() or 'unknown'
158 160 start_interval = convert_date(metric['timestamp'])
159 161 start_interval = start_interval.replace(second=0, microsecond=0)
160 162
161 163 for view_name, view_metrics in metric['metrics']:
162 164 key = '%s%s%s' % (metric['server'], start_interval, view_name)
163 165 if start_interval not in metrics:
164 166 metrics[key] = {"requests": 0, "main": 0, "sql": 0,
165 167 "nosql": 0, "remote": 0, "tmpl": 0,
166 168 "custom": 0, 'sql_calls': 0,
167 169 'nosql_calls': 0,
168 170 'remote_calls': 0, 'tmpl_calls': 0,
169 171 'custom_calls': 0,
170 172 "start_interval": start_interval,
171 173 "server_name": server_name,
172 174 "view_name": view_name
173 175 }
174 176 metrics[key]["requests"] += int(view_metrics['requests'])
175 177 metrics[key]["main"] += round(view_metrics['main'], 5)
176 178 metrics[key]["sql"] += round(view_metrics['sql'], 5)
177 179 metrics[key]["nosql"] += round(view_metrics['nosql'], 5)
178 180 metrics[key]["remote"] += round(view_metrics['remote'], 5)
179 181 metrics[key]["tmpl"] += round(view_metrics['tmpl'], 5)
180 182 metrics[key]["custom"] += round(view_metrics.get('custom', 0.0),
181 183 5)
182 184 metrics[key]["sql_calls"] += int(
183 185 view_metrics.get('sql_calls', 0))
184 186 metrics[key]["nosql_calls"] += int(
185 187 view_metrics.get('nosql_calls', 0))
186 188 metrics[key]["remote_calls"] += int(
187 189 view_metrics.get('remote_calls', 0))
188 190 metrics[key]["tmpl_calls"] += int(
189 191 view_metrics.get('tmpl_calls', 0))
190 192 metrics[key]["custom_calls"] += int(
191 193 view_metrics.get('custom_calls', 0))
192 194
193 195 if not metrics[key]["requests"]:
194 196 # fix this here because validator can't
195 197 metrics[key]["requests"] = 1
196 198 # metrics dict is being built to minimize
197 199 # the amount of queries used
198 200 # in case we get multiple rows from same minute
199 201
200 202 normalized_metrics = []
201 203 for metric in metrics.values():
202 204 new_metric = {
203 205 'namespace': 'appenlight.request_metric',
204 206 'timestamp': metric.pop('start_interval'),
205 207 'server_name': metric['server_name'],
206 208 'tags': list(metric.items())
207 209 }
208 210 normalized_metrics.append(new_metric)
209 211
210 212 tasks.add_metrics.delay(application.resource_id, params,
211 213 normalized_metrics, proto_version)
212 214
213 215 log.info('REQUEST METRICS call {} {} client:{}'.format(
214 216 application.resource_name, proto_version,
215 217 request.headers.get('user_agent')))
216 218 return 'OK: request metrics accepted'
217 219
218 220
219 221 @view_config(route_name='api_general_metrics', renderer='string',
220 222 permission='create', require_csrf=False)
221 223 @view_config(route_name='api_general_metric', renderer='string',
222 224 permission='create', require_csrf=False)
223 225 def general_metrics_create(request):
224 226 """
225 227 Endpoint for general metrics aggregation
226 228 """
227 229 application = request.context.resource
228 230 if request.method.upper() == 'OPTIONS':
229 231 return check_cors(request, application)
230 232 else:
231 233 check_cors(request, application, should_return=False)
232 234
233 235 params = dict(request.params.copy())
234 236 proto_version = parse_proto(params.get('protocol_version', ''))
235 237 payload = request.unsafe_json_body
236 238 sequence_accepted = request.matched_route.name == 'api_general_metrics'
237 239 if sequence_accepted:
238 schema = GeneralMetricsListSchema().bind(
239 utcnow=datetime.datetime.utcnow())
240 if application.allow_permanent_storage:
241 schema = GeneralMetricsPermanentListSchema().bind(
242 utcnow=datetime.datetime.utcnow())
243 else:
244 schema = GeneralMetricsListSchema().bind(
245 utcnow=datetime.datetime.utcnow())
240 246 else:
241 schema = GeneralMetricSchema().bind(utcnow=datetime.datetime.utcnow())
247 if application.allow_permanent_storage:
248 schema = GeneralMetricPermanentSchema().bind(
249 utcnow=datetime.datetime.utcnow())
250 else:
251 schema = GeneralMetricSchema().bind(
252 utcnow=datetime.datetime.utcnow())
242 253
243 254 deserialized_metrics = schema.deserialize(payload)
244 255 if sequence_accepted is False:
245 256 deserialized_metrics = [deserialized_metrics]
246 257
247 258 rate_limiting(request, application, 'per_application_metrics_rate_limit',
248 259 len(deserialized_metrics))
249 260
250 261 tasks.add_metrics.delay(application.resource_id, params,
251 262 deserialized_metrics, proto_version)
252 263
253 264 log.info('METRICS call {} {} client:{}'.format(
254 265 application.resource_name, proto_version,
255 266 request.headers.get('user_agent')))
256 267 return 'OK: Metrics accepted'
257 268
258 269
259 270 @view_config(route_name='api_reports', renderer='string', permission='create',
260 271 require_csrf=False)
261 272 @view_config(route_name='api_slow_reports', renderer='string',
262 273 permission='create', require_csrf=False)
263 274 @view_config(route_name='api_report', renderer='string', permission='create',
264 275 require_csrf=False)
265 276 def reports_create(request):
266 277 """
267 278 Endpoint for exception and slowness reports
268 279 """
269 280 # route_url('reports')
270 281 application = request.context.resource
271 282 if request.method.upper() == 'OPTIONS':
272 283 return check_cors(request, application)
273 284 else:
274 285 check_cors(request, application, should_return=False)
275 286 params = dict(request.params.copy())
276 287 proto_version = parse_proto(params.get('protocol_version', ''))
277 288 payload = request.unsafe_json_body
278 289 sequence_accepted = request.matched_route.name == 'api_reports'
279 290
280 291 if sequence_accepted:
281 292 schema = ReportListSchema_0_5().bind(
282 293 utcnow=datetime.datetime.utcnow())
283 294 else:
284 295 schema = ReportSchema_0_5().bind(
285 296 utcnow=datetime.datetime.utcnow())
286 297
287 298 deserialized_reports = schema.deserialize(payload)
288 299 if sequence_accepted is False:
289 300 deserialized_reports = [deserialized_reports]
290 301 if deserialized_reports:
291 302 rate_limiting(request, application,
292 303 'per_application_reports_rate_limit',
293 304 len(deserialized_reports))
294 305
295 306 # pprint.pprint(deserialized_reports)
296 307 tasks.add_reports.delay(application.resource_id, params,
297 308 deserialized_reports)
298 309 log.info('REPORT call %s, %s client:%s' % (
299 310 application,
300 311 proto_version,
301 312 request.headers.get('user_agent'))
302 313 )
303 314 return 'OK: Reports accepted'
304 315
305 316
306 317 @view_config(route_name='api_airbrake', renderer='string', permission='create',
307 318 require_csrf=False)
308 319 def airbrake_xml_compat(request):
309 320 """
310 321 Airbrake compatible endpoint for XML reports
311 322 """
312 323 application = request.context.resource
313 324 if request.method.upper() == 'OPTIONS':
314 325 return check_cors(request, application)
315 326 else:
316 327 check_cors(request, application, should_return=False)
317 328
318 329 params = request.params.copy()
319 330
320 331 error_dict = parse_airbrake_xml(request)
321 332 schema = ReportListSchema_0_5().bind(utcnow=datetime.datetime.utcnow())
322 333 deserialized_reports = schema.deserialize([error_dict])
323 334 rate_limiting(request, application, 'per_application_reports_rate_limit',
324 335 len(deserialized_reports))
325 336
326 337 tasks.add_reports.delay(application.resource_id, params,
327 338 deserialized_reports)
328 339 log.info('%s AIRBRAKE call for application %s, api_ver:%s client:%s' % (
329 340 500, application.resource_name,
330 341 request.params.get('protocol_version', 'unknown'),
331 342 request.headers.get('user_agent'))
332 343 )
333 344 return '<notice><id>no-id</id><url>%s</url></notice>' % \
334 345 request.registry.settings['mailing.app_url']
335 346
336 347
337 348 def decompress_gzip(data):
338 349 try:
339 350 fp = io.StringIO(data)
340 351 with GzipFile(fileobj=fp) as f:
341 352 return f.read()
342 353 except Exception as exc:
343 354 raise
344 355 log.error(exc)
345 356 raise HTTPBadRequest()
346 357
347 358
348 359 def decompress_zlib(data):
349 360 try:
350 361 return zlib.decompress(data)
351 362 except Exception as exc:
352 363 raise
353 364 log.error(exc)
354 365 raise HTTPBadRequest()
355 366
356 367
357 368 def decode_b64(data):
358 369 try:
359 370 return base64.b64decode(data)
360 371 except Exception as exc:
361 372 raise
362 373 log.error(exc)
363 374 raise HTTPBadRequest()
364 375
365 376
366 377 @view_config(route_name='api_sentry', renderer='string', permission='create',
367 378 require_csrf=False)
368 379 @view_config(route_name='api_sentry_slash', renderer='string',
369 380 permission='create', require_csrf=False)
370 381 def sentry_compat(request):
371 382 """
372 383 Sentry compatible endpoint
373 384 """
374 385 application = request.context.resource
375 386 if request.method.upper() == 'OPTIONS':
376 387 return check_cors(request, application)
377 388 else:
378 389 check_cors(request, application, should_return=False)
379 390
380 391 # handle various report encoding
381 392 content_encoding = request.headers.get('Content-Encoding')
382 393 content_type = request.headers.get('Content-Type')
383 394 if content_encoding == 'gzip':
384 395 body = decompress_gzip(request.body)
385 396 elif content_encoding == 'deflate':
386 397 body = decompress_zlib(request.body)
387 398 else:
388 399 body = request.body
389 400 # attempt to fix string before decoding for stupid clients
390 401 if content_type == 'application/x-www-form-urlencoded':
391 402 body = urllib.parse.unquote(body.decode('utf8'))
392 403 check_char = '{' if isinstance(body, str) else b'{'
393 404 if not body.startswith(check_char):
394 405 try:
395 406 body = decode_b64(body)
396 407 body = decompress_zlib(body)
397 408 except Exception as exc:
398 409 log.info(exc)
399 410
400 411 try:
401 412 json_body = json.loads(body.decode('utf8'))
402 413 except ValueError:
403 414 raise JSONException("Incorrect JSON")
404 415
405 416 event, event_type = parse_sentry_event(json_body)
406 417
407 418 if event_type == ParsedSentryEventType.LOG:
408 419 if application.allow_permanent_storage:
409 420 schema = LogSchemaPermanent().bind(
410 421 utcnow=datetime.datetime.utcnow())
411 422 else:
412 423 schema = LogSchema().bind(
413 424 utcnow=datetime.datetime.utcnow())
414 425 deserialized_logs = schema.deserialize(event)
415 426 non_pkey_logs = [deserialized_logs]
416 427 log.debug('%s non-pkey logs received: %s' % (application,
417 428 len(non_pkey_logs)))
418 429 tasks.add_logs.delay(application.resource_id, {}, non_pkey_logs)
419 430 if event_type == ParsedSentryEventType.ERROR_REPORT:
420 431 schema = ReportSchema_0_5().bind(utcnow=datetime.datetime.utcnow())
421 432 deserialized_reports = [schema.deserialize(event)]
422 433 rate_limiting(request, application,
423 434 'per_application_reports_rate_limit',
424 435 len(deserialized_reports))
425 436 tasks.add_reports.delay(application.resource_id, {},
426 437 deserialized_reports)
427 438 return 'OK: Events accepted'
General Comments 0
You need to be logged in to leave comments. Login now