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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
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.
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<8.0.0,>=7.1.0
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
  [![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
65
  [![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
 
66
- This is a middleware for FastAPI HTTP access logs. It is based on **'beans-logging'** package.
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
- default.all.file_handler:
184
+ http.access.file_handler:
167
185
  enabled: true
168
- default.err.file_handler:
186
+ sink: "http/{app_name}.http-access.log"
187
+ http.err.file_handler:
169
188
  enabled: true
170
- default.all.json_handler:
189
+ sink: "http/{app_name}.http-err.log"
190
+ http.access.json_handler:
171
191
  enabled: true
172
- default.err.json_handler:
192
+ sink: "http.json/{app_name}.http-access.json.log"
193
+ http.err.json_handler:
173
194
  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"
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
- [**`logger.py`**](./examples/logger.py):
205
+ [**`config.py`**](./examples/config.py):
195
206
 
196
207
  ```python
197
- from typing import TYPE_CHECKING
208
+ import os
198
209
 
199
- if TYPE_CHECKING:
200
- from loguru import Record
210
+ from pydantic_settings import BaseSettings
201
211
 
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
- )
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
- 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
- )
226
+ config = MainConfig(**_config_data)
236
227
 
237
228
 
238
229
  __all__ = [
239
- "logger",
240
- "logger_loader",
230
+ "MainConfig",
231
+ "config",
241
232
  ]
242
233
  ```
243
234
 
244
- [**`main.py`**](./examples/main.py):
235
+ [**`logger.py`**](./examples/logger.py):
245
236
 
246
237
  ```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__}")
238
+ from beans_logging_fastapi import logger
274
239
 
275
- yield
276
- logger.info("Praparing to shutdown...")
277
- logger.success("Finished preparation to shutdown.")
240
+ __all__ = [
241
+ "logger",
242
+ ]
243
+ ```
278
244
 
245
+ [**`router.py`**](./examples/router.py):
279
246
 
280
- app = FastAPI(lifespan=lifespan, version=__version__)
247
+ ```python
248
+ from pydantic import validate_call
249
+ from fastapi import FastAPI, APIRouter, HTTPException
250
+ from fastapi.responses import RedirectResponse
281
251
 
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
- )
252
+ router = APIRouter()
291
253
 
292
254
 
293
- @app.get("/")
255
+ @router.get("/")
294
256
  def root():
295
257
  return {"Hello": "World"}
296
258
 
297
259
 
298
- @app.get("/items/{item_id}")
299
- def read_item(item_id: int, q: Union[str, None] = None):
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
- @app.get("/continue", status_code=100)
265
+ @router.get("/continue", status_code=100)
304
266
  def get_continue():
305
267
  return {}
306
268
 
307
269
 
308
- @app.get("/redirect")
270
+ @router.get("/redirect")
309
271
  def redirect():
310
272
  return RedirectResponse("/")
311
273
 
312
274
 
313
- @app.get("/error")
275
+ @router.get("/error")
314
276
  def error():
315
277
  raise HTTPException(status_code=500)
316
278
 
317
279
 
318
- if __name__ == "__main__":
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="main: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=True,
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
- [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]
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: "app"
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
- custom_serialize: false
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: ["uvicorn.access"]
470
+ mute_modules: [uvicorn.access]
390
471
  handlers:
391
- default.all.std_handler:
392
- type: STD
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
- default.all.file_handler:
397
- type: FILE
479
+ h_type: FILE
398
480
  sink: "{app_name}.all.log"
481
+ err_file_handler:
399
482
  enabled: true
400
- default.err.file_handler:
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
- default.all.json_handler:
406
- type: FILE
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
- default.err.json_handler:
411
- type: FILE
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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
- ]