• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

HowFast / apm-python / 06b7e795-a650-4df1-9f26-225db7666945

24 Dec 2024 12:58AM UTC coverage: 92.333%. Remained the same
06b7e795-a650-4df1-9f26-225db7666945

Pull #21

circleci

web-flow
Bump jinja2 from 3.1.4 to 3.1.5

Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.4...3.1.5)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #21: Bump jinja2 from 3.1.4 to 3.1.5

53 of 61 branches covered (86.89%)

Branch coverage included in aggregate %.

224 of 239 relevant lines covered (93.72%)

0.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/howfast_apm/flask.py
1
import logging
1✔
2
from typing import List
1✔
3
from datetime import datetime, timezone
1✔
4
from timeit import default_timer as timer
1✔
5
from flask.signals import request_started
1✔
6
from flask import Flask, request
1✔
7
from werkzeug import local, exceptions
1✔
8

9
from .core import CoreAPM
1✔
10
from .utils import is_in_blacklist, compile_endpoints
1✔
11

12
logger = logging.getLogger('howfast_apm')
1✔
13

14

15
class HowFastFlaskMiddleware(CoreAPM):
1✔
16
    """
17
    Flask middleware to measure how much time is spent per endpoint.
18

19
    This implementation is purposedly naive and potentially slow, but its goal is to validate the
20
    PoC. It should be replaced/improved in the future, based on the results of the PoC.
21
    """
22

23
    def __init__(
1✔
24
            self,
25
            # The Flask application to analyze
26
            app: Flask,
27
            # The HowFast app ID to use
28
            app_id: str = None,
29
            # Endpoints not to monitor
30
            endpoints_blacklist: List[str] = None,
31
            # Other configuration parameters passed to the CoreAPM constructor
32
            **kwargs,
33
    ):
34
        super().__init__(**kwargs)
1✔
35

36
        self.app = app
1✔
37
        self.wsgi_app = app.wsgi_app
1✔
38

39
        # We need to store thread local information, let's use Werkzeug's context locals
40
        # (see https://werkzeug.palletsprojects.com/en/1.0.x/local/)
41
        self.local = local.Local()
1✔
42
        self.local_manager = local.LocalManager([self.local])
1✔
43

44
        # Overwrite the passed WSGI application
45
        app.wsgi_app = self.local_manager.make_middleware(self)
1✔
46

47
        if endpoints_blacklist:
1✔
48
            self.endpoints_blacklist = compile_endpoints(*endpoints_blacklist)
1✔
49
        else:
50
            self.endpoints_blacklist = []
1✔
51

52
        # Setup the queue and the background thread
53
        self.setup(app_id)
1✔
54

55
        request_started.connect(self._request_started)
1✔
56

57
    def __call__(self, environ, start_response):
1✔
58
        if not self.app_id:
1✔
59
            # HF APM not configured, return early to save some time
60
            # TODO: wouldn't it be better to just not replace the WSGI app?
61
            return self.wsgi_app(environ, start_response)
1✔
62

63
        uri = environ.get('PATH_INFO')
1✔
64

65
        if is_in_blacklist(uri, self.endpoints_blacklist):
1✔
66
            # Endpoint blacklist, return now
67
            return self.wsgi_app(environ, start_response)
1✔
68

69
        method = environ.get('REQUEST_METHOD')
1✔
70

71
        response_status: str = None
1✔
72

73
        def _start_response_wrapped(status, *args, **kwargs):
1✔
74
            nonlocal response_status
75
            # We wrap the start_response callback to access the response status line (eg "200 OK")
76
            response_status = status
1✔
77
            return start_response(status, *args, **kwargs)
1✔
78

79
        time_request_started = datetime.now(timezone.utc)
1✔
80

81
        try:
1✔
82
            # Time the function execution
83
            start = timer()
1✔
84
            return_value = self.wsgi_app(environ, _start_response_wrapped)
1✔
85
            # Stop the timer as soon as possible to get the best measure of the function's execution time
86
            end = timer()
1✔
87
        except BaseException:
1✔
88
            # The WSGI app raised an exception, let's still save the point before raising the
89
            # exception again
90
            # First, "stop" the timer now to get the good measure of the function's execution time
91
            end = timer()
1✔
92
            # The real response status will actually be set by the server that interacts with the
93
            # WSGI app, but we cannot instrument it from here, so we just assume a common string.
94
            response_status = "500 INTERNAL SERVER ERROR"
1✔
95
            raise
1✔
96
        finally:
97
            elapsed = end - start
1✔
98

99
            self.save_point(
1✔
100
                time_request_started=time_request_started,
101
                time_elapsed=elapsed,
102
                method=method,
103
                uri=uri,
104
                response_status=response_status,
105
                # Request metadata
106
                endpoint_name=getattr(self.local, 'endpoint_name', None),
107
                url_rule=getattr(self.local, 'url_rule', None),
108
                is_not_found=getattr(self.local, 'is_not_found', None),
109
            )
110
            # TODO: remove this once overhead has been measured in production
111
            logger.info("overhead when saving the point: %.3fms", (timer() - end) * 1000)
1✔
112

113
        return return_value
1✔
114

115
    def _request_started(self, sender, **kwargs):
1✔
116
        with sender.app_context():
1✔
117
            self._save_request_metadata()
1✔
118

119
    def _save_request_metadata(self):
1✔
120
        """ Extract and save request metadata in the context local """
121
        # This will yield strings like:
122
        # * "monitor" (when the endpoint is defined using a resource)
123
        # * "apm-collection.store_points" (when the endpoint is defined with a blueprint)
124
        # The endpoint name will always be lowercase
125
        self.local.endpoint_name = request.endpoint
1✔
126
        # This will yield strings like "/v1.1/apm/<int:apm_id>/endpoint"
127
        self.local.url_rule = request.url_rule.rule if request.url_rule is not None else None
1✔
128
        # We want to tell the difference between a "real" 404 and a 404 returned by an existing view
129
        self.local.is_not_found = isinstance(request.routing_exception, exceptions.NotFound)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc