beans-logging-fastapi 1.1.1__py3-none-any.whl → 2.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 +1 -1
- beans_logging_fastapi/__version__.py +1 -3
- beans_logging_fastapi/_async_log.py +38 -26
- beans_logging_fastapi/_base.py +25 -18
- beans_logging_fastapi/_filters.py +9 -4
- beans_logging_fastapi/_formats.py +53 -35
- beans_logging_fastapi/_handlers.py +56 -42
- beans_logging_fastapi/_middlewares.py +40 -30
- beans_logging_fastapi-2.0.0.dist-info/METADATA +497 -0
- beans_logging_fastapi-2.0.0.dist-info/RECORD +13 -0
- {beans_logging_fastapi-1.1.1.dist-info → beans_logging_fastapi-2.0.0.dist-info}/WHEEL +1 -1
- beans_logging_fastapi-1.1.1.dist-info/LICENCE.txt → beans_logging_fastapi-2.0.0.dist-info/licenses/LICENSE.txt +1 -1
- {beans_logging_fastapi-1.1.1.dist-info → beans_logging_fastapi-2.0.0.dist-info}/top_level.txt +0 -1
- beans_logging_fastapi-1.1.1.dist-info/METADATA +0 -385
- beans_logging_fastapi-1.1.1.dist-info/RECORD +0 -16
- tests/__init__.py +0 -1
- tests/conftest.py +0 -16
- tests/test_beans_logging_fastapi.py +0 -24
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
1
|
import time
|
|
4
2
|
from uuid import uuid4
|
|
5
|
-
from typing import
|
|
3
|
+
from typing import Any
|
|
4
|
+
from collections.abc import Callable
|
|
6
5
|
|
|
7
6
|
from fastapi import Request, Response
|
|
8
7
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
@@ -30,11 +29,11 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
30
29
|
self.has_cf_headers = has_cf_headers
|
|
31
30
|
|
|
32
31
|
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
33
|
-
_http_info:
|
|
32
|
+
_http_info: dict[str, Any] = {}
|
|
34
33
|
if hasattr(request.state, "http_info") and isinstance(
|
|
35
34
|
request.state.http_info, dict
|
|
36
35
|
):
|
|
37
|
-
_http_info:
|
|
36
|
+
_http_info: dict[str, Any] = request.state.http_info
|
|
38
37
|
|
|
39
38
|
_http_info["request_id"] = uuid4().hex
|
|
40
39
|
if "X-Request-ID" in request.headers:
|
|
@@ -42,18 +41,20 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
42
41
|
elif "X-Correlation-ID" in request.headers:
|
|
43
42
|
_http_info["request_id"] = request.headers.get("X-Correlation-ID")
|
|
44
43
|
|
|
45
|
-
|
|
44
|
+
# Set request_id to request state:
|
|
46
45
|
request.state.request_id = _http_info["request_id"]
|
|
47
46
|
|
|
48
|
-
|
|
47
|
+
if request.client:
|
|
48
|
+
_http_info["client_host"] = request.client.host
|
|
49
|
+
|
|
49
50
|
_http_info["request_proto"] = request.url.scheme
|
|
50
51
|
_http_info["request_host"] = (
|
|
51
52
|
request.url.hostname if request.url.hostname else ""
|
|
52
53
|
)
|
|
53
54
|
if (request.url.port != 80) and (request.url.port != 443):
|
|
54
|
-
_http_info[
|
|
55
|
-
"request_host"
|
|
56
|
-
|
|
55
|
+
_http_info["request_host"] = (
|
|
56
|
+
f"{_http_info['request_host']}:{request.url.port}"
|
|
57
|
+
)
|
|
57
58
|
|
|
58
59
|
_http_info["request_port"] = request.url.port
|
|
59
60
|
_http_info["http_version"] = request.scope["http_version"]
|
|
@@ -63,7 +64,7 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
63
64
|
_http_info["client_host"] = request.headers.get("X-Real-IP")
|
|
64
65
|
elif "X-Forwarded-For" in request.headers:
|
|
65
66
|
_http_info["client_host"] = request.headers.get(
|
|
66
|
-
"X-Forwarded-For"
|
|
67
|
+
"X-Forwarded-For", ""
|
|
67
68
|
).split(",")[0]
|
|
68
69
|
_http_info["h_x_forwarded_for"] = request.headers.get("X-Forwarded-For")
|
|
69
70
|
|
|
@@ -77,12 +78,14 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
77
78
|
|
|
78
79
|
if "X-Forwarded-Port" in request.headers:
|
|
79
80
|
try:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
_x_forwarded_port = request.headers.get("X-Forwarded-Port")
|
|
82
|
+
if _x_forwarded_port:
|
|
83
|
+
_http_info["request_port"] = int(_x_forwarded_port)
|
|
84
|
+
|
|
83
85
|
except ValueError:
|
|
84
86
|
logger.warning(
|
|
85
|
-
f"`X-Forwarded-Port` header value '{request.headers.get('X-Forwarded-Port')}' is invalid,
|
|
87
|
+
f"`X-Forwarded-Port` header value '{request.headers.get('X-Forwarded-Port')}' is invalid, "
|
|
88
|
+
"should be parseable to <int>!"
|
|
86
89
|
)
|
|
87
90
|
|
|
88
91
|
if "Via" in request.headers:
|
|
@@ -138,7 +141,7 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
138
141
|
if "}" in _http_info["url_path"]:
|
|
139
142
|
_http_info["url_path"] = _http_info["url_path"].replace("}", "}}")
|
|
140
143
|
if "<" in _http_info["url_path"]:
|
|
141
|
-
_http_info["url_path"] = _http_info["url_path"].replace("<", "
|
|
144
|
+
_http_info["url_path"] = _http_info["url_path"].replace("<", "\\<")
|
|
142
145
|
if request.url.query:
|
|
143
146
|
_http_info["url_path"] = f"{request.url.path}?{request.url.query}"
|
|
144
147
|
|
|
@@ -157,7 +160,7 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
157
160
|
if hasattr(request.state, "user_id"):
|
|
158
161
|
_http_info["user_id"] = str(request.state.user_id)
|
|
159
162
|
|
|
160
|
-
|
|
163
|
+
# Set http info to request state:
|
|
161
164
|
request.state.http_info = _http_info
|
|
162
165
|
response: Response = await call_next(request)
|
|
163
166
|
return response
|
|
@@ -172,28 +175,30 @@ class ResponseHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
172
175
|
"""
|
|
173
176
|
|
|
174
177
|
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
175
|
-
_http_info:
|
|
178
|
+
_http_info: dict[str, Any] = {}
|
|
176
179
|
_start_time: int = time.perf_counter_ns()
|
|
177
|
-
|
|
180
|
+
# Process request:
|
|
178
181
|
response: Response = await call_next(request)
|
|
179
|
-
|
|
182
|
+
# Response processed.
|
|
180
183
|
_end_time: int = time.perf_counter_ns()
|
|
181
184
|
_response_time: float = round((_end_time - _start_time) / 1_000_000, 1)
|
|
182
185
|
|
|
183
186
|
if hasattr(request.state, "http_info") and isinstance(
|
|
184
187
|
request.state.http_info, dict
|
|
185
188
|
):
|
|
186
|
-
_http_info:
|
|
189
|
+
_http_info: dict[str, Any] = request.state.http_info
|
|
187
190
|
|
|
188
191
|
_http_info["response_time"] = _response_time
|
|
189
192
|
if "X-Process-Time" in response.headers:
|
|
190
193
|
try:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
_x_process_time = response.headers.get("X-Process-Time")
|
|
195
|
+
if _x_process_time:
|
|
196
|
+
_http_info["response_time"] = float(_x_process_time)
|
|
197
|
+
|
|
194
198
|
except ValueError:
|
|
195
199
|
logger.warning(
|
|
196
|
-
f"`X-Process-Time` header value '{response.headers.get('X-Process-Time')}' is invalid,
|
|
200
|
+
f"`X-Process-Time` header value '{response.headers.get('X-Process-Time')}' is invalid, "
|
|
201
|
+
"should be parseable to <float>!"
|
|
197
202
|
)
|
|
198
203
|
else:
|
|
199
204
|
response.headers["X-Process-Time"] = str(_http_info["response_time"])
|
|
@@ -208,16 +213,21 @@ class ResponseHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
208
213
|
_http_info["content_length"] = 0
|
|
209
214
|
if "Content-Length" in response.headers:
|
|
210
215
|
try:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
216
|
+
_content_length = response.headers.get("Content-Length")
|
|
217
|
+
if _content_length:
|
|
218
|
+
_http_info["content_length"] = int(_content_length)
|
|
219
|
+
|
|
214
220
|
except ValueError:
|
|
215
221
|
logger.warning(
|
|
216
|
-
f"`Content-Length` header value '{response.headers.get('Content-Length')}' is invalid,
|
|
222
|
+
f"`Content-Length` header value '{response.headers.get('Content-Length')}' is invalid, "
|
|
223
|
+
"should be parseable to <int>!"
|
|
217
224
|
)
|
|
218
225
|
|
|
219
226
|
request.state.http_info = _http_info
|
|
220
227
|
return response
|
|
221
228
|
|
|
222
229
|
|
|
223
|
-
__all__ = [
|
|
230
|
+
__all__ = [
|
|
231
|
+
"RequestHTTPInfoMiddleware",
|
|
232
|
+
"ResponseHTTPInfoMiddleware",
|
|
233
|
+
]
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: beans_logging_fastapi
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: This is a middleware for FastAPI HTTP access logs. It is based on 'beans-logging' package.
|
|
5
|
+
Author-email: Batkhuu Byambajav <batkhuu10@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/bybatkhuu/module-fastapi-logging
|
|
7
|
+
Project-URL: Documentation, https://fastapi-logging-docs.bybatkhuu.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/bybatkhuu/module-fastapi-logging.git
|
|
9
|
+
Project-URL: Issues, https://github.com/bybatkhuu/module-fastapi-logging/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/bybatkhuu/module-fastapi-logging/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: beans_logging_fastapi,fastapi-logging,fastapi-logging-middleware,fastapi-middleware,logging-middleware,middleware,beans-logging,http-access-logging,logging,logger,loguru
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Requires-Python: <4.0,>=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE.txt
|
|
24
|
+
Requires-Dist: fastapi<1.0.0,>=0.99.1
|
|
25
|
+
Requires-Dist: beans-logging<8.0.0,>=7.1.0
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest<10.0.0,>=8.0.2; extra == "test"
|
|
28
|
+
Requires-Dist: pytest-cov<8.0.0,>=5.0.0; extra == "test"
|
|
29
|
+
Requires-Dist: pytest-xdist<4.0.0,>=3.6.1; extra == "test"
|
|
30
|
+
Requires-Dist: pytest-benchmark<6.0.0,>=5.0.1; extra == "test"
|
|
31
|
+
Provides-Extra: build
|
|
32
|
+
Requires-Dist: setuptools<81.0.0,>=70.3.0; extra == "build"
|
|
33
|
+
Requires-Dist: wheel<1.0.0,>=0.43.0; extra == "build"
|
|
34
|
+
Requires-Dist: build<2.0.0,>=1.1.1; extra == "build"
|
|
35
|
+
Requires-Dist: twine<7.0.0,>=6.0.1; extra == "build"
|
|
36
|
+
Provides-Extra: docs
|
|
37
|
+
Requires-Dist: pylint<5.0.0,>=3.0.4; extra == "docs"
|
|
38
|
+
Requires-Dist: mkdocs-material<10.0.0,>=9.5.50; extra == "docs"
|
|
39
|
+
Requires-Dist: mkdocs-awesome-nav<4.0.0,>=3.0.0; extra == "docs"
|
|
40
|
+
Requires-Dist: mkdocstrings[python]<2.0.0,>=0.24.3; extra == "docs"
|
|
41
|
+
Requires-Dist: mike<3.0.0,>=2.1.3; extra == "docs"
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: pytest<10.0.0,>=8.0.2; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-cov<8.0.0,>=5.0.0; extra == "dev"
|
|
45
|
+
Requires-Dist: pytest-xdist<4.0.0,>=3.6.1; extra == "dev"
|
|
46
|
+
Requires-Dist: pytest-benchmark<6.0.0,>=5.0.1; extra == "dev"
|
|
47
|
+
Requires-Dist: setuptools<81.0.0,>=70.3.0; extra == "dev"
|
|
48
|
+
Requires-Dist: wheel<1.0.0,>=0.43.0; extra == "dev"
|
|
49
|
+
Requires-Dist: build<2.0.0,>=1.1.1; extra == "dev"
|
|
50
|
+
Requires-Dist: twine<7.0.0,>=6.0.1; extra == "dev"
|
|
51
|
+
Requires-Dist: pylint<5.0.0,>=3.0.4; extra == "dev"
|
|
52
|
+
Requires-Dist: mkdocs-material<10.0.0,>=9.5.50; extra == "dev"
|
|
53
|
+
Requires-Dist: mkdocs-awesome-nav<4.0.0,>=3.0.0; extra == "dev"
|
|
54
|
+
Requires-Dist: mkdocstrings[python]<2.0.0,>=0.24.3; extra == "dev"
|
|
55
|
+
Requires-Dist: mike<3.0.0,>=2.1.3; extra == "dev"
|
|
56
|
+
Requires-Dist: pyright<2.0.0,>=1.1.392; extra == "dev"
|
|
57
|
+
Requires-Dist: pre-commit<5.0.0,>=4.0.1; extra == "dev"
|
|
58
|
+
Dynamic: license-file
|
|
59
|
+
|
|
60
|
+
# FastAPI Logging (beans-logging-fastapi)
|
|
61
|
+
|
|
62
|
+
[](https://choosealicense.com/licenses/mit)
|
|
63
|
+
[](https://github.com/bybatkhuu/module-fastapi-logging/actions/workflows/2.build-publish.yml)
|
|
64
|
+
[](https://github.com/bybatkhuu/module-fastapi-logging/releases)
|
|
65
|
+
|
|
66
|
+
This is a middleware for FastAPI HTTP access logs. It is based on **'beans-logging'** package.
|
|
67
|
+
|
|
68
|
+
## ✨ Features
|
|
69
|
+
|
|
70
|
+
- **Logger** based on **'beans-logging'** package
|
|
71
|
+
- **FastAPI** HTTP access logging **middleware**
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 🛠 Installation
|
|
76
|
+
|
|
77
|
+
### 1. 🚧 Prerequisites
|
|
78
|
+
|
|
79
|
+
- Install **Python (>= v3.10)** and **pip (>= 23)**:
|
|
80
|
+
- **[RECOMMENDED] [Miniconda (v3)](https://www.anaconda.com/docs/getting-started/miniconda/install)**
|
|
81
|
+
- *[arm64/aarch64] [Miniforge (v3)](https://github.com/conda-forge/miniforge)*
|
|
82
|
+
- *[Python virutal environment] [venv](https://docs.python.org/3/library/venv.html)*
|
|
83
|
+
|
|
84
|
+
[OPTIONAL] For **DEVELOPMENT** environment:
|
|
85
|
+
|
|
86
|
+
- Install [**git**](https://git-scm.com/downloads)
|
|
87
|
+
- Setup an [**SSH key**](https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh)
|
|
88
|
+
|
|
89
|
+
### 2. 📦 Install the package
|
|
90
|
+
|
|
91
|
+
[NOTE] Choose one of the following methods to install the package **[A ~ F]**:
|
|
92
|
+
|
|
93
|
+
**OPTION A.** [**RECOMMENDED**] Install from **PyPi**:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
pip install -U beans-logging-fastapi
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**OPTION B.** Install latest version directly from **GitHub** repository:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
pip install git+https://github.com/bybatkhuu/module-fastapi-logging.git
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**OPTION C.** Install from the downloaded **source code**:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
git clone https://github.com/bybatkhuu/module-fastapi-logging.git && \
|
|
109
|
+
cd ./module-fastapi-logging
|
|
110
|
+
|
|
111
|
+
# Install directly from the source code:
|
|
112
|
+
pip install .
|
|
113
|
+
|
|
114
|
+
# Or install with editable mode:
|
|
115
|
+
pip install -e .
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**OPTION D.** Install for **DEVELOPMENT** environment:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
pip install -e .[dev]
|
|
122
|
+
|
|
123
|
+
# Install pre-commit hooks:
|
|
124
|
+
pre-commit install
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**OPTION E.** Install from **pre-built release** files:
|
|
128
|
+
|
|
129
|
+
1. Download **`.whl`** or **`.tar.gz`** file from [**releases**](https://github.com/bybatkhuu/module-fastapi-logging/releases)
|
|
130
|
+
2. Install with pip:
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
# Install from .whl file:
|
|
134
|
+
pip install ./beans_logging_fastapi-[VERSION]-py3-none-any.whl
|
|
135
|
+
|
|
136
|
+
# Or install from .tar.gz file:
|
|
137
|
+
pip install ./beans_logging_fastapi-[VERSION].tar.gz
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**OPTION F.** Copy the **module** into the project directory (for **testing**):
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
# Install python dependencies:
|
|
144
|
+
pip install -r ./requirements.txt
|
|
145
|
+
|
|
146
|
+
# Copy the module source code into the project:
|
|
147
|
+
cp -r ./src/beans_logging_fastapi [PROJECT_DIR]
|
|
148
|
+
# For example:
|
|
149
|
+
cp -r ./src/beans_logging_fastapi /some/path/project/
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 🚸 Usage/Examples
|
|
153
|
+
|
|
154
|
+
To use `beans_logging_fastapi`:
|
|
155
|
+
|
|
156
|
+
### **FastAPI**
|
|
157
|
+
|
|
158
|
+
[**`configs/logger.yml`**](./examples/configs/logger.yml):
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
logger:
|
|
162
|
+
app_name: "fastapi-app"
|
|
163
|
+
intercept:
|
|
164
|
+
mute_modules: ["uvicorn.access"]
|
|
165
|
+
handlers:
|
|
166
|
+
default.all.file_handler:
|
|
167
|
+
enabled: true
|
|
168
|
+
default.err.file_handler:
|
|
169
|
+
enabled: true
|
|
170
|
+
default.all.json_handler:
|
|
171
|
+
enabled: true
|
|
172
|
+
default.err.json_handler:
|
|
173
|
+
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"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
[**`.env`**](./examples/.env):
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
ENV=development
|
|
191
|
+
DEBUG=true
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
[**`logger.py`**](./examples/logger.py):
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from typing import TYPE_CHECKING
|
|
198
|
+
|
|
199
|
+
if TYPE_CHECKING:
|
|
200
|
+
from loguru import Record
|
|
201
|
+
|
|
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
|
+
)
|
|
208
|
+
|
|
209
|
+
logger_loader = LoggerLoader()
|
|
210
|
+
logger: Logger = logger_loader.load()
|
|
211
|
+
|
|
212
|
+
|
|
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
|
+
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
|
+
|
|
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
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
__all__ = [
|
|
239
|
+
"logger",
|
|
240
|
+
"logger_loader",
|
|
241
|
+
]
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
[**`main.py`**](./examples/main.py):
|
|
245
|
+
|
|
246
|
+
```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__
|
|
267
|
+
|
|
268
|
+
|
|
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
|
+
|
|
279
|
+
|
|
280
|
+
app = FastAPI(lifespan=lifespan, version=__version__)
|
|
281
|
+
|
|
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
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@app.get("/")
|
|
294
|
+
def root():
|
|
295
|
+
return {"Hello": "World"}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@app.get("/items/{item_id}")
|
|
299
|
+
def read_item(item_id: int, q: Union[str, None] = None):
|
|
300
|
+
return {"item_id": item_id, "q": q}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@app.get("/continue", status_code=100)
|
|
304
|
+
def get_continue():
|
|
305
|
+
return {}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@app.get("/redirect")
|
|
309
|
+
def redirect():
|
|
310
|
+
return RedirectResponse("/")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@app.get("/error")
|
|
314
|
+
def error():
|
|
315
|
+
raise HTTPException(status_code=500)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
if __name__ == "__main__":
|
|
319
|
+
uvicorn.run(
|
|
320
|
+
app="main:app",
|
|
321
|
+
host="0.0.0.0",
|
|
322
|
+
port=8000,
|
|
323
|
+
access_log=False,
|
|
324
|
+
server_header=False,
|
|
325
|
+
proxy_headers=True,
|
|
326
|
+
forwarded_allow_ips="*",
|
|
327
|
+
)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Run the [**`examples`**](./examples):
|
|
331
|
+
|
|
332
|
+
```sh
|
|
333
|
+
cd ./examples
|
|
334
|
+
# Install python dependencies for examples:
|
|
335
|
+
pip install -r ./requirements.txt
|
|
336
|
+
|
|
337
|
+
uvicorn main:app --host=0.0.0.0 --port=8000
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Output**:
|
|
341
|
+
|
|
342
|
+
```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]
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
👍
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## ⚙️ Configuration
|
|
366
|
+
|
|
367
|
+
[**`templates/configs/config.yml`**](./templates/configs/config.yml):
|
|
368
|
+
|
|
369
|
+
```yaml
|
|
370
|
+
logger:
|
|
371
|
+
# app_name: "app"
|
|
372
|
+
default:
|
|
373
|
+
level:
|
|
374
|
+
base: INFO
|
|
375
|
+
err: WARNING
|
|
376
|
+
format_str: "[{time:YYYY-MM-DD HH:mm:ss.SSS Z} | {extra[level_short]:<5} | {name}:{line}]: {message}"
|
|
377
|
+
file:
|
|
378
|
+
logs_dir: "./logs"
|
|
379
|
+
rotate_size: 10000000
|
|
380
|
+
rotate_time: "00:00:00"
|
|
381
|
+
retention: 90
|
|
382
|
+
encoding: utf8
|
|
383
|
+
custom_serialize: false
|
|
384
|
+
intercept:
|
|
385
|
+
enabled: true
|
|
386
|
+
only_base: false
|
|
387
|
+
ignore_modules: []
|
|
388
|
+
include_modules: []
|
|
389
|
+
mute_modules: ["uvicorn.access"]
|
|
390
|
+
handlers:
|
|
391
|
+
default.all.std_handler:
|
|
392
|
+
type: STD
|
|
393
|
+
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
|
+
colorize: true
|
|
395
|
+
enabled: true
|
|
396
|
+
default.all.file_handler:
|
|
397
|
+
type: FILE
|
|
398
|
+
sink: "{app_name}.all.log"
|
|
399
|
+
enabled: true
|
|
400
|
+
default.err.file_handler:
|
|
401
|
+
type: FILE
|
|
402
|
+
sink: "{app_name}.err.log"
|
|
403
|
+
error: true
|
|
404
|
+
enabled: true
|
|
405
|
+
default.all.json_handler:
|
|
406
|
+
type: FILE
|
|
407
|
+
sink: "json/{app_name}.json.all.log"
|
|
408
|
+
serialize: true
|
|
409
|
+
enabled: true
|
|
410
|
+
default.err.json_handler:
|
|
411
|
+
type: FILE
|
|
412
|
+
sink: "json/{app_name}.json.err.log"
|
|
413
|
+
serialize: true
|
|
414
|
+
error: true
|
|
415
|
+
enabled: true
|
|
416
|
+
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
|
+
```
|
|
428
|
+
|
|
429
|
+
### 🌎 Environment Variables
|
|
430
|
+
|
|
431
|
+
[**`.env.example`**](./.env.example):
|
|
432
|
+
|
|
433
|
+
```sh
|
|
434
|
+
# ENV=LOCAL
|
|
435
|
+
# DEBUG=false
|
|
436
|
+
# TZ=UTC
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## 🧪 Running Tests
|
|
442
|
+
|
|
443
|
+
To run tests, run the following command:
|
|
444
|
+
|
|
445
|
+
```sh
|
|
446
|
+
# Install python test dependencies:
|
|
447
|
+
pip install .[test]
|
|
448
|
+
|
|
449
|
+
# Run tests:
|
|
450
|
+
python -m pytest -sv -o log_cli=true
|
|
451
|
+
# Or use the test script:
|
|
452
|
+
./scripts/test.sh -l -v -c
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## 🏗️ Build Package
|
|
456
|
+
|
|
457
|
+
To build the python package, run the following command:
|
|
458
|
+
|
|
459
|
+
```sh
|
|
460
|
+
# Install python build dependencies:
|
|
461
|
+
pip install -r ./requirements/requirements.build.txt
|
|
462
|
+
|
|
463
|
+
# Build python package:
|
|
464
|
+
python -m build
|
|
465
|
+
# Or use the build script:
|
|
466
|
+
./scripts/build.sh
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
## 📝 Generate Docs
|
|
470
|
+
|
|
471
|
+
To build the documentation, run the following command:
|
|
472
|
+
|
|
473
|
+
```sh
|
|
474
|
+
# Install python documentation dependencies:
|
|
475
|
+
pip install -r ./requirements/requirements.docs.txt
|
|
476
|
+
|
|
477
|
+
# Serve documentation locally (for development):
|
|
478
|
+
mkdocs serve -a 0.0.0.0:8000
|
|
479
|
+
# Or use the docs script:
|
|
480
|
+
./scripts/docs.sh
|
|
481
|
+
|
|
482
|
+
# Or build documentation:
|
|
483
|
+
mkdocs build
|
|
484
|
+
# Or use the docs script:
|
|
485
|
+
./scripts/docs.sh -b
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
## 📚 Documentation
|
|
489
|
+
|
|
490
|
+
- [Docs](./docs)
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## 📑 References
|
|
495
|
+
|
|
496
|
+
- <https://packaging.python.org/en/latest/tutorials/packaging-projects>
|
|
497
|
+
- <https://python-packaging.readthedocs.io/en/latest>
|
|
@@ -0,0 +1,13 @@
|
|
|
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,,
|