beans-logging-fastapi 1.1.1__py3-none-any.whl → 3.0.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.
- beans_logging_fastapi/__init__.py +6 -24
- beans_logging_fastapi/__version__.py +1 -3
- beans_logging_fastapi/{_async_log.py → _async.py} +39 -27
- beans_logging_fastapi/_core.py +90 -0
- beans_logging_fastapi/config.py +168 -0
- beans_logging_fastapi/constants.py +12 -0
- beans_logging_fastapi/{_filters.py → filters.py} +9 -4
- beans_logging_fastapi/formats.py +95 -0
- beans_logging_fastapi/{_middlewares.py → middlewares.py} +137 -30
- beans_logging_fastapi-3.0.0.dist-info/METADATA +586 -0
- beans_logging_fastapi-3.0.0.dist-info/RECORD +14 -0
- {beans_logging_fastapi-1.1.1.dist-info → beans_logging_fastapi-3.0.0.dist-info}/WHEEL +1 -1
- beans_logging_fastapi-1.1.1.dist-info/LICENCE.txt → beans_logging_fastapi-3.0.0.dist-info/licenses/LICENSE.txt +1 -1
- {beans_logging_fastapi-1.1.1.dist-info → beans_logging_fastapi-3.0.0.dist-info}/top_level.txt +0 -1
- beans_logging_fastapi/_base.py +0 -100
- beans_logging_fastapi/_formats.py +0 -77
- beans_logging_fastapi/_handlers.py +0 -82
- beans_logging_fastapi-1.1.1.dist-info/METADATA +0 -385
- beans_logging_fastapi-1.1.1.dist-info/RECORD +0 -16
- tests/__init__.py +0 -1
- tests/conftest.py +0 -16
- tests/test_beans_logging_fastapi.py +0 -24
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
1
|
import time
|
|
4
2
|
from uuid import uuid4
|
|
5
|
-
from typing import
|
|
3
|
+
from typing import Any
|
|
4
|
+
from collections.abc import Callable
|
|
6
5
|
|
|
7
6
|
from fastapi import Request, Response
|
|
7
|
+
from fastapi.concurrency import run_in_threadpool
|
|
8
8
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
9
9
|
|
|
10
10
|
from beans_logging import logger
|
|
@@ -30,11 +30,11 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
30
30
|
self.has_cf_headers = has_cf_headers
|
|
31
31
|
|
|
32
32
|
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
33
|
-
_http_info:
|
|
33
|
+
_http_info: dict[str, Any] = {}
|
|
34
34
|
if hasattr(request.state, "http_info") and isinstance(
|
|
35
35
|
request.state.http_info, dict
|
|
36
36
|
):
|
|
37
|
-
_http_info:
|
|
37
|
+
_http_info: dict[str, Any] = request.state.http_info
|
|
38
38
|
|
|
39
39
|
_http_info["request_id"] = uuid4().hex
|
|
40
40
|
if "X-Request-ID" in request.headers:
|
|
@@ -42,18 +42,20 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
42
42
|
elif "X-Correlation-ID" in request.headers:
|
|
43
43
|
_http_info["request_id"] = request.headers.get("X-Correlation-ID")
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
# Set request_id to request state:
|
|
46
46
|
request.state.request_id = _http_info["request_id"]
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
if request.client:
|
|
49
|
+
_http_info["client_host"] = request.client.host
|
|
50
|
+
|
|
49
51
|
_http_info["request_proto"] = request.url.scheme
|
|
50
52
|
_http_info["request_host"] = (
|
|
51
53
|
request.url.hostname if request.url.hostname else ""
|
|
52
54
|
)
|
|
53
55
|
if (request.url.port != 80) and (request.url.port != 443):
|
|
54
|
-
_http_info[
|
|
55
|
-
"request_host"
|
|
56
|
-
|
|
56
|
+
_http_info["request_host"] = (
|
|
57
|
+
f"{_http_info['request_host']}:{request.url.port}"
|
|
58
|
+
)
|
|
57
59
|
|
|
58
60
|
_http_info["request_port"] = request.url.port
|
|
59
61
|
_http_info["http_version"] = request.scope["http_version"]
|
|
@@ -63,7 +65,7 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
63
65
|
_http_info["client_host"] = request.headers.get("X-Real-IP")
|
|
64
66
|
elif "X-Forwarded-For" in request.headers:
|
|
65
67
|
_http_info["client_host"] = request.headers.get(
|
|
66
|
-
"X-Forwarded-For"
|
|
68
|
+
"X-Forwarded-For", ""
|
|
67
69
|
).split(",")[0]
|
|
68
70
|
_http_info["h_x_forwarded_for"] = request.headers.get("X-Forwarded-For")
|
|
69
71
|
|
|
@@ -77,12 +79,14 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
77
79
|
|
|
78
80
|
if "X-Forwarded-Port" in request.headers:
|
|
79
81
|
try:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
_x_forwarded_port = request.headers.get("X-Forwarded-Port")
|
|
83
|
+
if _x_forwarded_port:
|
|
84
|
+
_http_info["request_port"] = int(_x_forwarded_port)
|
|
85
|
+
|
|
83
86
|
except ValueError:
|
|
84
87
|
logger.warning(
|
|
85
|
-
f"`X-Forwarded-Port` header value '{request.headers.get('X-Forwarded-Port')}' is invalid,
|
|
88
|
+
f"`X-Forwarded-Port` header value '{request.headers.get('X-Forwarded-Port')}' is invalid, "
|
|
89
|
+
"should be parseable to <int>!"
|
|
86
90
|
)
|
|
87
91
|
|
|
88
92
|
if "Via" in request.headers:
|
|
@@ -138,7 +142,7 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
138
142
|
if "}" in _http_info["url_path"]:
|
|
139
143
|
_http_info["url_path"] = _http_info["url_path"].replace("}", "}}")
|
|
140
144
|
if "<" in _http_info["url_path"]:
|
|
141
|
-
_http_info["url_path"] = _http_info["url_path"].replace("<", "
|
|
145
|
+
_http_info["url_path"] = _http_info["url_path"].replace("<", "\\<")
|
|
142
146
|
if request.url.query:
|
|
143
147
|
_http_info["url_path"] = f"{request.url.path}?{request.url.query}"
|
|
144
148
|
|
|
@@ -157,7 +161,7 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
157
161
|
if hasattr(request.state, "user_id"):
|
|
158
162
|
_http_info["user_id"] = str(request.state.user_id)
|
|
159
163
|
|
|
160
|
-
|
|
164
|
+
# Set http info to request state:
|
|
161
165
|
request.state.http_info = _http_info
|
|
162
166
|
response: Response = await call_next(request)
|
|
163
167
|
return response
|
|
@@ -172,28 +176,30 @@ class ResponseHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
172
176
|
"""
|
|
173
177
|
|
|
174
178
|
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
175
|
-
_http_info:
|
|
179
|
+
_http_info: dict[str, Any] = {}
|
|
176
180
|
_start_time: int = time.perf_counter_ns()
|
|
177
|
-
|
|
181
|
+
# Process request:
|
|
178
182
|
response: Response = await call_next(request)
|
|
179
|
-
|
|
183
|
+
# Response processed.
|
|
180
184
|
_end_time: int = time.perf_counter_ns()
|
|
181
185
|
_response_time: float = round((_end_time - _start_time) / 1_000_000, 1)
|
|
182
186
|
|
|
183
187
|
if hasattr(request.state, "http_info") and isinstance(
|
|
184
188
|
request.state.http_info, dict
|
|
185
189
|
):
|
|
186
|
-
_http_info:
|
|
190
|
+
_http_info: dict[str, Any] = request.state.http_info
|
|
187
191
|
|
|
188
192
|
_http_info["response_time"] = _response_time
|
|
189
193
|
if "X-Process-Time" in response.headers:
|
|
190
194
|
try:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
_x_process_time = response.headers.get("X-Process-Time")
|
|
196
|
+
if _x_process_time:
|
|
197
|
+
_http_info["response_time"] = float(_x_process_time)
|
|
198
|
+
|
|
194
199
|
except ValueError:
|
|
195
200
|
logger.warning(
|
|
196
|
-
f"`X-Process-Time` header value '{response.headers.get('X-Process-Time')}' is invalid,
|
|
201
|
+
f"`X-Process-Time` header value '{response.headers.get('X-Process-Time')}' is invalid, "
|
|
202
|
+
"should be parseable to <float>!"
|
|
197
203
|
)
|
|
198
204
|
else:
|
|
199
205
|
response.headers["X-Process-Time"] = str(_http_info["response_time"])
|
|
@@ -208,16 +214,117 @@ class ResponseHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
208
214
|
_http_info["content_length"] = 0
|
|
209
215
|
if "Content-Length" in response.headers:
|
|
210
216
|
try:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
217
|
+
_content_length = response.headers.get("Content-Length")
|
|
218
|
+
if _content_length:
|
|
219
|
+
_http_info["content_length"] = int(_content_length)
|
|
220
|
+
|
|
214
221
|
except ValueError:
|
|
215
222
|
logger.warning(
|
|
216
|
-
f"`Content-Length` header value '{response.headers.get('Content-Length')}' is invalid,
|
|
223
|
+
f"`Content-Length` header value '{response.headers.get('Content-Length')}' is invalid, "
|
|
224
|
+
"should be parseable to <int>!"
|
|
217
225
|
)
|
|
218
226
|
|
|
219
227
|
request.state.http_info = _http_info
|
|
220
228
|
return response
|
|
221
229
|
|
|
222
230
|
|
|
223
|
-
|
|
231
|
+
class HttpAccessLogMiddleware(BaseHTTPMiddleware):
|
|
232
|
+
"""Http access log middleware for FastAPI.
|
|
233
|
+
|
|
234
|
+
Inherits:
|
|
235
|
+
BaseHTTPMiddleware: Base HTTP middleware class from starlette.
|
|
236
|
+
|
|
237
|
+
Attributes:
|
|
238
|
+
_DEBUG_FORMAT_STR (str ): Default http access log debug message format. Defaults to
|
|
239
|
+
'<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'.
|
|
240
|
+
_FORMAT_STR (str ): Default http access log message format. Defaults to
|
|
241
|
+
'<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"
|
|
242
|
+
{status_code} {content_length}B {response_time}ms'.
|
|
243
|
+
|
|
244
|
+
debug_format_str (str ): Http access log debug message format. Defaults to
|
|
245
|
+
`HttpAccessLogMiddleware._DEBUG_FORMAT_STR`.
|
|
246
|
+
format_str (str ): Http access log message format. Defaults to `HttpAccessLogMiddleware._FORMAT_STR`.
|
|
247
|
+
use_debug_log (bool): If True, use debug log to log http access log. Defaults to True.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
_DEBUG_FORMAT_STR = '<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
|
|
251
|
+
_FORMAT_STR = (
|
|
252
|
+
'<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
|
|
253
|
+
" {status_code} {content_length}B {response_time}ms"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def __init__(
|
|
257
|
+
self,
|
|
258
|
+
app,
|
|
259
|
+
debug_format_str: str = _DEBUG_FORMAT_STR,
|
|
260
|
+
format_str: str = _FORMAT_STR,
|
|
261
|
+
use_debug_log: bool = True,
|
|
262
|
+
):
|
|
263
|
+
super().__init__(app)
|
|
264
|
+
self.debug_format_str = debug_format_str
|
|
265
|
+
self.format_str = format_str
|
|
266
|
+
self.use_debug_log = use_debug_log
|
|
267
|
+
|
|
268
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
269
|
+
_logger = logger.opt(colors=True, record=True)
|
|
270
|
+
|
|
271
|
+
_http_info: dict[str, Any] = {}
|
|
272
|
+
if hasattr(request.state, "http_info") and isinstance(
|
|
273
|
+
request.state.http_info, dict
|
|
274
|
+
):
|
|
275
|
+
_http_info: dict[str, Any] = request.state.http_info
|
|
276
|
+
|
|
277
|
+
# Debug log:
|
|
278
|
+
if self.use_debug_log:
|
|
279
|
+
_debug_msg = self.debug_format_str.format(**_http_info)
|
|
280
|
+
|
|
281
|
+
# _logger.debug(_debug_msg)
|
|
282
|
+
await run_in_threadpool(
|
|
283
|
+
_logger.debug,
|
|
284
|
+
_debug_msg,
|
|
285
|
+
)
|
|
286
|
+
# Debug log
|
|
287
|
+
|
|
288
|
+
# Process request:
|
|
289
|
+
response: Response = await call_next(request)
|
|
290
|
+
# Response processed.
|
|
291
|
+
|
|
292
|
+
if hasattr(request.state, "http_info") and isinstance(
|
|
293
|
+
request.state.http_info, dict
|
|
294
|
+
):
|
|
295
|
+
_http_info: dict[str, Any] = request.state.http_info
|
|
296
|
+
|
|
297
|
+
# Http access log:
|
|
298
|
+
_LEVEL = "INFO"
|
|
299
|
+
_format_str = self.format_str
|
|
300
|
+
if _http_info["status_code"] < 200:
|
|
301
|
+
_LEVEL = "DEBUG"
|
|
302
|
+
_format_str = f'<d>{_format_str.replace("{status_code}", "<n><b><k>{status_code}</k></b></n>")}</d>'
|
|
303
|
+
elif (200 <= _http_info["status_code"]) and (_http_info["status_code"] < 300):
|
|
304
|
+
_LEVEL = "SUCCESS"
|
|
305
|
+
_format_str = f'<w>{_format_str.replace("{status_code}", "<lvl>{status_code}</lvl>")}</w>'
|
|
306
|
+
elif (300 <= _http_info["status_code"]) and (_http_info["status_code"] < 400):
|
|
307
|
+
_LEVEL = "INFO"
|
|
308
|
+
_format_str = f'<d>{_format_str.replace("{status_code}", "<n><b><c>{status_code}</c></b></n>")}</d>'
|
|
309
|
+
elif (400 <= _http_info["status_code"]) and (_http_info["status_code"] < 500):
|
|
310
|
+
_LEVEL = "WARNING"
|
|
311
|
+
_format_str = _format_str.replace("{status_code}", "<r>{status_code}</r>")
|
|
312
|
+
elif 500 <= _http_info["status_code"]:
|
|
313
|
+
_LEVEL = "ERROR"
|
|
314
|
+
_format_str = (
|
|
315
|
+
f'{_format_str.replace("{status_code}", "<n>{status_code}</n>")}'
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
_msg = _format_str.format(**_http_info)
|
|
319
|
+
# _logger.bind(http_info=_http_info).log(_LEVEL, _msg)
|
|
320
|
+
await run_in_threadpool(_logger.bind(http_info=_http_info).log, _LEVEL, _msg)
|
|
321
|
+
# Http access log
|
|
322
|
+
|
|
323
|
+
return response
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
__all__ = [
|
|
327
|
+
"RequestHTTPInfoMiddleware",
|
|
328
|
+
"ResponseHTTPInfoMiddleware",
|
|
329
|
+
"HttpAccessLogMiddleware",
|
|
330
|
+
]
|