apitally 0.13.0a1__py3-none-any.whl → 0.14.0__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/{asyncio.py → client_asyncio.py} +55 -16
- apitally/client/client_base.py +97 -0
- apitally/client/{threading.py → client_threading.py} +51 -10
- apitally/client/consumers.py +66 -0
- apitally/client/request_logging.py +340 -0
- apitally/client/requests.py +86 -0
- apitally/client/server_errors.py +126 -0
- apitally/client/validation_errors.py +58 -0
- apitally/common.py +10 -1
- apitally/django.py +112 -46
- apitally/django_ninja.py +2 -2
- apitally/django_rest_framework.py +2 -2
- apitally/fastapi.py +2 -2
- apitally/flask.py +100 -26
- apitally/litestar.py +122 -54
- apitally/starlette.py +90 -29
- {apitally-0.13.0a1.dist-info → apitally-0.14.0.dist-info}/METADATA +8 -9
- apitally-0.14.0.dist-info/RECORD +24 -0
- {apitally-0.13.0a1.dist-info → apitally-0.14.0.dist-info}/WHEEL +1 -1
- apitally/client/base.py +0 -404
- apitally-0.13.0a1.dist-info/RECORD +0 -19
- {apitally-0.13.0a1.dist-info → apitally-0.14.0.dist-info}/licenses/LICENSE +0 -0
apitally/litestar.py
CHANGED
@@ -13,12 +13,17 @@ from litestar.handlers import HTTPRouteHandler
|
|
13
13
|
from litestar.plugins import InitPluginProtocol
|
14
14
|
from litestar.types import ASGIApp, Message, Receive, Scope, Send
|
15
15
|
|
16
|
-
from apitally.client.
|
17
|
-
from apitally.client.
|
18
|
-
from apitally.
|
16
|
+
from apitally.client.client_asyncio import ApitallyClient
|
17
|
+
from apitally.client.consumers import Consumer as ApitallyConsumer
|
18
|
+
from apitally.client.request_logging import (
|
19
|
+
BODY_TOO_LARGE,
|
20
|
+
MAX_BODY_SIZE,
|
21
|
+
RequestLoggingConfig,
|
22
|
+
)
|
23
|
+
from apitally.common import get_versions, parse_int
|
19
24
|
|
20
25
|
|
21
|
-
__all__ = ["ApitallyPlugin", "ApitallyConsumer"]
|
26
|
+
__all__ = ["ApitallyPlugin", "ApitallyConsumer", "RequestLoggingConfig"]
|
22
27
|
|
23
28
|
|
24
29
|
class ApitallyPlugin(InitPluginProtocol):
|
@@ -26,15 +31,23 @@ class ApitallyPlugin(InitPluginProtocol):
|
|
26
31
|
self,
|
27
32
|
client_id: str,
|
28
33
|
env: str = "dev",
|
34
|
+
request_logging_config: Optional[RequestLoggingConfig] = None,
|
29
35
|
app_version: Optional[str] = None,
|
30
36
|
filter_openapi_paths: bool = True,
|
31
37
|
identify_consumer_callback: Optional[Callable[[Request], Union[str, ApitallyConsumer, None]]] = None,
|
32
38
|
) -> None:
|
33
|
-
self.client = ApitallyClient(client_id=client_id, env=env)
|
39
|
+
self.client = ApitallyClient(client_id=client_id, env=env, request_logging_config=request_logging_config)
|
34
40
|
self.app_version = app_version
|
35
41
|
self.filter_openapi_paths = filter_openapi_paths
|
36
42
|
self.identify_consumer_callback = identify_consumer_callback
|
43
|
+
|
37
44
|
self.openapi_path = "/schema"
|
45
|
+
self.capture_request_body = (
|
46
|
+
self.client.request_logger.config.enabled and self.client.request_logger.config.log_request_body
|
47
|
+
)
|
48
|
+
self.capture_response_body = (
|
49
|
+
self.client.request_logger.config.enabled and self.client.request_logger.config.log_response_body
|
50
|
+
)
|
38
51
|
|
39
52
|
def on_app_init(self, app_config: AppConfig) -> AppConfig:
|
40
53
|
app_config.on_startup.append(self.on_startup)
|
@@ -67,23 +80,39 @@ class ApitallyPlugin(InitPluginProtocol):
|
|
67
80
|
def middleware_factory(self, app: ASGIApp) -> ASGIApp:
|
68
81
|
async def middleware(scope: Scope, receive: Receive, send: Send) -> None:
|
69
82
|
if scope["type"] == "http" and scope["method"] != "OPTIONS":
|
83
|
+
timestamp = time.time()
|
70
84
|
request = Request(scope)
|
85
|
+
request_size = parse_int(request.headers.get("Content-Length"))
|
86
|
+
request_body = b""
|
87
|
+
request_body_too_large = request_size is not None and request_size > MAX_BODY_SIZE
|
71
88
|
response_status = 0
|
72
89
|
response_time = 0.0
|
73
90
|
response_headers = Headers()
|
74
91
|
response_body = b""
|
75
|
-
|
92
|
+
response_body_too_large = False
|
93
|
+
response_size: Optional[int] = None
|
76
94
|
response_chunked = False
|
77
95
|
start_time = time.perf_counter()
|
78
96
|
|
97
|
+
async def receive_wrapper() -> Message:
|
98
|
+
nonlocal request_body, request_body_too_large
|
99
|
+
message = await receive()
|
100
|
+
if message["type"] == "http.request" and self.capture_request_body and not request_body_too_large:
|
101
|
+
request_body += message.get("body", b"")
|
102
|
+
if len(request_body) > MAX_BODY_SIZE:
|
103
|
+
request_body_too_large = True
|
104
|
+
request_body = b""
|
105
|
+
return message
|
106
|
+
|
79
107
|
async def send_wrapper(message: Message) -> None:
|
80
108
|
nonlocal \
|
81
109
|
response_time, \
|
82
110
|
response_status, \
|
83
111
|
response_headers, \
|
84
112
|
response_body, \
|
85
|
-
|
86
|
-
response_chunked
|
113
|
+
response_body_too_large, \
|
114
|
+
response_chunked, \
|
115
|
+
response_size
|
87
116
|
if message["type"] == "http.response.start":
|
88
117
|
response_time = time.perf_counter() - start_time
|
89
118
|
response_status = message["status"]
|
@@ -92,20 +121,28 @@ class ApitallyPlugin(InitPluginProtocol):
|
|
92
121
|
response_headers.get("Transfer-Encoding") == "chunked"
|
93
122
|
or "Content-Length" not in response_headers
|
94
123
|
)
|
124
|
+
response_size = parse_int(response_headers.get("Content-Length")) if not response_chunked else 0
|
125
|
+
response_body_too_large = response_size is not None and response_size > MAX_BODY_SIZE
|
95
126
|
elif message["type"] == "http.response.body":
|
96
|
-
if response_chunked:
|
127
|
+
if response_chunked and response_size is not None:
|
97
128
|
response_size += len(message.get("body", b""))
|
98
|
-
if response_status == 400:
|
129
|
+
if (self.capture_response_body or response_status == 400) and not response_body_too_large:
|
99
130
|
response_body += message.get("body", b"")
|
131
|
+
if len(response_body) > MAX_BODY_SIZE:
|
132
|
+
response_body_too_large = True
|
133
|
+
response_body = b""
|
100
134
|
await send(message)
|
101
135
|
|
102
|
-
await app(scope,
|
136
|
+
await app(scope, receive_wrapper, send_wrapper)
|
103
137
|
self.add_request(
|
138
|
+
timestamp=timestamp,
|
104
139
|
request=request,
|
140
|
+
request_body=request_body if not request_body_too_large else BODY_TOO_LARGE,
|
141
|
+
request_size=request_size,
|
105
142
|
response_status=response_status,
|
106
143
|
response_time=response_time,
|
107
144
|
response_headers=response_headers,
|
108
|
-
response_body=response_body,
|
145
|
+
response_body=response_body if not response_body_too_large else BODY_TOO_LARGE,
|
109
146
|
response_size=response_size,
|
110
147
|
)
|
111
148
|
else:
|
@@ -115,64 +152,95 @@ class ApitallyPlugin(InitPluginProtocol):
|
|
115
152
|
|
116
153
|
def add_request(
|
117
154
|
self,
|
155
|
+
timestamp: float,
|
118
156
|
request: Request,
|
157
|
+
request_body: bytes,
|
158
|
+
request_size: Optional[int],
|
119
159
|
response_status: int,
|
120
160
|
response_time: float,
|
121
161
|
response_headers: Headers,
|
122
162
|
response_body: bytes,
|
123
|
-
response_size: int
|
163
|
+
response_size: Optional[int],
|
124
164
|
) -> None:
|
125
|
-
if response_status < 100
|
165
|
+
if response_status < 100:
|
126
166
|
return # pragma: no cover
|
127
167
|
path = self.get_path(request)
|
128
|
-
if
|
168
|
+
if self.filter_path(path):
|
129
169
|
return
|
170
|
+
|
130
171
|
consumer = self.get_consumer(request)
|
131
172
|
consumer_identifier = consumer.identifier if consumer else None
|
132
173
|
self.client.consumer_registry.add_or_update_consumer(consumer)
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
path=path,
|
137
|
-
status_code=response_status,
|
138
|
-
response_time=response_time,
|
139
|
-
request_size=request.headers.get("Content-Length"),
|
140
|
-
response_size=response_size or response_headers.get("Content-Length"),
|
141
|
-
)
|
142
|
-
if response_status == 400 and response_body and len(response_body) < 4096:
|
143
|
-
with contextlib.suppress(json.JSONDecodeError):
|
144
|
-
parsed_body = json.loads(response_body)
|
145
|
-
if (
|
146
|
-
isinstance(parsed_body, dict)
|
147
|
-
and "detail" in parsed_body
|
148
|
-
and isinstance(parsed_body["detail"], str)
|
149
|
-
and "validation" in parsed_body["detail"].lower()
|
150
|
-
and "extra" in parsed_body
|
151
|
-
and isinstance(parsed_body["extra"], list)
|
152
|
-
):
|
153
|
-
self.client.validation_error_counter.add_validation_errors(
|
154
|
-
consumer=consumer_identifier,
|
155
|
-
method=request.method,
|
156
|
-
path=path,
|
157
|
-
detail=[
|
158
|
-
{
|
159
|
-
"loc": [error.get("source", "body")] + error["key"].split("."),
|
160
|
-
"msg": error["message"],
|
161
|
-
"type": "",
|
162
|
-
}
|
163
|
-
for error in parsed_body["extra"]
|
164
|
-
if "key" in error and "message" in error
|
165
|
-
],
|
166
|
-
)
|
167
|
-
if response_status == 500 and "exception" in request.state:
|
168
|
-
self.client.server_error_counter.add_server_error(
|
174
|
+
|
175
|
+
if path is not None:
|
176
|
+
self.client.request_counter.add_request(
|
169
177
|
consumer=consumer_identifier,
|
170
178
|
method=request.method,
|
171
179
|
path=path,
|
172
|
-
|
180
|
+
status_code=response_status,
|
181
|
+
response_time=response_time,
|
182
|
+
request_size=request_size,
|
183
|
+
response_size=response_size,
|
184
|
+
)
|
185
|
+
|
186
|
+
if response_status == 400 and response_body and len(response_body) < 4096:
|
187
|
+
with contextlib.suppress(json.JSONDecodeError):
|
188
|
+
parsed_body = json.loads(response_body)
|
189
|
+
if (
|
190
|
+
isinstance(parsed_body, dict)
|
191
|
+
and "detail" in parsed_body
|
192
|
+
and isinstance(parsed_body["detail"], str)
|
193
|
+
and "validation" in parsed_body["detail"].lower()
|
194
|
+
and "extra" in parsed_body
|
195
|
+
and isinstance(parsed_body["extra"], list)
|
196
|
+
):
|
197
|
+
self.client.validation_error_counter.add_validation_errors(
|
198
|
+
consumer=consumer_identifier,
|
199
|
+
method=request.method,
|
200
|
+
path=path,
|
201
|
+
detail=[
|
202
|
+
{
|
203
|
+
"loc": [error.get("source", "body")] + error["key"].split("."),
|
204
|
+
"msg": error["message"],
|
205
|
+
"type": "",
|
206
|
+
}
|
207
|
+
for error in parsed_body["extra"]
|
208
|
+
if "key" in error and "message" in error
|
209
|
+
],
|
210
|
+
)
|
211
|
+
|
212
|
+
if response_status == 500 and "exception" in request.state:
|
213
|
+
self.client.server_error_counter.add_server_error(
|
214
|
+
consumer=consumer_identifier,
|
215
|
+
method=request.method,
|
216
|
+
path=path,
|
217
|
+
exception=request.state["exception"],
|
218
|
+
)
|
219
|
+
|
220
|
+
if self.client.request_logger.enabled:
|
221
|
+
self.client.request_logger.log_request(
|
222
|
+
request={
|
223
|
+
"timestamp": timestamp,
|
224
|
+
"method": request.method,
|
225
|
+
"path": path,
|
226
|
+
"url": str(request.url),
|
227
|
+
"headers": [(k, v) for k, v in request.headers.items()],
|
228
|
+
"size": request_size,
|
229
|
+
"consumer": consumer_identifier,
|
230
|
+
"body": request_body,
|
231
|
+
},
|
232
|
+
response={
|
233
|
+
"status_code": response_status,
|
234
|
+
"response_time": response_time,
|
235
|
+
"headers": [(k, v) for k, v in response_headers.items()],
|
236
|
+
"size": response_size,
|
237
|
+
"body": response_body,
|
238
|
+
},
|
173
239
|
)
|
174
240
|
|
175
241
|
def get_path(self, request: Request) -> Optional[str]:
|
242
|
+
if not request.route_handler.paths:
|
243
|
+
return None
|
176
244
|
path: List[str] = []
|
177
245
|
for layer in request.route_handler.ownership_layers:
|
178
246
|
if isinstance(layer, HTTPRouteHandler):
|
@@ -183,8 +251,8 @@ class ApitallyPlugin(InitPluginProtocol):
|
|
183
251
|
path.append(layer.path.lstrip("/"))
|
184
252
|
return "/" + "/".join(filter(None, path))
|
185
253
|
|
186
|
-
def filter_path(self, path: str) -> bool:
|
187
|
-
if self.filter_openapi_paths and self.openapi_path:
|
254
|
+
def filter_path(self, path: Optional[str]) -> bool:
|
255
|
+
if path is not None and self.filter_openapi_paths and self.openapi_path:
|
188
256
|
return path == self.openapi_path or path.startswith(self.openapi_path + "/")
|
189
257
|
return False # pragma: no cover
|
190
258
|
|
apitally/starlette.py
CHANGED
@@ -4,7 +4,7 @@ import asyncio
|
|
4
4
|
import contextlib
|
5
5
|
import json
|
6
6
|
import time
|
7
|
-
from typing import Any, Callable, Dict, List, Optional,
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
8
8
|
from warnings import warn
|
9
9
|
|
10
10
|
from httpx import HTTPStatusError
|
@@ -15,12 +15,17 @@ from starlette.schemas import EndpointInfo, SchemaGenerator
|
|
15
15
|
from starlette.testclient import TestClient
|
16
16
|
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
17
17
|
|
18
|
-
from apitally.client.
|
19
|
-
from apitally.client.
|
20
|
-
from apitally.
|
18
|
+
from apitally.client.client_asyncio import ApitallyClient
|
19
|
+
from apitally.client.consumers import Consumer as ApitallyConsumer
|
20
|
+
from apitally.client.request_logging import (
|
21
|
+
BODY_TOO_LARGE,
|
22
|
+
MAX_BODY_SIZE,
|
23
|
+
RequestLoggingConfig,
|
24
|
+
)
|
25
|
+
from apitally.common import get_versions, parse_int
|
21
26
|
|
22
27
|
|
23
|
-
__all__ = ["ApitallyMiddleware", "ApitallyConsumer"]
|
28
|
+
__all__ = ["ApitallyMiddleware", "ApitallyConsumer", "RequestLoggingConfig"]
|
24
29
|
|
25
30
|
|
26
31
|
class ApitallyMiddleware:
|
@@ -29,20 +34,26 @@ class ApitallyMiddleware:
|
|
29
34
|
app: ASGIApp,
|
30
35
|
client_id: str,
|
31
36
|
env: str = "dev",
|
37
|
+
request_logging_config: Optional[RequestLoggingConfig] = None,
|
32
38
|
app_version: Optional[str] = None,
|
33
39
|
openapi_url: Optional[str] = "/openapi.json",
|
34
|
-
filter_unhandled_paths: bool = True,
|
35
40
|
identify_consumer_callback: Optional[Callable[[Request], Union[str, ApitallyConsumer, None]]] = None,
|
36
41
|
) -> None:
|
37
42
|
self.app = app
|
38
|
-
self.filter_unhandled_paths = filter_unhandled_paths
|
39
43
|
self.identify_consumer_callback = identify_consumer_callback
|
40
|
-
self.client = ApitallyClient(client_id=client_id, env=env)
|
44
|
+
self.client = ApitallyClient(client_id=client_id, env=env, request_logging_config=request_logging_config)
|
41
45
|
self.client.start_sync_loop()
|
42
46
|
self._delayed_set_startup_data_task: Optional[asyncio.Task] = None
|
43
47
|
self.delayed_set_startup_data(app_version, openapi_url)
|
44
48
|
_register_shutdown_handler(app, self.client.handle_shutdown)
|
45
49
|
|
50
|
+
self.capture_request_body = (
|
51
|
+
self.client.request_logger.config.enabled and self.client.request_logger.config.log_request_body
|
52
|
+
)
|
53
|
+
self.capture_response_body = (
|
54
|
+
self.client.request_logger.config.enabled and self.client.request_logger.config.log_response_body
|
55
|
+
)
|
56
|
+
|
46
57
|
def delayed_set_startup_data(self, app_version: Optional[str] = None, openapi_url: Optional[str] = None) -> None:
|
47
58
|
self._delayed_set_startup_data_task = asyncio.create_task(
|
48
59
|
self._delayed_set_startup_data(app_version, openapi_url)
|
@@ -57,24 +68,40 @@ class ApitallyMiddleware:
|
|
57
68
|
|
58
69
|
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
59
70
|
if scope["type"] == "http" and scope["method"] != "OPTIONS":
|
71
|
+
timestamp = time.time()
|
60
72
|
request = Request(scope)
|
73
|
+
request_size = parse_int(request.headers.get("Content-Length"))
|
74
|
+
request_body = b""
|
75
|
+
request_body_too_large = request_size is not None and request_size > MAX_BODY_SIZE
|
61
76
|
response_status = 0
|
62
77
|
response_time: Optional[float] = None
|
63
78
|
response_headers = Headers()
|
64
79
|
response_body = b""
|
65
|
-
|
80
|
+
response_body_too_large = False
|
81
|
+
response_size: Optional[int] = None
|
66
82
|
response_chunked = False
|
67
83
|
exception: Optional[BaseException] = None
|
68
84
|
start_time = time.perf_counter()
|
69
85
|
|
86
|
+
async def receive_wrapper() -> Message:
|
87
|
+
nonlocal request_body, request_body_too_large
|
88
|
+
message = await receive()
|
89
|
+
if message["type"] == "http.request" and self.capture_request_body and not request_body_too_large:
|
90
|
+
request_body += message.get("body", b"")
|
91
|
+
if len(request_body) > MAX_BODY_SIZE:
|
92
|
+
request_body_too_large = True
|
93
|
+
request_body = b""
|
94
|
+
return message
|
95
|
+
|
70
96
|
async def send_wrapper(message: Message) -> None:
|
71
97
|
nonlocal \
|
72
98
|
response_time, \
|
73
99
|
response_status, \
|
74
100
|
response_headers, \
|
75
101
|
response_body, \
|
76
|
-
|
77
|
-
response_chunked
|
102
|
+
response_body_too_large, \
|
103
|
+
response_chunked, \
|
104
|
+
response_size
|
78
105
|
if message["type"] == "http.response.start":
|
79
106
|
response_time = time.perf_counter() - start_time
|
80
107
|
response_status = message["status"]
|
@@ -83,15 +110,20 @@ class ApitallyMiddleware:
|
|
83
110
|
response_headers.get("Transfer-Encoding") == "chunked"
|
84
111
|
or "Content-Length" not in response_headers
|
85
112
|
)
|
113
|
+
response_size = parse_int(response_headers.get("Content-Length")) if not response_chunked else 0
|
114
|
+
response_body_too_large = response_size is not None and response_size > MAX_BODY_SIZE
|
86
115
|
elif message["type"] == "http.response.body":
|
87
|
-
if response_chunked:
|
116
|
+
if response_chunked and response_size is not None:
|
88
117
|
response_size += len(message.get("body", b""))
|
89
|
-
if response_status == 422:
|
118
|
+
if (self.capture_response_body or response_status == 422) and not response_body_too_large:
|
90
119
|
response_body += message.get("body", b"")
|
120
|
+
if len(response_body) > MAX_BODY_SIZE:
|
121
|
+
response_body_too_large = True
|
122
|
+
response_body = b""
|
91
123
|
await send(message)
|
92
124
|
|
93
125
|
try:
|
94
|
-
await self.app(scope,
|
126
|
+
await self.app(scope, receive_wrapper, send_wrapper)
|
95
127
|
except BaseException as e:
|
96
128
|
exception = e
|
97
129
|
raise e from None
|
@@ -99,11 +131,14 @@ class ApitallyMiddleware:
|
|
99
131
|
if response_time is None:
|
100
132
|
response_time = time.perf_counter() - start_time
|
101
133
|
self.add_request(
|
134
|
+
timestamp=timestamp,
|
102
135
|
request=request,
|
136
|
+
request_body=request_body if not request_body_too_large else BODY_TOO_LARGE,
|
137
|
+
request_size=request_size,
|
103
138
|
response_status=response_status,
|
104
139
|
response_time=response_time,
|
105
140
|
response_headers=response_headers,
|
106
|
-
response_body=response_body,
|
141
|
+
response_body=response_body if not response_body_too_large else BODY_TOO_LARGE,
|
107
142
|
response_size=response_size,
|
108
143
|
exception=exception,
|
109
144
|
)
|
@@ -112,29 +147,34 @@ class ApitallyMiddleware:
|
|
112
147
|
|
113
148
|
def add_request(
|
114
149
|
self,
|
150
|
+
timestamp: float,
|
115
151
|
request: Request,
|
152
|
+
request_body: bytes,
|
153
|
+
request_size: Optional[int],
|
116
154
|
response_status: int,
|
117
155
|
response_time: float,
|
118
156
|
response_headers: Headers,
|
119
157
|
response_body: bytes,
|
120
|
-
response_size: int
|
158
|
+
response_size: Optional[int],
|
121
159
|
exception: Optional[BaseException] = None,
|
122
160
|
) -> None:
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
161
|
+
path = self.get_path(request)
|
162
|
+
|
163
|
+
consumer = self.get_consumer(request)
|
164
|
+
consumer_identifier = consumer.identifier if consumer else None
|
165
|
+
self.client.consumer_registry.add_or_update_consumer(consumer)
|
166
|
+
|
167
|
+
if path is not None:
|
128
168
|
if response_status == 0 and exception is not None:
|
129
169
|
response_status = 500
|
130
170
|
self.client.request_counter.add_request(
|
131
171
|
consumer=consumer_identifier,
|
132
172
|
method=request.method,
|
133
|
-
path=
|
173
|
+
path=path,
|
134
174
|
status_code=response_status,
|
135
175
|
response_time=response_time,
|
136
|
-
request_size=
|
137
|
-
response_size=response_size
|
176
|
+
request_size=request_size,
|
177
|
+
response_size=response_size,
|
138
178
|
)
|
139
179
|
if response_status == 422 and response_body and response_headers.get("Content-Type") == "application/json":
|
140
180
|
with contextlib.suppress(json.JSONDecodeError):
|
@@ -144,24 +184,45 @@ class ApitallyMiddleware:
|
|
144
184
|
self.client.validation_error_counter.add_validation_errors(
|
145
185
|
consumer=consumer_identifier,
|
146
186
|
method=request.method,
|
147
|
-
path=
|
187
|
+
path=path,
|
148
188
|
detail=body["detail"],
|
149
189
|
)
|
150
190
|
if response_status == 500 and exception is not None:
|
151
191
|
self.client.server_error_counter.add_server_error(
|
152
192
|
consumer=consumer_identifier,
|
153
193
|
method=request.method,
|
154
|
-
path=
|
194
|
+
path=path,
|
155
195
|
exception=exception,
|
156
196
|
)
|
157
197
|
|
198
|
+
if self.client.request_logger.enabled:
|
199
|
+
self.client.request_logger.log_request(
|
200
|
+
request={
|
201
|
+
"timestamp": timestamp,
|
202
|
+
"method": request.method,
|
203
|
+
"path": path,
|
204
|
+
"url": str(request.url),
|
205
|
+
"headers": request.headers.items(),
|
206
|
+
"size": request_size,
|
207
|
+
"consumer": consumer_identifier,
|
208
|
+
"body": request_body,
|
209
|
+
},
|
210
|
+
response={
|
211
|
+
"status_code": response_status,
|
212
|
+
"response_time": response_time,
|
213
|
+
"headers": response_headers.items(),
|
214
|
+
"size": response_size,
|
215
|
+
"body": response_body,
|
216
|
+
},
|
217
|
+
)
|
218
|
+
|
158
219
|
@staticmethod
|
159
|
-
def
|
220
|
+
def get_path(request: Request) -> Optional[str]:
|
160
221
|
for route in request.app.routes:
|
161
222
|
match, _ = route.matches(request.scope)
|
162
223
|
if match == Match.FULL:
|
163
|
-
return route.path
|
164
|
-
return
|
224
|
+
return route.path
|
225
|
+
return None
|
165
226
|
|
166
227
|
def get_consumer(self, request: Request) -> Optional[ApitallyConsumer]:
|
167
228
|
if hasattr(request.state, "apitally_consumer") and request.state.apitally_consumer:
|
@@ -1,34 +1,33 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: apitally
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.14.0
|
4
4
|
Summary: Simple API monitoring & analytics for REST APIs built with FastAPI, Flask, Django, Starlette and Litestar.
|
5
|
-
Project-URL:
|
6
|
-
Project-URL:
|
7
|
-
Project-URL:
|
5
|
+
Project-URL: Homepage, https://apitally.io
|
6
|
+
Project-URL: Documentation, https://docs.apitally.io
|
7
|
+
Project-URL: Repository, https://github.com/apitally/apitally-py
|
8
8
|
Author-email: Apitally <hello@apitally.io>
|
9
9
|
License: MIT License
|
10
|
-
|
11
|
-
Classifier: Development Status :: 4 - Beta
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
12
11
|
Classifier: Environment :: Web Environment
|
13
12
|
Classifier: Framework :: Django
|
14
13
|
Classifier: Framework :: FastAPI
|
15
14
|
Classifier: Framework :: Flask
|
16
15
|
Classifier: Intended Audience :: Developers
|
16
|
+
Classifier: Intended Audience :: Information Technology
|
17
17
|
Classifier: License :: OSI Approved :: MIT License
|
18
18
|
Classifier: Programming Language :: Python
|
19
19
|
Classifier: Programming Language :: Python :: 3
|
20
20
|
Classifier: Programming Language :: Python :: 3 :: Only
|
21
|
-
Classifier: Programming Language :: Python :: 3.7
|
22
21
|
Classifier: Programming Language :: Python :: 3.8
|
23
22
|
Classifier: Programming Language :: Python :: 3.9
|
24
23
|
Classifier: Programming Language :: Python :: 3.10
|
25
24
|
Classifier: Programming Language :: Python :: 3.11
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
26
26
|
Classifier: Topic :: Internet
|
27
27
|
Classifier: Topic :: Internet :: WWW/HTTP
|
28
28
|
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
|
29
29
|
Classifier: Topic :: Software Development
|
30
|
-
Classifier: Topic ::
|
31
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
30
|
+
Classifier: Topic :: System :: Monitoring
|
32
31
|
Classifier: Typing :: Typed
|
33
32
|
Requires-Python: <4.0,>=3.8
|
34
33
|
Requires-Dist: backoff>=2.0.0
|
@@ -0,0 +1,24 @@
|
|
1
|
+
apitally/__init__.py,sha256=ShXQBVjyiSOHxoQJS2BvNG395W4KZfqMxZWBAR0MZrE,22
|
2
|
+
apitally/common.py,sha256=Y8MRuTUHFUeQkcDrCLUxnqIPRpYIiW8S43T0QUab-_A,1267
|
3
|
+
apitally/django.py,sha256=2Wg89-NpHGm1Yc25DjtDRVdvW8H3ugSslhxPOQKiXXY,16497
|
4
|
+
apitally/django_ninja.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
|
5
|
+
apitally/django_rest_framework.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
|
6
|
+
apitally/fastapi.py,sha256=IfKfgsmIY8_AtnuMTW2sW4qnkya61CAE2vBoIpcc9tk,169
|
7
|
+
apitally/flask.py,sha256=Q-3_nrCPkinZ8QERVoa_jiZaEmZoY43oIdj5UGx2tk4,9170
|
8
|
+
apitally/litestar.py,sha256=tEaoqRJJNtbijjyrI3ue82ePhZBUk6A2yuXIjlN5oMQ,12951
|
9
|
+
apitally/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
apitally/starlette.py,sha256=ooXr9StS8gU3y8mfyrscUdMiPIBfYMxX5Y6LfIMYQgA,12370
|
11
|
+
apitally/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
+
apitally/client/client_asyncio.py,sha256=cCCtA8Lf-FOfXCstVZEAT3_0bnwH_dU1xe0YzhfLUYU,6691
|
13
|
+
apitally/client/client_base.py,sha256=w5AXAbg3hw5Qds5rovCZFtePB9bHNcJsr9l7kDgbroc,3733
|
14
|
+
apitally/client/client_threading.py,sha256=lLtr89LXrD2roDeUORJkx7QeQOPQqt162Vo8bSExPh4,7101
|
15
|
+
apitally/client/consumers.py,sha256=w_AFQhVgdtJVt7pVySBvSZwQg-2JVqmD2JQtVBoMkus,2626
|
16
|
+
apitally/client/logging.py,sha256=QMsKIIAFo92PNBUleeTgsrsQa7SEal-oJa1oOHUr1wI,507
|
17
|
+
apitally/client/request_logging.py,sha256=4N_JdkkrNVAFhlSAie3Kio3F1ARObYvFOH6D1mFppZg,12361
|
18
|
+
apitally/client/requests.py,sha256=RdJyvIqQGVHvS-wjpAPUwcO7byOJ6jO8dYqNTU2Furg,3685
|
19
|
+
apitally/client/server_errors.py,sha256=axEhOxqV5SWjk0QCZTLVv2UMIaTfqPc81Typ4DXt66A,4646
|
20
|
+
apitally/client/validation_errors.py,sha256=6G8WYWFgJs9VH9swvkPXJGuOJgymj5ooWA9OwjUTbuM,1964
|
21
|
+
apitally-0.14.0.dist-info/METADATA,sha256=hb6VtcV4Q6gTwsNCZvjssvGYTXXf9fGsFe7SlMX8xBs,7577
|
22
|
+
apitally-0.14.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
23
|
+
apitally-0.14.0.dist-info/licenses/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
|
24
|
+
apitally-0.14.0.dist-info/RECORD,,
|