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.
- beans_logging_fastapi/__init__.py +6 -24
- beans_logging_fastapi/__version__.py +1 -1
- beans_logging_fastapi/{_async_log.py → _async.py} +3 -3
- beans_logging_fastapi/_core.py +90 -0
- beans_logging_fastapi/config.py +168 -0
- beans_logging_fastapi/constants.py +12 -0
- beans_logging_fastapi/{_formats.py → formats.py} +5 -5
- beans_logging_fastapi/{_middlewares.py → middlewares.py} +97 -0
- {beans_logging_fastapi-2.0.0.dist-info → beans_logging_fastapi-3.0.0.dist-info}/METADATA +233 -144
- beans_logging_fastapi-3.0.0.dist-info/RECORD +14 -0
- {beans_logging_fastapi-2.0.0.dist-info → beans_logging_fastapi-3.0.0.dist-info}/WHEEL +1 -1
- beans_logging_fastapi/_base.py +0 -107
- beans_logging_fastapi/_handlers.py +0 -96
- beans_logging_fastapi-2.0.0.dist-info/RECORD +0 -13
- /beans_logging_fastapi/{_filters.py → filters.py} +0 -0
- {beans_logging_fastapi-2.0.0.dist-info → beans_logging_fastapi-3.0.0.dist-info}/licenses/LICENSE.txt +0 -0
- {beans_logging_fastapi-2.0.0.dist-info → beans_logging_fastapi-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,31 +1,13 @@
|
|
|
1
|
-
|
|
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__ = "
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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<
|
|
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
|
[](https://github.com/bybatkhuu/module-fastapi-logging/actions/workflows/2.build-publish.yml)
|
|
64
65
|
[](https://github.com/bybatkhuu/module-fastapi-logging/releases)
|
|
65
66
|
|
|
66
|
-
This is a
|
|
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
|
-
|
|
184
|
+
http.access.file_handler:
|
|
167
185
|
enabled: true
|
|
168
|
-
|
|
186
|
+
sink: "http/{app_name}.http-access.log"
|
|
187
|
+
http.err.file_handler:
|
|
169
188
|
enabled: true
|
|
170
|
-
|
|
189
|
+
sink: "http/{app_name}.http-err.log"
|
|
190
|
+
http.access.json_handler:
|
|
171
191
|
enabled: true
|
|
172
|
-
|
|
192
|
+
sink: "http.json/{app_name}.http-access.json.log"
|
|
193
|
+
http.err.json_handler:
|
|
173
194
|
enabled: true
|
|
174
|
-
|
|
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
|
-
[**`
|
|
205
|
+
[**`config.py`**](./examples/config.py):
|
|
195
206
|
|
|
196
207
|
```python
|
|
197
|
-
|
|
208
|
+
import os
|
|
198
209
|
|
|
199
|
-
|
|
200
|
-
from loguru import Record
|
|
210
|
+
from pydantic_settings import BaseSettings
|
|
201
211
|
|
|
202
|
-
from
|
|
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
|
-
|
|
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
|
-
"
|
|
240
|
-
"
|
|
230
|
+
"MainConfig",
|
|
231
|
+
"config",
|
|
241
232
|
]
|
|
242
233
|
```
|
|
243
234
|
|
|
244
|
-
[**`
|
|
235
|
+
[**`logger.py`**](./examples/logger.py):
|
|
245
236
|
|
|
246
237
|
```python
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
+
```python
|
|
248
|
+
from pydantic import validate_call
|
|
249
|
+
from fastapi import FastAPI, APIRouter, HTTPException
|
|
250
|
+
from fastapi.responses import RedirectResponse
|
|
281
251
|
|
|
282
|
-
|
|
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
|
-
@
|
|
255
|
+
@router.get("/")
|
|
294
256
|
def root():
|
|
295
257
|
return {"Hello": "World"}
|
|
296
258
|
|
|
297
259
|
|
|
298
|
-
@
|
|
299
|
-
def read_item(item_id: int, q:
|
|
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
|
-
@
|
|
265
|
+
@router.get("/continue", status_code=100)
|
|
304
266
|
def get_continue():
|
|
305
267
|
return {}
|
|
306
268
|
|
|
307
269
|
|
|
308
|
-
@
|
|
270
|
+
@router.get("/redirect")
|
|
309
271
|
def redirect():
|
|
310
272
|
return RedirectResponse("/")
|
|
311
273
|
|
|
312
274
|
|
|
313
|
-
@
|
|
275
|
+
@router.get("/error")
|
|
314
276
|
def error():
|
|
315
277
|
raise HTTPException(status_code=500)
|
|
316
278
|
|
|
317
279
|
|
|
318
|
-
|
|
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=
|
|
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=
|
|
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
|
-
[
|
|
344
|
-
[
|
|
345
|
-
[
|
|
346
|
-
[
|
|
347
|
-
[
|
|
348
|
-
[
|
|
349
|
-
[
|
|
350
|
-
[
|
|
351
|
-
[
|
|
352
|
-
[
|
|
353
|
-
|
|
354
|
-
[
|
|
355
|
-
[
|
|
356
|
-
[
|
|
357
|
-
[
|
|
358
|
-
[
|
|
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:
|
|
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: [
|
|
470
|
+
mute_modules: [uvicorn.access]
|
|
390
471
|
handlers:
|
|
391
472
|
default.all.std_handler:
|
|
392
|
-
|
|
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
|
-
|
|
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
|
-
|
|
412
|
-
|
|
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,,
|
beans_logging_fastapi/_base.py
DELETED
|
@@ -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
|
{beans_logging_fastapi-2.0.0.dist-info → beans_logging_fastapi-3.0.0.dist-info}/licenses/LICENSE.txt
RENAMED
|
File without changes
|
{beans_logging_fastapi-2.0.0.dist-info → beans_logging_fastapi-3.0.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|