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.
@@ -1,10 +1,10 @@
1
- # -*- coding: utf-8 -*-
2
-
3
1
  import time
4
2
  from uuid import uuid4
5
- from typing import Callable, Dict, Any
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: Dict[str, Any] = {}
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: Dict[str, Any] = request.state.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
- ## Set request_id to request state:
45
+ # Set request_id to request state:
46
46
  request.state.request_id = _http_info["request_id"]
47
47
 
48
- _http_info["client_host"] = request.client.host
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
- ] = f"{_http_info['request_host']}:{request.url.port}"
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
- _http_info["request_port"] = int(
81
- request.headers.get("X-Forwarded-Port")
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, should be parseable to <int>!"
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
- ## Set http info to request state:
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: Dict[str, Any] = {}
179
+ _http_info: dict[str, Any] = {}
176
180
  _start_time: int = time.perf_counter_ns()
177
- ## Process request:
181
+ # Process request:
178
182
  response: Response = await call_next(request)
179
- ## Response processed.
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: Dict[str, Any] = request.state.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
- _http_info["response_time"] = float(
192
- response.headers.get("X-Process-Time")
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, should be parseable to <float>!"
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
- _http_info["content_length"] = int(
212
- response.headers.get("Content-Length")
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, should be parseable to <int>!"
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
- __all__ = ["RequestHTTPInfoMiddleware", "ResponseHTTPInfoMiddleware"]
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
+ ]