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.
@@ -1,8 +1,7 @@
1
- # -*- coding: utf-8 -*-
2
-
3
1
  import time
4
2
  from uuid import uuid4
5
- from typing import Callable, Dict, Any
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: Dict[str, Any] = {}
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: Dict[str, Any] = request.state.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
- ## Set request_id to request state:
44
+ # Set request_id to request state:
46
45
  request.state.request_id = _http_info["request_id"]
47
46
 
48
- _http_info["client_host"] = request.client.host
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
- ] = f"{_http_info['request_host']}:{request.url.port}"
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
- _http_info["request_port"] = int(
81
- request.headers.get("X-Forwarded-Port")
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, should be parseable to <int>!"
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
- ## Set http info to request state:
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: Dict[str, Any] = {}
178
+ _http_info: dict[str, Any] = {}
176
179
  _start_time: int = time.perf_counter_ns()
177
- ## Process request:
180
+ # Process request:
178
181
  response: Response = await call_next(request)
179
- ## Response processed.
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: Dict[str, Any] = request.state.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
- _http_info["response_time"] = float(
192
- response.headers.get("X-Process-Time")
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, should be parseable to <float>!"
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
- _http_info["content_length"] = int(
212
- response.headers.get("Content-Length")
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, should be parseable to <int>!"
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__ = ["RequestHTTPInfoMiddleware", "ResponseHTTPInfoMiddleware"]
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
+ [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit)
63
+ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/bybatkhuu/module-fastapi-logging/2.build-publish.yml?logo=GitHub)](https://github.com/bybatkhuu/module-fastapi-logging/actions/workflows/2.build-publish.yml)
64
+ [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/bybatkhuu/module-fastapi-logging?logo=GitHub&color=blue)](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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.42.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 Batkhuu Byambajav
3
+ Copyright (c) 2025 Batkhuu Byambajav
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal