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