|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
# Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
|
|
|
#
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
# You may obtain a copy of the License at
|
|
|
#
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
#
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
# See the License for the specific language governing permissions and
|
|
|
# limitations under the License.
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
import math
|
|
|
import uuid
|
|
|
import hashlib
|
|
|
import copy
|
|
|
import urllib.parse
|
|
|
import logging
|
|
|
import sqlalchemy as sa
|
|
|
|
|
|
from appenlight.models import Base, Datastores
|
|
|
from appenlight.lib.utils.date_utils import convert_date
|
|
|
from appenlight.lib.utils import convert_es_type
|
|
|
from appenlight.models.slow_call import SlowCall
|
|
|
from appenlight.lib.utils import channelstream_request
|
|
|
from appenlight.lib.enums import ReportType, Language
|
|
|
from pyramid.threadlocal import get_current_registry, get_current_request
|
|
|
from sqlalchemy.dialects.postgresql import JSON
|
|
|
from ziggurat_foundations.models.base import BaseModel
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
REPORT_TYPE_MATRIX = {
|
|
|
'http_status': {"type": 'int',
|
|
|
"ops": ('eq', 'ne', 'ge', 'le',)},
|
|
|
'group:priority': {"type": 'int',
|
|
|
"ops": ('eq', 'ne', 'ge', 'le',)},
|
|
|
'duration': {"type": 'float',
|
|
|
"ops": ('ge', 'le',)},
|
|
|
'url_domain': {"type": 'unicode',
|
|
|
"ops": ('eq', 'ne', 'startswith', 'endswith', 'contains',)},
|
|
|
'url_path': {"type": 'unicode',
|
|
|
"ops": ('eq', 'ne', 'startswith', 'endswith', 'contains',)},
|
|
|
'error': {"type": 'unicode',
|
|
|
"ops": ('eq', 'ne', 'startswith', 'endswith', 'contains',)},
|
|
|
'tags:server_name': {"type": 'unicode',
|
|
|
"ops": ('eq', 'ne', 'startswith', 'endswith',
|
|
|
'contains',)},
|
|
|
'traceback': {"type": 'unicode',
|
|
|
"ops": ('contains',)},
|
|
|
'group:occurences': {"type": 'int',
|
|
|
"ops": ('eq', 'ne', 'ge', 'le',)}
|
|
|
}
|
|
|
|
|
|
|
|
|
class Report(Base, BaseModel):
|
|
|
__tablename__ = 'reports'
|
|
|
__table_args__ = {'implicit_returning': False}
|
|
|
|
|
|
id = sa.Column(sa.Integer, nullable=False, primary_key=True)
|
|
|
group_id = sa.Column(sa.BigInteger,
|
|
|
sa.ForeignKey('reports_groups.id', ondelete='cascade',
|
|
|
onupdate='cascade'))
|
|
|
resource_id = sa.Column(sa.Integer(), nullable=False, index=True)
|
|
|
report_type = sa.Column(sa.Integer(), nullable=False, index=True)
|
|
|
error = sa.Column(sa.UnicodeText(), index=True)
|
|
|
extra = sa.Column(JSON(), default={})
|
|
|
request = sa.Column(JSON(), nullable=False, default={})
|
|
|
ip = sa.Column(sa.String(39), index=True, default='')
|
|
|
username = sa.Column(sa.Unicode(255), default='')
|
|
|
user_agent = sa.Column(sa.Unicode(255), default='')
|
|
|
url = sa.Column(sa.UnicodeText(), index=True)
|
|
|
request_id = sa.Column(sa.Text())
|
|
|
request_stats = sa.Column(JSON(), nullable=False, default={})
|
|
|
traceback = sa.Column(JSON(), nullable=False, default=None)
|
|
|
traceback_hash = sa.Column(sa.Text())
|
|
|
start_time = sa.Column(sa.DateTime(), default=datetime.utcnow,
|
|
|
server_default=sa.func.now())
|
|
|
end_time = sa.Column(sa.DateTime())
|
|
|
duration = sa.Column(sa.Float, default=0)
|
|
|
http_status = sa.Column(sa.Integer, index=True)
|
|
|
url_domain = sa.Column(sa.Unicode(100), index=True)
|
|
|
url_path = sa.Column(sa.Unicode(255), index=True)
|
|
|
tags = sa.Column(JSON(), nullable=False, default={})
|
|
|
language = sa.Column(sa.Integer(), default=0)
|
|
|
# this is used to determine partition for the report
|
|
|
report_group_time = sa.Column(sa.DateTime(), default=datetime.utcnow,
|
|
|
server_default=sa.func.now())
|
|
|
|
|
|
logs = sa.orm.relationship(
|
|
|
'Log',
|
|
|
lazy='dynamic',
|
|
|
passive_deletes=True,
|
|
|
passive_updates=True,
|
|
|
primaryjoin="and_(Report.request_id==Log.request_id, "
|
|
|
"Log.request_id != None, Log.request_id != '')",
|
|
|
foreign_keys='[Log.request_id]')
|
|
|
|
|
|
slow_calls = sa.orm.relationship('SlowCall',
|
|
|
backref='detail',
|
|
|
cascade="all, delete-orphan",
|
|
|
passive_deletes=True,
|
|
|
passive_updates=True,
|
|
|
order_by='SlowCall.timestamp')
|
|
|
|
|
|
def set_data(self, data, resource, protocol_version=None):
|
|
|
self.http_status = data['http_status']
|
|
|
self.priority = data['priority']
|
|
|
self.error = data['error']
|
|
|
report_language = data.get('language', '').lower()
|
|
|
self.language = getattr(Language, report_language, Language.unknown)
|
|
|
# we need temp holder here to decide later
|
|
|
# if we want to to commit the tags if report is marked for creation
|
|
|
self.tags = {
|
|
|
'server_name': data['server'],
|
|
|
'view_name': data['view_name']
|
|
|
}
|
|
|
if data.get('tags'):
|
|
|
for tag_tuple in data['tags']:
|
|
|
self.tags[tag_tuple[0]] = tag_tuple[1]
|
|
|
self.traceback = data['traceback']
|
|
|
stripped_traceback = self.stripped_traceback()
|
|
|
tb_repr = repr(stripped_traceback).encode('utf8')
|
|
|
self.traceback_hash = hashlib.sha1(tb_repr).hexdigest()
|
|
|
url_info = urllib.parse.urlsplit(
|
|
|
data.get('url', ''), allow_fragments=False)
|
|
|
self.url_domain = url_info.netloc[:128]
|
|
|
self.url_path = url_info.path[:2048]
|
|
|
self.occurences = data['occurences']
|
|
|
if self.error:
|
|
|
self.report_type = ReportType.error
|
|
|
else:
|
|
|
self.report_type = ReportType.slow
|
|
|
|
|
|
# but if its status 404 its 404 type
|
|
|
if self.http_status in [404, '404'] or self.error == '404 Not Found':
|
|
|
self.report_type = ReportType.not_found
|
|
|
self.error = ''
|
|
|
|
|
|
self.generate_grouping_hash(data.get('appenlight.group_string',
|
|
|
data.get('group_string')),
|
|
|
resource.default_grouping,
|
|
|
protocol_version)
|
|
|
|
|
|
# details
|
|
|
if data['http_status'] in [404, '404']:
|
|
|
data = {"username": data["username"],
|
|
|
"ip": data["ip"],
|
|
|
"url": data["url"],
|
|
|
"user_agent": data["user_agent"]}
|
|
|
if data.get('HTTP_REFERER') or data.get('http_referer'):
|
|
|
data['HTTP_REFERER'] = data.get(
|
|
|
'HTTP_REFERER', '') or data.get('http_referer', '')
|
|
|
|
|
|
self.resource_id = resource.resource_id
|
|
|
self.username = data['username']
|
|
|
self.user_agent = data['user_agent']
|
|
|
self.ip = data['ip']
|
|
|
self.extra = {}
|
|
|
if data.get('extra'):
|
|
|
for extra_tuple in data['extra']:
|
|
|
self.extra[extra_tuple[0]] = extra_tuple[1]
|
|
|
|
|
|
self.url = data['url']
|
|
|
self.request_id = data.get('request_id', '').replace('-', '') or str(
|
|
|
uuid.uuid4())
|
|
|
request_data = data.get('request', {})
|
|
|
|
|
|
self.request = request_data
|
|
|
self.request_stats = data.get('request_stats', {})
|
|
|
traceback = data.get('traceback')
|
|
|
if not traceback:
|
|
|
traceback = data.get('frameinfo')
|
|
|
self.traceback = traceback
|
|
|
start_date = convert_date(data.get('start_time'))
|
|
|
if not self.start_time or self.start_time < start_date:
|
|
|
self.start_time = start_date
|
|
|
|
|
|
self.end_time = convert_date(data.get('end_time'), False)
|
|
|
self.duration = 0
|
|
|
|
|
|
if self.start_time and self.end_time:
|
|
|
d = self.end_time - self.start_time
|
|
|
self.duration = d.total_seconds()
|
|
|
|
|
|
# update tags with other vars
|
|
|
if self.username:
|
|
|
self.tags['user_name'] = self.username
|
|
|
self.tags['report_language'] = Language.key_from_value(self.language)
|
|
|
|
|
|
def add_slow_calls(self, data, report_group):
|
|
|
slow_calls = []
|
|
|
for call in data.get('slow_calls', []):
|
|
|
sc_inst = SlowCall()
|
|
|
sc_inst.set_data(call, resource_id=self.resource_id,
|
|
|
report_group=report_group)
|
|
|
slow_calls.append(sc_inst)
|
|
|
self.slow_calls.extend(slow_calls)
|
|
|
return slow_calls
|
|
|
|
|
|
def get_dict(self, request, details=False, exclude_keys=None,
|
|
|
include_keys=None):
|
|
|
from appenlight.models.services.report_group import ReportGroupService
|
|
|
instance_dict = super(Report, self).get_dict()
|
|
|
instance_dict['req_stats'] = self.req_stats()
|
|
|
instance_dict['group'] = {}
|
|
|
instance_dict['group']['id'] = self.report_group.id
|
|
|
instance_dict['group'][
|
|
|
'total_reports'] = self.report_group.total_reports
|
|
|
instance_dict['group']['last_report'] = self.report_group.last_report
|
|
|
instance_dict['group']['priority'] = self.report_group.priority
|
|
|
instance_dict['group']['occurences'] = self.report_group.occurences
|
|
|
instance_dict['group'][
|
|
|
'last_timestamp'] = self.report_group.last_timestamp
|
|
|
instance_dict['group'][
|
|
|
'first_timestamp'] = self.report_group.first_timestamp
|
|
|
instance_dict['group']['public'] = self.report_group.public
|
|
|
instance_dict['group']['fixed'] = self.report_group.fixed
|
|
|
instance_dict['group']['read'] = self.report_group.read
|
|
|
instance_dict['group'][
|
|
|
'average_duration'] = self.report_group.average_duration
|
|
|
|
|
|
instance_dict[
|
|
|
'resource_name'] = self.report_group.application.resource_name
|
|
|
instance_dict['report_type'] = self.report_type
|
|
|
|
|
|
if instance_dict['http_status'] == 404 and not instance_dict['error']:
|
|
|
instance_dict['error'] = '404 Not Found'
|
|
|
|
|
|
if details:
|
|
|
instance_dict['affected_users_count'] = \
|
|
|
ReportGroupService.affected_users_count(self.report_group)
|
|
|
instance_dict['top_affected_users'] = [
|
|
|
{'username': u.username, 'count': u.count} for u in
|
|
|
ReportGroupService.top_affected_users(self.report_group)]
|
|
|
instance_dict['application'] = {'integrations': []}
|
|
|
for integration in self.report_group.application.integrations:
|
|
|
if integration.front_visible:
|
|
|
instance_dict['application']['integrations'].append(
|
|
|
{'name': integration.integration_name,
|
|
|
'action': integration.integration_action})
|
|
|
instance_dict['comments'] = [c.get_dict() for c in
|
|
|
self.report_group.comments]
|
|
|
|
|
|
instance_dict['group']['next_report'] = None
|
|
|
instance_dict['group']['previous_report'] = None
|
|
|
next_in_group = self.get_next_in_group(request)
|
|
|
previous_in_group = self.get_previous_in_group(request)
|
|
|
if next_in_group:
|
|
|
instance_dict['group']['next_report'] = next_in_group
|
|
|
if previous_in_group:
|
|
|
instance_dict['group']['previous_report'] = previous_in_group
|
|
|
|
|
|
# slow call ordering
|
|
|
def find_parent(row, data):
|
|
|
for r in reversed(data):
|
|
|
try:
|
|
|
if (row['timestamp'] > r['timestamp'] and
|
|
|
row['end_time'] < r['end_time']):
|
|
|
return r
|
|
|
except TypeError as e:
|
|
|
log.warning('reports_view.find_parent: %s' % e)
|
|
|
return None
|
|
|
|
|
|
new_calls = []
|
|
|
calls = [c.get_dict() for c in self.slow_calls]
|
|
|
while calls:
|
|
|
# start from end
|
|
|
for x in range(len(calls) - 1, -1, -1):
|
|
|
parent = find_parent(calls[x], calls)
|
|
|
if parent:
|
|
|
parent['children'].append(calls[x])
|
|
|
else:
|
|
|
# no parent at all? append to new calls anyways
|
|
|
new_calls.append(calls[x])
|
|
|
# print 'append', calls[x]
|
|
|
del calls[x]
|
|
|
break
|
|
|
instance_dict['slow_calls'] = new_calls
|
|
|
|
|
|
instance_dict['front_url'] = self.get_public_url(request)
|
|
|
|
|
|
exclude_keys_list = exclude_keys or []
|
|
|
include_keys_list = include_keys or []
|
|
|
for k in list(instance_dict.keys()):
|
|
|
if k == 'group':
|
|
|
continue
|
|
|
if (k in exclude_keys_list or
|
|
|
(k not in include_keys_list and include_keys)):
|
|
|
del instance_dict[k]
|
|
|
return instance_dict
|
|
|
|
|
|
def get_previous_in_group(self, request):
|
|
|
query = {
|
|
|
"size": 1,
|
|
|
"query": {
|
|
|
"filtered": {
|
|
|
"filter": {
|
|
|
"and": [{"term": {"group_id": self.group_id}},
|
|
|
{"range": {"pg_id": {"lt": self.id}}}]
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
"sort": [
|
|
|
{"_doc": {"order": "desc"}},
|
|
|
],
|
|
|
}
|
|
|
result = request.es_conn.search(query, index=self.partition_id,
|
|
|
doc_type='report')
|
|
|
if result['hits']['total']:
|
|
|
return result['hits']['hits'][0]['_source']['pg_id']
|
|
|
|
|
|
def get_next_in_group(self, request):
|
|
|
query = {
|
|
|
"size": 1,
|
|
|
"query": {
|
|
|
"filtered": {
|
|
|
"filter": {
|
|
|
"and": [{"term": {"group_id": self.group_id}},
|
|
|
{"range": {"pg_id": {"gt": self.id}}}]
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
"sort": [
|
|
|
{"_doc": {"order": "asc"}},
|
|
|
],
|
|
|
}
|
|
|
result = request.es_conn.search(query, index=self.partition_id,
|
|
|
doc_type='report')
|
|
|
if result['hits']['total']:
|
|
|
return result['hits']['hits'][0]['_source']['pg_id']
|
|
|
|
|
|
def get_public_url(self, request=None, report_group=None, _app_url=None):
|
|
|
"""
|
|
|
Returns url that user can use to visit specific report
|
|
|
"""
|
|
|
if not request:
|
|
|
request = get_current_request()
|
|
|
url = request.route_url('/', _app_url=_app_url)
|
|
|
if report_group:
|
|
|
return (url + 'ui/report/%s/%s') % (report_group.id, self.id)
|
|
|
return (url + 'ui/report/%s/%s') % (self.group_id, self.id)
|
|
|
|
|
|
def req_stats(self):
|
|
|
stats = self.request_stats.copy()
|
|
|
stats['percentages'] = {}
|
|
|
stats['percentages']['main'] = 100.0
|
|
|
main = stats.get('main', 0.0)
|
|
|
if not main:
|
|
|
return None
|
|
|
for name, call_time in stats.items():
|
|
|
if ('calls' not in name and 'main' not in name and
|
|
|
'percentages' not in name):
|
|
|
stats['main'] -= call_time
|
|
|
stats['percentages'][name] = math.floor(
|
|
|
(call_time / main * 100.0))
|
|
|
stats['percentages']['main'] -= stats['percentages'][name]
|
|
|
if stats['percentages']['main'] < 0.0:
|
|
|
stats['percentages']['main'] = 0.0
|
|
|
stats['main'] = 0.0
|
|
|
return stats
|
|
|
|
|
|
def generate_grouping_hash(self, hash_string=None, default_grouping=None,
|
|
|
protocol_version=None):
|
|
|
"""
|
|
|
Generates SHA1 hash that will be used to group reports together
|
|
|
"""
|
|
|
if not hash_string:
|
|
|
location = self.tags.get('view_name') or self.url_path;
|
|
|
server_name = self.tags.get('server_name') or ''
|
|
|
if default_grouping == 'url_traceback':
|
|
|
hash_string = '%s_%s_%s' % (self.traceback_hash, location,
|
|
|
self.error)
|
|
|
if self.language == Language.javascript:
|
|
|
hash_string = '%s_%s' % (self.traceback_hash, self.error)
|
|
|
|
|
|
elif default_grouping == 'traceback_server':
|
|
|
hash_string = '%s_%s' % (self.traceback_hash, server_name)
|
|
|
if self.language == Language.javascript:
|
|
|
hash_string = '%s_%s' % (self.traceback_hash, server_name)
|
|
|
else:
|
|
|
hash_string = '%s_%s' % (self.error, location)
|
|
|
month = datetime.utcnow().date().replace(day=1)
|
|
|
hash_string = '{}_{}'.format(month, hash_string)
|
|
|
binary_string = hash_string.encode('utf8')
|
|
|
self.grouping_hash = hashlib.sha1(binary_string).hexdigest()
|
|
|
return self.grouping_hash
|
|
|
|
|
|
def stripped_traceback(self):
|
|
|
"""
|
|
|
Traceback without local vars
|
|
|
"""
|
|
|
stripped_traceback = copy.deepcopy(self.traceback)
|
|
|
|
|
|
if isinstance(stripped_traceback, list):
|
|
|
for row in stripped_traceback:
|
|
|
row.pop('vars', None)
|
|
|
return stripped_traceback
|
|
|
|
|
|
def notify_channel(self, report_group):
|
|
|
"""
|
|
|
Sends notification to websocket channel
|
|
|
"""
|
|
|
settings = get_current_registry().settings
|
|
|
log.info('notify channelstream')
|
|
|
if self.report_type != ReportType.error:
|
|
|
return
|
|
|
payload = {
|
|
|
'type': 'message',
|
|
|
"user": '__system__',
|
|
|
"channel": 'app_%s' % self.resource_id,
|
|
|
'message': {
|
|
|
'topic': 'front_dashboard.new_topic',
|
|
|
'report': {
|
|
|
'group': {
|
|
|
'priority': report_group.priority,
|
|
|
'first_timestamp': report_group.first_timestamp,
|
|
|
'last_timestamp': report_group.last_timestamp,
|
|
|
'average_duration': report_group.average_duration,
|
|
|
'occurences': report_group.occurences
|
|
|
},
|
|
|
'report_id': self.id,
|
|
|
'group_id': self.group_id,
|
|
|
'resource_id': self.resource_id,
|
|
|
'http_status': self.http_status,
|
|
|
'url_domain': self.url_domain,
|
|
|
'url_path': self.url_path,
|
|
|
'error': self.error or '',
|
|
|
'server': self.tags.get('server_name'),
|
|
|
'view_name': self.tags.get('view_name'),
|
|
|
'front_url': self.get_public_url(),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
}
|
|
|
channelstream_request(settings['cometd.secret'], '/message', [payload],
|
|
|
servers=[settings['cometd_servers']])
|
|
|
|
|
|
def es_doc(self):
|
|
|
tags = {}
|
|
|
tag_list = []
|
|
|
for name, value in self.tags.items():
|
|
|
name = name.replace('.', '_')
|
|
|
tag_list.append(name)
|
|
|
tags[name] = {
|
|
|
"values": convert_es_type(value),
|
|
|
"numeric_values": value if (
|
|
|
isinstance(value, (int, float)) and
|
|
|
not isinstance(value, bool)) else None}
|
|
|
|
|
|
if 'user_name' not in self.tags and self.username:
|
|
|
tags["user_name"] = {"value": [self.username],
|
|
|
"numeric_value": None}
|
|
|
return {
|
|
|
'_id': str(self.id),
|
|
|
'pg_id': str(self.id),
|
|
|
'resource_id': self.resource_id,
|
|
|
'http_status': self.http_status or '',
|
|
|
'start_time': self.start_time,
|
|
|
'end_time': self.end_time,
|
|
|
'url_domain': self.url_domain if self.url_domain else '',
|
|
|
'url_path': self.url_path if self.url_path else '',
|
|
|
'duration': self.duration,
|
|
|
'error': self.error if self.error else '',
|
|
|
'report_type': self.report_type,
|
|
|
'request_id': self.request_id,
|
|
|
'ip': self.ip,
|
|
|
'group_id': str(self.group_id),
|
|
|
'_parent': str(self.group_id),
|
|
|
'tags': tags,
|
|
|
'tag_list': tag_list
|
|
|
}
|
|
|
|
|
|
@property
|
|
|
def partition_id(self):
|
|
|
return 'rcae_r_%s' % self.report_group_time.strftime('%Y_%m')
|
|
|
|
|
|
def partition_range(self):
|
|
|
start_date = self.report_group_time.date().replace(day=1)
|
|
|
end_date = start_date + timedelta(days=40)
|
|
|
end_date = end_date.replace(day=1)
|
|
|
return start_date, end_date
|
|
|
|
|
|
|
|
|
def after_insert(mapper, connection, target):
|
|
|
if not hasattr(target, '_skip_ft_index'):
|
|
|
data = target.es_doc()
|
|
|
data.pop('_id', None)
|
|
|
Datastores.es.index(target.partition_id, 'report', data,
|
|
|
parent=target.group_id, id=target.id)
|
|
|
|
|
|
|
|
|
def after_update(mapper, connection, target):
|
|
|
if not hasattr(target, '_skip_ft_index'):
|
|
|
data = target.es_doc()
|
|
|
data.pop('_id', None)
|
|
|
Datastores.es.index(target.partition_id, 'report', data,
|
|
|
parent=target.group_id, id=target.id)
|
|
|
|
|
|
|
|
|
def after_delete(mapper, connection, target):
|
|
|
if not hasattr(target, '_skip_ft_index'):
|
|
|
query = {'term': {'pg_id': target.id}}
|
|
|
Datastores.es.delete_by_query(target.partition_id, 'report', query)
|
|
|
|
|
|
|
|
|
sa.event.listen(Report, 'after_insert', after_insert)
|
|
|
sa.event.listen(Report, 'after_update', after_update)
|
|
|
sa.event.listen(Report, 'after_delete', after_delete)
|
|
|
|