apitally 0.14.4__tar.gz → 0.14.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {apitally-0.14.4 → apitally-0.14.6}/.github/workflows/publish.yaml +1 -1
  2. {apitally-0.14.4 → apitally-0.14.6}/.github/workflows/tests.yaml +14 -4
  3. {apitally-0.14.4 → apitally-0.14.6}/.pre-commit-config.yaml +6 -2
  4. {apitally-0.14.4 → apitally-0.14.6}/PKG-INFO +61 -31
  5. {apitally-0.14.4 → apitally-0.14.6}/README.md +60 -30
  6. {apitally-0.14.4 → apitally-0.14.6}/apitally/client/client_asyncio.py +2 -0
  7. {apitally-0.14.4 → apitally-0.14.6}/apitally/client/client_base.py +1 -0
  8. {apitally-0.14.4 → apitally-0.14.6}/apitally/client/client_threading.py +1 -0
  9. {apitally-0.14.4 → apitally-0.14.6}/apitally/client/request_logging.py +15 -12
  10. {apitally-0.14.4 → apitally-0.14.6}/apitally/django.py +8 -2
  11. {apitally-0.14.4 → apitally-0.14.6}/apitally/flask.py +6 -1
  12. {apitally-0.14.4 → apitally-0.14.6}/apitally/litestar.py +10 -2
  13. {apitally-0.14.4 → apitally-0.14.6}/apitally/starlette.py +10 -2
  14. {apitally-0.14.4 → apitally-0.14.6}/pyproject.toml +4 -3
  15. {apitally-0.14.4 → apitally-0.14.6}/tests/test_client_request_logging.py +6 -1
  16. {apitally-0.14.4 → apitally-0.14.6}/tests/test_flask.py +3 -3
  17. {apitally-0.14.4 → apitally-0.14.6}/tests/test_litestar.py +6 -0
  18. {apitally-0.14.4 → apitally-0.14.6}/uv.lock +618 -358
  19. {apitally-0.14.4 → apitally-0.14.6}/.github/workflows/summary.yaml +0 -0
  20. {apitally-0.14.4 → apitally-0.14.6}/.gitignore +0 -0
  21. {apitally-0.14.4 → apitally-0.14.6}/LICENSE +0 -0
  22. {apitally-0.14.4 → apitally-0.14.6}/Makefile +0 -0
  23. {apitally-0.14.4 → apitally-0.14.6}/apitally/__init__.py +0 -0
  24. {apitally-0.14.4 → apitally-0.14.6}/apitally/client/__init__.py +0 -0
  25. {apitally-0.14.4 → apitally-0.14.6}/apitally/client/consumers.py +0 -0
  26. {apitally-0.14.4 → apitally-0.14.6}/apitally/client/logging.py +0 -0
  27. {apitally-0.14.4 → apitally-0.14.6}/apitally/client/requests.py +0 -0
  28. {apitally-0.14.4 → apitally-0.14.6}/apitally/client/server_errors.py +0 -0
  29. {apitally-0.14.4 → apitally-0.14.6}/apitally/client/validation_errors.py +0 -0
  30. {apitally-0.14.4 → apitally-0.14.6}/apitally/common.py +0 -0
  31. {apitally-0.14.4 → apitally-0.14.6}/apitally/django_ninja.py +0 -0
  32. {apitally-0.14.4 → apitally-0.14.6}/apitally/django_rest_framework.py +0 -0
  33. {apitally-0.14.4 → apitally-0.14.6}/apitally/fastapi.py +0 -0
  34. {apitally-0.14.4 → apitally-0.14.6}/apitally/py.typed +0 -0
  35. {apitally-0.14.4 → apitally-0.14.6}/renovate.json +0 -0
  36. {apitally-0.14.4 → apitally-0.14.6}/tests/__init__.py +0 -0
  37. {apitally-0.14.4 → apitally-0.14.6}/tests/conftest.py +0 -0
  38. {apitally-0.14.4 → apitally-0.14.6}/tests/constants.py +0 -0
  39. {apitally-0.14.4 → apitally-0.14.6}/tests/django_ninja_urls.py +0 -0
  40. {apitally-0.14.4 → apitally-0.14.6}/tests/django_rest_framework_urls.py +0 -0
  41. {apitally-0.14.4 → apitally-0.14.6}/tests/test_client_asyncio.py +0 -0
  42. {apitally-0.14.4 → apitally-0.14.6}/tests/test_client_consumers.py +0 -0
  43. {apitally-0.14.4 → apitally-0.14.6}/tests/test_client_requests.py +0 -0
  44. {apitally-0.14.4 → apitally-0.14.6}/tests/test_client_server_errors.py +0 -0
  45. {apitally-0.14.4 → apitally-0.14.6}/tests/test_client_threading.py +0 -0
  46. {apitally-0.14.4 → apitally-0.14.6}/tests/test_client_validation_errors.py +0 -0
  47. {apitally-0.14.4 → apitally-0.14.6}/tests/test_django_ninja.py +0 -0
  48. {apitally-0.14.4 → apitally-0.14.6}/tests/test_django_rest_framework.py +0 -0
  49. {apitally-0.14.4 → apitally-0.14.6}/tests/test_fastapi.py +0 -0
  50. {apitally-0.14.4 → apitally-0.14.6}/tests/test_starlette.py +0 -0
@@ -16,7 +16,7 @@ jobs:
16
16
  - name: Install uv
17
17
  uses: astral-sh/setup-uv@v5
18
18
  with:
19
- version: "0.5.14"
19
+ version: "0.5.21"
20
20
  enable-cache: true
21
21
  - name: Build package
22
22
  run: uv build
@@ -16,7 +16,7 @@ jobs:
16
16
  - uses: actions/checkout@v4
17
17
  - uses: actions/setup-python@v5
18
18
  with:
19
- python-version: "3.12"
19
+ python-version: "3.13"
20
20
  - uses: pre-commit/action@v3.0.1
21
21
 
22
22
  test-coverage:
@@ -26,7 +26,7 @@ jobs:
26
26
  - name: Install uv
27
27
  uses: astral-sh/setup-uv@v5
28
28
  with:
29
- version: "0.5.14"
29
+ version: "0.5.21"
30
30
  enable-cache: true
31
31
  - name: Install Python
32
32
  run: uv python install 3.13
@@ -46,7 +46,7 @@ jobs:
46
46
  strategy:
47
47
  fail-fast: false
48
48
  matrix:
49
- python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
49
+ python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
50
50
  deps:
51
51
  - starlette
52
52
  - fastapi starlette
@@ -78,12 +78,22 @@ jobs:
78
78
  deps: djangorestframework==3.10.* django==2.2.* uritemplate
79
79
  - python: "3.12"
80
80
  deps: litestar==2.0.1
81
+ - python: "3.13"
82
+ deps: fastapi==0.100.1 starlette
83
+ - python: "3.13"
84
+ deps: fastapi==0.87.0 starlette
85
+ - python: "3.13"
86
+ deps: djangorestframework==3.12.* django==3.2.* uritemplate
87
+ - python: "3.13"
88
+ deps: djangorestframework==3.10.* django==2.2.* uritemplate
89
+ - python: "3.13"
90
+ deps: litestar==2.0.1
81
91
  steps:
82
92
  - uses: actions/checkout@v4
83
93
  - name: Install uv
84
94
  uses: astral-sh/setup-uv@v5
85
95
  with:
86
- version: "0.5.14"
96
+ version: "0.5.21"
87
97
  enable-cache: true
88
98
  - name: Install Python
89
99
  run: uv python install ${{ matrix.python }}
@@ -1,5 +1,5 @@
1
1
  default_language_version:
2
- python: "3.12"
2
+ python: "3.13"
3
3
  repos:
4
4
  - repo: https://github.com/pre-commit/pre-commit-hooks
5
5
  rev: v5.0.0
@@ -8,8 +8,12 @@ repos:
8
8
  - id: trailing-whitespace
9
9
  - id: mixed-line-ending
10
10
  - repo: https://github.com/charliermarsh/ruff-pre-commit
11
- rev: v0.8.6
11
+ rev: v0.9.9
12
12
  hooks:
13
13
  - id: ruff
14
14
  args: ["--fix", "--exit-non-zero-on-fix"]
15
15
  - id: ruff-format
16
+ - repo: https://github.com/astral-sh/uv-pre-commit
17
+ rev: 0.6.3
18
+ hooks:
19
+ - id: uv-lock
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apitally
3
- Version: 0.14.4
3
+ Version: 0.14.6
4
4
  Summary: Simple API monitoring & analytics for REST APIs built with FastAPI, Flask, Django, Starlette and Litestar.
5
5
  Project-URL: Homepage, https://apitally.io
6
6
  Project-URL: Documentation, https://docs.apitally.io
@@ -62,18 +62,19 @@ Requires-Dist: starlette<1.0.0,>=0.21.0; extra == 'starlette'
62
62
  Description-Content-Type: text/markdown
63
63
 
64
64
  <p align="center">
65
- <picture>
66
- <source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-vertical-dark.png">
67
- <source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-vertical-light.png">
68
- <img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-vertical-light.png" width="150">
69
- </picture>
65
+ <a href="https://apitally.io" target="_blank">
66
+ <picture>
67
+ <source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-vertical-dark.png">
68
+ <source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-vertical-light.png">
69
+ <img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-vertical-light.png" width="150">
70
+ </picture>
71
+ </a>
70
72
  </p>
71
73
 
72
- <p align="center"><b>Analytics, logging & monitoring for REST APIs.</b></p>
74
+ <p align="center"><b>Simple, privacy-focused API monitoring & analytics</b></p>
73
75
 
74
- <p align="center"><i>Apitally helps you understand how your APIs are being used and alerts you when things go wrong.<br>It's super easy to use and designed to protect your data privacy.</i></p>
75
-
76
- <p align="center">🔗 <b><a href="https://apitally.io" target="_blank">apitally.io</a></b></p>
76
+ <p align="center"><i>Apitally helps you understand how your APIs are being used and alerts you when things go wrong.<br>Just add two lines of code to your project to get started.</i></p>
77
+ <br>
77
78
 
78
79
  ![Apitally screenshots](https://assets.apitally.io/screenshots/overview.png)
79
80
 
@@ -89,10 +90,10 @@ This client library for Apitally currently supports the following Python web
89
90
  frameworks:
90
91
 
91
92
  - [FastAPI](https://docs.apitally.io/frameworks/fastapi)
92
- - [Starlette](https://docs.apitally.io/frameworks/starlette)
93
- - [Flask](https://docs.apitally.io/frameworks/flask)
94
- - [Django Ninja](https://docs.apitally.io/frameworks/django-ninja)
95
93
  - [Django REST Framework](https://docs.apitally.io/frameworks/django-rest-framework)
94
+ - [Django Ninja](https://docs.apitally.io/frameworks/django-ninja)
95
+ - [Flask](https://docs.apitally.io/frameworks/flask)
96
+ - [Starlette](https://docs.apitally.io/frameworks/starlette)
96
97
  - [Litestar](https://docs.apitally.io/frameworks/litestar)
97
98
 
98
99
  Learn more about Apitally on our 🌎 [website](https://apitally.io) or check out
@@ -100,10 +101,21 @@ the 📚 [documentation](https://docs.apitally.io).
100
101
 
101
102
  ## Key features
102
103
 
103
- - Middleware for different frameworks to capture metadata about API endpoints,
104
- requests and responses
105
- - Non-blocking clients that aggregate and send captured data to Apitally in
106
- regular intervals
104
+ ### API analytics
105
+
106
+ Track traffic, error and performance metrics for your API, each endpoint and individual API consumers, allowing you to make informed, data-driven engineering and product decisions.
107
+
108
+ ### Error tracking
109
+
110
+ Understand which validation rules in your endpoints cause client errors. Capture error details and stack traces for 500 error responses, and have them linked to Sentry issues automatically.
111
+
112
+ ### Request logging
113
+
114
+ Drill down from insights to individual requests or use powerful filtering to understand how consumers have interacted with your API. Configure exactly what is included in the logs to meet your requirements.
115
+
116
+ ### API monitoring & alerting
117
+
118
+ Get notified immediately if something isn't right using custom alerts, synthetic uptime checks and heartbeat monitoring. Notifications can be delivered via email, Slack or Microsoft Teams.
107
119
 
108
120
  ## Install
109
121
 
@@ -140,6 +152,25 @@ app.add_middleware(
140
152
  )
141
153
  ```
142
154
 
155
+ ### Django
156
+
157
+ This is an example of how to add the Apitally middleware to a Django Ninja or
158
+ Django REST Framework application. For further instructions, see our
159
+ [setup guide for Django](https://docs.apitally.io/frameworks/django).
160
+
161
+ In your Django `settings.py` file:
162
+
163
+ ```python
164
+ MIDDLEWARE = [
165
+ "apitally.django.ApitallyMiddleware",
166
+ # Other middleware ...
167
+ ]
168
+ APITALLY_MIDDLEWARE = {
169
+ "client_id": "your-client-id",
170
+ "env": "dev", # or "prod" etc.
171
+ }
172
+ ```
173
+
143
174
  ### Flask
144
175
 
145
176
  This is an example of how to add the Apitally middleware to a Flask application.
@@ -158,23 +189,22 @@ app.wsgi_app = ApitallyMiddleware(
158
189
  )
159
190
  ```
160
191
 
161
- ### Django
162
-
163
- This is an example of how to add the Apitally middleware to a Django Ninja or
164
- Django REST Framework application. For further instructions, see our
165
- [setup guide for Django](https://docs.apitally.io/frameworks/django).
192
+ ### Starlette
166
193
 
167
- In your Django `settings.py` file:
194
+ This is an example of how to add the Apitally middleware to a Starlette application.
195
+ For further instructions, see our
196
+ [setup guide for Starlette](https://docs.apitally.io/frameworks/starlette).
168
197
 
169
198
  ```python
170
- MIDDLEWARE = [
171
- "apitally.django.ApitallyMiddleware",
172
- # Other middleware ...
173
- ]
174
- APITALLY_MIDDLEWARE = {
175
- "client_id": "your-client-id",
176
- "env": "dev", # or "prod" etc.
177
- }
199
+ from starlette.applications import Starlette
200
+ from apitally.starlette import ApitallyMiddleware
201
+
202
+ app = Starlette(routes=[...])
203
+ app.add_middleware(
204
+ ApitallyMiddleware,
205
+ client_id="your-client-id",
206
+ env="dev", # or "prod" etc.
207
+ )
178
208
  ```
179
209
 
180
210
  ### Litestar
@@ -1,16 +1,17 @@
1
1
  <p align="center">
2
- <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-vertical-dark.png">
4
- <source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-vertical-light.png">
5
- <img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-vertical-light.png" width="150">
6
- </picture>
2
+ <a href="https://apitally.io" target="_blank">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-vertical-dark.png">
5
+ <source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-vertical-light.png">
6
+ <img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-vertical-light.png" width="150">
7
+ </picture>
8
+ </a>
7
9
  </p>
8
10
 
9
- <p align="center"><b>Analytics, logging & monitoring for REST APIs.</b></p>
11
+ <p align="center"><b>Simple, privacy-focused API monitoring & analytics</b></p>
10
12
 
11
- <p align="center"><i>Apitally helps you understand how your APIs are being used and alerts you when things go wrong.<br>It's super easy to use and designed to protect your data privacy.</i></p>
12
-
13
- <p align="center">🔗 <b><a href="https://apitally.io" target="_blank">apitally.io</a></b></p>
13
+ <p align="center"><i>Apitally helps you understand how your APIs are being used and alerts you when things go wrong.<br>Just add two lines of code to your project to get started.</i></p>
14
+ <br>
14
15
 
15
16
  ![Apitally screenshots](https://assets.apitally.io/screenshots/overview.png)
16
17
 
@@ -26,10 +27,10 @@ This client library for Apitally currently supports the following Python web
26
27
  frameworks:
27
28
 
28
29
  - [FastAPI](https://docs.apitally.io/frameworks/fastapi)
29
- - [Starlette](https://docs.apitally.io/frameworks/starlette)
30
- - [Flask](https://docs.apitally.io/frameworks/flask)
31
- - [Django Ninja](https://docs.apitally.io/frameworks/django-ninja)
32
30
  - [Django REST Framework](https://docs.apitally.io/frameworks/django-rest-framework)
31
+ - [Django Ninja](https://docs.apitally.io/frameworks/django-ninja)
32
+ - [Flask](https://docs.apitally.io/frameworks/flask)
33
+ - [Starlette](https://docs.apitally.io/frameworks/starlette)
33
34
  - [Litestar](https://docs.apitally.io/frameworks/litestar)
34
35
 
35
36
  Learn more about Apitally on our 🌎 [website](https://apitally.io) or check out
@@ -37,10 +38,21 @@ the 📚 [documentation](https://docs.apitally.io).
37
38
 
38
39
  ## Key features
39
40
 
40
- - Middleware for different frameworks to capture metadata about API endpoints,
41
- requests and responses
42
- - Non-blocking clients that aggregate and send captured data to Apitally in
43
- regular intervals
41
+ ### API analytics
42
+
43
+ Track traffic, error and performance metrics for your API, each endpoint and individual API consumers, allowing you to make informed, data-driven engineering and product decisions.
44
+
45
+ ### Error tracking
46
+
47
+ Understand which validation rules in your endpoints cause client errors. Capture error details and stack traces for 500 error responses, and have them linked to Sentry issues automatically.
48
+
49
+ ### Request logging
50
+
51
+ Drill down from insights to individual requests or use powerful filtering to understand how consumers have interacted with your API. Configure exactly what is included in the logs to meet your requirements.
52
+
53
+ ### API monitoring & alerting
54
+
55
+ Get notified immediately if something isn't right using custom alerts, synthetic uptime checks and heartbeat monitoring. Notifications can be delivered via email, Slack or Microsoft Teams.
44
56
 
45
57
  ## Install
46
58
 
@@ -77,6 +89,25 @@ app.add_middleware(
77
89
  )
78
90
  ```
79
91
 
92
+ ### Django
93
+
94
+ This is an example of how to add the Apitally middleware to a Django Ninja or
95
+ Django REST Framework application. For further instructions, see our
96
+ [setup guide for Django](https://docs.apitally.io/frameworks/django).
97
+
98
+ In your Django `settings.py` file:
99
+
100
+ ```python
101
+ MIDDLEWARE = [
102
+ "apitally.django.ApitallyMiddleware",
103
+ # Other middleware ...
104
+ ]
105
+ APITALLY_MIDDLEWARE = {
106
+ "client_id": "your-client-id",
107
+ "env": "dev", # or "prod" etc.
108
+ }
109
+ ```
110
+
80
111
  ### Flask
81
112
 
82
113
  This is an example of how to add the Apitally middleware to a Flask application.
@@ -95,23 +126,22 @@ app.wsgi_app = ApitallyMiddleware(
95
126
  )
96
127
  ```
97
128
 
98
- ### Django
99
-
100
- This is an example of how to add the Apitally middleware to a Django Ninja or
101
- Django REST Framework application. For further instructions, see our
102
- [setup guide for Django](https://docs.apitally.io/frameworks/django).
129
+ ### Starlette
103
130
 
104
- In your Django `settings.py` file:
131
+ This is an example of how to add the Apitally middleware to a Starlette application.
132
+ For further instructions, see our
133
+ [setup guide for Starlette](https://docs.apitally.io/frameworks/starlette).
105
134
 
106
135
  ```python
107
- MIDDLEWARE = [
108
- "apitally.django.ApitallyMiddleware",
109
- # Other middleware ...
110
- ]
111
- APITALLY_MIDDLEWARE = {
112
- "client_id": "your-client-id",
113
- "env": "dev", # or "prod" etc.
114
- }
136
+ from starlette.applications import Starlette
137
+ from apitally.starlette import ApitallyMiddleware
138
+
139
+ app = Starlette(routes=[...])
140
+ app.add_middleware(
141
+ ApitallyMiddleware,
142
+ client_id="your-client-id",
143
+ env="dev", # or "prod" etc.
144
+ )
115
145
  ```
116
146
 
117
147
  ### Litestar
@@ -81,6 +81,7 @@ class ApitallyClient(ApitallyClientBase):
81
81
  self._stop_sync_loop = True
82
82
 
83
83
  async def handle_shutdown(self) -> None:
84
+ self.enabled = False
84
85
  if self._sync_loop_task is not None:
85
86
  self._sync_loop_task.cancel()
86
87
  # Send any remaining data before exiting
@@ -164,6 +165,7 @@ class ApitallyClient(ApitallyClientBase):
164
165
 
165
166
  def _handle_hub_response(self, response: httpx.Response) -> None:
166
167
  if response.status_code == 404:
168
+ self.enabled = False
167
169
  self.stop_sync_loop()
168
170
  logger.error("Invalid Apitally client ID: %s", self.client_id)
169
171
  elif response.status_code == 422:
@@ -52,6 +52,7 @@ class ApitallyClientBase(ABC):
52
52
 
53
53
  self.client_id = client_id
54
54
  self.env = env
55
+ self.enabled = True
55
56
  self.instance_uuid = str(uuid4())
56
57
  self.request_counter = RequestCounter()
57
58
  self.validation_error_counter = ValidationErrorCounter()
@@ -190,6 +190,7 @@ class ApitallyClient(ApitallyClientBase):
190
190
 
191
191
  def _handle_hub_response(self, response: requests.Response) -> None:
192
192
  if response.status_code == 404:
193
+ self.enabled = False
193
194
  self.stop_sync_loop()
194
195
  logger.error("Invalid Apitally client ID: %s", self.client_id)
195
196
  elif response.status_code == 422:
@@ -5,7 +5,6 @@ import tempfile
5
5
  import threading
6
6
  import time
7
7
  from collections import deque
8
- from contextlib import suppress
9
8
  from dataclasses import dataclass, field
10
9
  from functools import lru_cache
11
10
  from io import BufferedReader
@@ -29,14 +28,14 @@ MASKED = "******"
29
28
  ALLOWED_CONTENT_TYPES = ["application/json", "text/plain"]
30
29
  EXCLUDE_PATH_PATTERNS = [
31
30
  r"/_?healthz?$",
32
- r"/_?health[_-]?checks?$",
33
- r"/_?heart[_-]?beats?$",
31
+ r"/_?health[\-_]?checks?$",
32
+ r"/_?heart[\-_]?beats?$",
34
33
  r"/ping$",
35
34
  r"/ready$",
36
35
  r"/live$",
37
36
  ]
38
37
  EXCLUDE_USER_AGENT_PATTERNS = [
39
- r"health[_- ]?check",
38
+ r"health[\-_ ]?check",
40
39
  r"microsoft-azure-application-lb",
41
40
  r"googlehc",
42
41
  r"kube-probe",
@@ -178,8 +177,6 @@ class RequestLogger:
178
177
 
179
178
  query = self._mask_query_params(parsed_url.query) if self.config.log_query_params else ""
180
179
  request["url"] = urlunparse(parsed_url._replace(query=query))
181
- request["headers"] = self._mask_headers(request["headers"]) if self.config.log_request_headers else []
182
- response["headers"] = self._mask_headers(response["headers"]) if self.config.log_response_headers else []
183
180
 
184
181
  if not self.config.log_request_body or not self._has_supported_content_type(request["headers"]):
185
182
  request["body"] = None
@@ -215,6 +212,9 @@ class RequestLogger:
215
212
  if response["body"] is not None and len(response["body"]) > MAX_BODY_SIZE:
216
213
  response["body"] = BODY_TOO_LARGE
217
214
 
215
+ request["headers"] = self._mask_headers(request["headers"]) if self.config.log_request_headers else []
216
+ response["headers"] = self._mask_headers(response["headers"]) if self.config.log_response_headers else []
217
+
218
218
  item = {
219
219
  "uuid": str(uuid4()),
220
220
  "request": _skip_empty_values(request),
@@ -307,19 +307,22 @@ class RequestLogger:
307
307
  @staticmethod
308
308
  def _match_patterns(value: str, patterns: List[str]) -> bool:
309
309
  for pattern in patterns:
310
- with suppress(re.error):
311
- if re.search(pattern, value, re.I) is not None:
312
- return True
310
+ if re.search(pattern, value, re.I) is not None:
311
+ return True
313
312
  return False
314
313
 
314
+ @staticmethod
315
+ def _get_user_agent(headers: List[Tuple[str, str]]) -> Optional[str]:
316
+ return next((v for k, v in headers if k.lower() == "user-agent"), None)
317
+
315
318
  @staticmethod
316
319
  def _has_supported_content_type(headers: List[Tuple[str, str]]) -> bool:
317
320
  content_type = next((v for k, v in headers if k.lower() == "content-type"), None)
318
- return content_type is not None and any(content_type.startswith(t) for t in ALLOWED_CONTENT_TYPES)
321
+ return RequestLogger.is_supported_content_type(content_type)
319
322
 
320
323
  @staticmethod
321
- def _get_user_agent(headers: List[Tuple[str, str]]) -> Optional[str]:
322
- return next((v for k, v in headers if k.lower() == "user-agent"), None)
324
+ def is_supported_content_type(content_type: Optional[str]) -> bool:
325
+ return content_type is not None and any(content_type.startswith(t) for t in ALLOWED_CONTENT_TYPES)
323
326
 
324
327
 
325
328
  def _check_writable_fs() -> bool:
@@ -19,6 +19,7 @@ from apitally.client.logging import get_logger
19
19
  from apitally.client.request_logging import (
20
20
  BODY_TOO_LARGE,
21
21
  MAX_BODY_SIZE,
22
+ RequestLogger,
22
23
  RequestLoggingConfig,
23
24
  )
24
25
  from apitally.common import get_versions, parse_int
@@ -113,7 +114,7 @@ class ApitallyMiddleware:
113
114
  )
114
115
 
115
116
  def __call__(self, request: HttpRequest) -> HttpResponse:
116
- if request.method is not None and request.method != "OPTIONS":
117
+ if self.client.enabled and request.method is not None and request.method != "OPTIONS":
117
118
  timestamp = time.time()
118
119
  request_size = parse_int(request.headers.get("Content-Length"))
119
120
  request_body = b""
@@ -133,7 +134,12 @@ class ApitallyMiddleware:
133
134
  else (len(response.content) if not response.streaming else None)
134
135
  )
135
136
  response_body = b""
136
- if self.capture_response_body and not response.streaming:
137
+ response_content_type = response.get("Content-Type")
138
+ if (
139
+ self.capture_response_body
140
+ and not response.streaming
141
+ and RequestLogger.is_supported_content_type(response_content_type)
142
+ ):
137
143
  response_body = (
138
144
  response.content if response_size is not None and response_size <= MAX_BODY_SIZE else BODY_TOO_LARGE
139
145
  )
@@ -17,6 +17,7 @@ from apitally.client.consumers import Consumer as ApitallyConsumer
17
17
  from apitally.client.request_logging import (
18
18
  BODY_TOO_LARGE,
19
19
  MAX_BODY_SIZE,
20
+ RequestLogger,
20
21
  RequestLoggingConfig,
21
22
  )
22
23
  from apitally.common import get_versions
@@ -74,6 +75,9 @@ class ApitallyMiddleware:
74
75
  self.client.set_startup_data(data)
75
76
 
76
77
  def __call__(self, environ: WSGIEnvironment, start_response: StartResponse) -> Iterable[bytes]:
78
+ if not self.client.enabled:
79
+ return self.wsgi_app(environ, start_response)
80
+
77
81
  timestamp = time.time()
78
82
  response_headers = Headers([])
79
83
  status_code = 0
@@ -100,7 +104,8 @@ class ApitallyMiddleware:
100
104
  response_time = time.perf_counter() - start_time
101
105
 
102
106
  response_body = b""
103
- if self.capture_response_body:
107
+ response_content_type = response_headers.get("Content-Type")
108
+ if self.capture_response_body and RequestLogger.is_supported_content_type(response_content_type):
104
109
  response_size = response_headers.get("Content-Length", type=int)
105
110
  if response_size is not None and response_size > MAX_BODY_SIZE:
106
111
  response_body = BODY_TOO_LARGE
@@ -19,6 +19,7 @@ from apitally.client.consumers import Consumer as ApitallyConsumer
19
19
  from apitally.client.request_logging import (
20
20
  BODY_TOO_LARGE,
21
21
  MAX_BODY_SIZE,
22
+ RequestLogger,
22
23
  RequestLoggingConfig,
23
24
  )
24
25
  from apitally.common import get_versions, parse_int
@@ -86,7 +87,7 @@ class ApitallyPlugin(InitPluginProtocol):
86
87
 
87
88
  def middleware_factory(self, app: ASGIApp) -> ASGIApp:
88
89
  async def middleware(scope: Scope, receive: Receive, send: Send) -> None:
89
- if scope["type"] == "http" and scope["method"] != "OPTIONS":
90
+ if self.client.enabled and scope["type"] == "http" and scope["method"] != "OPTIONS":
90
91
  timestamp = time.time()
91
92
  request = Request(scope)
92
93
  request_size = parse_int(request.headers.get("Content-Length"))
@@ -99,6 +100,7 @@ class ApitallyPlugin(InitPluginProtocol):
99
100
  response_body_too_large = False
100
101
  response_size: Optional[int] = None
101
102
  response_chunked = False
103
+ response_content_type: Optional[str] = None
102
104
  start_time = time.perf_counter()
103
105
 
104
106
  async def receive_wrapper() -> Message:
@@ -119,6 +121,7 @@ class ApitallyPlugin(InitPluginProtocol):
119
121
  response_body, \
120
122
  response_body_too_large, \
121
123
  response_chunked, \
124
+ response_content_type, \
122
125
  response_size
123
126
  if message["type"] == "http.response.start":
124
127
  response_time = time.perf_counter() - start_time
@@ -128,12 +131,17 @@ class ApitallyPlugin(InitPluginProtocol):
128
131
  response_headers.get("Transfer-Encoding") == "chunked"
129
132
  or "Content-Length" not in response_headers
130
133
  )
134
+ response_content_type = response_headers.get("Content-Type")
131
135
  response_size = parse_int(response_headers.get("Content-Length")) if not response_chunked else 0
132
136
  response_body_too_large = response_size is not None and response_size > MAX_BODY_SIZE
133
137
  elif message["type"] == "http.response.body":
134
138
  if response_chunked and response_size is not None:
135
139
  response_size += len(message.get("body", b""))
136
- if (self.capture_response_body or response_status == 400) and not response_body_too_large:
140
+ if (
141
+ (self.capture_response_body or response_status == 400)
142
+ and RequestLogger.is_supported_content_type(response_content_type)
143
+ and not response_body_too_large
144
+ ):
137
145
  response_body += message.get("body", b"")
138
146
  if len(response_body) > MAX_BODY_SIZE:
139
147
  response_body_too_large = True
@@ -20,6 +20,7 @@ from apitally.client.consumers import Consumer as ApitallyConsumer
20
20
  from apitally.client.request_logging import (
21
21
  BODY_TOO_LARGE,
22
22
  MAX_BODY_SIZE,
23
+ RequestLogger,
23
24
  RequestLoggingConfig,
24
25
  )
25
26
  from apitally.common import get_versions, parse_int
@@ -73,7 +74,7 @@ class ApitallyMiddleware:
73
74
  self.client.set_startup_data(data)
74
75
 
75
76
  async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
76
- if scope["type"] == "http" and scope["method"] != "OPTIONS":
77
+ if self.client.enabled and scope["type"] == "http" and scope["method"] != "OPTIONS":
77
78
  timestamp = time.time()
78
79
  request = Request(scope)
79
80
  request_size = parse_int(request.headers.get("Content-Length"))
@@ -86,6 +87,7 @@ class ApitallyMiddleware:
86
87
  response_body_too_large = False
87
88
  response_size: Optional[int] = None
88
89
  response_chunked = False
90
+ response_content_type: Optional[str] = None
89
91
  exception: Optional[BaseException] = None
90
92
  start_time = time.perf_counter()
91
93
 
@@ -107,6 +109,7 @@ class ApitallyMiddleware:
107
109
  response_body, \
108
110
  response_body_too_large, \
109
111
  response_chunked, \
112
+ response_content_type, \
110
113
  response_size
111
114
  if message["type"] == "http.response.start":
112
115
  response_time = time.perf_counter() - start_time
@@ -116,12 +119,17 @@ class ApitallyMiddleware:
116
119
  response_headers.get("Transfer-Encoding") == "chunked"
117
120
  or "Content-Length" not in response_headers
118
121
  )
122
+ response_content_type = response_headers.get("Content-Type")
119
123
  response_size = parse_int(response_headers.get("Content-Length")) if not response_chunked else 0
120
124
  response_body_too_large = response_size is not None and response_size > MAX_BODY_SIZE
121
125
  elif message["type"] == "http.response.body":
122
126
  if response_chunked and response_size is not None:
123
127
  response_size += len(message.get("body", b""))
124
- if (self.capture_response_body or response_status == 422) and not response_body_too_large:
128
+ if (
129
+ (self.capture_response_body or response_status == 422)
130
+ and RequestLogger.is_supported_content_type(response_content_type)
131
+ and not response_body_too_large
132
+ ):
125
133
  response_body += message.get("body", b"")
126
134
  if len(response_body) > MAX_BODY_SIZE:
127
135
  response_body_too_large = True
@@ -80,10 +80,11 @@ Repository = "https://github.com/apitally/apitally-py"
80
80
  [dependency-groups]
81
81
  dev = [
82
82
  "ipykernel~=6.29.0",
83
- "mypy~=1.14.0",
83
+ "mypy~=1.14.0; python_version<'3.9'",
84
+ "mypy~=1.15.0; python_version>='3.9'",
84
85
  "pre-commit~=3.5.0; python_version<'3.9'",
85
- "pre-commit~=4.0.1; python_version>='3.9'",
86
- "ruff~=0.8.0",
86
+ "pre-commit~=4.1.0; python_version>='3.9'",
87
+ "ruff~=0.9.1",
87
88
  ]
88
89
  test = [
89
90
  "pytest~=7.4.4; python_version<'3.9'",