beans-logging 4.3.3__py3-none-any.whl → 6.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.
@@ -1,3 +1,3 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
- __version__ = "4.3.3"
3
+ __version__ = "6.0.0"
beans_logging/_base.py CHANGED
@@ -310,8 +310,8 @@ class LoggerLoader:
310
310
  if _is_debug and (self.config.level != "TRACE"):
311
311
  self.config.level = "DEBUG"
312
312
 
313
- if "BEANS_LOGGING_DIR" in os.environ:
314
- self.config.file.logs_dir = os.getenv("BEANS_LOGGING_DIR")
313
+ if "BEANS_LOGGING_LOGS_DIR" in os.environ:
314
+ self.config.file.logs_dir = os.getenv("BEANS_LOGGING_LOGS_DIR")
315
315
 
316
316
  # if self.config.stream.use_color:
317
317
  # ## Checking terminal could support xterm colors:
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: beans-logging
3
- Version: 4.3.3
3
+ Version: 6.0.0
4
4
  Summary: 'beans_logging' is a python package for simple logger and easily managing logging modules. It is a Loguru based custom logging package for python projects.
5
5
  Home-page: https://github.com/bybatkhuu/module.python-logging
6
- Download-URL: https://github.com/bybatkhuu/module.python-logging/archive/v4.3.3.tar.gz
6
+ Download-URL: https://github.com/bybatkhuu/module.python-logging/archive/v6.0.0.tar.gz
7
7
  Author: Batkhuu Byambajav
8
8
  Author-email: batkhuu10@gmail.com
9
9
  License: MIT
@@ -22,6 +22,8 @@ License-File: LICENSE.txt
22
22
  Requires-Dist: PyYAML <7.0,>=6.0
23
23
  Requires-Dist: pydantic !=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.10.0
24
24
  Requires-Dist: loguru <1.0.0,>=0.7.2
25
+ Provides-Extra: fastapi
26
+ Requires-Dist: beans-logging-fastapi <2.0.0,>=1.0.0 ; extra == 'fastapi'
25
27
 
26
28
  # beans_logging
27
29
 
@@ -47,7 +49,6 @@ It is a `Loguru` based custom logging package for python projects.
47
49
  - Custom logging **formats**
48
50
  - **Multiprocess** compatibility (Linux, macOS - 'fork')
49
51
  - Add custom **handlers**
50
- - **FastAPI** HTTP access logging **middleware**
51
52
  - **Base** logging module
52
53
  - Support **Pydantic-v1** and **Pydantic-v2**
53
54
 
@@ -236,157 +237,12 @@ Traceback (most recent call last):
236
237
  ZeroDivisionError: division by zero
237
238
  ```
238
239
 
239
- ### **Advanced (FastAPI)**
240
+ ### **FastAPI**
240
241
 
241
- [**`configs/logger.yml`**](https://github.com/bybatkhuu/module.python-logging/blob/main/examples/advanced/configs/logger.yml):
242
+ Checkout `beans_logging_fastapi` package: <https://github.com/bybatkhuu/module.fastapi-logging>
242
243
 
243
- ```yaml
244
- logger:
245
- app_name: "fastapi-app"
246
- level: "TRACE"
247
- use_diagnose: false
248
- stream:
249
- use_color: true
250
- use_icon: false
251
- format_str: "[<c>{time:YYYY-MM-DD HH:mm:ss.SSS Z}</c> | <level>{level_short:<5}</level> | <w>{name}:{line}</w>]: <level>{message}</level>"
252
- std_handler:
253
- enabled: true
254
- file:
255
- logs_dir: "./logs"
256
- rotate_size: 10000000 # 10MB
257
- rotate_time: "00:00:00"
258
- backup_count: 90
259
- log_handlers:
260
- enabled: true
261
- format_str: "[{time:YYYY-MM-DD HH:mm:ss.SSS Z} | {level_short:<5} | {name}:{line}]: {message}"
262
- log_path: "{app_name}.std.all.log"
263
- err_path: "{app_name}.std.err.log"
264
- json_handlers:
265
- enabled: true
266
- use_custom: false
267
- log_path: "json/{app_name}.json.all.log"
268
- err_path: "json/{app_name}.json.err.log"
269
- intercept:
270
- auto_load:
271
- enabled: true
272
- only_base: false
273
- ignore_modules: []
274
- include_modules: []
275
- mute_modules: ["uvicorn.access", "uvicorn.error"]
276
- extra:
277
- http_std_debug_format: '<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
278
- 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'
279
- http_file_enabled: true
280
- http_log_path: "http/{app_name}.http.access.log"
281
- http_err_path: "http/{app_name}.http.err.log"
282
- http_json_enabled: true
283
- http_json_path: "json.http/{app_name}.json.http.access.log"
284
- http_json_err_path: "json.http/{app_name}.json.http.err.log"
285
- ```
286
-
287
- [**`.env`**](https://github.com/bybatkhuu/module.python-logging/blob/main/examples/advanced/.env):
288
-
289
- ```sh
290
- ENV=development
291
- DEBUG=true
292
- ```
293
-
294
- [**`logger.py`**](https://github.com/bybatkhuu/module.python-logging/blob/main/examples/advanced/logger.py):
295
-
296
- ```python
297
- from beans_logging import Logger, LoggerLoader
298
- from beans_logging.fastapi import add_http_file_handler, add_http_file_json_handler
299
-
300
-
301
- logger_loader = LoggerLoader()
302
- logger: Logger = logger_loader.load()
303
-
304
- if logger_loader.config.extra.http_file_enabled:
305
- add_http_file_handler(
306
- logger_loader=logger_loader,
307
- log_path=logger_loader.config.extra.http_log_path,
308
- err_path=logger_loader.config.extra.http_err_path,
309
- )
310
-
311
- if logger_loader.config.extra.http_json_enabled:
312
- add_http_file_json_handler(
313
- logger_loader=logger_loader,
314
- log_path=logger_loader.config.extra.http_json_path,
315
- err_path=logger_loader.config.extra.http_json_err_path,
316
- )
317
- ```
318
-
319
- [**`main.py`**](https://github.com/bybatkhuu/module.python-logging/blob/main/examples/advanced/main.py):
320
-
321
- ```python
322
- from typing import Union
323
- from contextlib import asynccontextmanager
324
-
325
- from dotenv import load_dotenv
326
- from fastapi import FastAPI, HTTPException
327
- from fastapi.responses import RedirectResponse
328
-
329
- load_dotenv()
330
-
331
- from beans_logging.fastapi import HttpAccessLogMiddleware
332
-
333
- from logger import logger, logger_loader
334
- from __version__ import __version__
335
-
336
-
337
- @asynccontextmanager
338
- async def lifespan(app: FastAPI):
339
- logger.info("Preparing to startup...")
340
- logger.success("Finished preparation to startup.")
341
- logger.info(f"API version: {__version__}")
342
-
343
- yield
344
- logger.info("Praparing to shutdown...")
345
- logger.success("Finished preparation to shutdown.")
346
-
347
- app = FastAPI(lifespan=lifespan, version=__version__)
348
- app.add_middleware(
349
- HttpAccessLogMiddleware,
350
- has_proxy_headers=True,
351
- debug_format=logger_loader.config.extra.http_std_debug_format,
352
- msg_format=logger_loader.config.extra.http_std_msg_format,
353
- )
354
-
355
- @app.get("/")
356
- def root():
357
- return {"Hello": "World"}
358
- ```
359
-
360
- Run the [**`examples/advanced`**](https://github.com/bybatkhuu/module.python-logging/tree/main/examples/advanced):
361
-
362
- ```sh
363
- cd ./examples/advanced
364
- # Install python dependencies for examples:
365
- pip install -r ./requirements.txt
366
-
367
- uvicorn main:app --host=0.0.0.0 --port=8000
368
- ```
369
-
370
- **Output**:
371
-
372
- ```txt
373
- [2023-09-01 00:00:00.000 +09:00 | TRACE | beans_logging._base:576]: Intercepted modules: ['watchfiles.watcher', 'dotenv', 'asyncio', 'dotenv.main', 'watchfiles.main', 'concurrent.futures', 'uvicorn', 'fastapi', 'concurrent', 'watchfiles']; Muted modules: ['uvicorn.access', 'uvicorn.error'];
374
- [2023-09-01 00:00:00.000 +09:00 | INFO | uvicorn.server:76]: Started server process [17146]
375
- [2023-09-01 00:00:00.000 +09:00 | INFO | uvicorn.lifespan.on:46]: Waiting for application startup.
376
- [2023-09-01 00:00:00.000 +09:00 | INFO | main:21]: Preparing to startup...
377
- [2023-09-01 00:00:00.000 +09:00 | OK | main:22]: Finished preparation to startup.
378
- [2023-09-01 00:00:00.000 +09:00 | INFO | main:23]: API version: 0.0.1-000000
379
- [2023-09-01 00:00:00.000 +09:00 | INFO | uvicorn.lifespan.on:60]: Application startup complete.
380
- [2023-09-01 00:00:00.000 +09:00 | INFO | uvicorn.server:218]: Uvicorn running on http://0.0.0.0:9000 (Press CTRL+C to quit)
381
- [2023-09-01 00:00:00.000 +09:00 | DEBUG | anyio._backends._asyncio:833]: [f635ebbc3f2348db9dcff681be1bd52a] 127.0.0.1 - "GET / HTTP/1.1"
382
- [2023-09-01 00:00:00.000 +09:00 | OK | anyio._backends._asyncio:833]: [f635ebbc3f2348db9dcff681be1bd52a] 127.0.0.1 - "GET / HTTP/1.1" 200 17B 0.7ms
383
- ^C[2023-09-01 00:00:00.000 +09:00 | INFO | uvicorn.server:264]: Shutting down
384
- [2023-09-01 00:00:00.000 +09:00 | INFO | uvicorn.lifespan.on:65]: Waiting for application shutdown.
385
- [2023-09-01 00:00:00.000 +09:00 | INFO | main:26]: Praparing to shutdown...
386
- [2023-09-01 00:00:00.000 +09:00 | OK | main:27]: Finished preparation to shutdown.
387
- [2023-09-01 00:00:00.000 +09:00 | INFO | uvicorn.lifespan.on:76]: Application shutdown complete.
388
- [2023-09-01 00:00:00.000 +09:00 | INFO | uvicorn.server:86]: Finished server process [17146]
389
- ```
244
+ - FastAPI HTTP access logging middleware
245
+ - Install with pip: `pip install -U beans-logging[fastapi]` or `pip install -U beans-logging-fastapi`
390
246
 
391
247
  ---
392
248
 
@@ -412,7 +268,7 @@ DEBUG=true
412
268
 
413
269
  BEANS_LOGGING_DISABLE_DEFAULT=false
414
270
  BEANS_LOGGING_CONFIG_PATH="./configs/logger.yml"
415
- BEANS_LOGGING_DIR="./logs"
271
+ BEANS_LOGGING_LOGS_DIR="./logs"
416
272
  ```
417
273
 
418
274
  ## Configuration
@@ -467,3 +323,4 @@ logger:
467
323
  - <https://github.com/Delgan/loguru>
468
324
  - <https://loguru.readthedocs.io/en/stable/api/logger.html>
469
325
  - <https://loguru.readthedocs.io/en/stable/resources/recipes.html>
326
+ - <https://github.com/bybatkhuu/module.fastapi-logging>
@@ -1,6 +1,6 @@
1
1
  beans_logging/__init__.py,sha256=TxuozMCLvGkG7sATP2uFCQws6TRqwp36yc08zUVGcb4,297
2
- beans_logging/__version__.py,sha256=W4CzCRFuV9YuXmNhRvRPGf26WttayL_i8CrPbFpRB1k,47
3
- beans_logging/_base.py,sha256=dL8l8DtNX2CZV6g6WZd_05X7me-joYZskpJbVlPjeaM,24561
2
+ beans_logging/__version__.py,sha256=Skn1HZtKosF7bkVFNvpnIh3dEl9kV9eFp7ZGRzT6uQI,47
3
+ beans_logging/_base.py,sha256=HEf9Vyyj3oS0m5wuKd50LQDCVZNmsBEM1LntrYbnGjs,24571
4
4
  beans_logging/_consts.py,sha256=b_-uytryZFCxlD2aKXzdS0oQpAsaK6JLi-w5bUeG1yE,345
5
5
  beans_logging/_handlers.py,sha256=WQjr-dGAnjsegI2LHJl_Fubxb0reSCr8A6IvR7eN8dk,1133
6
6
  beans_logging/_utils.py,sha256=Smb0cxkTdVxA-IE-oqi3sGhpsQdtNquQSw2bQxN0pDM,2742
@@ -10,16 +10,11 @@ beans_logging/formats.py,sha256=gxyJT9ZUWQvjrL4x6fU5bLKybGsFkCpFDG5OpndqDhc,1349
10
10
  beans_logging/rotation.py,sha256=GspZX7wff_igZjbGSysCboz8fUgDHofGRL10OaNlZ3I,2137
11
11
  beans_logging/schemas.py,sha256=syMsmwbDvDE1odnaIX18PEIEpWyItfDDnisZr1AsOPs,5641
12
12
  beans_logging/sinks.py,sha256=C_y53i_QJuNZs_zBitb87d_tfsLhin2D9DtImPV5OHg,356
13
- beans_logging/fastapi/__init__.py,sha256=rK10G__Ery8lTujJLkRjLO_OTJX8o0QLAf5xNRKrXhw,248
14
- beans_logging/fastapi/_filters.py,sha256=BNkIxCW1_GkmrpZdIH7SsP69_p989_ru0kjjBB7LKzg,509
15
- beans_logging/fastapi/_formats.py,sha256=xyz8fiultiB4SVOnvxt-6kW3nb3LsIv-3YeKbJj-xmg,1727
16
- beans_logging/fastapi/_handlers.py,sha256=eoV-fDKgmZHBV3IfcV01rirH29Devd8DqnvTgzZCZpc,2726
17
- beans_logging/fastapi/_middlewares.py,sha256=UkFrpKP8XbtXE2lJU7F3N0XuJF48awkL7CCdNtf657I,11315
18
13
  tests/__init__.py,sha256=iwhKnzeBJLKxpRVjvzwiRE63_zNpIBfaKLITauVph-0,24
19
14
  tests/conftest.py,sha256=ycEL83-UMU-fcXQUZWTCNEPcBOZ38pzhoCPpXpCjIEM,319
20
15
  tests/test_beans_logging.py,sha256=qyiM24QEAi7lVs-QBmjh_e48bCHk8nNYwMOlZIb6phw,1733
21
- beans_logging-4.3.3.dist-info/LICENSE.txt,sha256=8jrXqC7FZbke39LPGo_mUFR81CkoUCP_vyefZjlQDOg,1074
22
- beans_logging-4.3.3.dist-info/METADATA,sha256=M3tCSQAtdFpmuwVtvHuIQBjSEZDyfMBvFh5zmD6Jaz4,15291
23
- beans_logging-4.3.3.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
24
- beans_logging-4.3.3.dist-info/top_level.txt,sha256=wSVo6vZwIqyOwwsbVBQceBQ_VJKuErw8OQvDiHcp8uU,20
25
- beans_logging-4.3.3.dist-info/RECORD,,
16
+ beans_logging-6.0.0.dist-info/LICENSE.txt,sha256=8jrXqC7FZbke39LPGo_mUFR81CkoUCP_vyefZjlQDOg,1074
17
+ beans_logging-6.0.0.dist-info/METADATA,sha256=jd6oaW9K3_v7vYmOt3RnGf3HODeTwkGrERzxwSCsMCM,9805
18
+ beans_logging-6.0.0.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
19
+ beans_logging-6.0.0.dist-info/top_level.txt,sha256=wSVo6vZwIqyOwwsbVBQceBQ_VJKuErw8OQvDiHcp8uU,20
20
+ beans_logging-6.0.0.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- # -*- coding: utf-8 -*-
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 HttpAccessLogMiddleware
@@ -1,22 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- from beans_logging.filters import use_all_filter
4
-
5
-
6
- def use_http_filter(record: dict) -> bool:
7
- """Filter message only for http access log handler by checking 'http_info' key in extra.
8
-
9
- Args:
10
- record (dict): Log record as dictionary.
11
-
12
- Returns:
13
- bool: True if record has 'http_info' key in extra, False otherwise.
14
- """
15
-
16
- if not use_all_filter(record):
17
- return False
18
-
19
- if "http_info" not in record["extra"]:
20
- return False
21
-
22
- return True
@@ -1,62 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
-
4
- def http_file_format(record: dict) -> str:
5
- """Http access log file format.
6
-
7
- Args:
8
- record (dict): Log record as dictionary.
9
-
10
- Returns:
11
- str: Format for http access log record.
12
- """
13
-
14
- _MSG_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}'
15
-
16
- if "http_info" not in record["extra"]:
17
- return ""
18
-
19
- if "http_message" not in record:
20
- _http_info = record["extra"]["http_info"]
21
- if "datetime" not in _http_info:
22
- _http_info["datetime"] = record["time"].isoformat()
23
-
24
- if "content_length" not in _http_info:
25
- _http_info["content_length"] = 0
26
-
27
- if "h_referer" not in _http_info:
28
- _http_info["h_referer"] = "-"
29
-
30
- if "h_user_agent" not in _http_info:
31
- _http_info["h_user_agent"] = "-"
32
-
33
- if "response_time" not in _http_info:
34
- _http_info["response_time"] = 0
35
-
36
- record["extra"]["http_info"] = _http_info
37
-
38
- _msg = _MSG_FORMAT.format(**_http_info)
39
- record["http_message"] = _msg
40
-
41
- return "{http_message}\n"
42
-
43
-
44
- def http_file_json_format(record: dict) -> str:
45
- """Http access json log file format.
46
-
47
- Args:
48
- record (dict): Log record as dictionary.
49
-
50
- Returns:
51
- str: Format for http access json log record.
52
- """
53
-
54
- if "http_info" not in record["extra"]:
55
- return ""
56
-
57
- _http_info = record["extra"]["http_info"]
58
- if "datetime" not in _http_info:
59
- _http_info["datetime"] = record["time"].isoformat()
60
- record["extra"]["http_info"] = _http_info
61
-
62
- return "{extra[http_info]}\n"
@@ -1,79 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- from typing import Union, Callable
4
-
5
- import pydantic
6
-
7
- if "2.0.0" <= pydantic.__version__:
8
- from pydantic import validate_call
9
- else:
10
- from pydantic import validate_arguments as validate_call
11
-
12
- from beans_logging import LoggerLoader
13
-
14
- from ._filters import use_http_filter
15
- from ._formats import http_file_format, http_file_json_format
16
-
17
-
18
- @validate_call(config=dict(arbitrary_types_allowed=True))
19
- def add_http_file_handler(
20
- logger_loader: LoggerLoader,
21
- log_path: str = "http/{app_name}.http.access.log",
22
- err_path: str = "http/{app_name}.http.err.log",
23
- formatter: Union[Callable, str] = http_file_format,
24
- ):
25
- """Add http access log file and error file handler.
26
-
27
- Args:
28
- logger_loader (LoggerLoader, required): LoggerLoader instance.
29
- log_path (str, optional): Log file path. Defaults to "http/{app_name}.http.access.log".
30
- err_path (str, optional): Error log file path. Defaults to "http/{app_name}.http.err.log".
31
- formatter (Union[Callable, str], optional): Log formatter. Defaults to `http_file_format` function.
32
- """
33
-
34
- logger_loader.add_custom_handler(
35
- handler_name="FILE.HTTP",
36
- sink=log_path,
37
- filter=use_http_filter,
38
- format=formatter,
39
- )
40
-
41
- logger_loader.add_custom_handler(
42
- handler_name="FILE.HTTP_ERR",
43
- sink=err_path,
44
- level="WARNING",
45
- filter=use_http_filter,
46
- format=formatter,
47
- )
48
-
49
-
50
- @validate_call(config=dict(arbitrary_types_allowed=True))
51
- def add_http_file_json_handler(
52
- logger_loader: LoggerLoader,
53
- log_path: str = "json.http/{app_name}.json.http.access.log",
54
- err_path: str = "json.http/{app_name}.json.http.err.log",
55
- formatter: Union[Callable, str] = http_file_json_format,
56
- ):
57
- """Add http access json log file and json error file handler.
58
-
59
- Args:
60
- logger_loader (LoggerLoader, required): LoggerLoader instance.
61
- log_path (str, optional): Json log file path. Defaults to "http.json/{app_name}.json.http.access.log".
62
- err_path (str, optional): Json error log file path. Defaults to "http.json/{app_name}.json.http.err.log".
63
- formatter (Union[Callable, str], optional): Log formatter. Defaults to `http_file_json_format` function.
64
- """
65
-
66
- logger_loader.add_custom_handler(
67
- handler_name="FILE.JSON.HTTP",
68
- sink=log_path,
69
- filter=use_http_filter,
70
- format=formatter,
71
- )
72
-
73
- logger_loader.add_custom_handler(
74
- handler_name="FILE.JSON.HTTP_ERR",
75
- sink=err_path,
76
- level="WARNING",
77
- filter=use_http_filter,
78
- format=formatter,
79
- )
@@ -1,262 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- import json
4
- import time
5
- from uuid import uuid4
6
-
7
- from fastapi import Request, Response
8
- from fastapi.concurrency import run_in_threadpool
9
- from starlette.middleware.base import BaseHTTPMiddleware
10
-
11
- from beans_logging import logger
12
-
13
-
14
- class HttpAccessLogMiddleware(BaseHTTPMiddleware):
15
- """Http access log middleware for FastAPI.
16
-
17
- Inherits:
18
- BaseHTTPMiddleware: Base HTTP middleware class from starlette.
19
-
20
- Attributes:
21
- _DEBUG_FORMAT (str ): Default http access log debug message format. Defaults to '<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'.
22
- _MSG_FORMAT (str ): Default http access log message format. Defaults to '<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'.
23
-
24
- has_proxy_headers (bool): If True, use proxy headers to get http request info. Defaults to False.
25
- has_cf_headers (bool): If True, use cloudflare headers to get http request info. Defaults to False.
26
- debug_format (str ): Http access log debug message format. Defaults to `HttpAccessLogMiddleware._DEBUG_FORMAT`.
27
- msg_format (str ): Http access log message format. Defaults to `HttpAccessLogMiddleware._MSG_FORMAT`.
28
- use_debug_log (bool): If True, use debug log to log http access log. Defaults to True.
29
- """
30
-
31
- _DEBUG_FORMAT = '<n>[{request_id}]</n> {client_host} {user_id} "<u>{method} {url_path}</u> HTTP/{http_version}"'
32
- _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'
33
-
34
- def __init__(
35
- self,
36
- app,
37
- has_proxy_headers: bool = False,
38
- has_cf_headers: bool = False,
39
- debug_format: str = _DEBUG_FORMAT,
40
- msg_format: str = _MSG_FORMAT,
41
- use_debug_log: bool = True,
42
- ):
43
- super().__init__(app)
44
- self.has_proxy_headers = has_proxy_headers
45
- self.has_cf_headers = has_cf_headers
46
- self.debug_format = debug_format
47
- self.msg_format = msg_format
48
- self.use_debug_log = use_debug_log
49
-
50
- async def dispatch(self, request: Request, call_next) -> Response:
51
- _logger = logger.opt(colors=True, record=True)
52
-
53
- _http_info = {}
54
- _http_info["request_id"] = uuid4().hex
55
- if "X-Request-ID" in request.headers:
56
- _http_info["request_id"] = request.headers.get("X-Request-ID")
57
- elif "X-Correlation-ID" in request.headers:
58
- _http_info["request_id"] = request.headers.get("X-Correlation-ID")
59
-
60
- ## Set request_id to request state:
61
- request.state.request_id = _http_info["request_id"]
62
-
63
- _http_info["client_host"] = request.client.host
64
- _http_info["request_proto"] = request.url.scheme
65
- _http_info["request_host"] = (
66
- request.url.hostname if request.url.hostname else ""
67
- )
68
- if (request.url.port != 80) and (request.url.port != 443):
69
- _http_info[
70
- "request_host"
71
- ] = f"{_http_info['request_host']}:{request.url.port}"
72
-
73
- _http_info["request_port"] = request.url.port
74
- _http_info["http_version"] = request.scope["http_version"]
75
-
76
- if self.has_proxy_headers:
77
- if "X-Real-IP" in request.headers:
78
- _http_info["client_host"] = request.headers.get("X-Real-IP")
79
- elif "X-Forwarded-For" in request.headers:
80
- _http_info["client_host"] = request.headers.get(
81
- "X-Forwarded-For"
82
- ).split(",")[0]
83
- _http_info["h_x_forwarded_for"] = request.headers.get("X-Forwarded-For")
84
-
85
- if "X-Forwarded-Proto" in request.headers:
86
- _http_info["request_proto"] = request.headers.get("X-Forwarded-Proto")
87
-
88
- if "X-Forwarded-Host" in request.headers:
89
- _http_info["request_host"] = request.headers.get("X-Forwarded-Host")
90
- elif "Host" in request.headers:
91
- _http_info["request_host"] = request.headers.get("Host")
92
-
93
- if "X-Forwarded-Port" in request.headers:
94
- try:
95
- _http_info["request_port"] = int(
96
- request.headers.get("X-Forwarded-Port")
97
- )
98
- except ValueError:
99
- logger.warning(
100
- f"`X-Forwarded-Port` header value '{request.headers.get('X-Forwarded-Port')}' is invalid, should be parseable to <int>!"
101
- )
102
-
103
- if "Via" in request.headers:
104
- _http_info["h_via"] = request.headers.get("Via")
105
-
106
- if self.has_cf_headers:
107
- if "CF-Connecting-IP" in request.headers:
108
- _http_info["client_host"] = request.headers.get("CF-Connecting-IP")
109
- _http_info["h_cf_connecting_ip"] = request.headers.get(
110
- "CF-Connecting-IP"
111
- )
112
- elif "True-Client-IP" in request.headers:
113
- _http_info["client_host"] = request.headers.get("True-Client-IP")
114
- _http_info["h_true_client_ip"] = request.headers.get(
115
- "True-Client-IP"
116
- )
117
-
118
- if "CF-IPCountry" in request.headers:
119
- _http_info["client_country"] = request.headers.get("CF-IPCountry")
120
- _http_info["h_cf_ipcountry"] = request.headers.get("CF-IPCountry")
121
-
122
- if "CF-RAY" in request.headers:
123
- _http_info["h_cf_ray"] = request.headers.get("CF-RAY")
124
-
125
- if "cf-ipcontinent" in request.headers:
126
- _http_info["h_cf_ipcontinent"] = request.headers.get(
127
- "cf-ipcontinent"
128
- )
129
-
130
- if "cf-ipcity" in request.headers:
131
- _http_info["h_cf_ipcity"] = request.headers.get("cf-ipcity")
132
-
133
- if "cf-iplongitude" in request.headers:
134
- _http_info["h_cf_iplongitude"] = request.headers.get(
135
- "cf-iplongitude"
136
- )
137
-
138
- if "cf-iplatitude" in request.headers:
139
- _http_info["h_cf_iplatitude"] = request.headers.get("cf-iplatitude")
140
-
141
- if "cf-postal-code" in request.headers:
142
- _http_info["h_cf_postal_code"] = request.headers.get(
143
- "cf-postal-code"
144
- )
145
-
146
- if "cf-timezone" in request.headers:
147
- _http_info["h_cf_timezone"] = request.headers.get("cf-timezone")
148
-
149
- _http_info["method"] = request.method
150
- _http_info["url_path"] = request.url.path
151
- if "{" in _http_info["url_path"]:
152
- _http_info["url_path"] = _http_info["url_path"].replace("{", "{{")
153
-
154
- if "}" in _http_info["url_path"]:
155
- _http_info["url_path"] = _http_info["url_path"].replace("}", "}}")
156
-
157
- if request.url.query:
158
- _http_info["url_path"] = f"{request.url.path}?{request.url.query}"
159
-
160
- _http_info["url_query_params"] = request.query_params._dict
161
- _http_info["url_path_params"] = request.path_params
162
-
163
- _http_info["h_referer"] = request.headers.get("Referer", "-")
164
- _http_info["h_user_agent"] = request.headers.get("User-Agent", "-")
165
- _http_info["h_accept"] = request.headers.get("Accept", "")
166
- _http_info["h_content_type"] = request.headers.get("Content-Type", "")
167
-
168
- if "Origin" in request.headers:
169
- _http_info["h_origin"] = request.headers.get("Origin")
170
-
171
- _http_info["user_id"] = "-"
172
- if hasattr(request.state, "user_id"):
173
- _http_info["user_id"] = str(request.state.user_id)
174
-
175
- ## Debug log:
176
- if self.use_debug_log:
177
- _debug_msg = self.debug_format.format(**_http_info)
178
-
179
- # _logger.debug(_debug_msg)
180
- await run_in_threadpool(
181
- _logger.debug,
182
- _debug_msg,
183
- )
184
- ## Debug log
185
-
186
- ## Set http info to request state:
187
- request.state.http_info = _http_info
188
-
189
- _start_time = time.time()
190
- ## Process request:
191
- response = await call_next(request)
192
- ## Response processed.
193
- _http_info["response_time"] = round((time.time() - _start_time) * 1000, 1)
194
-
195
- if "X-Process-Time" in response.headers:
196
- try:
197
- _http_info["response_time"] = float(
198
- response.headers.get("X-Process-Time")
199
- )
200
- except ValueError:
201
- logger.warning(
202
- f"`X-Process-Time` header value '{response.headers.get('X-Process-Time')}' is invalid, should be parseable to <float>!"
203
- )
204
- else:
205
- response.headers["X-Process-Time"] = str(_http_info["response_time"])
206
-
207
- if "X-Request-ID" not in response.headers:
208
- response.headers["X-Request-ID"] = _http_info["request_id"]
209
-
210
- if hasattr(request.state, "user_id"):
211
- _http_info["user_id"] = str(request.state.user_id)
212
-
213
- _http_info["status_code"] = response.status_code
214
- _http_info["content_length"] = 0
215
- if "Content-Length" in response.headers:
216
- try:
217
- _http_info["content_length"] = int(
218
- response.headers.get("Content-Length")
219
- )
220
- except ValueError:
221
- logger.warning(
222
- f"`Content-Length` header value '{response.headers.get('Content-Length')}' is invalid, should be parseable to <int>!"
223
- )
224
-
225
- try:
226
- json.dumps(_http_info)
227
- except TypeError:
228
- logger.warning(
229
- "Can not serialize `http_info` to json string in HttpAccessLogMiddleware!"
230
- )
231
-
232
- ## Http access log:
233
- _LEVEL = "INFO"
234
- _msg_format = self.msg_format
235
- if _http_info["status_code"] < 200:
236
- _LEVEL = "DEBUG"
237
- _msg_format = f'<d>{_msg_format.replace("{status_code}", "<n><b><k>{status_code}</k></b></n>")}</d>'
238
- elif (200 <= _http_info["status_code"]) and (_http_info["status_code"] < 300):
239
- _LEVEL = "SUCCESS"
240
- _msg_format = f'<w>{_msg_format.replace("{status_code}", "<lvl>{status_code}</lvl>")}</w>'
241
- elif (300 <= _http_info["status_code"]) and (_http_info["status_code"] < 400):
242
- _LEVEL = "INFO"
243
- _msg_format = f'<d>{_msg_format.replace("{status_code}", "<n><b><c>{status_code}</c></b></n>")}</d>'
244
- elif (400 <= _http_info["status_code"]) and (_http_info["status_code"] < 500):
245
- _LEVEL = "WARNING"
246
- _msg_format = _msg_format.replace("{status_code}", "<r>{status_code}</r>")
247
- elif 500 <= _http_info["status_code"]:
248
- _LEVEL = "ERROR"
249
- _msg_format = (
250
- f'{_msg_format.replace("{status_code}", "<n>{status_code}</n>")}'
251
- )
252
-
253
- _msg = _msg_format.format(**_http_info)
254
- # _logger.bind(http_info=_http_info).log(_LEVEL, _msg)
255
- await run_in_threadpool(
256
- _logger.bind(http_info=_http_info).log,
257
- _LEVEL,
258
- _msg,
259
- )
260
- ## Http access log
261
-
262
- return response