beans-logging-fastapi 1.1.0__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 +41 -29
- 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.0.dist-info → beans_logging_fastapi-2.0.0.dist-info}/WHEEL +1 -1
- beans_logging_fastapi-1.1.0.dist-info/LICENCE.txt → beans_logging_fastapi-2.0.0.dist-info/licenses/LICENSE.txt +1 -1
- {beans_logging_fastapi-1.1.0.dist-info → beans_logging_fastapi-2.0.0.dist-info}/top_level.txt +0 -1
- beans_logging_fastapi-1.1.0.dist-info/METADATA +0 -385
- beans_logging_fastapi-1.1.0.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:
|
|
@@ -137,6 +140,8 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
137
140
|
_http_info["url_path"] = _http_info["url_path"].replace("{", "{{")
|
|
138
141
|
if "}" in _http_info["url_path"]:
|
|
139
142
|
_http_info["url_path"] = _http_info["url_path"].replace("}", "}}")
|
|
143
|
+
if "<" in _http_info["url_path"]:
|
|
144
|
+
_http_info["url_path"] = _http_info["url_path"].replace("<", "\\<")
|
|
140
145
|
if request.url.query:
|
|
141
146
|
_http_info["url_path"] = f"{request.url.path}?{request.url.query}"
|
|
142
147
|
|
|
@@ -155,7 +160,7 @@ class RequestHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
155
160
|
if hasattr(request.state, "user_id"):
|
|
156
161
|
_http_info["user_id"] = str(request.state.user_id)
|
|
157
162
|
|
|
158
|
-
|
|
163
|
+
# Set http info to request state:
|
|
159
164
|
request.state.http_info = _http_info
|
|
160
165
|
response: Response = await call_next(request)
|
|
161
166
|
return response
|
|
@@ -170,28 +175,30 @@ class ResponseHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
170
175
|
"""
|
|
171
176
|
|
|
172
177
|
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
173
|
-
_http_info:
|
|
178
|
+
_http_info: dict[str, Any] = {}
|
|
174
179
|
_start_time: int = time.perf_counter_ns()
|
|
175
|
-
|
|
180
|
+
# Process request:
|
|
176
181
|
response: Response = await call_next(request)
|
|
177
|
-
|
|
182
|
+
# Response processed.
|
|
178
183
|
_end_time: int = time.perf_counter_ns()
|
|
179
184
|
_response_time: float = round((_end_time - _start_time) / 1_000_000, 1)
|
|
180
185
|
|
|
181
186
|
if hasattr(request.state, "http_info") and isinstance(
|
|
182
187
|
request.state.http_info, dict
|
|
183
188
|
):
|
|
184
|
-
_http_info:
|
|
189
|
+
_http_info: dict[str, Any] = request.state.http_info
|
|
185
190
|
|
|
186
191
|
_http_info["response_time"] = _response_time
|
|
187
192
|
if "X-Process-Time" in response.headers:
|
|
188
193
|
try:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
|
|
192
198
|
except ValueError:
|
|
193
199
|
logger.warning(
|
|
194
|
-
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>!"
|
|
195
202
|
)
|
|
196
203
|
else:
|
|
197
204
|
response.headers["X-Process-Time"] = str(_http_info["response_time"])
|
|
@@ -206,16 +213,21 @@ class ResponseHTTPInfoMiddleware(BaseHTTPMiddleware):
|
|
|
206
213
|
_http_info["content_length"] = 0
|
|
207
214
|
if "Content-Length" in response.headers:
|
|
208
215
|
try:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
216
|
+
_content_length = response.headers.get("Content-Length")
|
|
217
|
+
if _content_length:
|
|
218
|
+
_http_info["content_length"] = int(_content_length)
|
|
219
|
+
|
|
212
220
|
except ValueError:
|
|
213
221
|
logger.warning(
|
|
214
|
-
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>!"
|
|
215
224
|
)
|
|
216
225
|
|
|
217
226
|
request.state.http_info = _http_info
|
|
218
227
|
return response
|
|
219
228
|
|
|
220
229
|
|
|
221
|
-
__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,,
|