Skip to content
This repository was archived by the owner on Sep 2, 2022. It is now read-only.

Commit a283e74

Browse files
authored
Merge pull request #20 from apilytics/flask
Add a middleware for sending metrics from Flask applications
2 parents d6c04e8 + be6389b commit a283e74

10 files changed

Lines changed: 538 additions & 62 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `apilytics.flask.apilytics_middleware` for sending metrics from Flask applications.
13+
14+
### Fixed
15+
16+
- Fix FastAPI middleware suggested installation order; it should ideally be the *last* one added.
17+
1018
## [1.5.0] - 2022-03-17
1119

1220
### Added

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,26 @@ from fastapi import FastAPI
5454

5555
app = FastAPI()
5656

57-
# Ideally the first middleware you add.
57+
# Ideally the last middleware you add.
5858
app.add_middleware(ApilyticsMiddleware, api_key=os.getenv("APILYTICS_API_KEY"))
5959
```
6060

61+
### Flask
62+
63+
`app.py`:
64+
65+
```python
66+
import os
67+
68+
from apilytics.flask import apilytics_middleware
69+
from flask import Flask
70+
71+
app = Flask(__name__)
72+
73+
# Ideally wrap your app with the middleware before you do anything else with it.
74+
app = apilytics_middleware(app, api_key=os.getenv("APILYTICS_API_KEY"))
75+
```
76+
6177
### Other Python Frameworks
6278

6379
You can easily build your own middleware which measures the execution time and sends the metrics:

apilytics/core.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def __init__(
6060
ip: Optional[str] = None,
6161
apilytics_integration: Optional[str] = None,
6262
integrated_library: Optional[str] = None,
63+
prevent_send_on_exit: bool = False,
6364
) -> None:
6465
"""
6566
Initialize the context manager with info from the HTTP request object.
@@ -79,6 +80,8 @@ def __init__(
7980
e.g. "apilytics-python-django". No need to pass this when calling from user code.
8081
integrated_library: Name and version of the integration that this is used in,
8182
e.g. "django/3.2.1". No need to pass this when calling from user code.
83+
prevent_send_on_exit: Don't immediately send the metrics when the context
84+
manager exits. Useful for advanced deferred sending scenarios.
8285
"""
8386
self._api_key = api_key
8487
self._path = path
@@ -96,6 +99,8 @@ def __init__(
9699
library=integrated_library or "",
97100
)
98101

102+
self._prevent_send_on_exit = prevent_send_on_exit
103+
99104
def __enter__(self) -> "ApilyticsSender":
100105
"""Start the timer, measuring how long the ``with`` block takes to execute."""
101106
self._start_time_ns = time.perf_counter_ns()
@@ -107,15 +112,9 @@ def __exit__(
107112
exc_val: Optional[BaseException],
108113
exc_tb: Optional[types.TracebackType],
109114
) -> None:
110-
"""Send metrics to Apilytics in a fire-and-forget background task."""
111-
self._end_time_ns = time.perf_counter_ns()
112-
if not hasattr(self, "_executor"):
113-
# Use only a single background thread and share the pool to minimize
114-
# resource hogging.
115-
self.__class__._executor = concurrent.futures.ThreadPoolExecutor(
116-
max_workers=1
117-
)
118-
self._executor.submit(self._send_metrics)
115+
if self._prevent_send_on_exit:
116+
return
117+
self.send()
119118

120119
def set_response_info(
121120
self, *, status_code: Optional[int] = None, response_size: Optional[int] = None
@@ -133,6 +132,17 @@ def set_response_info(
133132
self._status_code = status_code
134133
self._response_size = response_size
135134

135+
def send(self) -> None:
136+
"""Send metrics to Apilytics in a fire-and-forget background task."""
137+
self._end_time_ns = time.perf_counter_ns()
138+
if not hasattr(self, "_executor"):
139+
# Use only a single background thread and share the pool to minimize
140+
# resource hogging.
141+
self.__class__._executor = concurrent.futures.ThreadPoolExecutor(
142+
max_workers=1
143+
)
144+
self._executor.submit(self._send_metrics)
145+
136146
def _send_metrics(self) -> None:
137147
memory_usage, memory_total = _get_used_and_total_memory()
138148
cpu_usage = _get_cpu_usage()

apilytics/fastapi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ class ApilyticsMiddleware(starlette.middleware.base.BaseHTTPMiddleware):
1212
"""
1313
FastAPI middleware that sends API analytics data to Apilytics (https://apilytics.io).
1414
15-
This should ideally be the first middleware you add to your app.
15+
This should ideally be the last middleware you add to your app.
1616
1717
Examples:
1818
main.py::
1919
20-
from fastapi import FastAPI
2120
from apilytics.fastapi import ApilyticsMiddleware
21+
from fastapi import FastAPI
2222
2323
app = FastAPI()
2424

apilytics/flask.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from typing import Optional, TypeVar, cast
2+
3+
import flask
4+
5+
import apilytics.core
6+
7+
__all__ = ["apilytics_middleware"]
8+
9+
T = TypeVar("T", bound=flask.Flask)
10+
11+
12+
def apilytics_middleware(app: T, api_key: Optional[str]) -> T:
13+
"""
14+
Flask middleware that sends API analytics data to Apilytics (https://apilytics.io).
15+
16+
This should ideally be the outermost middleware you wrap your app with.
17+
18+
Examples:
19+
app.py::
20+
21+
from apilytics.flask import apilytics_middleware
22+
from flask import Flask
23+
24+
app = Flask(__name__)
25+
26+
app = apilytics_middleware(app, api_key="<your-api-key>")
27+
"""
28+
if not api_key:
29+
return app
30+
31+
def before_request() -> None:
32+
with apilytics.core.ApilyticsSender(
33+
api_key=cast(str, api_key), # Type not inferred from the early return.
34+
path=flask.request.path,
35+
query=flask.request.query_string.decode(flask.request.url_charset),
36+
method=flask.request.method,
37+
request_size=len(flask.request.data),
38+
user_agent=flask.request.headers.get("user-agent"),
39+
ip=flask.request.headers.get("x-forwarded-for", "").split(",")[0].strip(),
40+
apilytics_integration="apilytics-python-flask",
41+
integrated_library=f"flask/{flask.__version__}",
42+
prevent_send_on_exit=True,
43+
) as sender:
44+
flask.g.apilytics_sender = sender
45+
46+
def after_request(response: flask.Response) -> flask.Response:
47+
sender = flask.g.apilytics_sender
48+
size = response.headers.get("content-length")
49+
sender.set_response_info(
50+
status_code=response.status_code,
51+
response_size=int(size) if size is not None else None,
52+
)
53+
return response
54+
55+
def teardown_request(exc: Optional[BaseException]) -> None:
56+
sender = flask.g.apilytics_sender
57+
sender.send()
58+
59+
app.before_request(before_request)
60+
app.after_request(after_request)
61+
app.teardown_request(teardown_request)
62+
return app

0 commit comments

Comments
 (0)