예제 #1
0
    def __init__(self, wsgi_app_reference, config: MetricsApiConfig):
        """
        Constructs and initializes MetricsMiddleware WSGI middleware to be passed into
        the currently running WSGI web server.

        Args:
            wsgi_app_reference ([type]): Reference to the current WSGI application,
                which will be wrapped
            config (MetricsApiConfig): Instance of MetricsApiConfig object
        """
        self.config = config
        self.app = wsgi_app_reference
        self.metrics_core = Metrics(config)
예제 #2
0
class ReadMeMetrics:
    def __init__(self, config: MetricsApiConfig, app: Flask = None):
        self.config = config
        self.metrics_core = Metrics(config)
        if app is not None:
            self.init_app(app)

    def init_app(self, app: Flask):
        self.config.LOGGER.info(
            f"Configuring {app.name} hooks to call ReadMeMetrics extension functions"
        )
        app.before_request(self.before_request)
        app.after_request(self.after_request)

    def before_request(self):
        try:
            request.rm_start_dt = datetime.utcnow().strftime(
                "%Y-%m-%dT%H:%M:%SZ")
            request.rm_start_ts = int(time.time() * 1000)
            if "Content-Length" in request.headers or request.data:
                request.rm_content_length = request.headers[
                    "Content-Length"] or "0"
                request.rm_body = request.data or ""
        except Exception as e:
            # Errors in the Metrics SDK should never cause the application to
            # throw an error. Log it but don't re-raise.
            self.config.LOGGER.exception(e)

    def after_request(self, response):
        try:
            response_info = ResponseInfoWrapper(
                response.headers,
                response.status,
                content_type=None,
                content_length=None,
                body=response.data,
            )
            self.metrics_core.process(request, response_info)
        except Exception as e:
            # Errors in the Metrics SDK should never cause the application to
            # throw an error. Log it but don't re-raise.
            self.config.LOGGER.exception(e)
        finally:
            return response
예제 #3
0
class MetricsMiddleware:
    def __init__(self, get_response, config=None):
        self.get_response = get_response
        self.config = config or settings.README_METRICS_CONFIG
        assert isinstance(self.config, MetricsApiConfig)
        self.metrics_core = Metrics(self.config)

    def __call__(self, request):
        try:
            request.rm_start_dt = str(datetime.utcnow())
            request.rm_start_ts = int(time.time() * 1000)
            if request.headers["Content-Length"] or request.body:
                request.rm_content_length = request.headers[
                    "Content-Length"] or "0"
                request.rm_body = request.body or ""
        except Exception as e:
            # Errors in the Metrics SDK should never cause the application to
            # throw an error. Log it but don't re-raise.
            self.config.LOGGER.exception(e)

        response = self.get_response(request)

        try:
            try:
                body = response.content.decode("utf-8")
            except UnicodeDecodeError:
                body = "[NOT VALID UTF-8]"
            response_info = ResponseInfoWrapper(
                response.headers,
                response.status_code,
                content_type=None,
                content_length=None,
                body=body,
            )
            self.metrics_core.process(request, response_info)
        except Exception as e:
            # Errors in the Metrics SDK should never cause the application to
            # throw an error. Log it but don't re-raise.
            self.config.LOGGER.exception(e)
        finally:
            return response
예제 #4
0
class MetricsMiddleware:
    """Core middleware class for ReadMe Metrics

    Attributes:
        config (MetricsApiConfig): Contains the configuration settings for the running
            middleware instance
    """
    def __init__(self, wsgi_app_reference, config: MetricsApiConfig):
        """
        Constructs and initializes MetricsMiddleware WSGI middleware to be passed into
        the currently running WSGI web server.

        Args:
            wsgi_app_reference ([type]): Reference to the current WSGI application,
                which will be wrapped
            config (MetricsApiConfig): Instance of MetricsApiConfig object
        """
        self.config = config
        self.app = wsgi_app_reference
        self.metrics_core = Metrics(config)

    def __call__(self, environ, start_response):
        """Method that is called by the running WSGI server.

        You should NOT be calling this method yourself under normal circumstances.
        """
        response_headers = {}
        response_status = 0
        iterable = None
        req = Request(environ)

        def _start_response(_status, _response_headers, *args):
            write = start_response(_status, _response_headers, *args)

            # Populate response info (headers & status)
            nonlocal response_headers, response_status

            response_headers = _response_headers
            response_status = _status

            return write

        try:
            req.rm_start_dt = str(datetime.datetime.now())
            req.rm_start_ts = int(time.time() * 1000)

            if req.method == "POST":
                # The next 4 lines are a workaround for a serious shortcoming in the
                # WSGI spec.
                #
                # The data can only be read once, after which the socket is exhausted
                # and cannot be read again. As such, we read the data and then
                # repopulate the variable so that it can be used by other code down the
                # pipeline.
                #
                # For more info: https://stackoverflow.com/a/13106009/643951

                # the environment variable CONTENT_LENGTH may be empty or missing
                try:
                    content_length = int(environ.get("CONTENT_LENGTH", 0))
                except (ValueError):
                    content_length = 0
                content_body = environ["wsgi.input"].read(content_length)

                # guarding check to close stream
                if hasattr(environ["CONTENT_LENGTH"], "close"):
                    environ["wsgi.input"].close()

                environ["wsgi.input"] = io.BytesIO(content_body)

                req.rm_content_length = content_length
                req.rm_body = content_body

            iterable = self.app(environ, _start_response)
            for data in iterable:
                res_ctype = ""
                res_clength = 0

                htype = next(
                    (h for h in response_headers if h[0] == "Content-Type"),
                    None)

                hlength = next(
                    (h for h in response_headers if h[0] == "Content-Length"),
                    None)

                if htype and hlength:
                    res_ctype = htype[1]
                    res_clength = int(hlength[1])

                # Populate response body
                res = ResponseInfoWrapper(
                    response_headers,
                    response_status,
                    res_ctype,
                    res_clength,
                    data.decode("utf-8"),
                )

                # Send off data to be queued (and processed) by ReadMe if allowed
                if self.config.ALLOWED_HTTP_HOSTS:
                    if environ["HTTP_HOST"] in self.config.ALLOWED_HTTP_HOSTS:
                        self.metrics_core.process(req, res)
                else:
                    # If the allowed_http_hosts has not been set (None by default), send off the data to be queued
                    self.metrics_core.process(req, res)

                yield data

        finally:
            # Undocumented in WSGI spec but the iterable has to be closed
            if hasattr(iterable, "close"):
                iterable.close()
예제 #5
0
 def __init__(self, get_response, config=None):
     self.get_response = get_response
     self.config = config or settings.README_METRICS_CONFIG
     assert isinstance(self.config, MetricsApiConfig)
     self.metrics_core = Metrics(self.config)
예제 #6
0
 def __init__(self, config: MetricsApiConfig, app: Flask = None):
     self.config = config
     self.metrics_core = Metrics(config)
     if app is not None:
         self.init_app(app)