Source code for analog.report

"""Analog log report object."""
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
from collections import Counter, defaultdict, OrderedDict
import time

from analog.renderers import Renderer
from analog.utils import PrefixMatchingCounter

try:
    from statistics import mean, median
except ImportError:
    from analog.statistics import mean, median

from analog import LOG


[docs]class ListStats(object): """Statistic analysis of a list of values. Provides the mean, median and 90th, 75th and 25th percentiles. """
[docs] def __init__(self, elements): """Calculate some stats from list of values. :param elements: list of values. :type elements: ``list`` """ self.mean = mean(elements) if elements else None self.median = median(elements) if elements else None
[docs]class Report(object): """Log analysis report object. Provides these statistical metrics: * Number for requests. * Response request method (HTTP verb) distribution. * Response status code distribution. * Requests per path. * Response time statistics (mean, median). * Response upstream time statistics (mean, median). * Response body size in bytes statistics (mean, median). * Per path request method (HTTP verb) distribution. * Per path response status code distribution. * Per path response time statistics (mean, median). * Per path response upstream time statistics (mean, median). * Per path response body size in bytes statistics (mean, median). """
[docs] def __init__(self, verbs, status_codes): """Create new log report object. Use ``add()`` method to add log entries to be analyzed. :param verbs: HTTP verbs to be tracked. :type verbs: ``list`` :param status_codes: status_codes to be tracked. May be prefixes, e.g. ["100", "2", "3", "4", "404" ] :type status_codes: ``list`` :returns: Report analysis object :rtype: :py:class:`analog.report.Report` """ def verb_counter(): return Counter({verb: 0 for verb in verbs}) def status_counter(): return PrefixMatchingCounter( {str(code): 0 for code in status_codes}) self._start_time = time.clock() self.execution_time = None self.requests = 0 self._verbs = verb_counter() self._status = status_counter() self._times = [] self._upstream_times = [] self._body_bytes = [] self._path_requests = Counter() self._path_verbs = defaultdict(verb_counter) self._path_status = defaultdict(status_counter) self._path_times = defaultdict(list) self._path_upstream_times = defaultdict(list) self._path_body_bytes = defaultdict(list)
[docs] def finish(self): """Stop execution timer.""" end_time = time.clock() self.execution_time = end_time - self._start_time
[docs] def add(self, path, verb, status, time, upstream_time, body_bytes): """Add a log entry to the report. Any request with ``verb`` not matching any of ``self._verbs`` or ``status`` not matching any of ``self._status`` is ignored. :param path: monitored request path. :type path: ``str`` :param verb: HTTP method (GET, POST, ...) :type verb: ``str`` :param status: response status code. :type status: ``int`` :param time: response time in seconds. :type time: ``float`` :param upstream_time: upstream response time in seconds. :type upstream_time: ``float`` :param body_bytes: response body size in bytes. :type body_bytes: ``float`` """ # Only keep entries with verbs/status codes that are being tracked if verb not in self._verbs or self._status.match(status) is None: LOG.debug("Ignoring log entry for non-tracked verb ({verb}) or " "status code ({status!s}).".format(verb=verb, status=status)) return self.requests += 1 self._verbs[verb] += 1 self._status.inc(str(status)) self._times.append(time) self._upstream_times.append(upstream_time) self._body_bytes.append(body_bytes) self._path_requests[path] += 1 self._path_verbs[path][verb] += 1 self._path_status[path].inc(status) self._path_times[path].append(time) self._path_upstream_times[path].append(upstream_time) self._path_body_bytes[path].append(body_bytes)
@property def verbs(self): """List request methods of all matched requests, ordered by frequency. :returns: tuples of HTTP verb and occurrency count. :rtype: ``list`` of ``tuple`` """ return self._verbs.most_common() @property def status(self): """List status codes of all matched requests, ordered by frequency. :returns: tuples of status code and occurrency count. :rtype: ``list`` of ``tuple`` """ return self._status.most_common() @property def times(self): """Response time statistics of all matched requests. :returns: response time statistics. :rtype: :py:class:`analog.report.ListStats` """ return ListStats(self._times) @property def upstream_times(self): """Response upstream time statistics of all matched requests. :returns: response upstream time statistics. :rtype: :py:class:`analog.report.ListStats` """ return ListStats(self._upstream_times) @property def body_bytes(self): """Response body size in bytes of all matched requests. :returns: response body size statistics. :rtype: :py:class:`analog.report.ListStats` """ return ListStats(self._body_bytes) @property def path_requests(self): """List paths of all matched requests, ordered by frequency. :returns: tuples of path and occurrency count. :rtype: ``list`` of ``tuple`` """ return self._path_requests.most_common() @property def path_verbs(self): """List request methods (HTTP verbs) of all matched requests per path. Verbs are grouped by path and ordered by frequency. :returns: path mapping of tuples of verb and occurrency count. :rtype: ``dict`` of ``list`` of ``tuple`` """ return OrderedDict( sorted(((path, counter.most_common()) for path, counter in self._path_verbs.items()), key=lambda item: item[0])) @property def path_status(self): """List status codes of all matched requests per path. Status codes are grouped by path and ordered by frequency. :returns: path mapping of tuples of status code and occurrency count. :rtype: ``dict`` of ``list`` of ``tuple`` """ return OrderedDict( sorted(((path, counter.most_common()) for path, counter in self._path_status.items()), key=lambda item: item[0])) @property def path_times(self): """Response time statistics of all matched requests per path. :returns: path mapping of response time statistics. :rtype: ``dict`` of :py:class:`analog.report.ListStats` """ return OrderedDict( sorted(((path, ListStats(values)) for path, values in self._path_times.items()), key=lambda item: item[0])) @property def path_upstream_times(self): """Response upstream time statistics of all matched requests per path. :returns: path mapping of response upstream time statistics. :rtype: ``dict`` of :py:class:`analog.report.ListStats` """ return OrderedDict( sorted(((path, ListStats(values)) for path, values in self._path_upstream_times.items()), key=lambda item: item[0])) @property def path_body_bytes(self): """Response body size in bytes of all matched requests per path. :returns: path mapping of body size statistics. :rtype: ``dict`` of :py:class:`analog.report.ListStats` """ return OrderedDict( sorted(((path, ListStats(values)) for path, values in self._path_body_bytes.items()), key=lambda item: item[0]))
[docs] def render(self, path_stats, output_format): """Render report data into ``output_format``. :param path_stats: include per path statistics in output. :type path_stats: ``bool`` :param output_format: name of report renderer. :type output_format: ``str`` :raises: :py:class:`analog.exceptions.UnknownRendererError` or unknown ``output_format`` identifiers. :returns: rendered report data. :rtype: ``str`` """ renderer = Renderer.by_name(name=output_format) return renderer.render(self, path_stats=path_stats)