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,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: beans_logging_fastapi
|
|
3
|
-
Version:
|
|
4
|
-
Summary: This is a
|
|
3
|
+
Version: 4.0.0
|
|
4
|
+
Summary: This is a HTTP access log module for FastAPI 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
|
|
7
7
|
Project-URL: Documentation, https://fastapi-logging-docs.bybatkhuu.dev
|
|
@@ -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<10.0.0,>=9.0.0
|
|
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: fastapi[standard]<1.0.0,>=0.99.1; 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 log 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__
|
|
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__}")
|
|
238
|
+
from beans_logging_fastapi import logger
|
|
274
239
|
|
|
275
|
-
|
|
276
|
-
logger
|
|
277
|
-
|
|
240
|
+
__all__ = [
|
|
241
|
+
"logger",
|
|
242
|
+
]
|
|
243
|
+
```
|
|
278
244
|
|
|
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.002 +09:00 | TRACE | beans_logging.intercepters:96]: Intercepted modules: ['uvicorn', 'potato_util', 'fastapi', 'uvicorn.error', 'watchfiles.watcher', 'concurrent.futures', 'watchfiles', 'asyncio', 'concurrent', 'potato_util._base', 'dotenv', 'dotenv.main', 'watchfiles.main', 'potato_util.io', 'potato_util.io._sync']; Muted modules: ['uvicorn.access'];
|
|
412
|
+
[2026-01-01 12:00:00.003 +09:00 | INFO | uvicorn.server:84]: Started server process [88375]
|
|
413
|
+
[2026-01-01 12:00:00.003 +09:00 | INFO | uvicorn.lifespan.on:48]: Waiting for application startup.
|
|
414
|
+
[2026-01-01 12:00:00.004 +09:00 | TRACE | lifespan:19]: TRACE diagnosis is ON!
|
|
415
|
+
[2026-01-01 12:00:00.004 +09:00 | DEBUG | lifespan:20]: DEBUG mode is ON!
|
|
416
|
+
[2026-01-01 12:00:00.004 +09:00 | INFO | lifespan:21]: Preparing to startup...
|
|
417
|
+
[2026-01-01 12:00:00.004 +09:00 | OK | lifespan:24]: Finished preparation to startup.
|
|
418
|
+
[2026-01-01 12:00:00.004 +09:00 | INFO | lifespan:25]: Version: 0.0.0
|
|
419
|
+
[2026-01-01 12:00:00.005 +09:00 | INFO | uvicorn.lifespan.on:62]: Application startup complete.
|
|
420
|
+
[2026-01-01 12:00:00.006 +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.775 +09:00 | DEBUG ]: [80138308bc00406387fb804cf6cc0e11] 127.0.0.1 - "GET / HTTP/1.1"
|
|
422
|
+
[2026-01-01 12:00:01.783 +09:00 | OK ]: [80138308bc00406387fb804cf6cc0e11] 127.0.0.1 - "GET / HTTP/1.1" 200 17B 5.7ms
|
|
423
|
+
^C[2026-01-01 12:00:02.368 +09:00 | INFO | uvicorn.server:264]: Shutting down
|
|
424
|
+
[2026-01-01 12:00:02.470 +09:00 | INFO | uvicorn.lifespan.on:67]: Waiting for application shutdown.
|
|
425
|
+
[2026-01-01 12:00:02.472 +09:00 | INFO | lifespan:29]: Preparing to shutdown...
|
|
426
|
+
[2026-01-01 12:00:02.472 +09:00 | OK | lifespan:31]: Finished preparation to shutdown.
|
|
427
|
+
[2026-01-01 12:00:02.473 +09:00 | INFO | uvicorn.lifespan.on:76]: Application shutdown complete.
|
|
428
|
+
[2026-01-01 12:00:02.474 +09:00 | INFO | uvicorn.server:94]: Finished server process [88375]
|
|
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
|
|
@@ -380,50 +450,74 @@ logger:
|
|
|
380
450
|
rotate_time: "00:00:00"
|
|
381
451
|
retention: 90
|
|
382
452
|
encoding: utf8
|
|
383
|
-
|
|
453
|
+
use_custom_serialize: false
|
|
454
|
+
http:
|
|
455
|
+
std:
|
|
456
|
+
msg_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_msg_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_msg_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
|
-
|
|
392
|
-
|
|
472
|
+
all_std_handler:
|
|
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
|
|
477
|
+
all_file_handler:
|
|
395
478
|
enabled: true
|
|
396
|
-
|
|
397
|
-
type: FILE
|
|
479
|
+
h_type: FILE
|
|
398
480
|
sink: "{app_name}.all.log"
|
|
481
|
+
err_file_handler:
|
|
399
482
|
enabled: true
|
|
400
|
-
|
|
401
|
-
type: FILE
|
|
483
|
+
h_type: FILE
|
|
402
484
|
sink: "{app_name}.err.log"
|
|
403
485
|
error: true
|
|
486
|
+
all_json_handler:
|
|
404
487
|
enabled: true
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
sink: "json/{app_name}.json.all.log"
|
|
488
|
+
h_type: FILE
|
|
489
|
+
sink: "json/{app_name}.all.json.log"
|
|
408
490
|
serialize: true
|
|
491
|
+
err_json_handler:
|
|
409
492
|
enabled: true
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
sink: "json/{app_name}.json.err.log"
|
|
493
|
+
h_type: FILE
|
|
494
|
+
sink: "json/{app_name}.err.json.log"
|
|
413
495
|
serialize: true
|
|
414
496
|
error: true
|
|
497
|
+
http_access_std_handler:
|
|
498
|
+
enabled: true
|
|
499
|
+
h_type: STD
|
|
500
|
+
format: "[<c>{time:YYYY-MM-DD HH:mm:ss.SSS Z}</c> | <level>{extra[level_short]:<5}</level> ]: <level>{message}</level>"
|
|
501
|
+
colorize: true
|
|
502
|
+
http_access_file_handler:
|
|
415
503
|
enabled: true
|
|
504
|
+
h_type: FILE
|
|
505
|
+
sink: "http/{app_name}.http-access.log"
|
|
506
|
+
http_err_file_handler:
|
|
507
|
+
enabled: true
|
|
508
|
+
h_type: FILE
|
|
509
|
+
sink: "http/{app_name}.http-err.log"
|
|
510
|
+
error: true
|
|
511
|
+
http_access_json_handler:
|
|
512
|
+
enabled: true
|
|
513
|
+
h_type: FILE
|
|
514
|
+
sink: "http.json/{app_name}.http-access.json.log"
|
|
515
|
+
http_err_json_handler:
|
|
516
|
+
enabled: true
|
|
517
|
+
h_type: FILE
|
|
518
|
+
sink: "http.json/{app_name}.http-err.json.log"
|
|
519
|
+
error: true
|
|
416
520
|
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
521
|
```
|
|
428
522
|
|
|
429
523
|
### 🌎 Environment Variables
|
|
@@ -475,7 +569,7 @@ To build the documentation, run the following command:
|
|
|
475
569
|
pip install -r ./requirements/requirements.docs.txt
|
|
476
570
|
|
|
477
571
|
# Serve documentation locally (for development):
|
|
478
|
-
mkdocs serve -a 0.0.0.0:8000
|
|
572
|
+
mkdocs serve -a 0.0.0.0:8000 --livereload
|
|
479
573
|
# Or use the docs script:
|
|
480
574
|
./scripts/docs.sh
|
|
481
575
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
beans_logging_fastapi/__init__.py,sha256=5nnhUvYdtaMSZ1t549nhYfxOzcKsC4YPx8nEqJWfZYw,225
|
|
2
|
+
beans_logging_fastapi/__version__.py,sha256=SQa6Ci17DORmGXi_wkOW9pPlUojF-qtjH6NV1WOThsU,22
|
|
3
|
+
beans_logging_fastapi/_async.py,sha256=8WncsgrofroUm1z8kgUkfZYP9vwrr9Lri31PrR0NTko,3495
|
|
4
|
+
beans_logging_fastapi/_core.py,sha256=Tsc99l6i0Cryrh-UJ-uUfW5FG1O3PtQjE9QNBuJ3Eyk,2915
|
|
5
|
+
beans_logging_fastapi/config.py,sha256=GvEoPBexT0HXFhqd1vbiJ-XCwkBPwPqYmL33tJTGkK0,5687
|
|
6
|
+
beans_logging_fastapi/constants.py,sha256=SgiS27cpih3QHejlf6db3ZnFBEEzJzC0T62l8B0ZXxs,695
|
|
7
|
+
beans_logging_fastapi/filters.py,sha256=p6DgcmSByDwkw0i30AYczi26nPxWsPRfzaY8LhN1Q1I,1681
|
|
8
|
+
beans_logging_fastapi/formats.py,sha256=7zr-EIYg9vRs1zlhY9If68X9rC0xDBNG9Xc7QH9_elg,2590
|
|
9
|
+
beans_logging_fastapi/middlewares.py,sha256=ZOmp5oCg9RqnetSOBvwdQlOy87ix76ayS6HosbPG62k,14158
|
|
10
|
+
beans_logging_fastapi-4.0.0.dist-info/licenses/LICENSE.txt,sha256=CUTK-r0BWIg1r0bBiemAcMhakgV0N7HuRhw6rQ-A9A4,1074
|
|
11
|
+
beans_logging_fastapi-4.0.0.dist-info/METADATA,sha256=qOii9qAR3j67H8M3A1CW3-n4bfKTDFGNAvhwzD3FEYc,17459
|
|
12
|
+
beans_logging_fastapi-4.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
13
|
+
beans_logging_fastapi-4.0.0.dist-info/top_level.txt,sha256=PXoqVo9HGfyd81gDi3D2mXMYPM9JKITL0ycFftJxlhw,22
|
|
14
|
+
beans_logging_fastapi-4.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
|
-
]
|