beans-logging-fastapi 2.0.0__py3-none-any.whl → 4.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,31 +1,13 @@
1
- # flake8: noqa
1
+ from beans_logging import logger
2
2
 
3
- from ._filters import use_http_filter
4
- from ._formats import http_file_format, http_file_json_format
5
- from ._handlers import add_http_file_handler, add_http_file_json_handler
6
- from ._middlewares import RequestHTTPInfoMiddleware, ResponseHTTPInfoMiddleware
7
- from ._base import HttpAccessLogMiddleware
8
- from ._async_log import *
9
3
  from .__version__ import __version__
4
+ from .config import LoggerConfigPM
5
+ from ._core import add_logger
10
6
 
11
7
 
12
8
  __all__ = [
13
- "use_http_filter",
14
- "http_file_format",
15
- "http_file_json_format",
16
- "add_http_file_handler",
17
- "add_http_file_json_handler",
18
- "RequestHTTPInfoMiddleware",
19
- "ResponseHTTPInfoMiddleware",
20
- "HttpAccessLogMiddleware",
21
- "async_log_http_error",
22
- "async_log_trace",
23
- "async_log_debug",
24
- "async_log_info",
25
- "async_log_success",
26
- "async_log_warning",
27
- "async_log_error",
28
- "async_log_critical",
29
- "async_log_level",
30
9
  "__version__",
10
+ "logger",
11
+ "add_logger",
12
+ "LoggerConfigPM",
31
13
  ]
@@ -1 +1 @@
1
- __version__ = "2.0.0"
1
+ __version__ = "4.0.0"
@@ -11,17 +11,17 @@ from beans_logging import logger, Logger
11
11
  async def async_log_http_error(
12
12
  request: Request,
13
13
  status_code: int,
14
- msg_format: str = (
15
- '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> '
16
- 'HTTP/{http_version}" <n>{status_code}</n>'
14
+ msg_format_str: str = (
15
+ '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u>'
16
+ ' HTTP/{http_version}" <n>{status_code}</n>'
17
17
  ),
18
18
  ) -> None:
19
19
  """Log HTTP error for unhandled Exception.
20
20
 
21
21
  Args:
22
- request (Request, required): Request instance.
23
- status_code (int , required): HTTP status code.
24
- msg_format (str , optional): Message format. Defaults to
22
+ request (Request, required): Request instance.
23
+ status_code (int , required): HTTP status code.
24
+ msg_format_str (str , optional): Message format. Defaults to
25
25
  '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"
26
26
  <n>{status_code}</n>'.
27
27
  """
@@ -33,7 +33,7 @@ async def async_log_http_error(
33
33
  _http_info: dict[str, Any] = request.state.http_info
34
34
  _http_info["status_code"] = status_code
35
35
 
36
- _msg = msg_format.format(**_http_info)
36
+ _msg = msg_format_str.format(**_http_info)
37
37
  _logger: Logger = logger.opt(colors=True, record=True).bind(http_info=_http_info)
38
38
  await run_in_threadpool(_logger.error, _msg)
39
39
  return
@@ -0,0 +1,93 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from pydantic import validate_call
4
+ from fastapi import FastAPI
5
+
6
+ if TYPE_CHECKING:
7
+ from loguru import Logger
8
+ else:
9
+ from loguru._logger import Logger
10
+
11
+ from beans_logging import LoggerLoader
12
+
13
+ from .constants import (
14
+ HTTP_ACCESS_STD_HANDLER_NAME,
15
+ HTTP_ACCESS_FILE_HANDLER_NAME,
16
+ HTTP_ERR_FILE_HANDLER_NAME,
17
+ HTTP_ACCESS_JSON_HANDLER_NAME,
18
+ HTTP_ERR_JSON_HANDLER_NAME,
19
+ )
20
+ from .config import LoggerConfigPM
21
+ from .filters import http_std_filter, http_all_file_filter
22
+ from .formats import http_file_format, http_json_format
23
+ from .middlewares import (
24
+ HttpAccessLogMiddleware,
25
+ RequestHTTPInfoMiddleware,
26
+ ResponseHTTPInfoMiddleware,
27
+ )
28
+
29
+
30
+ @validate_call(config={"arbitrary_types_allowed": True})
31
+ def add_logger(
32
+ app: FastAPI,
33
+ config: LoggerConfigPM,
34
+ has_proxy_headers: bool | None = None,
35
+ has_cf_headers: bool | None = None,
36
+ ) -> "Logger":
37
+ """Add and initialize logger middlewares and handlers to FastAPI application.
38
+
39
+ Args:
40
+ app (FastAPI , required): FastAPI application instance.
41
+ config (LoggerConfigPM, required): Logger configuration model.
42
+ has_proxy_headers (bool | None , optional): Whether to use proxy headers. Defaults to None.
43
+ has_cf_headers (bool | None , optional): Whether to use Cloudflare headers. Defaults to None.
44
+
45
+ Returns:
46
+ Logger: Initialized Logger instance.
47
+ """
48
+
49
+ logger_loader = LoggerLoader(config=config)
50
+
51
+ if has_proxy_headers is None:
52
+ has_proxy_headers = config.http.headers.has_proxy
53
+
54
+ if has_cf_headers is None:
55
+ has_cf_headers = config.http.headers.has_cf
56
+
57
+ app.add_middleware(ResponseHTTPInfoMiddleware)
58
+ app.add_middleware(
59
+ HttpAccessLogMiddleware,
60
+ debug_msg_format_str=config.http.std.debug_msg_format_str,
61
+ msg_format_str=config.http.std.msg_format_str,
62
+ )
63
+ app.add_middleware(
64
+ RequestHTTPInfoMiddleware,
65
+ has_proxy_headers=has_proxy_headers,
66
+ has_cf_headers=has_cf_headers,
67
+ )
68
+
69
+ for _name, _handler in logger_loader.config.handlers.items():
70
+ if _name == HTTP_ACCESS_STD_HANDLER_NAME:
71
+ _handler.filter_ = http_std_filter
72
+ elif (_name == HTTP_ACCESS_FILE_HANDLER_NAME) or (
73
+ _name == HTTP_ERR_FILE_HANDLER_NAME
74
+ ):
75
+ _handler.filter_ = http_all_file_filter
76
+ _handler.format_ = lambda record: http_file_format(
77
+ record=record,
78
+ format_str=config.http.file.format_str,
79
+ tz=config.http.file.tz,
80
+ )
81
+ elif (_name == HTTP_ACCESS_JSON_HANDLER_NAME) or (
82
+ _name == HTTP_ERR_JSON_HANDLER_NAME
83
+ ):
84
+ _handler.filter_ = http_all_file_filter
85
+ _handler.format_ = http_json_format
86
+
87
+ logger: Logger = logger_loader.load()
88
+ return logger
89
+
90
+
91
+ __all__ = [
92
+ "add_logger",
93
+ ]
@@ -0,0 +1,177 @@
1
+ from typing import Any
2
+
3
+ import potato_util as utils
4
+ from pydantic import Field, field_validator
5
+
6
+ from beans_logging.constants import LogHandlerTypeEnum, DEFAULT_HANDLER_NAMES
7
+ from beans_logging.schemas import LogHandlerPM
8
+ from beans_logging.config import (
9
+ get_default_handlers as get_base_handlers,
10
+ ExtraBaseModel,
11
+ InterceptConfigPM,
12
+ LoggerConfigPM as BaseLoggerConfigPM,
13
+ )
14
+
15
+ from .constants import (
16
+ HTTP_ACCESS_STD_HANDLER_NAME,
17
+ HTTP_ACCESS_FILE_HANDLER_NAME,
18
+ HTTP_ERR_FILE_HANDLER_NAME,
19
+ HTTP_ACCESS_JSON_HANDLER_NAME,
20
+ HTTP_ERR_JSON_HANDLER_NAME,
21
+ )
22
+
23
+
24
+ def get_default_handlers() -> dict[str, LogHandlerPM]:
25
+ """Get fastapi default log handlers.
26
+
27
+ Returns:
28
+ dict[str, LogHandlerPM]: Default handlers as dictionary.
29
+ """
30
+
31
+ _base_handlers = get_base_handlers()
32
+ for _name, _handler in _base_handlers.items():
33
+ if _name in DEFAULT_HANDLER_NAMES:
34
+ _handler.enabled = True
35
+
36
+ _http_handlers: dict[str, LogHandlerPM] = {
37
+ HTTP_ACCESS_STD_HANDLER_NAME: LogHandlerPM(
38
+ h_type=LogHandlerTypeEnum.STD,
39
+ format_=(
40
+ "[<c>{time:YYYY-MM-DD HH:mm:ss.SSS Z}</c> | <level>{extra[level_short]:<5}</level> ]:"
41
+ " <level>{message}</level>"
42
+ ),
43
+ colorize=True,
44
+ ),
45
+ HTTP_ACCESS_FILE_HANDLER_NAME: LogHandlerPM(
46
+ h_type=LogHandlerTypeEnum.FILE,
47
+ sink="http/{app_name}.http-access.log",
48
+ ),
49
+ HTTP_ERR_FILE_HANDLER_NAME: LogHandlerPM(
50
+ h_type=LogHandlerTypeEnum.FILE,
51
+ sink="http/{app_name}.http-err.log",
52
+ error=True,
53
+ ),
54
+ HTTP_ACCESS_JSON_HANDLER_NAME: LogHandlerPM(
55
+ h_type=LogHandlerTypeEnum.FILE,
56
+ sink="http.json/{app_name}.http-access.json.log",
57
+ ),
58
+ HTTP_ERR_JSON_HANDLER_NAME: LogHandlerPM(
59
+ h_type=LogHandlerTypeEnum.FILE,
60
+ sink="http.json/{app_name}.http-err.json.log",
61
+ error=True,
62
+ ),
63
+ }
64
+
65
+ _default_handlers = {**_base_handlers, **_http_handlers}
66
+ return _default_handlers
67
+
68
+
69
+ def get_default_intercept() -> InterceptConfigPM:
70
+ _default_intercept = InterceptConfigPM(mute_modules=["uvicorn.access"])
71
+ return _default_intercept
72
+
73
+
74
+ class StdConfigPM(ExtraBaseModel):
75
+ msg_format_str: str = Field(
76
+ default=(
77
+ '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
78
+ " {status_code} {content_length}B {response_time}ms"
79
+ ),
80
+ min_length=8,
81
+ max_length=512,
82
+ )
83
+ err_msg_format_str: str = Field(
84
+ default=(
85
+ '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
86
+ " <n>{status_code}</n>"
87
+ ),
88
+ min_length=8,
89
+ max_length=512,
90
+ )
91
+ debug_msg_format_str: str = Field(
92
+ default='<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"',
93
+ min_length=8,
94
+ max_length=512,
95
+ )
96
+
97
+
98
+ class FileConfigPM(ExtraBaseModel):
99
+ format_str: str = Field(
100
+ default=(
101
+ '{client_host} {request_id} {user_id} [{datetime}] "{method} {url_path} HTTP/{http_version}"'
102
+ ' {status_code} {content_length} "{h_referer}" "{h_user_agent}" {response_time}'
103
+ ),
104
+ min_length=8,
105
+ max_length=512,
106
+ )
107
+ tz: str = Field(default="localtime", min_length=2, max_length=64)
108
+
109
+
110
+ class HeadersConfigPM(ExtraBaseModel):
111
+ has_proxy: bool = Field(default=False)
112
+ has_cf: bool = Field(default=False)
113
+
114
+
115
+ class HttpConfigPM(ExtraBaseModel):
116
+ std: StdConfigPM = Field(default_factory=StdConfigPM)
117
+ file: FileConfigPM = Field(default_factory=FileConfigPM)
118
+ headers: HeadersConfigPM = Field(default_factory=HeadersConfigPM)
119
+
120
+
121
+ class LoggerConfigPM(BaseLoggerConfigPM):
122
+ http: HttpConfigPM = Field(default_factory=HttpConfigPM)
123
+ intercept: InterceptConfigPM = Field(default_factory=get_default_intercept)
124
+ handlers: dict[str, LogHandlerPM] = Field(default_factory=get_default_handlers)
125
+
126
+ @field_validator("handlers", mode="before")
127
+ @classmethod
128
+ def _check_handlers(cls, val: Any) -> dict[str, LogHandlerPM]:
129
+
130
+ _default_handlers = get_default_handlers()
131
+
132
+ if not val:
133
+ val = _default_handlers
134
+ return val
135
+
136
+ if not isinstance(val, dict):
137
+ raise TypeError(
138
+ f"'handlers' attribute type {type(val).__name__} is invalid, must be a dict of <LogHandlerPM> or dict!"
139
+ )
140
+
141
+ for _key, _handler in val.items():
142
+ if not isinstance(_handler, (LogHandlerPM, dict)):
143
+ raise TypeError(
144
+ f"'handlers' attribute's '{_key}' key -> value type {type(_handler).__name__} is invalid, must be "
145
+ f"<LogHandlerPM> or dict!"
146
+ )
147
+
148
+ if isinstance(_handler, LogHandlerPM):
149
+ val[_key] = _handler.model_dump(
150
+ by_alias=True, exclude_unset=True, exclude_none=True
151
+ )
152
+
153
+ _default_dict = {
154
+ _key: _handler.model_dump(
155
+ by_alias=True, exclude_unset=True, exclude_none=True
156
+ )
157
+ for _key, _handler in _default_handlers.items()
158
+ }
159
+
160
+ if _default_dict != val:
161
+ val = utils.deep_merge(_default_dict, val)
162
+
163
+ for _key, _handler in val.items():
164
+ val[_key] = LogHandlerPM(**_handler)
165
+
166
+ return val
167
+
168
+
169
+ __all__ = [
170
+ "LoggerConfigPM",
171
+ "HttpConfigPM",
172
+ "StdConfigPM",
173
+ "FileConfigPM",
174
+ "HeadersConfigPM",
175
+ "get_default_intercept",
176
+ "get_default_handlers",
177
+ ]
@@ -0,0 +1,23 @@
1
+ HTTP_ACCESS_STD_HANDLER_NAME = "http_access_std_handler"
2
+ HTTP_ACCESS_FILE_HANDLER_NAME = "http_access_file_handler"
3
+ HTTP_ERR_FILE_HANDLER_NAME = "http_err_file_handler"
4
+ HTTP_ACCESS_JSON_HANDLER_NAME = "http_access_json_handler"
5
+ HTTP_ERR_JSON_HANDLER_NAME = "http_err_json_handler"
6
+
7
+ HTTP_HANDLER_NAMES = [
8
+ HTTP_ACCESS_STD_HANDLER_NAME,
9
+ HTTP_ACCESS_FILE_HANDLER_NAME,
10
+ HTTP_ERR_FILE_HANDLER_NAME,
11
+ HTTP_ACCESS_JSON_HANDLER_NAME,
12
+ HTTP_ERR_JSON_HANDLER_NAME,
13
+ ]
14
+
15
+
16
+ __all__ = [
17
+ "HTTP_ACCESS_STD_HANDLER_NAME",
18
+ "HTTP_ACCESS_FILE_HANDLER_NAME",
19
+ "HTTP_ERR_FILE_HANDLER_NAME",
20
+ "HTTP_ACCESS_JSON_HANDLER_NAME",
21
+ "HTTP_ERR_JSON_HANDLER_NAME",
22
+ "HTTP_HANDLER_NAMES",
23
+ ]
@@ -0,0 +1,72 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from loguru import Record
5
+
6
+ from beans_logging.filters import all_handlers_filter
7
+
8
+ from .constants import HTTP_ACCESS_STD_HANDLER_NAME
9
+
10
+
11
+ def http_filter(record: "Record") -> bool:
12
+ """Filter message only for http access log handler by checking 'http_info' key in extra.
13
+
14
+ Args:
15
+ record (Record, required): Log record as dictionary.
16
+
17
+ Returns:
18
+ bool: True if record has 'http_info' key in extra, False otherwise.
19
+ """
20
+
21
+ if not all_handlers_filter(record):
22
+ return False
23
+
24
+ if "http_info" not in record["extra"]:
25
+ return False
26
+
27
+ return True
28
+
29
+
30
+ def http_std_filter(record: "Record") -> bool:
31
+ """Filter message only for http std log handler.
32
+
33
+ Args:
34
+ record (Record, required): Log record as dictionary.
35
+
36
+ Returns:
37
+ bool: True if record does not have 'disable_{HTTP_ACCESS_STD_HANDLER_NAME}' key in extra, False otherwise.
38
+ """
39
+
40
+ if not http_filter(record):
41
+ return False
42
+
43
+ if record["extra"].get(f"disable_{HTTP_ACCESS_STD_HANDLER_NAME}", False):
44
+ return False
45
+
46
+ return True
47
+
48
+
49
+ def http_all_file_filter(record: "Record") -> bool:
50
+ """Filter message only for http file log handler.
51
+
52
+ Args:
53
+ record (Record, required): Log record as dictionary.
54
+
55
+ Returns:
56
+ bool: True if record does not have 'disable_http_all_file_handlers' key in extra, False otherwise.
57
+ """
58
+
59
+ if not http_filter(record):
60
+ return False
61
+
62
+ if record["extra"].get("disable_http_all_file_handlers", False):
63
+ return False
64
+
65
+ return True
66
+
67
+
68
+ __all__ = [
69
+ "http_filter",
70
+ "http_std_filter",
71
+ "http_all_file_filter",
72
+ ]
@@ -9,9 +9,9 @@ if TYPE_CHECKING:
9
9
 
10
10
  def http_file_format(
11
11
  record: "Record",
12
- msg_format: str = (
13
- '{client_host} {request_id} {user_id} [{datetime}] "{method} {url_path} HTTP/{http_version}" '
14
- '{status_code} {content_length} "{h_referer}" "{h_user_agent}" {response_time}'
12
+ format_str: str = (
13
+ '{client_host} {request_id} {user_id} [{datetime}] "{method} {url_path} HTTP/{http_version}"'
14
+ ' {status_code} {content_length} "{h_referer}" "{h_user_agent}" {response_time}'
15
15
  ),
16
16
  tz: str = "localtime",
17
17
  ) -> str:
@@ -19,7 +19,7 @@ def http_file_format(
19
19
 
20
20
  Args:
21
21
  record (Record, required): Log record as dictionary.
22
- msg_format (str , optional): Log message format.
22
+ format_str (str , optional): Log message format.
23
23
  tz (str , optional): Timezone for datetime field. Defaults to 'localtime'.
24
24
 
25
25
  Returns:
@@ -56,13 +56,13 @@ def http_file_format(
56
56
  _http_info["response_time"] = 0
57
57
 
58
58
  record["extra"]["http_info"] = _http_info
59
- _msg = msg_format.format(**_http_info)
59
+ _msg = format_str.format(**_http_info)
60
60
 
61
61
  record["extra"]["http_message"] = _msg
62
62
  return "{extra[http_message]}\n"
63
63
 
64
64
 
65
- def http_file_json_format(record: "Record") -> str:
65
+ def http_json_format(record: "Record") -> str:
66
66
  """Http access json log file format.
67
67
 
68
68
  Args:
@@ -91,5 +91,5 @@ def http_file_json_format(record: "Record") -> str:
91
91
 
92
92
  __all__ = [
93
93
  "http_file_format",
94
- "http_file_json_format",
94
+ "http_json_format",
95
95
  ]
@@ -4,6 +4,7 @@ from typing import Any
4
4
  from collections.abc import Callable
5
5
 
6
6
  from fastapi import Request, Response
7
+ from fastapi.concurrency import run_in_threadpool
7
8
  from starlette.middleware.base import BaseHTTPMiddleware
8
9
 
9
10
  from beans_logging import logger
@@ -227,7 +228,115 @@ class ResponseHTTPInfoMiddleware(BaseHTTPMiddleware):
227
228
  return response
228
229
 
229
230
 
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_MSG_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
+ _MSG_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_msg_format_str (str ): Http access log debug message format.
245
+ Defaults to `HttpAccessLogMiddleware._DEBUG_MSG_FORMAT_STR`.
246
+ msg_format_str (str ): Http access log message format.
247
+ Defaults to `HttpAccessLogMiddleware._MSG_FORMAT_STR`.
248
+ use_debug_log (bool): If True, use debug log to log http access log. Defaults to True.
249
+ """
250
+
251
+ _DEBUG_MSG_FORMAT_STR = (
252
+ '<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u>'
253
+ ' HTTP/{http_version}"'
254
+ )
255
+ _MSG_FORMAT_STR = (
256
+ '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
257
+ " {status_code} {content_length}B {response_time}ms"
258
+ )
259
+
260
+ def __init__(
261
+ self,
262
+ app,
263
+ debug_msg_format_str: str = _DEBUG_MSG_FORMAT_STR,
264
+ msg_format_str: str = _MSG_FORMAT_STR,
265
+ use_debug_log: bool = True,
266
+ ):
267
+ super().__init__(app)
268
+ self.debug_msg_format_str = debug_msg_format_str
269
+ self.msg_format_str = msg_format_str
270
+ self.use_debug_log = use_debug_log
271
+
272
+ async def dispatch(self, request: Request, call_next) -> Response:
273
+ _logger = logger.opt(colors=True, record=True).bind(
274
+ disable_all_std_handler=True
275
+ )
276
+
277
+ _http_info: dict[str, Any] = {}
278
+ if hasattr(request.state, "http_info") and isinstance(
279
+ request.state.http_info, dict
280
+ ):
281
+ _http_info: dict[str, Any] = request.state.http_info
282
+
283
+ # Debug log:
284
+ if self.use_debug_log:
285
+ _debug_msg = self.debug_msg_format_str.format(**_http_info)
286
+
287
+ # _logger.bind(
288
+ # http_info=_http_info, disable_http_all_file_handlers=True
289
+ # ).debug(_debug_msg)
290
+ await run_in_threadpool(
291
+ _logger.bind(
292
+ http_info=_http_info, disable_http_all_file_handlers=True
293
+ ).debug,
294
+ _debug_msg,
295
+ )
296
+ # Debug log
297
+
298
+ # Process request:
299
+ response: Response = await call_next(request)
300
+ # Response processed.
301
+
302
+ if hasattr(request.state, "http_info") and isinstance(
303
+ request.state.http_info, dict
304
+ ):
305
+ _http_info: dict[str, Any] = request.state.http_info
306
+
307
+ # Http access log:
308
+ _LEVEL = "INFO"
309
+ _msg_format_str = self.msg_format_str
310
+ if _http_info["status_code"] < 200:
311
+ _LEVEL = "DEBUG"
312
+ _msg_format_str = f'<d>{_msg_format_str.replace("{status_code}", "<n><b><k>{status_code}</k></b></n>")}</d>'
313
+ elif (200 <= _http_info["status_code"]) and (_http_info["status_code"] < 300):
314
+ _LEVEL = "SUCCESS"
315
+ _msg_format_str = f'<w>{_msg_format_str.replace("{status_code}", "<lvl>{status_code}</lvl>")}</w>'
316
+ elif (300 <= _http_info["status_code"]) and (_http_info["status_code"] < 400):
317
+ _LEVEL = "INFO"
318
+ _msg_format_str = f'<d>{_msg_format_str.replace("{status_code}", "<n><b><c>{status_code}</c></b></n>")}</d>'
319
+ elif (400 <= _http_info["status_code"]) and (_http_info["status_code"] < 500):
320
+ _LEVEL = "WARNING"
321
+ _msg_format_str = _msg_format_str.replace(
322
+ "{status_code}", "<r>{status_code}</r>"
323
+ )
324
+ elif 500 <= _http_info["status_code"]:
325
+ _LEVEL = "ERROR"
326
+ _msg_format_str = (
327
+ f'{_msg_format_str.replace("{status_code}", "<n>{status_code}</n>")}'
328
+ )
329
+
330
+ _msg = _msg_format_str.format(**_http_info)
331
+ # _logger.bind(http_info=_http_info).log(_LEVEL, _msg)
332
+ await run_in_threadpool(_logger.bind(http_info=_http_info).log, _LEVEL, _msg)
333
+ # Http access log
334
+
335
+ return response
336
+
337
+
230
338
  __all__ = [
231
339
  "RequestHTTPInfoMiddleware",
232
340
  "ResponseHTTPInfoMiddleware",
341
+ "HttpAccessLogMiddleware",
233
342
  ]