"""RPLogger class for low-level logging in tests.""" import sys import logging from contextlib import contextmanager from functools import wraps from six import PY2 class RPLogger(logging.getLoggerClass()): """RPLogger class for logging tests.""" def __init__(self, name, level=0): """ Initialize RPLogger instance. :param name: logger name :param level: level of logs """ super(RPLogger, self).__init__(name, level=level) def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, attachment=None): """ Low-level logging routine which creates a LogRecord and then calls. all the handlers of this logger to handle the record :param level: level of log :param msg: message in log body :param args: additional args :param exc_info: system exclusion info :param extra: extra info :param stack_info: stacktrace info :param attachment: attachment file """ sinfo = None if logging._srcfile: # IronPython doesn't track Python frames, so findCaller raises an # exception on some versions of IronPython. We trap it here so that # IronPython can use logging. try: if PY2: # In python2.7 findCaller() don't accept any parameters # and returns 3 elements fn, lno, func = self.findCaller() else: fn, lno, func, sinfo = self.findCaller(stack_info) except ValueError: # pragma: no cover fn, lno, func = '(unknown file)', 0, '(unknown function)' else: fn, lno, func = '(unknown file)', 0, '(unknown function)' if exc_info and not isinstance(exc_info, tuple): exc_info = sys.exc_info() if PY2: # In python2.7 makeRecord() accepts everything but sinfo record = self.makeRecord(self.name, level, fn, lno, msg, args, exc_info, func, extra) else: record = self.makeRecord(self.name, level, fn, lno, msg, args, exc_info, func, extra, sinfo) if not getattr(record, 'attachment', None): record.attachment = attachment self.handle(record) class RPLogHandler(logging.Handler): """RPLogHandler class for logging tests.""" # Map loglevel codes from `logging` module to ReportPortal text names: _loglevel_map = { logging.NOTSET: 'TRACE', logging.DEBUG: 'DEBUG', logging.INFO: 'INFO', logging.WARNING: 'WARN', logging.ERROR: 'ERROR', logging.CRITICAL: 'ERROR', } _sorted_levelnos = sorted(_loglevel_map.keys(), reverse=True) def __init__(self, py_test_service, level=logging.NOTSET, filter_client_logs=False, endpoint=None): """ Initialize RPLogHandler instance. :param py_test_service: RP Service instance :param level: level of logging :param filter_client_logs: if True throw away logs emitted by a ReportPortal client :param endpoint: link to send reports """ super(RPLogHandler, self).__init__(level) self.py_test_service = py_test_service self.filter_client_logs = filter_client_logs self.ignored_record_names = ('reportportal_client', 'pytest_reportportal') self.endpoint = endpoint def filter(self, record): """ Filter the reportportal_client messages. :param record: a log record to filter :return: bool - False if it is an agent or client log and 'filter_client_logs' attribute is True, other way always True """ if not self.filter_client_logs: return True if record.name.startswith(self.ignored_record_names): return False if record.name == 'urllib3.connectionpool': # Filter the reportportal_client requests instance # urllib3 usage if self.endpoint in self.format(record): return False return True def emit(self, record): """ Emit function. :param record: a log Record of requests :return: log ID """ msg = '' try: msg = self.format(record) except (KeyboardInterrupt, SystemExit): raise except Exception: self.handleError(record) for level in self._sorted_levelnos: if level <= record.levelno: return self.py_test_service.post_log( msg, loglevel=self._loglevel_map[level], attachment=record.__dict__.get('attachment', None), ) @contextmanager def patching_logger_class(): """ Add patch for RPLogger class. Updated attachment in logs :return: wrapped function """ logger_class = logging.getLoggerClass() original_log = logger_class._log original_makeRecord = logger_class.makeRecord try: def wrap_log(original_func): @wraps(original_func) def _log(self, *args, **kwargs): attachment = kwargs.pop('attachment', None) if attachment is not None: kwargs.setdefault('extra', {}).update( {'attachment': attachment}) return original_func(self, *args, **kwargs) return _log def wrap_makeRecord(original_func): @wraps(original_func) def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None, sinfo=None): if extra is not None: attachment = extra.pop('attachment', None) else: attachment = None try: # Python 3.5 record = original_func(self, name, level, fn, lno, msg, args, exc_info, func=func, extra=extra, sinfo=sinfo) except TypeError: # Python 2.7 record = original_func(self, name, level, fn, lno, msg, args, exc_info, func=func, extra=extra) record.attachment = attachment return record return makeRecord if not issubclass(logger_class, RPLogger): logger_class._log = wrap_log(logger_class._log) logger_class.makeRecord = wrap_makeRecord(logger_class.makeRecord) logging.setLoggerClass(RPLogger) yield finally: if not issubclass(logger_class, RPLogger): logger_class._log = original_log logger_class.makeRecord = original_makeRecord logging.setLoggerClass(logger_class)