dave-shawley/divak-tornado

View on GitHub
divak/api.py

Summary

Maintainability
A
0 mins
Test Coverage
import logging
import uuid
import weakref

from tornado import gen, web
import tornado.log

import divak.internals


class Recorder(web.Application):
    """Imbues an application with recording abilities."""

    def __init__(self, *args, **kwargs):
        super(Recorder, self).__init__(*args, **kwargs)
        self.add_transform(divak.internals.EnsureRequestIdTransformer)
        divak.internals.initialize_logging()

    def set_divak_service(self, service_name):
        """
        Set the name of the service for reporting purposes.

        :param str service_name: name to use when reporting to an
            observer

        """

    def add_divak_propagator(self, propagator):
        """
        Add a propagation instance that inspects each request.

        :param propagator: a propagator instance to inspect requests
            and potentially modify responses

        """
        propagator.install(self)

    def add_divak_reporter(self, reporter):
        """
        Add a reporter instance.

        :param reporter: a reporter instance to receive observations

        """

    def log_request(self, handler):
        """
        Override ``log_request`` to improve logging format.

        :param tornado.web.RequestHandler handler: the handler that
            processed the request

        """
        if handler.get_status() < 400:
            log_method = tornado.log.access_log.info
        elif handler.get_status() < 500:
            log_method = tornado.log.access_log.warning
        else:
            log_method = tornado.log.access_log.error

        request = handler.request  # type: tornado.httpserver.HTTPRequest
        args = {'remoteip': '127.0.0.1',
                'status': handler.get_status(),
                'elapsed': request.request_time(),
                'method': request.method,
                'uri': request.uri,
                'useragent': request.headers.get('User-Agent', '-'),
                'divak_request_id': getattr(request, 'divak_request_id', '-')}
        log_method(
            '{remoteip} "{method} {uri}" {status} "{useragent}" '
            '{elapsed:.6f}'.format(**args), extra=args)


class RequestIdPropagator(object):
    """
    Propagates Request-IDs between services.

    :param str header_name: the name of the request header to propagate.
        If this value is unspecified, then the header name defaults to
        ``Request-ID``.
    :keyword value_factory: if this keyword is specified, then it's value
        is called to generate a response header if a new header value is
        required.  If this value is unspecified, then a UUID4 will be
        generated.

    This class implements propagation of a request header into the
    response.  If the incoming request does not include a matching header,
    then a new value will be generated by calling `value_factory`.  You
    can disable the generation of new values by setting `value_factory`
    to :data:`None`.

    An instance of :class:`.HeaderRelayTransformer` is wired in to
    transform the request output by inserting the header value from the
    ``divak_request_id`` property of the active request.

    """

    def __init__(self, header_name='Request-Id', *args, **kwargs):
        super(RequestIdPropagator, self).__init__()
        self._header_name = header_name
        self._value_factory = kwargs.get('value_factory', uuid.uuid4)

    def install(self, application):
        """
        Install the propagator into the application.

        :param tornado.web.Application application: the application
            to install this propagator into
        :returns: :data:`True` if the propagator wants to be called
            in the future or :data:`False` otherwise
        :rtype: bool

        """
        application.add_transform(self.handle_request)
        return False

    def handle_request(self, request):
        """
        Initial transform function.

        :param tornado.web.httpserver.HTTPRequest request:
            the request that is being processed
        :return: a new instance of :class:`.HeaderRelayTransformer`
            that is configured to insert the request ID header
        :rtype: HeaderRelayTransformer

        This function is called to process each request.  It pulls the
        header value out of the request and assigns it to
        ``request.divak_request_id``.  If the incoming request does not
        have a request ID header, then a new value may be generated before
        assigning it.  Finally, a new instance of
        :class:`HeaderRelayTransformer` is created to process the output
        generated by processing `request`.

        """
        header_value = request.headers.get(self._header_name, None)
        if header_value is None and self._value_factory is not None:
            header_value = str(self._value_factory())
        request.divak_request_id = header_value
        return HeaderRelayTransformer(self._header_name, request)


class HeaderRelayTransformer(object):
    """
    Tornado transformer that relays a header from request to response.

    :param str header_name: the name of the header to relay from
        request to response
    :param value_factory: callable that generates a new value

    Setting `value_factory` to :data:`None` disables the generation
    of response header values when the header is missing from the
    request.

    This class implements the under-documented Tornado transform
    interface.  Transforms are called when the application starts
    processing a request.  Per-request instances of this class are
    returned from :meth:`.RequestIdPropagator.handle_request` with
    the appropriate header name and value to insert when the first
    chunk is processed.

    """

    def __init__(self, header_name, request):
        # using a weakref here to prevent cyclic references since
        # the transformer instance is attached to the request
        super(HeaderRelayTransformer, self).__init__()
        self._header_name = header_name
        self._get_request = weakref.ref(request)

    def transform_first_chunk(self, status_code, headers, chunk,
                              include_footers):
        """
        Called to process the first chunk.

        :param int status_code: status code that is going to be
            returned in the response
        :param tornado.httputil.HTTPHeaders headers: response headers
        :param chunk: the data chunk to transform
        :param bool include_footers: should footers be included?
        :return: the status code, headers, and chunk to use as a tuple

        This method will inject ``request.divak_request_id`` into
        `headers` if the value is not :data:`None`.  The remaining
        parameters are passed through as-is.

        """
        request = self._get_request()
        if request is not None and request.divak_request_id is not None:
            # would like to use setdefault but HTTPHeaders does
            # not support it in tornado 4.3
            if headers.get(self._header_name, None) is None:
                headers[self._header_name] = request.divak_request_id
        return status_code, headers, chunk

    def transform_chunk(self, chunk, include_footers):
        """
        Called to transform subsequent chunks.

        :param chunk: the data chunk to transform
        :param bool include_footers: should footers be included?
        :return: the possibly transformed chunk

        This implementation returns `chunk` as-is.

        """
        return chunk


class Logger(web.RequestHandler):
    """
    Imbues a :class:`tornado.web.RequestHandler` with a contextual logger.

    This class adds a ``logger`` attribute that inserts divak tags into
    the logging record.  Tags added by calling :meth:`.add_divak_tag` are
    automatically made available in log messages.  The ``divak_request_id``
    value is guaranteed to be available in all log messages provided that
    you are using :class:`.Application` in your application's class list.

    The ``logger`` attribute is set in :meth:`.prepare` and will wrap
    an existing ``logger`` attribute or create a new one using the self's
    class module and class name as the logger name.

    .. attribute:: logger

       A :class:`logging.LoggerAdapter` that inserts divak tags into log
       records using the ``extra`` dict.

    """

    def __init__(self, *args, **kwargs):
        self._logging_context = {}
        super(Logger, self).__init__(*args, **kwargs)

    @gen.coroutine
    def prepare(self):
        if hasattr(self, 'logger'):
            logger = self.logger
        else:
            full_name = '{}.{}'.format(self.__class__.__module__,
                                       self.__class__.__name__)
            logger = logging.getLogger(full_name)
        self.logger = logging.LoggerAdapter(logger, self._logging_context)
        self._logging_context['divak_request_id'] = (
            self.request.divak_request_id)

        maybe_future = super(Logger, self).prepare()
        if maybe_future:  # pragma: no cover -- pure paranoia
            yield maybe_future