beans-logging-fastapi 2.0.0__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,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__ = "3.0.0"
@@ -11,7 +11,7 @@ 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 = (
14
+ format_str: str = (
15
15
  '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> '
16
16
  'HTTP/{http_version}" <n>{status_code}</n>'
17
17
  ),
@@ -21,7 +21,7 @@ async def async_log_http_error(
21
21
  Args:
22
22
  request (Request, required): Request instance.
23
23
  status_code (int , required): HTTP status code.
24
- msg_format (str , optional): Message format. Defaults to
24
+ 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 = 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,90 @@
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_FILE_HANDLER_NAME,
15
+ HTTP_ERR_FILE_HANDLER_NAME,
16
+ HTTP_ACCESS_JSON_HANDLER_NAME,
17
+ HTTP_ERR_JSON_HANDLER_NAME,
18
+ )
19
+ from .config import LoggerConfigPM
20
+ from .filters import use_http_filter
21
+ from .formats import http_file_format, http_file_json_format
22
+ from .middlewares import (
23
+ HttpAccessLogMiddleware,
24
+ RequestHTTPInfoMiddleware,
25
+ ResponseHTTPInfoMiddleware,
26
+ )
27
+
28
+
29
+ @validate_call(config={"arbitrary_types_allowed": True})
30
+ def add_logger(
31
+ app: FastAPI,
32
+ config: LoggerConfigPM,
33
+ has_proxy_headers: bool | None = None,
34
+ has_cf_headers: bool | None = None,
35
+ ) -> "Logger":
36
+ """Add and initialize logger middlewares and handlers to FastAPI application.
37
+
38
+ Args:
39
+ app (FastAPI , required): FastAPI application instance.
40
+ config (LoggerConfigPM, required): Logger configuration model.
41
+ has_proxy_headers (bool | None , optional): Whether to use proxy headers. Defaults to None.
42
+ has_cf_headers (bool | None , optional): Whether to use Cloudflare headers. Defaults to None.
43
+
44
+ Returns:
45
+ Logger: Initialized Logger instance.
46
+ """
47
+
48
+ logger_loader = LoggerLoader(config=config)
49
+
50
+ if has_proxy_headers is None:
51
+ has_proxy_headers = config.http.headers.has_proxy
52
+
53
+ if has_cf_headers is None:
54
+ has_cf_headers = config.http.headers.has_cf
55
+
56
+ app.add_middleware(ResponseHTTPInfoMiddleware)
57
+ app.add_middleware(
58
+ HttpAccessLogMiddleware,
59
+ debug_format_str=config.http.std.debug_format_str,
60
+ format_str=config.http.std.format_str,
61
+ )
62
+ app.add_middleware(
63
+ RequestHTTPInfoMiddleware,
64
+ has_proxy_headers=has_proxy_headers,
65
+ has_cf_headers=has_cf_headers,
66
+ )
67
+
68
+ for _name, _handler in logger_loader.config.handlers.items():
69
+ if (_name == HTTP_ACCESS_FILE_HANDLER_NAME) or (
70
+ _name == HTTP_ERR_FILE_HANDLER_NAME
71
+ ):
72
+ _handler.filter_ = use_http_filter
73
+ _handler.format_ = lambda record: http_file_format(
74
+ record=record,
75
+ format_str=config.http.file.format_str,
76
+ tz=config.http.file.tz,
77
+ )
78
+ elif (_name == HTTP_ACCESS_JSON_HANDLER_NAME) or (
79
+ _name == HTTP_ERR_JSON_HANDLER_NAME
80
+ ):
81
+ _handler.filter_ = use_http_filter
82
+ _handler.format_ = http_file_json_format
83
+
84
+ logger: Logger = logger_loader.load()
85
+ return logger
86
+
87
+
88
+ __all__ = [
89
+ "add_logger",
90
+ ]
@@ -0,0 +1,168 @@
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
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_FILE_HANDLER_NAME,
17
+ HTTP_ERR_FILE_HANDLER_NAME,
18
+ HTTP_ACCESS_JSON_HANDLER_NAME,
19
+ HTTP_ERR_JSON_HANDLER_NAME,
20
+ )
21
+
22
+
23
+ def get_default_handlers() -> dict[str, LogHandlerPM]:
24
+ """Get fastapi default log handlers.
25
+
26
+ Returns:
27
+ dict[str, LogHandlerPM]: Default handlers as dictionary.
28
+ """
29
+
30
+ _base_handlers = get_base_handlers()
31
+ for _name, _handler in _base_handlers.items():
32
+ if _name.startswith("default"):
33
+ _handler.enabled = True
34
+
35
+ _http_handlers: dict[str, LogHandlerPM] = {
36
+ HTTP_ACCESS_FILE_HANDLER_NAME: LogHandlerPM(
37
+ h_type=LogHandlerTypeEnum.FILE,
38
+ sink="http/{app_name}.http-access.log",
39
+ ),
40
+ HTTP_ERR_FILE_HANDLER_NAME: LogHandlerPM(
41
+ h_type=LogHandlerTypeEnum.FILE,
42
+ sink="http/{app_name}.http-err.log",
43
+ error=True,
44
+ ),
45
+ HTTP_ACCESS_JSON_HANDLER_NAME: LogHandlerPM(
46
+ h_type=LogHandlerTypeEnum.FILE,
47
+ sink="http.json/{app_name}.http-access.json.log",
48
+ ),
49
+ HTTP_ERR_JSON_HANDLER_NAME: LogHandlerPM(
50
+ h_type=LogHandlerTypeEnum.FILE,
51
+ sink="http.json/{app_name}.http-err.json.log",
52
+ error=True,
53
+ ),
54
+ }
55
+
56
+ _default_handlers = {**_base_handlers, **_http_handlers}
57
+ return _default_handlers
58
+
59
+
60
+ def get_default_intercept() -> InterceptConfigPM:
61
+ _default_intercept = InterceptConfigPM(mute_modules=["uvicorn.access"])
62
+ return _default_intercept
63
+
64
+
65
+ class StdConfigPM(ExtraBaseModel):
66
+ format_str: str = Field(
67
+ default=(
68
+ '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
69
+ " {status_code} {content_length}B {response_time}ms"
70
+ ),
71
+ min_length=8,
72
+ max_length=512,
73
+ )
74
+ err_format_str: str = Field(
75
+ default=(
76
+ '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
77
+ " <n>{status_code}</n>"
78
+ ),
79
+ min_length=8,
80
+ max_length=512,
81
+ )
82
+ debug_format_str: str = Field(
83
+ default='<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"',
84
+ min_length=8,
85
+ max_length=512,
86
+ )
87
+
88
+
89
+ class FileConfigPM(ExtraBaseModel):
90
+ format_str: str = Field(
91
+ default=(
92
+ '{client_host} {request_id} {user_id} [{datetime}] "{method} {url_path} HTTP/{http_version}"'
93
+ ' {status_code} {content_length} "{h_referer}" "{h_user_agent}" {response_time}'
94
+ ),
95
+ min_length=8,
96
+ max_length=512,
97
+ )
98
+ tz: str = Field(default="localtime", min_length=2, max_length=64)
99
+
100
+
101
+ class HeadersConfigPM(ExtraBaseModel):
102
+ has_proxy: bool = Field(default=False)
103
+ has_cf: bool = Field(default=False)
104
+
105
+
106
+ class HttpConfigPM(ExtraBaseModel):
107
+ std: StdConfigPM = Field(default_factory=StdConfigPM)
108
+ file: FileConfigPM = Field(default_factory=FileConfigPM)
109
+ headers: HeadersConfigPM = Field(default_factory=HeadersConfigPM)
110
+
111
+
112
+ class LoggerConfigPM(BaseLoggerConfigPM):
113
+ http: HttpConfigPM = Field(default_factory=HttpConfigPM)
114
+ intercept: InterceptConfigPM = Field(default_factory=get_default_intercept)
115
+ handlers: dict[str, LogHandlerPM] = Field(default_factory=get_default_handlers)
116
+
117
+ @field_validator("handlers", mode="before")
118
+ @classmethod
119
+ def _check_handlers(cls, val: Any) -> dict[str, LogHandlerPM]:
120
+
121
+ _default_handlers = get_default_handlers()
122
+
123
+ if not val:
124
+ val = _default_handlers
125
+ return val
126
+
127
+ if not isinstance(val, dict):
128
+ raise TypeError(
129
+ f"'handlers' attribute type {type(val).__name__} is invalid, must be a dict of <LogHandlerPM> or dict!"
130
+ )
131
+
132
+ for _key, _handler in val.items():
133
+ if not isinstance(_handler, (LogHandlerPM, dict)):
134
+ raise TypeError(
135
+ f"'handlers' attribute's '{_key}' key -> value type {type(_handler).__name__} is invalid, must be "
136
+ f"<LogHandlerPM> or dict!"
137
+ )
138
+
139
+ if isinstance(_handler, LogHandlerPM):
140
+ val[_key] = _handler.model_dump(
141
+ by_alias=True, exclude_unset=True, exclude_none=True
142
+ )
143
+
144
+ _default_dict = {
145
+ _key: _handler.model_dump(
146
+ by_alias=True, exclude_unset=True, exclude_none=True
147
+ )
148
+ for _key, _handler in _default_handlers.items()
149
+ }
150
+
151
+ if _default_dict != val:
152
+ val = utils.deep_merge(_default_dict, val)
153
+
154
+ for _key, _handler in val.items():
155
+ val[_key] = LogHandlerPM(**_handler)
156
+
157
+ return val
158
+
159
+
160
+ __all__ = [
161
+ "LoggerConfigPM",
162
+ "HttpConfigPM",
163
+ "StdConfigPM",
164
+ "FileConfigPM",
165
+ "HeadersConfigPM",
166
+ "get_default_intercept",
167
+ "get_default_handlers",
168
+ ]
@@ -0,0 +1,12 @@
1
+ HTTP_ACCESS_FILE_HANDLER_NAME = "http.access.file_handler"
2
+ HTTP_ERR_FILE_HANDLER_NAME = "http.err.file_handler"
3
+ HTTP_ACCESS_JSON_HANDLER_NAME = "http.access.json_handler"
4
+ HTTP_ERR_JSON_HANDLER_NAME = "http.err.json_handler"
5
+
6
+
7
+ __all__ = [
8
+ "HTTP_ACCESS_FILE_HANDLER_NAME",
9
+ "HTTP_ERR_FILE_HANDLER_NAME",
10
+ "HTTP_ACCESS_JSON_HANDLER_NAME",
11
+ "HTTP_ERR_JSON_HANDLER_NAME",
12
+ ]
@@ -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,7 +56,7 @@ 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"
@@ -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,103 @@ 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_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
+
230
326
  __all__ = [
231
327
  "RequestHTTPInfoMiddleware",
232
328
  "ResponseHTTPInfoMiddleware",
329
+ "HttpAccessLogMiddleware",
233
330
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: beans_logging_fastapi
3
- Version: 2.0.0
3
+ Version: 3.0.0
4
4
  Summary: This is a middleware for FastAPI HTTP access logs. It is based on 'beans-logging' package.
5
5
  Author-email: Batkhuu Byambajav <batkhuu10@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/bybatkhuu/module-fastapi-logging
@@ -22,7 +22,7 @@ Requires-Python: <4.0,>=3.10
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE.txt
24
24
  Requires-Dist: fastapi<1.0.0,>=0.99.1
25
- Requires-Dist: beans-logging<8.0.0,>=7.1.0
25
+ Requires-Dist: beans-logging<9.0.0,>=8.0.2
26
26
  Provides-Extra: test
27
27
  Requires-Dist: pytest<10.0.0,>=8.0.2; extra == "test"
28
28
  Requires-Dist: pytest-cov<8.0.0,>=5.0.0; extra == "test"
@@ -55,6 +55,7 @@ Requires-Dist: mkdocstrings[python]<2.0.0,>=0.24.3; extra == "dev"
55
55
  Requires-Dist: mike<3.0.0,>=2.1.3; extra == "dev"
56
56
  Requires-Dist: pyright<2.0.0,>=1.1.392; extra == "dev"
57
57
  Requires-Dist: pre-commit<5.0.0,>=4.0.1; extra == "dev"
58
+ Requires-Dist: uvicorn[standard]<1.0.0,>=0.23.0; extra == "dev"
58
59
  Dynamic: license-file
59
60
 
60
61
  # FastAPI Logging (beans-logging-fastapi)
@@ -63,12 +64,15 @@ Dynamic: license-file
63
64
  [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/bybatkhuu/module-fastapi-logging/2.build-publish.yml?logo=GitHub)](https://github.com/bybatkhuu/module-fastapi-logging/actions/workflows/2.build-publish.yml)
64
65
  [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/bybatkhuu/module-fastapi-logging?logo=GitHub&color=blue)](https://github.com/bybatkhuu/module-fastapi-logging/releases)
65
66
 
66
- This is a middleware for FastAPI HTTP access logs. It is based on **'beans-logging'** package.
67
+ This is a HTTP access logging module for **FastAPI** based on **'beans-logging'** package.
67
68
 
68
69
  ## ✨ Features
69
70
 
70
71
  - **Logger** based on **'beans-logging'** package
71
72
  - **FastAPI** HTTP access logging **middleware**
73
+ - HTTP access log as structured JSON format
74
+ - Predefined **configuration** for HTTP access logs
75
+ - Easy to **install** and **use**
72
76
 
73
77
  ---
74
78
 
@@ -160,28 +164,35 @@ To use `beans_logging_fastapi`:
160
164
  ```yaml
161
165
  logger:
162
166
  app_name: "fastapi-app"
167
+ default:
168
+ level:
169
+ base: TRACE
170
+ http:
171
+ std:
172
+ format_str: '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}" {status_code} {content_length}B {response_time}ms'
173
+ err_format_str: '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}" <n>{status_code}</n>'
174
+ debug_format_str: '<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
175
+ file:
176
+ format_str: '{client_host} {request_id} {user_id} [{datetime}] "{method} {url_path} HTTP/{http_version}" {status_code} {content_length} "{h_referer}" "{h_user_agent}" {response_time}'
177
+ tz: "localtime"
178
+ headers:
179
+ has_proxy: false
180
+ has_cf: false
163
181
  intercept:
164
182
  mute_modules: ["uvicorn.access"]
165
183
  handlers:
166
- default.all.file_handler:
184
+ http.access.file_handler:
167
185
  enabled: true
168
- default.err.file_handler:
186
+ sink: "http/{app_name}.http-access.log"
187
+ http.err.file_handler:
169
188
  enabled: true
170
- default.all.json_handler:
189
+ sink: "http/{app_name}.http-err.log"
190
+ http.access.json_handler:
171
191
  enabled: true
172
- default.err.json_handler:
192
+ sink: "http.json/{app_name}.http-access.json.log"
193
+ http.err.json_handler:
173
194
  enabled: true
174
- extra:
175
- http_std_debug_format: '<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
176
- http_std_msg_format: '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}" {status_code} {content_length}B {response_time}ms'
177
- http_file_enabled: true
178
- http_file_format: '{client_host} {request_id} {user_id} [{datetime}] "{method} {url_path} HTTP/{http_version}" {status_code} {content_length} "{h_referer}" "{h_user_agent}" {response_time}'
179
- http_file_tz: "localtime"
180
- http_log_path: "http/{app_name}.http.access.log"
181
- http_err_path: "http/{app_name}.http.err.log"
182
- http_json_enabled: true
183
- http_json_path: "http.json/{app_name}.http.json.access.log"
184
- http_json_err_path: "http.json/{app_name}.http.json.err.log"
195
+ sink: "http.json/{app_name}.http-err.json.log"
185
196
  ```
186
197
 
187
198
  [**`.env`**](./examples/.env):
@@ -191,140 +202,197 @@ ENV=development
191
202
  DEBUG=true
192
203
  ```
193
204
 
194
- [**`logger.py`**](./examples/logger.py):
205
+ [**`config.py`**](./examples/config.py):
195
206
 
196
207
  ```python
197
- from typing import TYPE_CHECKING
208
+ import os
198
209
 
199
- if TYPE_CHECKING:
200
- from loguru import Record
210
+ from pydantic_settings import BaseSettings
201
211
 
202
- from beans_logging import Logger, LoggerLoader
203
- from beans_logging_fastapi import (
204
- add_http_file_handler,
205
- add_http_file_json_handler,
206
- http_file_format,
207
- )
212
+ from potato_util import io as io_utils
213
+ from beans_logging_fastapi import LoggerConfigPM
208
214
 
209
- logger_loader = LoggerLoader()
210
- logger: Logger = logger_loader.load()
211
215
 
216
+ _config_path = os.path.join(os.getcwd(), "configs", "logger.yml")
217
+ _config_data = {}
218
+ if os.path.isfile(_config_path):
219
+ _config_data = io_utils.read_config_file(config_path=_config_path)
212
220
 
213
- def _http_file_format(record: "Record") -> str:
214
- _format = http_file_format(
215
- record=record,
216
- msg_format=logger_loader.config.extra.http_file_format, # type: ignore
217
- tz=logger_loader.config.extra.http_file_tz, # type: ignore
218
- )
219
- return _format
220
221
 
222
+ class MainConfig(BaseSettings):
223
+ logger: LoggerConfigPM = LoggerConfigPM()
221
224
 
222
- if logger_loader.config.extra.http_file_enabled: # type: ignore
223
- add_http_file_handler(
224
- logger_loader=logger_loader,
225
- log_path=logger_loader.config.extra.http_log_path, # type: ignore
226
- err_path=logger_loader.config.extra.http_err_path, # type: ignore
227
- formatter=_http_file_format,
228
- )
229
225
 
230
- if logger_loader.config.extra.http_json_enabled: # type: ignore
231
- add_http_file_json_handler(
232
- logger_loader=logger_loader,
233
- log_path=logger_loader.config.extra.http_json_path, # type: ignore
234
- err_path=logger_loader.config.extra.http_json_err_path, # type: ignore
235
- )
226
+ config = MainConfig(**_config_data)
236
227
 
237
228
 
238
229
  __all__ = [
239
- "logger",
240
- "logger_loader",
230
+ "MainConfig",
231
+ "config",
241
232
  ]
242
233
  ```
243
234
 
244
- [**`main.py`**](./examples/main.py):
235
+ [**`logger.py`**](./examples/logger.py):
245
236
 
246
237
  ```python
247
- #!/usr/bin/env python
248
-
249
- from typing import Union
250
- from contextlib import asynccontextmanager
251
-
252
- import uvicorn
253
- from dotenv import load_dotenv
254
- from fastapi import FastAPI, HTTPException
255
- from fastapi.responses import RedirectResponse
256
-
257
- load_dotenv()
258
-
259
- from beans_logging_fastapi import (
260
- HttpAccessLogMiddleware,
261
- RequestHTTPInfoMiddleware,
262
- ResponseHTTPInfoMiddleware,
263
- )
264
-
265
- from logger import logger, logger_loader
266
- from __version__ import __version__
238
+ from beans_logging_fastapi import logger
267
239
 
240
+ __all__ = [
241
+ "logger",
242
+ ]
243
+ ```
268
244
 
269
- @asynccontextmanager
270
- async def lifespan(app: FastAPI):
271
- logger.info("Preparing to startup...")
272
- logger.success("Finished preparation to startup.")
273
- logger.info(f"API version: {__version__}")
274
-
275
- yield
276
- logger.info("Praparing to shutdown...")
277
- logger.success("Finished preparation to shutdown.")
278
-
245
+ [**`router.py`**](./examples/router.py):
279
246
 
280
- app = FastAPI(lifespan=lifespan, version=__version__)
247
+ ```python
248
+ from pydantic import validate_call
249
+ from fastapi import FastAPI, APIRouter, HTTPException
250
+ from fastapi.responses import RedirectResponse
281
251
 
282
- app.add_middleware(ResponseHTTPInfoMiddleware)
283
- app.add_middleware(
284
- HttpAccessLogMiddleware,
285
- debug_format=logger_loader.config.extra.http_std_debug_format, # type: ignore
286
- msg_format=logger_loader.config.extra.http_std_msg_format, # type: ignore
287
- )
288
- app.add_middleware(
289
- RequestHTTPInfoMiddleware, has_proxy_headers=True, has_cf_headers=True
290
- )
252
+ router = APIRouter()
291
253
 
292
254
 
293
- @app.get("/")
255
+ @router.get("/")
294
256
  def root():
295
257
  return {"Hello": "World"}
296
258
 
297
259
 
298
- @app.get("/items/{item_id}")
299
- def read_item(item_id: int, q: Union[str, None] = None):
260
+ @router.get("/items/{item_id}")
261
+ def read_item(item_id: int, q: str | None = None):
300
262
  return {"item_id": item_id, "q": q}
301
263
 
302
264
 
303
- @app.get("/continue", status_code=100)
265
+ @router.get("/continue", status_code=100)
304
266
  def get_continue():
305
267
  return {}
306
268
 
307
269
 
308
- @app.get("/redirect")
270
+ @router.get("/redirect")
309
271
  def redirect():
310
272
  return RedirectResponse("/")
311
273
 
312
274
 
313
- @app.get("/error")
275
+ @router.get("/error")
314
276
  def error():
315
277
  raise HTTPException(status_code=500)
316
278
 
317
279
 
318
- if __name__ == "__main__":
280
+ @validate_call(config={"arbitrary_types_allowed": True})
281
+ def add_routers(app: FastAPI) -> None:
282
+ """Add routers to FastAPI app.
283
+
284
+ Args:
285
+ app (FastAPI): FastAPI app instance.
286
+ """
287
+
288
+ app.include_router(router)
289
+
290
+ return
291
+
292
+
293
+ __all__ = ["add_routers"]
294
+ ```
295
+
296
+ [**`bootstrap.py`**](./examples/bootstrap.py):
297
+
298
+ ```python
299
+ # Standard libraries
300
+ from typing import Any
301
+ from collections.abc import Callable
302
+
303
+ # Third-party libraries
304
+ import uvicorn
305
+ from uvicorn._types import ASGIApplication
306
+ from pydantic import validate_call
307
+ from fastapi import FastAPI
308
+
309
+ from beans_logging_fastapi import add_logger
310
+
311
+ # Internal modules
312
+ from __version__ import __version__
313
+ from config import config
314
+ from lifespan import lifespan
315
+ from router import add_routers
316
+
317
+
318
+ def create_app() -> FastAPI:
319
+ """Create FastAPI application instance.
320
+
321
+ Returns:
322
+ FastAPI: FastAPI application instance.
323
+ """
324
+
325
+ app = FastAPI(lifespan=lifespan, version=__version__)
326
+
327
+ # Add logger before any other components:
328
+ add_logger(app=app, config=config.logger)
329
+
330
+ # Add any other components after logger:
331
+ add_routers(app=app)
332
+
333
+ return app
334
+
335
+
336
+ @validate_call(config={"arbitrary_types_allowed": True})
337
+ def run_server(
338
+ app: FastAPI | ASGIApplication | Callable[..., Any] | str = "main:app",
339
+ ) -> None:
340
+ """Run uvicorn server.
341
+
342
+ Args:
343
+ app (Union[ASGIApplication, str], optional): ASGI application instance or module path.
344
+ """
345
+
319
346
  uvicorn.run(
320
- app="main:app",
321
- host="0.0.0.0",
347
+ app=app,
348
+ host="0.0.0.0", # nosec B104
322
349
  port=8000,
323
- access_log=False,
350
+ access_log=False, # Disable default uvicorn access log
324
351
  server_header=False,
325
- proxy_headers=True,
352
+ proxy_headers=False,
326
353
  forwarded_allow_ips="*",
327
354
  )
355
+
356
+ return
357
+
358
+
359
+ __all__ = [
360
+ "create_app",
361
+ "run_server",
362
+ ]
363
+ ```
364
+
365
+ [**`main.py`**](./examples/main.py):
366
+
367
+ ```python
368
+ #!/usr/bin/env python
369
+
370
+ # Third-party libraries
371
+ from dotenv import load_dotenv
372
+
373
+ load_dotenv(override=True)
374
+
375
+ # Internal modules
376
+ from bootstrap import create_app, run_server # noqa: E402
377
+ from logger import logger # noqa: E402
378
+
379
+
380
+ app = create_app()
381
+
382
+
383
+ def main() -> None:
384
+ """Main function."""
385
+
386
+ run_server(app=app)
387
+ return
388
+
389
+
390
+ if __name__ == "__main__":
391
+ logger.info("Starting server from 'main.py'...")
392
+ main()
393
+
394
+
395
+ __all__ = ["app"]
328
396
  ```
329
397
 
330
398
  Run the [**`examples`**](./examples):
@@ -340,22 +408,24 @@ uvicorn main:app --host=0.0.0.0 --port=8000
340
408
  **Output**:
341
409
 
342
410
  ```txt
343
- [2025-12-01 00:00:00.735 +09:00 | TRACE | beans_logging._intercept:96]: Intercepted modules: ['potato_util.io', 'concurrent', 'potato_util', 'fastapi', 'uvicorn.error', 'dotenv.main', 'potato_util._base', 'watchfiles.watcher', 'dotenv', 'potato_util.io._sync', 'asyncio', 'uvicorn', 'concurrent.futures', 'watchfiles', 'watchfiles.main']; Muted modules: ['uvicorn.access'];
344
- [2025-12-01 00:00:00.735 +09:00 | INFO | uvicorn.server:84]: Started server process [13580]
345
- [2025-12-01 00:00:00.735 +09:00 | INFO | uvicorn.lifespan.on:48]: Waiting for application startup.
346
- [2025-12-01 00:00:00.735 +09:00 | INFO | main:25]: Preparing to startup...
347
- [2025-12-01 00:00:00.735 +09:00 | OK | main:26]: Finished preparation to startup.
348
- [2025-12-01 00:00:00.735 +09:00 | INFO | main:27]: API version: 0.0.0
349
- [2025-12-01 00:00:00.735 +09:00 | INFO | uvicorn.lifespan.on:62]: Application startup complete.
350
- [2025-12-01 00:00:00.735 +09:00 | INFO | uvicorn.server:216]: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
351
- [2025-12-01 00:00:00.736 +09:00 | DEBUG | anyio._backends._asyncio:986]: [4386400aab364895ba272f3200d2a778] 127.0.0.1 - "GET / HTTP/1.1"
352
- [2025-12-01 00:00:00.736 +09:00 | OK | anyio._backends._asyncio:986]: [4386400aab364895ba272f3200d2a778] 127.0.0.1 - "GET / HTTP/1.1" 200 17B 0.9ms
353
- ^C[2025-12-01 00:00:00.750 +09:00 | INFO | uvicorn.server:264]: Shutting down
354
- [2025-12-01 00:00:00.750 +09:00 | INFO | uvicorn.lifespan.on:67]: Waiting for application shutdown.
355
- [2025-12-01 00:00:00.750 +09:00 | INFO | main:30]: Praparing to shutdown...
356
- [2025-12-01 00:00:00.750 +09:00 | OK | main:31]: Finished preparation to shutdown.
357
- [2025-12-01 00:00:00.750 +09:00 | INFO | uvicorn.lifespan.on:76]: Application shutdown complete.
358
- [2025-12-01 00:00:00.750 +09:00 | INFO | uvicorn.server:94]: Finished server process [13580]
411
+ [2026-01-01 12:00:00.907 +09:00 | TRACE | beans_logging.intercepters:96]: Intercepted modules: ['concurrent.futures', 'potato_util', 'watchfiles.watcher', 'potato_util.io._sync', 'concurrent', 'uvicorn', 'fastapi', 'uvicorn.error', 'dotenv.main', 'watchfiles.main', 'asyncio', 'dotenv', 'potato_util._base', 'potato_util.io', 'watchfiles']; Muted modules: ['uvicorn.access'];
412
+ [2026-01-01 12:00:00.908 +09:00 | INFO | uvicorn.server:84]: Started server process [64590]
413
+ [2026-01-01 12:00:00.909 +09:00 | INFO | uvicorn.lifespan.on:48]: Waiting for application startup.
414
+ [2026-01-01 12:00:00.909 +09:00 | TRACE | lifespan:19]: TRACE diagnosis is ON!
415
+ [2026-01-01 12:00:00.909 +09:00 | DEBUG | lifespan:20]: DEBUG mode is ON!
416
+ [2026-01-01 12:00:00.909 +09:00 | INFO | lifespan:21]: Preparing to startup...
417
+ [2026-01-01 12:00:00.909 +09:00 | OK | lifespan:24]: Finished preparation to startup.
418
+ [2026-01-01 12:00:00.909 +09:00 | INFO | lifespan:25]: Version: 0.0.0
419
+ [2026-01-01 12:00:00.909 +09:00 | INFO | uvicorn.lifespan.on:62]: Application startup complete.
420
+ [2026-01-01 12:00:00.911 +09:00 | INFO | uvicorn.server:216]: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
421
+ [2026-01-01 12:00:01.582 +09:00 | DEBUG | anyio._backends._asyncio:986]: [c433596f728744aaa1cde63399dd3995] 127.0.0.1 - "GET / HTTP/1.1"
422
+ [2026-01-01 12:00:01.586 +09:00 | OK | anyio._backends._asyncio:986]: [c433596f728744aaa1cde63399dd3995] 127.0.0.1 - "GET / HTTP/1.1" 200 17B 3.1ms
423
+ ^C[2026-01-01 12:00:02.074 +09:00 | INFO | uvicorn.server:264]: Shutting down
424
+ [2026-01-01 12:00:02.177 +09:00 | INFO | uvicorn.lifespan.on:67]: Waiting for application shutdown.
425
+ [2026-01-01 12:00:02.178 +09:00 | INFO | lifespan:29]: Preparing to shutdown...
426
+ [2026-01-01 12:00:02.179 +09:00 | OK | lifespan:31]: Finished preparation to shutdown.
427
+ [2026-01-01 12:00:02.179 +09:00 | INFO | uvicorn.lifespan.on:76]: Application shutdown complete.
428
+ [2026-01-01 12:00:02.180 +09:00 | INFO | uvicorn.server:94]: Finished server process [64590]
359
429
  ```
360
430
 
361
431
  👍
@@ -368,7 +438,7 @@ uvicorn main:app --host=0.0.0.0 --port=8000
368
438
 
369
439
  ```yaml
370
440
  logger:
371
- # app_name: "app"
441
+ # app_name: fastapi-app
372
442
  default:
373
443
  level:
374
444
  base: INFO
@@ -381,49 +451,68 @@ logger:
381
451
  retention: 90
382
452
  encoding: utf8
383
453
  custom_serialize: false
454
+ http:
455
+ std:
456
+ format_str: '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}" {status_code} {content_length}B {response_time}ms'
457
+ err_format_str: '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}" <n>{status_code}</n>'
458
+ debug_format_str: '<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
459
+ file:
460
+ format_str: '{client_host} {request_id} {user_id} [{datetime}] "{method} {url_path} HTTP/{http_version}" {status_code} {content_length} "{h_referer}" "{h_user_agent}" {response_time}'
461
+ tz: localtime
462
+ headers:
463
+ has_proxy: false
464
+ has_cf: false
384
465
  intercept:
385
466
  enabled: true
386
467
  only_base: false
387
468
  ignore_modules: []
388
469
  include_modules: []
389
- mute_modules: ["uvicorn.access"]
470
+ mute_modules: [uvicorn.access]
390
471
  handlers:
391
472
  default.all.std_handler:
392
- type: STD
473
+ enabled: true
474
+ h_type: STD
393
475
  format: "[<c>{time:YYYY-MM-DD HH:mm:ss.SSS Z}</c> | <level>{extra[level_short]:<5}</level> | <w>{name}:{line}</w>]: <level>{message}</level>"
394
476
  colorize: true
395
- enabled: true
396
477
  default.all.file_handler:
397
- type: FILE
398
- sink: "{app_name}.all.log"
399
478
  enabled: true
479
+ h_type: FILE
480
+ sink: "{app_name}.all.log"
400
481
  default.err.file_handler:
401
- type: FILE
482
+ enabled: true
483
+ h_type: FILE
402
484
  sink: "{app_name}.err.log"
403
485
  error: true
404
- enabled: true
405
486
  default.all.json_handler:
406
- type: FILE
407
- sink: "json/{app_name}.json.all.log"
408
- serialize: true
409
487
  enabled: true
488
+ h_type: FILE
489
+ sink: "json/{app_name}.all.json.log"
490
+ serialize: true
410
491
  default.err.json_handler:
411
- type: FILE
412
- sink: "json/{app_name}.json.err.log"
492
+ enabled: true
493
+ h_type: FILE
494
+ sink: "json/{app_name}.err.json.log"
413
495
  serialize: true
414
496
  error: true
497
+ http.access.file_handler:
498
+ enabled: true
499
+ h_type: FILE
500
+ sink: "http/{app_name}.http-access.log"
501
+ http.err.file_handler:
502
+ enabled: true
503
+ h_type: FILE
504
+ sink: "http/{app_name}.http-err.log"
505
+ error: true
506
+ http.access.json_handler:
507
+ enabled: true
508
+ h_type: FILE
509
+ sink: "http.json/{app_name}.http-access.json.log"
510
+ http.err.json_handler:
415
511
  enabled: true
512
+ h_type: FILE
513
+ sink: "http.json/{app_name}.http-err.json.log"
514
+ error: true
416
515
  extra:
417
- http_std_debug_format: '<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
418
- http_std_msg_format: '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}" {status_code} {content_length}B {response_time}ms'
419
- http_file_enabled: true
420
- http_file_format: '{client_host} {request_id} {user_id} [{datetime}] "{method} {url_path} HTTP/{http_version}" {status_code} {content_length} "{h_referer}" "{h_user_agent}" {response_time}'
421
- http_file_tz: "localtime"
422
- http_log_path: "http/{app_name}.http.access.log"
423
- http_err_path: "http/{app_name}.http.err.log"
424
- http_json_enabled: true
425
- http_json_path: "http.json/{app_name}.http.json.access.log"
426
- http_json_err_path: "http.json/{app_name}.http.json.err.log"
427
516
  ```
428
517
 
429
518
  ### 🌎 Environment Variables
@@ -475,7 +564,7 @@ To build the documentation, run the following command:
475
564
  pip install -r ./requirements/requirements.docs.txt
476
565
 
477
566
  # Serve documentation locally (for development):
478
- mkdocs serve -a 0.0.0.0:8000
567
+ mkdocs serve -a 0.0.0.0:8000 --livereload
479
568
  # Or use the docs script:
480
569
  ./scripts/docs.sh
481
570
 
@@ -0,0 +1,14 @@
1
+ beans_logging_fastapi/__init__.py,sha256=5nnhUvYdtaMSZ1t549nhYfxOzcKsC4YPx8nEqJWfZYw,225
2
+ beans_logging_fastapi/__version__.py,sha256=EPmgXOdWKks5S__ZMH7Nu6xpAeVrZpfxaFy4pykuyeI,22
3
+ beans_logging_fastapi/_async.py,sha256=IcoQPeBfZwPLt-WBJBpc7g5pELh6plf7VYUel2KyM_c,3478
4
+ beans_logging_fastapi/_core.py,sha256=NUpQGvuLw5YlRNTvssBo4aqrE0lia4nhL1aDahZXncU,2744
5
+ beans_logging_fastapi/config.py,sha256=gt9Kh0-MOZdf0LMGV5gbyYucoTkF8He5QceUP-9vS8U,5298
6
+ beans_logging_fastapi/constants.py,sha256=pRsgi1pqkEDMKmSWDQeK72TSXiuVkJaEMTOUeB82E5A,382
7
+ beans_logging_fastapi/filters.py,sha256=hBhHqCWJr7c2CA7uDAje8R8x9RtpCcBHZSC6xuXLYDs,622
8
+ beans_logging_fastapi/formats.py,sha256=ENfCB54ftUZZFj5J9kKCsdCkeqzStG3l0_9vLBLi8Oc,2600
9
+ beans_logging_fastapi/middlewares.py,sha256=ToUS4jPUPcZHTr_d7DWLjfBI-E9FrspasRqkc4Gyg2M,13676
10
+ beans_logging_fastapi-3.0.0.dist-info/licenses/LICENSE.txt,sha256=CUTK-r0BWIg1r0bBiemAcMhakgV0N7HuRhw6rQ-A9A4,1074
11
+ beans_logging_fastapi-3.0.0.dist-info/METADATA,sha256=iL3NjWvF6KMVxaO7RytgEMuGxMoWAQBlyXqF0lsaBdM,17344
12
+ beans_logging_fastapi-3.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
13
+ beans_logging_fastapi-3.0.0.dist-info/top_level.txt,sha256=PXoqVo9HGfyd81gDi3D2mXMYPM9JKITL0ycFftJxlhw,22
14
+ beans_logging_fastapi-3.0.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,107 +0,0 @@
1
- from typing import Any
2
-
3
- from fastapi import Request, Response
4
- from fastapi.concurrency import run_in_threadpool
5
- from starlette.middleware.base import BaseHTTPMiddleware
6
-
7
- from beans_logging import logger
8
-
9
-
10
- class HttpAccessLogMiddleware(BaseHTTPMiddleware):
11
- """Http access log middleware for FastAPI.
12
-
13
- Inherits:
14
- BaseHTTPMiddleware: Base HTTP middleware class from starlette.
15
-
16
- Attributes:
17
- _DEBUG_FORMAT (str ): Default http access log debug message format. Defaults to
18
- '<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'.
19
- _MSG_FORMAT (str ): Default http access log message format. Defaults to
20
- '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"
21
- {status_code} {content_length}B {response_time}ms'.
22
-
23
- debug_format (str ): Http access log debug message format. Defaults to
24
- `HttpAccessLogMiddleware._DEBUG_FORMAT`.
25
- msg_format (str ): Http access log message format. Defaults to `HttpAccessLogMiddleware._MSG_FORMAT`.
26
- use_debug_log (bool): If True, use debug log to log http access log. Defaults to True.
27
- """
28
-
29
- _DEBUG_FORMAT = '<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
30
- _MSG_FORMAT = (
31
- '<n><w>[{request_id}]</w></n> {client_host} {user_id} "<u>{method} {url_path}</u> '
32
- 'HTTP/{http_version}" {status_code} {content_length}B {response_time}ms'
33
- )
34
-
35
- def __init__(
36
- self,
37
- app,
38
- debug_format: str = _DEBUG_FORMAT,
39
- msg_format: str = _MSG_FORMAT,
40
- use_debug_log: bool = True,
41
- ):
42
- super().__init__(app)
43
- self.debug_format = debug_format
44
- self.msg_format = msg_format
45
- self.use_debug_log = use_debug_log
46
-
47
- async def dispatch(self, request: Request, call_next) -> Response:
48
- _logger = logger.opt(colors=True, record=True)
49
-
50
- _http_info: dict[str, Any] = {}
51
- if hasattr(request.state, "http_info") and isinstance(
52
- request.state.http_info, dict
53
- ):
54
- _http_info: dict[str, Any] = request.state.http_info
55
-
56
- # Debug log:
57
- if self.use_debug_log:
58
- _debug_msg = self.debug_format.format(**_http_info)
59
-
60
- # _logger.debug(_debug_msg)
61
- await run_in_threadpool(
62
- _logger.debug,
63
- _debug_msg,
64
- )
65
- # Debug log
66
-
67
- # Process request:
68
- response: Response = await call_next(request)
69
- # Response processed.
70
-
71
- if hasattr(request.state, "http_info") and isinstance(
72
- request.state.http_info, dict
73
- ):
74
- _http_info: dict[str, Any] = request.state.http_info
75
-
76
- # Http access log:
77
- _LEVEL = "INFO"
78
- _msg_format = self.msg_format
79
- if _http_info["status_code"] < 200:
80
- _LEVEL = "DEBUG"
81
- _msg_format = f'<d>{_msg_format.replace("{status_code}", "<n><b><k>{status_code}</k></b></n>")}</d>'
82
- elif (200 <= _http_info["status_code"]) and (_http_info["status_code"] < 300):
83
- _LEVEL = "SUCCESS"
84
- _msg_format = f'<w>{_msg_format.replace("{status_code}", "<lvl>{status_code}</lvl>")}</w>'
85
- elif (300 <= _http_info["status_code"]) and (_http_info["status_code"] < 400):
86
- _LEVEL = "INFO"
87
- _msg_format = f'<d>{_msg_format.replace("{status_code}", "<n><b><c>{status_code}</c></b></n>")}</d>'
88
- elif (400 <= _http_info["status_code"]) and (_http_info["status_code"] < 500):
89
- _LEVEL = "WARNING"
90
- _msg_format = _msg_format.replace("{status_code}", "<r>{status_code}</r>")
91
- elif 500 <= _http_info["status_code"]:
92
- _LEVEL = "ERROR"
93
- _msg_format = (
94
- f'{_msg_format.replace("{status_code}", "<n>{status_code}</n>")}'
95
- )
96
-
97
- _msg = _msg_format.format(**_http_info)
98
- # _logger.bind(http_info=_http_info).log(_LEVEL, _msg)
99
- await run_in_threadpool(_logger.bind(http_info=_http_info).log, _LEVEL, _msg)
100
- # Http access log
101
-
102
- return response
103
-
104
-
105
- __all__ = [
106
- "HttpAccessLogMiddleware",
107
- ]
@@ -1,96 +0,0 @@
1
- from collections.abc import Callable
2
-
3
- from pydantic import validate_call
4
-
5
- from beans_logging import LoggerLoader
6
-
7
- from ._filters import use_http_filter
8
- from ._formats import http_file_format, http_file_json_format
9
-
10
-
11
- @validate_call(config={"arbitrary_types_allowed": True})
12
- def add_http_file_handler(
13
- logger_loader: LoggerLoader,
14
- log_path: str = "http/{app_name}.http.access.log",
15
- err_path: str = "http/{app_name}.http.err.log",
16
- formatter: Callable | str = http_file_format,
17
- ) -> None:
18
- """Add http access log file and error file handler.
19
-
20
- Args:
21
- logger_loader (LoggerLoader, required): LoggerLoader instance.
22
- log_path (str, optional): Log file path. Defaults to "http/{app_name}.http.access.log".
23
- err_path (str, optional): Error log file path. Defaults to "http/{app_name}.http.err.log".
24
- formatter (Union[Callable, str], optional): Log formatter. Defaults to `http_file_format` function.
25
- """
26
-
27
- logger_loader.add_handler(
28
- name="default.http.access.file_handler",
29
- handler={
30
- "type": "FILE",
31
- "sink": log_path,
32
- "filter": use_http_filter,
33
- "format": formatter,
34
- },
35
- )
36
-
37
- logger_loader.add_handler(
38
- name="default.http.err.file_handler",
39
- handler={
40
- "type": "FILE",
41
- "sink": err_path,
42
- "filter": use_http_filter,
43
- "format": formatter,
44
- "error": True,
45
- },
46
- )
47
-
48
- return
49
-
50
-
51
- @validate_call(config={"arbitrary_types_allowed": True})
52
- def add_http_file_json_handler(
53
- logger_loader: LoggerLoader,
54
- log_path: str = "http.json/{app_name}.http.json.access.log",
55
- err_path: str = "http.json/{app_name}.http.json.err.log",
56
- formatter: Callable | str = http_file_json_format,
57
- ) -> None:
58
- """Add http access json log file and json error file handler.
59
-
60
- Args:
61
- logger_loader (LoggerLoader, required): LoggerLoader instance.
62
- log_path (str, optional): Json log file path. Defaults to
63
- "http.json/{app_name}.http.json.access.log".
64
- err_path (str, optional): Json error log file path. Defaults to
65
- "http.json/{app_name}.http.json.err.log".
66
- formatter (Union[Callable, str], optional): Log formatter. Defaults to `http_file_json_format` function.
67
- """
68
-
69
- logger_loader.add_handler(
70
- name="default.http.access.json_handler",
71
- handler={
72
- "type": "FILE",
73
- "sink": log_path,
74
- "filter": use_http_filter,
75
- "format": formatter,
76
- },
77
- )
78
-
79
- logger_loader.add_handler(
80
- name="default.http.err.json_handler",
81
- handler={
82
- "type": "FILE",
83
- "sink": err_path,
84
- "filter": use_http_filter,
85
- "format": formatter,
86
- "error": True,
87
- },
88
- )
89
-
90
- return
91
-
92
-
93
- __all__ = [
94
- "add_http_file_handler",
95
- "add_http_file_json_handler",
96
- ]
@@ -1,13 +0,0 @@
1
- beans_logging_fastapi/__init__.py,sha256=hTuW6GTdPinZ1qamna7N0RsElfVJnkkOI0iLnMbQ7B8,865
2
- beans_logging_fastapi/__version__.py,sha256=_7OlQdbVkK4jad0CLdpI0grT-zEAb-qgFmH5mFzDXiA,22
3
- beans_logging_fastapi/_async_log.py,sha256=01VJ1cnzacDlIjiScm0hubhkj0xkeU5pBB_0fPJS-5w,3478
4
- beans_logging_fastapi/_base.py,sha256=mRAUxEBYU00IrE4QgaYDqrUxHhCiKp3cEzVNy2bDsM0,4259
5
- beans_logging_fastapi/_filters.py,sha256=hBhHqCWJr7c2CA7uDAje8R8x9RtpCcBHZSC6xuXLYDs,622
6
- beans_logging_fastapi/_formats.py,sha256=J5tu9QwU6jCNZvMt7y5WpfcM862GLvgGHGfvjh5k2T4,2600
7
- beans_logging_fastapi/_handlers.py,sha256=_v4VigZM2j1kphUPViKp21DiwR3CB1mM6MXz_NUAE7c,3128
8
- beans_logging_fastapi/_middlewares.py,sha256=gcx8NrgWv1OMFln4nvnaPgY6Np92Nv6OTMJf97EcH08,9558
9
- beans_logging_fastapi-2.0.0.dist-info/licenses/LICENSE.txt,sha256=CUTK-r0BWIg1r0bBiemAcMhakgV0N7HuRhw6rQ-A9A4,1074
10
- beans_logging_fastapi-2.0.0.dist-info/METADATA,sha256=aM3l5OHfrFiy1RYGOYHmOMl7fG0iTGmgXd1tV1F6oLQ,16031
11
- beans_logging_fastapi-2.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- beans_logging_fastapi-2.0.0.dist-info/top_level.txt,sha256=PXoqVo9HGfyd81gDi3D2mXMYPM9JKITL0ycFftJxlhw,22
13
- beans_logging_fastapi-2.0.0.dist-info/RECORD,,
File without changes