Source code for cardinal_pythonlib.wsgi.request_logging_mw

#!/usr/bin/env python
# cardinal_pythonlib/reqest_logging_mw.py

"""
===============================================================================

    Original code copyright (C) 2009-2022 Rudolf Cardinal (rudolf@pobox.com).

    This file is part of cardinal_pythonlib.

    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

        https://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.

===============================================================================

**WSGI middleware to log incoming request/response details.**

"""

import logging
from typing import List, Optional

from pendulum import DateTime as Pendulum

from cardinal_pythonlib.logs import BraceStyleAdapter
from cardinal_pythonlib.wsgi.constants import (
    TYPE_WSGI_APP,
    TYPE_WSGI_APP_RESULT,
    TYPE_WSGI_ENVIRON,
    TYPE_WSGI_EXC_INFO,
    TYPE_WSGI_RESPONSE_HEADERS,
    TYPE_WSGI_START_RESPONSE,
    TYPE_WSGI_START_RESP_RESULT,
    TYPE_WSGI_STATUS,
    WsgiEnvVar,
)  # nopep8

log = BraceStyleAdapter(logging.getLogger(__name__))


[docs]class RequestLoggingMiddleware(object): """ WSGI middleware to log incoming request details (+/- the response status code and timing information). """ def __init__( self, app: TYPE_WSGI_APP, logger: logging.Logger = log, loglevel: int = logging.INFO, show_request_immediately: bool = True, show_response: bool = True, show_timing: bool = True, ) -> None: """ Args: app: The WSGI application to wrap logger: The Python logger to write to loglevel: The log level to use (e.g. ``logging.DEBUG``, ``logging.INFO``) show_request_immediately: Show the request immediately, so it's written to the log before the WSGI app does its processing, and is guaranteed to be visible even if the WSGI app hangs? The only reason to use ``False`` is probably if you intend to show response and/or timing information and you want to minimize the number of lines written to the log; in this case, only a single line is written to the log (after the wrapped WSGI app has finished processing). show_response: Show the HTTP response code? show_timing: Show the time that the wrapped WSGI app took? """ self.app = app self.logger = logger self.loglevel = loglevel self.show_response = show_response self.show_request_immediately = show_request_immediately self.show_timing = show_timing self.two_parts = show_request_immediately and ( show_response or show_timing )
[docs] def log(self, msg) -> None: """ Writes a message to the chosen log. """ self.logger.log(self.loglevel, msg)
def __call__( self, environ: TYPE_WSGI_ENVIRON, start_response: TYPE_WSGI_START_RESPONSE, ) -> TYPE_WSGI_APP_RESULT: query_string = environ.get(WsgiEnvVar.QUERY_STRING, "") try: # https://stackoverflow.com/questions/7835030/obtaining-client-ip-address-from-a-wsgi-app-using-eventlet # noqa # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For # noqa forwarded_for = " [forwarded for {}]".format( environ[WsgiEnvVar.HTTP_X_FORWARDED_FOR] ) except KeyError: forwarded_for = "" request_details = ( '{remote}{fwd}: "{method} {path}{qmark}{query} {proto}"'.format( remote=environ.get(WsgiEnvVar.REMOTE_ADDR, ""), fwd=forwarded_for, method=environ.get(WsgiEnvVar.REQUEST_METHOD, ""), path=environ.get(WsgiEnvVar.PATH_INFO, ""), qmark="?" if query_string else "", query=query_string, proto=environ.get(WsgiEnvVar.SERVER_PROTOCOL, ""), ) ) msg_parts = [] # type: List[str] if self.show_request_immediately: msg_parts.append("Request from") msg_parts.append(request_details) self.log(" ".join(msg_parts)) msg_parts.clear() captured_status = None # type: Optional[int] def custom_start_response( status: TYPE_WSGI_STATUS, headers: TYPE_WSGI_RESPONSE_HEADERS, exc_info: TYPE_WSGI_EXC_INFO = None, ) -> TYPE_WSGI_START_RESP_RESULT: nonlocal captured_status captured_status = status return start_response(status, headers, exc_info) # noinspection PyBroadException try: if self.show_timing: t1 = Pendulum.utcnow() result = self.app(environ, custom_start_response) return result except Exception: msg_parts.append("[RAISED EXCEPTION]") raise finally: if self.show_request_immediately: msg_parts.append("Response to") else: msg_parts.append("Request from") msg_parts.append(request_details) if self.show_timing: t2 = Pendulum.utcnow() if self.show_response: if captured_status is not None: msg_parts.append(f"-> {captured_status}") else: msg_parts.append("[no response status]") if self.show_timing: # noinspection PyUnboundLocalVariable time_taken_s = (t2 - t1).total_seconds() msg_parts.append(f"[{time_taken_s} s]") if msg_parts: self.log(" ".join(msg_parts))