apitally 0.18.1__py3-none-any.whl → 0.18.2__py3-none-any.whl
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.
- apitally/client/request_logging.py +2 -2
- apitally/django.py +102 -103
- {apitally-0.18.1.dist-info → apitally-0.18.2.dist-info}/METADATA +8 -12
- {apitally-0.18.1.dist-info → apitally-0.18.2.dist-info}/RECORD +6 -6
- {apitally-0.18.1.dist-info → apitally-0.18.2.dist-info}/WHEEL +0 -0
- {apitally-0.18.1.dist-info → apitally-0.18.2.dist-info}/licenses/LICENSE +0 -0
@@ -97,8 +97,8 @@ class RequestLoggingConfig:
|
|
97
97
|
log_exception: Whether to log unhandled exceptions in case of server errors
|
98
98
|
mask_query_params: Query parameter names to mask in logs. Expects regular expressions.
|
99
99
|
mask_headers: Header names to mask in logs. Expects regular expressions.
|
100
|
-
mask_request_body_callback: Callback to mask the request body. Expects
|
101
|
-
mask_response_body_callback: Callback to mask the response body. Expects
|
100
|
+
mask_request_body_callback: Callback to mask the request body. Expects `request: RequestDict` as argument and returns the masked body as bytes or None.
|
101
|
+
mask_response_body_callback: Callback to mask the response body. Expects `request: RequestDict` and `response: ResponseDict` as arguments and returns the masked body as bytes or None.
|
102
102
|
exclude_paths: Paths to exclude from logging. Expects regular expressions.
|
103
103
|
exclude_callback: Callback to exclude requests from logging. Should expect two arguments, `request: RequestDict` and `response: ResponseDict`, and return True to exclude the request.
|
104
104
|
"""
|
apitally/django.py
CHANGED
@@ -123,115 +123,114 @@ class ApitallyMiddleware:
|
|
123
123
|
)
|
124
124
|
|
125
125
|
def __call__(self, request: HttpRequest) -> HttpResponse:
|
126
|
-
if self.client.enabled
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
126
|
+
if not self.client.enabled or request.method is None or request.method == "OPTIONS":
|
127
|
+
return self.get_response(request)
|
128
|
+
|
129
|
+
timestamp = time.time()
|
130
|
+
request_size = parse_int(request.headers.get("Content-Length"))
|
131
|
+
request_body = b""
|
132
|
+
if self.capture_request_body:
|
133
|
+
request_body = (
|
134
|
+
request.body
|
135
|
+
if request_size is not None and request_size <= MAX_BODY_SIZE and len(request.body) <= MAX_BODY_SIZE
|
136
|
+
else BODY_TOO_LARGE
|
137
|
+
)
|
137
138
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
139
|
+
start_time = time.perf_counter()
|
140
|
+
response = self.get_response(request)
|
141
|
+
response_time = time.perf_counter() - start_time
|
142
|
+
path = self.get_path(request)
|
143
|
+
|
144
|
+
if path is None:
|
145
|
+
return response
|
146
|
+
|
147
|
+
try:
|
148
|
+
consumer = self.get_consumer(request)
|
149
|
+
consumer_identifier = consumer.identifier if consumer else None
|
150
|
+
self.client.consumer_registry.add_or_update_consumer(consumer)
|
151
|
+
except Exception: # pragma: no cover
|
152
|
+
logger.exception("Failed to get consumer for request")
|
153
|
+
consumer_identifier = None
|
154
|
+
|
155
|
+
response_size = (
|
156
|
+
parse_int(response["Content-Length"])
|
157
|
+
if response.has_header("Content-Length")
|
158
|
+
else (len(response.content) if not response.streaming else None)
|
159
|
+
)
|
160
|
+
response_body = b""
|
161
|
+
response_content_type = response.get("Content-Type")
|
162
|
+
if (
|
163
|
+
self.capture_response_body
|
164
|
+
and not response.streaming
|
165
|
+
and RequestLogger.is_supported_content_type(response_content_type)
|
166
|
+
):
|
167
|
+
response_body = (
|
168
|
+
response.content if response_size is not None and response_size <= MAX_BODY_SIZE else BODY_TOO_LARGE
|
169
|
+
)
|
169
170
|
|
170
|
-
|
171
|
-
|
171
|
+
try:
|
172
|
+
self.client.request_counter.add_request(
|
173
|
+
consumer=consumer_identifier,
|
174
|
+
method=request.method,
|
175
|
+
path=path,
|
176
|
+
status_code=response.status_code,
|
177
|
+
response_time=response_time,
|
178
|
+
request_size=request_size,
|
179
|
+
response_size=response_size,
|
180
|
+
)
|
181
|
+
except Exception: # pragma: no cover
|
182
|
+
logger.exception("Failed to log request metadata")
|
183
|
+
|
184
|
+
if (
|
185
|
+
response.status_code == 422
|
186
|
+
and (content_type := response.get("Content-Type")) is not None
|
187
|
+
and content_type.startswith("application/json")
|
188
|
+
):
|
189
|
+
try:
|
190
|
+
body = try_json_loads(response.content, encoding=response.get("Content-Encoding"))
|
191
|
+
if isinstance(body, dict) and "detail" in body and isinstance(body["detail"], list):
|
192
|
+
# Log Django Ninja / Pydantic validation errors
|
193
|
+
self.client.validation_error_counter.add_validation_errors(
|
172
194
|
consumer=consumer_identifier,
|
173
195
|
method=request.method,
|
174
196
|
path=path,
|
175
|
-
|
176
|
-
response_time=response_time,
|
177
|
-
request_size=request_size,
|
178
|
-
response_size=response_size,
|
197
|
+
detail=body["detail"],
|
179
198
|
)
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
"timestamp": timestamp,
|
216
|
-
"method": request.method,
|
217
|
-
"path": path,
|
218
|
-
"url": request.build_absolute_uri(),
|
219
|
-
"headers": list(request.headers.items()),
|
220
|
-
"size": request_size,
|
221
|
-
"consumer": consumer_identifier,
|
222
|
-
"body": request_body,
|
223
|
-
},
|
224
|
-
response={
|
225
|
-
"status_code": response.status_code,
|
226
|
-
"response_time": response_time,
|
227
|
-
"headers": list(response.items()),
|
228
|
-
"size": response_size,
|
229
|
-
"body": response_body,
|
230
|
-
},
|
231
|
-
exception=getattr(request, "unhandled_exception", None),
|
232
|
-
)
|
233
|
-
else:
|
234
|
-
response = self.get_response(request)
|
199
|
+
except Exception: # pragma: no cover
|
200
|
+
logger.exception("Failed to log validation errors")
|
201
|
+
|
202
|
+
if response.status_code == 500 and hasattr(request, "unhandled_exception"):
|
203
|
+
try:
|
204
|
+
self.client.server_error_counter.add_server_error(
|
205
|
+
consumer=consumer_identifier,
|
206
|
+
method=request.method,
|
207
|
+
path=path,
|
208
|
+
exception=getattr(request, "unhandled_exception"),
|
209
|
+
)
|
210
|
+
except Exception: # pragma: no cover
|
211
|
+
logger.exception("Failed to log server error")
|
212
|
+
|
213
|
+
if self.client.request_logger.enabled:
|
214
|
+
self.client.request_logger.log_request(
|
215
|
+
request={
|
216
|
+
"timestamp": timestamp,
|
217
|
+
"method": request.method,
|
218
|
+
"path": path,
|
219
|
+
"url": request.build_absolute_uri(),
|
220
|
+
"headers": list(request.headers.items()),
|
221
|
+
"size": request_size,
|
222
|
+
"consumer": consumer_identifier,
|
223
|
+
"body": request_body,
|
224
|
+
},
|
225
|
+
response={
|
226
|
+
"status_code": response.status_code,
|
227
|
+
"response_time": response_time,
|
228
|
+
"headers": list(response.items()),
|
229
|
+
"size": response_size,
|
230
|
+
"body": response_body,
|
231
|
+
},
|
232
|
+
exception=getattr(request, "unhandled_exception", None),
|
233
|
+
)
|
235
234
|
|
236
235
|
return response
|
237
236
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: apitally
|
3
|
-
Version: 0.18.
|
3
|
+
Version: 0.18.2
|
4
4
|
Summary: Simple API monitoring & analytics for REST APIs built with FastAPI, Flask, Django, Starlette, Litestar and BlackSheep.
|
5
5
|
Project-URL: Homepage, https://apitally.io
|
6
6
|
Project-URL: Documentation, https://docs.apitally.io
|
@@ -67,21 +67,17 @@ Description-Content-Type: text/markdown
|
|
67
67
|
<p align="center">
|
68
68
|
<a href="https://apitally.io" target="_blank">
|
69
69
|
<picture>
|
70
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-
|
71
|
-
<source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-
|
72
|
-
<img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-
|
70
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-horizontal-new-dark.png">
|
71
|
+
<source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-horizontal-new-light.png">
|
72
|
+
<img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-horizontal-new-light.png" width="220">
|
73
73
|
</picture>
|
74
74
|
</a>
|
75
75
|
</p>
|
76
|
-
|
77
76
|
<p align="center"><b>Simple, privacy-focused API monitoring & analytics</b></p>
|
78
|
-
|
79
|
-
<
|
77
|
+
<p align="center" style="color: #ccc;">Apitally gives you the visibility you need to build better APIs – with just a few lines of code.</p>
|
78
|
+
<br>
|
79
|
+
<img alt="Apitally screenshots" src="https://assets.apitally.io/screenshots/overview.png">
|
80
80
|
<br>
|
81
|
-
|
82
|
-

|
83
|
-
|
84
|
-
---
|
85
81
|
|
86
82
|
# Apitally SDK for Python
|
87
83
|
|
@@ -138,7 +134,7 @@ pip install apitally[fastapi]
|
|
138
134
|
```
|
139
135
|
|
140
136
|
The available extras are: `fastapi`, `flask`, `django_rest_framework`,
|
141
|
-
`django_ninja`, `starlette` and `
|
137
|
+
`django_ninja`, `starlette`, `litestar` and `blacksheep`.
|
142
138
|
|
143
139
|
## Usage
|
144
140
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
apitally/__init__.py,sha256=ShXQBVjyiSOHxoQJS2BvNG395W4KZfqMxZWBAR0MZrE,22
|
2
2
|
apitally/blacksheep.py,sha256=KvcPFeiwQgWZmRglbm8SLaN6_WRs5kZ3SymB1IuLR-A,9616
|
3
3
|
apitally/common.py,sha256=azDxepViH0QW0MuufTHxeSQyLGzCkocAX_KPziWTx8A,1605
|
4
|
-
apitally/django.py,sha256=
|
4
|
+
apitally/django.py,sha256=7eSh1tzuu0wRl9PxJgXMBSz9DfyohCDIqd4ecQTOJ1M,18499
|
5
5
|
apitally/django_ninja.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
|
6
6
|
apitally/django_rest_framework.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
|
7
7
|
apitally/fastapi.py,sha256=IfKfgsmIY8_AtnuMTW2sW4qnkya61CAE2vBoIpcc9tk,169
|
@@ -15,12 +15,12 @@ apitally/client/client_base.py,sha256=DvivGeHd3dyOASRvkIo44Zh8RzdBMfH8_rROa2lFbg
|
|
15
15
|
apitally/client/client_threading.py,sha256=sxMRcxRgk1SxJjSq-qpIcDVmD3Q7Kv4CVT5zEUVt0KM,7257
|
16
16
|
apitally/client/consumers.py,sha256=w_AFQhVgdtJVt7pVySBvSZwQg-2JVqmD2JQtVBoMkus,2626
|
17
17
|
apitally/client/logging.py,sha256=QMsKIIAFo92PNBUleeTgsrsQa7SEal-oJa1oOHUr1wI,507
|
18
|
-
apitally/client/request_logging.py,sha256=
|
18
|
+
apitally/client/request_logging.py,sha256=FIJfbc4fYZnBIKebpJtrk19DcAxJylltghY7RFitWsE,14173
|
19
19
|
apitally/client/requests.py,sha256=SDptGOg9XvaEKFj2o3oxJz-JAuZzUrqpHnbOQixf99o,3794
|
20
20
|
apitally/client/sentry.py,sha256=qMjHdI0V7c50ruo1WjmjWc8g6oGDv724vSCvcuZ8G9k,1188
|
21
21
|
apitally/client/server_errors.py,sha256=4B2BKDFoIpoWc55UVH6AIdYSgzj6zxCdMNUW77JjhZw,3423
|
22
22
|
apitally/client/validation_errors.py,sha256=6G8WYWFgJs9VH9swvkPXJGuOJgymj5ooWA9OwjUTbuM,1964
|
23
|
-
apitally-0.18.
|
24
|
-
apitally-0.18.
|
25
|
-
apitally-0.18.
|
26
|
-
apitally-0.18.
|
23
|
+
apitally-0.18.2.dist-info/METADATA,sha256=J2Mr1Y-82i7_Batr5bwlTCEhMzcnirozN83VNZgIJ4U,9269
|
24
|
+
apitally-0.18.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
25
|
+
apitally-0.18.2.dist-info/licenses/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
|
26
|
+
apitally-0.18.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|