diff --git a/tsstats/__main__.py b/tsstats/__main__.py index 5e7e621..e469b92 100644 --- a/tsstats/__main__.py +++ b/tsstats/__main__.py @@ -69,8 +69,13 @@ def main(config=None, idmap=None, log=None, if not log or not output: raise InvalidConfiguration('log or output missing') - clients = parse_logs(log, ident_map=identmap, online_dc=noonlinedc) - render_template(clients, output=abspath(output)) + sid_clients = parse_logs(log, ident_map=identmap, online_dc=noonlinedc) + for sid, clients in sid_clients.items(): + if sid: + ext = '.{}'.format(sid) + else: + ext = '' + render_template(clients, output=abspath(output + ext)) if __name__ == '__main__': diff --git a/tsstats/log.py b/tsstats/log.py index 08570f6..5b3bf43 100644 --- a/tsstats/log.py +++ b/tsstats/log.py @@ -2,11 +2,15 @@ import logging import re +from collections import namedtuple from datetime import datetime from glob import glob +from os.path import basename from tsstats.client import Client, Clients +re_log_filename = re.compile(r'ts3server_(?P<date>\d{4}-\d\d-\d\d)' + '__(?P<time>\d\d_\d\d_\d\d.\d+)_(?P<sid>\d).log') re_log_entry = re.compile('(?P<timestamp>\d{4}-\d\d-\d\d\ \d\d:\d\d:\d\d.\d+)' '\|\ *(?P<level>\w+)\ *\|\ *(?P<component>\w+)\ *' '\|\ *(?P<sid>\d+)\ *\|\ *(?P<message>.*)') @@ -17,32 +21,83 @@ re_disconnect_invoker = re.compile( log_timestamp_format = '%Y-%m-%d %H:%M:%S.%f' +TimedLog = namedtuple('TimedLog', ['path', 'timestamp']) + logger = logging.getLogger('tsstats') def parse_logs(log_glob, ident_map=None, *args, **kwargs): ''' - parse logs specified by globbing pattern `log_glob` + parse logs from `log_glob` - :param log_glob: path to log-files (supports globbing) - :param ident_map: :doc:`identmap` + :param log_glob: path to server-logs (supports globbing) + :param ident_map: identmap used for Client-initializations :type log_glob: str :type ident_map: dict - :return: parsed clients - :rtype: tsstats.client.Clients + :return: clients bundled by virtual-server + :rtype: dict ''' - clients = Clients(ident_map) - for log_file in sorted(log_file for log_file in glob(log_glob)): - clients = parse_log(log_file, ident_map, clients, *args, **kwargs) - return clients + vserver_clients = {} + for virtualserver_id, logs in\ + _bundle_logs(log_file for log_file in glob(log_glob)).items(): + clients = Clients(ident_map) + for log in logs: + _parse_details(log.path, clients=clients, *args, **kwargs) + if len(clients) >= 1: + vserver_clients[virtualserver_id] = clients + return vserver_clients -def parse_log(log_path, ident_map=None, clients=None, online_dc=True): +def _bundle_logs(logs): ''' - parse log-file at `log_path` + bundle `logs` by virtualserver-id + and sort by timestamp from filename (if exists) + + :param logs: list of paths to logfiles + + :type logs: list + + :return: `logs` bundled by virtualserver-id and sorted by timestamp + :rtype: dict{str: [TimedLog]} + ''' + vserver_logfiles = {} # sid: [/path/to/log1, ..., /path/to/logn] + for log in logs: + # try to get date and sid from filename + match = re_log_filename.match(basename(log)) + if match: + match = match.groupdict() + timestamp = datetime.strptime('{0} {1}'.format( + match['date'], match['time'].replace('_', ':')), + log_timestamp_format) + tl = TimedLog(log, timestamp) + sid = match['sid'] + if sid in vserver_logfiles: + # if already exists, keep list sorted by timestamp + vserver_logfiles[sid].append(tl) + vserver_logfiles[sid] =\ + sorted(vserver_logfiles[sid], + key=lambda tl: tl.timestamp) + else: + # if not exists, just create a list + vserver_logfiles[match['sid']] = [tl] + else: + # fallback to plain sorting + vserver_logfiles.setdefault('', [])\ + .append(TimedLog(log, None)) + vserver_logfiles[''] =\ + sorted(vserver_logfiles[''], + key=lambda tl: tl.path) + return vserver_logfiles + + +def _parse_details(log_path, ident_map=None, clients=None, online_dc=True): + ''' + extract details from log-files + + detailed parsing is done here: onlinetime, kicks, pkicks, bans, pbans :param log_path: path to log-file :param ident_map: :doc:`identmap` @@ -57,7 +112,7 @@ def parse_log(log_path, ident_map=None, clients=None, online_dc=True): :return: parsed clients :rtype: tsstats.client.Clients ''' - if not clients: + if clients is None: clients = Clients(ident_map) log_file = open(log_path) # process lines diff --git a/tsstats/tests/test_log.py b/tsstats/tests/test_log.py index cd0a62f..1dc1689 100644 --- a/tsstats/tests/test_log.py +++ b/tsstats/tests/test_log.py @@ -1,17 +1,17 @@ -from datetime import timedelta +from datetime import datetime, timedelta from time import sleep import pytest from tsstats.exceptions import InvalidLog -from tsstats.log import parse_log, parse_logs +from tsstats.log import TimedLog, _bundle_logs, _parse_details, parse_logs testlog_path = 'tsstats/tests/res/test.log' @pytest.fixture def clients(): - return parse_log(testlog_path, online_dc=False) + return _parse_details(testlog_path, online_dc=False) def test_log_client_count(clients): @@ -39,20 +39,46 @@ def test_log_pbans(clients): assert clients['2'].pbans == 1 +@pytest.mark.parametrize("logs,bundled", [ + ( + ['l1.log', 'l2.log'], + {'': [TimedLog('l1.log', None), TimedLog('l2.log', None)]} + ), + ( + [ + 'ts3server_2016-06-06__14_22_09.527229_1.log', + 'ts3server_2017-07-07__15_23_10.638340_1.log' + ], + { + '1': [ + TimedLog('ts3server_2016-06-06__14_22_09.527229_1.log', + datetime(year=2016, month=6, day=6, hour=14, + minute=22, second=9, microsecond=527229)), + TimedLog('ts3server_2017-07-07__15_23_10.638340_1.log', + datetime(year=2017, month=7, day=7, hour=15, + minute=23, second=10, microsecond=638340)) + ] + } + ) +]) +def test_log_bundle(logs, bundled): + assert _bundle_logs(logs) == bundled + + def test_log_invalid(): with pytest.raises(InvalidLog): - parse_log('tsstats/tests/res/test.log.broken') - - -def test_log_multiple(): - assert len(parse_log(testlog_path, online_dc=False)) == \ - len(parse_logs(testlog_path, online_dc=False)) + _parse_details('tsstats/tests/res/test.log.broken') @pytest.mark.slowtest def test_log_client_online(): - clients = parse_log(testlog_path) + clients = _parse_details(testlog_path) old_onlinetime = int(clients['1'].onlinetime.total_seconds()) sleep(2) - clients = parse_log(testlog_path) + clients = _parse_details(testlog_path) assert int(clients['1'].onlinetime.total_seconds()) == old_onlinetime + 2 + + +def test_parse_logs(): + assert len(_parse_details(testlog_path)) ==\ + len(parse_logs(testlog_path)['']) diff --git a/tsstats/tests/test_template.py b/tsstats/tests/test_template.py index 9652cd5..8e90b45 100644 --- a/tsstats/tests/test_template.py +++ b/tsstats/tests/test_template.py @@ -5,12 +5,12 @@ from os import remove import pytest from bs4 import BeautifulSoup -from tsstats.log import parse_log +from tsstats.log import _parse_details from tsstats.template import render_template from tsstats.utils import seconds_to_text output_path = 'tsstats/tests/res/output.html' -clients = parse_log('tsstats/tests/res/test.log') +clients = _parse_details('tsstats/tests/res/test.log') logger = logging.getLogger('tsstats')