##// END OF EJS Templates
report: fix request stats format
ergo -
Show More
@@ -1,529 +1,529 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 4 #
5 5 # Licensed under the Apache License, Version 2.0 (the "License");
6 6 # you may not use this file except in compliance with the License.
7 7 # You may obtain a copy of the License at
8 8 #
9 9 # http://www.apache.org/licenses/LICENSE-2.0
10 10 #
11 11 # Unless required by applicable law or agreed to in writing, software
12 12 # distributed under the License is distributed on an "AS IS" BASIS,
13 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 14 # See the License for the specific language governing permissions and
15 15 # limitations under the License.
16 16
17 17 from datetime import datetime, timedelta
18 18 import math
19 19 import uuid
20 20 import hashlib
21 21 import copy
22 22 import urllib.parse
23 23 import logging
24 24 import sqlalchemy as sa
25 25
26 26 from appenlight.models import Base, Datastores
27 27 from appenlight.lib.utils.date_utils import convert_date
28 28 from appenlight.lib.utils import convert_es_type
29 29 from appenlight.models.slow_call import SlowCall
30 30 from appenlight.lib.utils import channelstream_request
31 31 from appenlight.lib.enums import ReportType, Language
32 32 from pyramid.threadlocal import get_current_registry, get_current_request
33 33 from sqlalchemy.dialects.postgresql import JSON
34 34 from ziggurat_foundations.models.base import BaseModel
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38 REPORT_TYPE_MATRIX = {
39 39 "http_status": {"type": "int", "ops": ("eq", "ne", "ge", "le")},
40 40 "group:priority": {"type": "int", "ops": ("eq", "ne", "ge", "le")},
41 41 "duration": {"type": "float", "ops": ("ge", "le")},
42 42 "url_domain": {
43 43 "type": "unicode",
44 44 "ops": ("eq", "ne", "startswith", "endswith", "contains"),
45 45 },
46 46 "url_path": {
47 47 "type": "unicode",
48 48 "ops": ("eq", "ne", "startswith", "endswith", "contains"),
49 49 },
50 50 "error": {
51 51 "type": "unicode",
52 52 "ops": ("eq", "ne", "startswith", "endswith", "contains"),
53 53 },
54 54 "tags:server_name": {
55 55 "type": "unicode",
56 56 "ops": ("eq", "ne", "startswith", "endswith", "contains"),
57 57 },
58 58 "traceback": {"type": "unicode", "ops": ("contains",)},
59 59 "group:occurences": {"type": "int", "ops": ("eq", "ne", "ge", "le")},
60 60 }
61 61
62 62
63 63 class Report(Base, BaseModel):
64 64 __tablename__ = "reports"
65 65 __table_args__ = {"implicit_returning": False}
66 66
67 67 id = sa.Column(sa.Integer, nullable=False, primary_key=True)
68 68 group_id = sa.Column(
69 69 sa.BigInteger,
70 70 sa.ForeignKey("reports_groups.id", ondelete="cascade", onupdate="cascade"),
71 71 )
72 72 resource_id = sa.Column(sa.Integer(), nullable=False, index=True)
73 73 report_type = sa.Column(sa.Integer(), nullable=False, index=True)
74 74 error = sa.Column(sa.UnicodeText(), index=True)
75 75 extra = sa.Column(JSON(), default={})
76 76 request = sa.Column(JSON(), nullable=False, default={})
77 77 ip = sa.Column(sa.String(39), index=True, default="")
78 78 username = sa.Column(sa.Unicode(255), default="")
79 79 user_agent = sa.Column(sa.Unicode(255), default="")
80 80 url = sa.Column(sa.UnicodeText(), index=True)
81 81 request_id = sa.Column(sa.Text())
82 82 request_stats = sa.Column(JSON(), nullable=False, default={})
83 83 traceback = sa.Column(JSON(), nullable=False, default=None)
84 84 traceback_hash = sa.Column(sa.Text())
85 85 start_time = sa.Column(
86 86 sa.DateTime(), default=datetime.utcnow, server_default=sa.func.now()
87 87 )
88 88 end_time = sa.Column(sa.DateTime())
89 89 duration = sa.Column(sa.Float, default=0)
90 90 http_status = sa.Column(sa.Integer, index=True)
91 91 url_domain = sa.Column(sa.Unicode(100), index=True)
92 92 url_path = sa.Column(sa.Unicode(255), index=True)
93 93 tags = sa.Column(JSON(), nullable=False, default={})
94 94 language = sa.Column(sa.Integer(), default=0)
95 95 # this is used to determine partition for the report
96 96 report_group_time = sa.Column(
97 97 sa.DateTime(), default=datetime.utcnow, server_default=sa.func.now()
98 98 )
99 99
100 100 logs = sa.orm.relationship(
101 101 "Log",
102 102 lazy="dynamic",
103 103 passive_deletes=True,
104 104 passive_updates=True,
105 105 primaryjoin="and_(Report.request_id==Log.request_id, "
106 106 "Log.request_id != None, Log.request_id != '')",
107 107 foreign_keys="[Log.request_id]",
108 108 )
109 109
110 110 slow_calls = sa.orm.relationship(
111 111 "SlowCall",
112 112 backref="detail",
113 113 cascade="all, delete-orphan",
114 114 passive_deletes=True,
115 115 passive_updates=True,
116 116 order_by="SlowCall.timestamp",
117 117 )
118 118
119 119 def set_data(self, data, resource, protocol_version=None):
120 120 self.http_status = data["http_status"]
121 121 self.priority = data["priority"]
122 122 self.error = data["error"]
123 123 report_language = data.get("language", "").lower()
124 124 self.language = getattr(Language, report_language, Language.unknown)
125 125 # we need temp holder here to decide later
126 126 # if we want to to commit the tags if report is marked for creation
127 127 self.tags = {"server_name": data["server"], "view_name": data["view_name"]}
128 128 if data.get("tags"):
129 129 for tag_tuple in data["tags"]:
130 130 self.tags[tag_tuple[0]] = tag_tuple[1]
131 131 self.traceback = data["traceback"]
132 132 stripped_traceback = self.stripped_traceback()
133 133 tb_repr = repr(stripped_traceback).encode("utf8")
134 134 self.traceback_hash = hashlib.sha1(tb_repr).hexdigest()
135 135 url_info = urllib.parse.urlsplit(data.get("url", ""), allow_fragments=False)
136 136 self.url_domain = url_info.netloc[:128]
137 137 self.url_path = url_info.path[:2048]
138 138 self.occurences = data["occurences"]
139 139 if self.error:
140 140 self.report_type = ReportType.error
141 141 else:
142 142 self.report_type = ReportType.slow
143 143
144 144 # but if its status 404 its 404 type
145 145 if self.http_status in [404, "404"] or self.error == "404 Not Found":
146 146 self.report_type = ReportType.not_found
147 147 self.error = ""
148 148
149 149 self.generate_grouping_hash(
150 150 data.get("appenlight.group_string", data.get("group_string")),
151 151 resource.default_grouping,
152 152 protocol_version,
153 153 )
154 154
155 155 # details
156 156 if data["http_status"] in [404, "404"]:
157 157 data = {
158 158 "username": data["username"],
159 159 "ip": data["ip"],
160 160 "url": data["url"],
161 161 "user_agent": data["user_agent"],
162 162 }
163 163 if data.get("HTTP_REFERER") or data.get("http_referer"):
164 164 data["HTTP_REFERER"] = data.get("HTTP_REFERER", "") or data.get(
165 165 "http_referer", ""
166 166 )
167 167
168 168 self.resource_id = resource.resource_id
169 169 self.username = data["username"]
170 170 self.user_agent = data["user_agent"]
171 171 self.ip = data["ip"]
172 172 self.extra = {}
173 173 if data.get("extra"):
174 174 for extra_tuple in data["extra"]:
175 175 self.extra[extra_tuple[0]] = extra_tuple[1]
176 176
177 177 self.url = data["url"]
178 178 self.request_id = data.get("request_id", "").replace("-", "") or str(
179 179 uuid.uuid4()
180 180 )
181 181 request_data = data.get("request", {})
182 182
183 183 self.request = request_data
184 self.request_stats = data.get("request_stats", {})
184 self.request_stats = data.get("request_stats") or {}
185 185 traceback = data.get("traceback")
186 186 if not traceback:
187 187 traceback = data.get("frameinfo")
188 188 self.traceback = traceback
189 189 start_date = convert_date(data.get("start_time"))
190 190 if not self.start_time or self.start_time < start_date:
191 191 self.start_time = start_date
192 192
193 193 self.end_time = convert_date(data.get("end_time"), False)
194 194 self.duration = 0
195 195
196 196 if self.start_time and self.end_time:
197 197 d = self.end_time - self.start_time
198 198 self.duration = d.total_seconds()
199 199
200 200 # update tags with other vars
201 201 if self.username:
202 202 self.tags["user_name"] = self.username
203 203 self.tags["report_language"] = Language.key_from_value(self.language)
204 204
205 205 def add_slow_calls(self, data, report_group):
206 206 slow_calls = []
207 207 for call in data.get("slow_calls", []):
208 208 sc_inst = SlowCall()
209 209 sc_inst.set_data(
210 210 call, resource_id=self.resource_id, report_group=report_group
211 211 )
212 212 slow_calls.append(sc_inst)
213 213 self.slow_calls.extend(slow_calls)
214 214 return slow_calls
215 215
216 216 def get_dict(self, request, details=False, exclude_keys=None, include_keys=None):
217 217 from appenlight.models.services.report_group import ReportGroupService
218 218
219 219 instance_dict = super(Report, self).get_dict()
220 220 instance_dict["req_stats"] = self.req_stats()
221 221 instance_dict["group"] = {}
222 222 instance_dict["group"]["id"] = self.report_group.id
223 223 instance_dict["group"]["total_reports"] = self.report_group.total_reports
224 224 instance_dict["group"]["last_report"] = self.report_group.last_report
225 225 instance_dict["group"]["priority"] = self.report_group.priority
226 226 instance_dict["group"]["occurences"] = self.report_group.occurences
227 227 instance_dict["group"]["last_timestamp"] = self.report_group.last_timestamp
228 228 instance_dict["group"]["first_timestamp"] = self.report_group.first_timestamp
229 229 instance_dict["group"]["public"] = self.report_group.public
230 230 instance_dict["group"]["fixed"] = self.report_group.fixed
231 231 instance_dict["group"]["read"] = self.report_group.read
232 232 instance_dict["group"]["average_duration"] = self.report_group.average_duration
233 233
234 234 instance_dict["resource_name"] = self.report_group.application.resource_name
235 235 instance_dict["report_type"] = self.report_type
236 236
237 237 if instance_dict["http_status"] == 404 and not instance_dict["error"]:
238 238 instance_dict["error"] = "404 Not Found"
239 239
240 240 if details:
241 241 instance_dict[
242 242 "affected_users_count"
243 243 ] = ReportGroupService.affected_users_count(self.report_group)
244 244 instance_dict["top_affected_users"] = [
245 245 {"username": u.username, "count": u.count}
246 246 for u in ReportGroupService.top_affected_users(self.report_group)
247 247 ]
248 248 instance_dict["application"] = {"integrations": []}
249 249 for integration in self.report_group.application.integrations:
250 250 if integration.front_visible:
251 251 instance_dict["application"]["integrations"].append(
252 252 {
253 253 "name": integration.integration_name,
254 254 "action": integration.integration_action,
255 255 }
256 256 )
257 257 instance_dict["comments"] = [
258 258 c.get_dict() for c in self.report_group.comments
259 259 ]
260 260
261 261 instance_dict["group"]["next_report"] = None
262 262 instance_dict["group"]["previous_report"] = None
263 263 next_in_group = self.get_next_in_group(request)
264 264 previous_in_group = self.get_previous_in_group(request)
265 265 if next_in_group:
266 266 instance_dict["group"]["next_report"] = next_in_group
267 267 if previous_in_group:
268 268 instance_dict["group"]["previous_report"] = previous_in_group
269 269
270 270 # slow call ordering
271 271 def find_parent(row, data):
272 272 for r in reversed(data):
273 273 try:
274 274 if (
275 275 row["timestamp"] > r["timestamp"]
276 276 and row["end_time"] < r["end_time"]
277 277 ):
278 278 return r
279 279 except TypeError as e:
280 280 log.warning("reports_view.find_parent: %s" % e)
281 281 return None
282 282
283 283 new_calls = []
284 284 calls = [c.get_dict() for c in self.slow_calls]
285 285 while calls:
286 286 # start from end
287 287 for x in range(len(calls) - 1, -1, -1):
288 288 parent = find_parent(calls[x], calls)
289 289 if parent:
290 290 parent["children"].append(calls[x])
291 291 else:
292 292 # no parent at all? append to new calls anyways
293 293 new_calls.append(calls[x])
294 294 # print 'append', calls[x]
295 295 del calls[x]
296 296 break
297 297 instance_dict["slow_calls"] = new_calls
298 298
299 299 instance_dict["front_url"] = self.get_public_url(request)
300 300
301 301 exclude_keys_list = exclude_keys or []
302 302 include_keys_list = include_keys or []
303 303 for k in list(instance_dict.keys()):
304 304 if k == "group":
305 305 continue
306 306 if k in exclude_keys_list or (k not in include_keys_list and include_keys):
307 307 del instance_dict[k]
308 308 return instance_dict
309 309
310 310 def get_previous_in_group(self, request):
311 311 query = {
312 312 "size": 1,
313 313 "query": {
314 314 "bool": {
315 315 "filter": [
316 316 {"term": {"group_id": self.group_id}},
317 317 {"range": {"pg_id": {"lt": self.id}}},
318 318 ]
319 319 }
320 320 },
321 321 "sort": [{"_doc": {"order": "desc"}}],
322 322 }
323 323 result = request.es_conn.search(
324 324 body=query, index=self.partition_id, doc_type="report"
325 325 )
326 326 if result["hits"]["total"]:
327 327 return result["hits"]["hits"][0]["_source"]["pg_id"]
328 328
329 329 def get_next_in_group(self, request):
330 330 query = {
331 331 "size": 1,
332 332 "query": {
333 333 "bool": {
334 334 "filter": [
335 335 {"term": {"group_id": self.group_id}},
336 336 {"range": {"pg_id": {"gt": self.id}}},
337 337 ]
338 338 }
339 339 },
340 340 "sort": [{"_doc": {"order": "asc"}}],
341 341 }
342 342 result = request.es_conn.search(
343 343 body=query, index=self.partition_id, doc_type="report"
344 344 )
345 345 if result["hits"]["total"]:
346 346 return result["hits"]["hits"][0]["_source"]["pg_id"]
347 347
348 348 def get_public_url(self, request=None, report_group=None, _app_url=None):
349 349 """
350 350 Returns url that user can use to visit specific report
351 351 """
352 352 if not request:
353 353 request = get_current_request()
354 354 url = request.route_url("/", _app_url=_app_url)
355 355 if report_group:
356 356 return (url + "ui/report/%s/%s") % (report_group.id, self.id)
357 357 return (url + "ui/report/%s/%s") % (self.group_id, self.id)
358 358
359 359 def req_stats(self):
360 360 stats = self.request_stats.copy()
361 361 stats["percentages"] = {}
362 362 stats["percentages"]["main"] = 100.0
363 363 main = stats.get("main", 0.0)
364 364 if not main:
365 365 return None
366 366 for name, call_time in stats.items():
367 367 if "calls" not in name and "main" not in name and "percentages" not in name:
368 368 stats["main"] -= call_time
369 369 stats["percentages"][name] = math.floor((call_time / main * 100.0))
370 370 stats["percentages"]["main"] -= stats["percentages"][name]
371 371 if stats["percentages"]["main"] < 0.0:
372 372 stats["percentages"]["main"] = 0.0
373 373 stats["main"] = 0.0
374 374 return stats
375 375
376 376 def generate_grouping_hash(
377 377 self, hash_string=None, default_grouping=None, protocol_version=None
378 378 ):
379 379 """
380 380 Generates SHA1 hash that will be used to group reports together
381 381 """
382 382 if not hash_string:
383 383 location = self.tags.get("view_name") or self.url_path
384 384 server_name = self.tags.get("server_name") or ""
385 385 if default_grouping == "url_traceback":
386 386 hash_string = "%s_%s_%s" % (self.traceback_hash, location, self.error)
387 387 if self.language == Language.javascript:
388 388 hash_string = "%s_%s" % (self.traceback_hash, self.error)
389 389
390 390 elif default_grouping == "traceback_server":
391 391 hash_string = "%s_%s" % (self.traceback_hash, server_name)
392 392 if self.language == Language.javascript:
393 393 hash_string = "%s_%s" % (self.traceback_hash, server_name)
394 394 else:
395 395 hash_string = "%s_%s" % (self.error, location)
396 396 month = datetime.utcnow().date().replace(day=1)
397 397 hash_string = "{}_{}".format(month, hash_string)
398 398 binary_string = hash_string.encode("utf8")
399 399 self.grouping_hash = hashlib.sha1(binary_string).hexdigest()
400 400 return self.grouping_hash
401 401
402 402 def stripped_traceback(self):
403 403 """
404 404 Traceback without local vars
405 405 """
406 406 stripped_traceback = copy.deepcopy(self.traceback)
407 407
408 408 if isinstance(stripped_traceback, list):
409 409 for row in stripped_traceback:
410 410 row.pop("vars", None)
411 411 return stripped_traceback
412 412
413 413 def notify_channel(self, report_group):
414 414 """
415 415 Sends notification to websocket channel
416 416 """
417 417 settings = get_current_registry().settings
418 418 log.info("notify channelstream")
419 419 if self.report_type != ReportType.error:
420 420 return
421 421 payload = {
422 422 "type": "message",
423 423 "user": "__system__",
424 424 "channel": "app_%s" % self.resource_id,
425 425 "message": {
426 426 "topic": "front_dashboard.new_topic",
427 427 "report": {
428 428 "group": {
429 429 "priority": report_group.priority,
430 430 "first_timestamp": report_group.first_timestamp,
431 431 "last_timestamp": report_group.last_timestamp,
432 432 "average_duration": report_group.average_duration,
433 433 "occurences": report_group.occurences,
434 434 },
435 435 "report_id": self.id,
436 436 "group_id": self.group_id,
437 437 "resource_id": self.resource_id,
438 438 "http_status": self.http_status,
439 439 "url_domain": self.url_domain,
440 440 "url_path": self.url_path,
441 441 "error": self.error or "",
442 442 "server": self.tags.get("server_name"),
443 443 "view_name": self.tags.get("view_name"),
444 444 "front_url": self.get_public_url(),
445 445 },
446 446 },
447 447 }
448 448 channelstream_request(
449 449 settings["cometd.secret"],
450 450 "/message",
451 451 [payload],
452 452 servers=[settings["cometd_servers"]],
453 453 )
454 454
455 455 def es_doc(self):
456 456 tags = {}
457 457 tag_list = []
458 458 for name, value in self.tags.items():
459 459 name = name.replace(".", "_")
460 460 tag_list.append(name)
461 461 tags[name] = {
462 462 "values": convert_es_type(value),
463 463 "numeric_values": value
464 464 if (isinstance(value, (int, float)) and not isinstance(value, bool))
465 465 else None,
466 466 }
467 467
468 468 if "user_name" not in self.tags and self.username:
469 469 tags["user_name"] = {"value": [self.username], "numeric_value": None}
470 470 return {
471 471 "_id": str(self.id),
472 472 "pg_id": str(self.id),
473 473 "resource_id": self.resource_id,
474 474 "http_status": self.http_status or "",
475 475 "start_time": self.start_time,
476 476 "end_time": self.end_time,
477 477 "url_domain": self.url_domain if self.url_domain else "",
478 478 "url_path": self.url_path if self.url_path else "",
479 479 "duration": self.duration,
480 480 "error": self.error if self.error else "",
481 481 "report_type": self.report_type,
482 482 "request_id": self.request_id,
483 483 "ip": self.ip,
484 484 "group_id": str(self.group_id),
485 485 "_parent": str(self.group_id),
486 486 "tags": tags,
487 487 "tag_list": tag_list,
488 488 }
489 489
490 490 @property
491 491 def partition_id(self):
492 492 return "rcae_r_%s" % self.report_group_time.strftime("%Y_%m")
493 493
494 494 def partition_range(self):
495 495 start_date = self.report_group_time.date().replace(day=1)
496 496 end_date = start_date + timedelta(days=40)
497 497 end_date = end_date.replace(day=1)
498 498 return start_date, end_date
499 499
500 500
501 501 def after_insert(mapper, connection, target):
502 502 if not hasattr(target, "_skip_ft_index"):
503 503 data = target.es_doc()
504 504 data.pop("_id", None)
505 505 Datastores.es.index(
506 506 target.partition_id, "report", data, parent=target.group_id, id=target.id
507 507 )
508 508
509 509
510 510 def after_update(mapper, connection, target):
511 511 if not hasattr(target, "_skip_ft_index"):
512 512 data = target.es_doc()
513 513 data.pop("_id", None)
514 514 Datastores.es.index(
515 515 target.partition_id, "report", data, parent=target.group_id, id=target.id
516 516 )
517 517
518 518
519 519 def after_delete(mapper, connection, target):
520 520 if not hasattr(target, "_skip_ft_index"):
521 521 query = {"query": {"term": {"pg_id": target.id}}}
522 522 Datastores.es.transport.perform_request(
523 523 "DELETE", "/{}/{}/_query".format(target.partition_id, "report"), body=query
524 524 )
525 525
526 526
527 527 sa.event.listen(Report, "after_insert", after_insert)
528 528 sa.event.listen(Report, "after_update", after_update)
529 529 sa.event.listen(Report, "after_delete", after_delete)
General Comments 4
Under Review
author

Auto status change to "Under Review"

Under Review
author

Auto status change to "Under Review"

You need to be logged in to leave comments. Login now