diff --git a/README.md b/README.md index 1744cf6..96e3c2a 100644 --- a/README.md +++ b/README.md @@ -218,22 +218,45 @@ ZeroDivisionError: division by zero logger: app_name: "fastapi-app" level: "TRACE" + use_diagnose: false + stream: + use_color: true + use_icon: false + format_str: "[{time:YYYY-MM-DD HH:mm:ss.SSS Z} | {level_short:<5} | {name}:{line}]: {message}" + std_handler: + enabled: true file: + logs_dir: "./logs" + rotate_size: 10000000 # 10MB + rotate_time: "00:00:00" + backup_count: 90 log_handlers: enabled: true + format_str: "[{time:YYYY-MM-DD HH:mm:ss.SSS Z} | {level_short:<5} | {name}:{line}]: {message}" + log_path: "{app_name}.std.all.log" + err_path: "{app_name}.std.err.log" json_handlers: enabled: true + use_custom: false log_path: "json/{app_name}.json.all.log" err_path: "json/{app_name}.json.err.log" intercept: + auto_load: + enabled: true + only_base: false + ignore_modules: [] + include_modules: [] mute_modules: ["uvicorn.access", "uvicorn.error"] extra: - http_std_msg_format: '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}" {status_code} {content_length}B {response_time}ms' http_std_debug_format: '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}"' + http_std_msg_format: '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}" {status_code} {content_length}B {response_time}ms' + http_file_enabled: true + http_file_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}' http_log_path: "http/{app_name}.http.access.log" http_err_path: "http/{app_name}.http.err.log" - http_json_path: "http.json/{app_name}.json.http.access.log" - http_json_err_path: "http.json/{app_name}.json.http.err.log" + http_json_enabled: true + http_json_path: "json.http/{app_name}.json.http.access.log" + http_json_err_path: "json.http/{app_name}.json.http.err.log" ``` [**`.env`**](https://github.com/bybatkhuu/module.python-logging/blob/main/examples/advanced/.env): @@ -246,32 +269,37 @@ DEBUG=true [**`logger.py`**](https://github.com/bybatkhuu/module.python-logging/blob/main/examples/advanced/logger.py): ```python -from beans_logging import LoggerLoader, Logger -from beans_logging.fastapi import add_file_http_handler, add_file_json_http_handler +from beans_logging import Logger, LoggerLoader +from beans_logging.fastapi import add_http_file_handler, add_http_file_json_handler logger_loader = LoggerLoader() logger: Logger = logger_loader.load() -add_file_http_handler( - logger_loader=logger_loader, - log_path=logger_loader.config.extra.http_log_path, - err_path=logger_loader.config.extra.http_err_path, -) -add_file_json_http_handler( - logger_loader=logger_loader, - log_path=logger_loader.config.extra.http_json_path, - err_path=logger_loader.config.extra.http_json_err_path, -) +if logger_loader.config.extra.http_file_enabled: + add_http_file_handler( + logger_loader=logger_loader, + log_path=logger_loader.config.extra.http_log_path, + err_path=logger_loader.config.extra.http_err_path, + ) + +if logger_loader.config.extra.http_json_enabled: + add_http_file_json_handler( + logger_loader=logger_loader, + log_path=logger_loader.config.extra.http_json_path, + err_path=logger_loader.config.extra.http_json_err_path, + ) ``` [**`app.py`**](https://github.com/bybatkhuu/module.python-logging/blob/main/examples/advanced/app.py): ```python +from typing import Union from contextlib import asynccontextmanager from dotenv import load_dotenv -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException +from fastapi.responses import RedirectResponse load_dotenv() @@ -291,16 +319,15 @@ async def lifespan(app: FastAPI): logger.info("Praparing to shutdown...") logger.success("Finished preparation to shutdown.") - app = FastAPI(lifespan=lifespan, version=__version__) app.add_middleware( HttpAccessLogMiddleware, has_proxy_headers=True, - msg_format=logger_loader.config.extra.http_std_msg_format, debug_format=logger_loader.config.extra.http_std_debug_format, + msg_format=logger_loader.config.extra.http_std_msg_format, + file_msg_format=logger_loader.config.extra.http_file_msg_format, ) - @app.get("/") def root(): return {"Hello": "World"} @@ -319,22 +346,22 @@ uvicorn app:app --host=0.0.0.0 --port=8000 **Output**: ```txt -[2023-09-01 19:04:44.342 +09:00 | TRACE | beans_logging._base:478]: Intercepted modules: ['watchfiles.watcher', 'asyncio', 'uvicorn', 'watchfiles', 'dotenv.main', 'watchfiles.main', 'fastapi', 'concurrent.futures', 'dotenv', 'concurrent']; Muted modules: ['uvicorn.access', 'uvicorn.error']; -[2023-09-01 19:04:44.360 +09:00 | INFO | uvicorn.server:76]: Started server process [50837] -[2023-09-01 19:04:44.360 +09:00 | INFO | uvicorn.lifespan.on:46]: Waiting for application startup. -[2023-09-01 19:04:44.360 +09:00 | INFO | app:21]: Preparing to startup... -[2023-09-01 19:04:44.361 +09:00 | OK | app:22]: Finished preparation to startup. -[2023-09-01 19:04:44.361 +09:00 | INFO | app:23]: API version: 0.0.1-000000 -[2023-09-01 19:04:44.361 +09:00 | INFO | uvicorn.lifespan.on:60]: Application startup complete. -[2023-09-01 19:04:44.363 +09:00 | INFO | uvicorn.server:218]: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) -[2023-09-01 19:04:46.594 +09:00 | DEBUG | beans_logging.fastapi._middleware:158]: [bcf605800a6d432d8d5021cfea8efd5f] 192.168.1.10 - "GET / HTTP/1.1" -[2023-09-01 19:04:46.596 +09:00 | OK | beans_logging.fastapi._middleware:221]: [bcf605800a6d432d8d5021cfea8efd5f] 192.168.1.10 - "GET / HTTP/1.1" 200 17B 1.1ms -^C[2023-09-01 19:04:48.017 +09:00 | INFO | uvicorn.server:264]: Shutting down -[2023-09-01 19:04:48.121 +09:00 | INFO | uvicorn.lifespan.on:65]: Waiting for application shutdown. -[2023-09-01 19:04:48.125 +09:00 | INFO | app:26]: Praparing to shutdown... -[2023-09-01 19:04:48.126 +09:00 | OK | app:27]: Finished preparation to shutdown. -[2023-09-01 19:04:48.127 +09:00 | INFO | uvicorn.lifespan.on:76]: Application shutdown complete. -[2023-09-01 19:04:48.128 +09:00 | INFO | uvicorn.server:86]: Finished server process [50837] +[2023-09-01 12:37:38.569 +09:00 | TRACE | beans_logging._base:499]: Intercepted modules: ['watchfiles.watcher', 'asyncio', 'watchfiles', 'concurrent', 'dotenv', 'concurrent.futures', 'fastapi', 'dotenv.main', 'uvicorn', 'watchfiles.main']; Muted modules: ['uvicorn.error', 'uvicorn.access']; +[2023-09-01 12:37:38.579 +09:00 | INFO | uvicorn.server:76]: Started server process [22599] +[2023-09-01 12:37:38.579 +09:00 | INFO | uvicorn.lifespan.on:46]: Waiting for application startup. +[2023-09-01 12:37:38.579 +09:00 | INFO | app:21]: Preparing to startup... +[2023-09-01 12:37:38.580 +09:00 | OK | app:22]: Finished preparation to startup. +[2023-09-01 12:37:38.580 +09:00 | INFO | app:23]: API version: 0.0.1-000000 +[2023-09-01 12:37:38.580 +09:00 | INFO | uvicorn.lifespan.on:60]: Application startup complete. +[2023-09-01 12:37:38.582 +09:00 | INFO | uvicorn.server:218]: Uvicorn running on http://0.0.0.0:9000 (Press CTRL+C to quit) +[2023-09-01 12:37:48.487 +09:00 | DEBUG | anyio._backends._asyncio:807]: [0b9f972939054a58ba10e7a39a12bd21] 127.0.0.1 - "GET / HTTP/1.1" +[2023-09-01 12:37:48.488 +09:00 | OK | anyio._backends._asyncio:807]: [0b9f972939054a58ba10e7a39a12bd21] 127.0.0.1 - "GET / HTTP/1.1" 200 17B 0.5ms +^C[2023-09-01 12:37:51.845 +09:00 | INFO | uvicorn.server:264]: Shutting down +[2023-09-01 12:37:51.949 +09:00 | INFO | uvicorn.lifespan.on:65]: Waiting for application shutdown. +[2023-09-01 12:37:51.951 +09:00 | INFO | app:26]: Praparing to shutdown... +[2023-09-01 12:37:51.952 +09:00 | OK | app:27]: Finished preparation to shutdown. +[2023-09-01 12:37:51.952 +09:00 | INFO | uvicorn.lifespan.on:76]: Application shutdown complete. +[2023-09-01 12:37:51.953 +09:00 | INFO | uvicorn.server:86]: Finished server process [22599] ``` --- diff --git a/beans_logging/__init__.py b/beans_logging/__init__.py index cc839be..81c7894 100644 --- a/beans_logging/__init__.py +++ b/beans_logging/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from .schema import LoggerConfigPM +from .schemas import LoggerConfigPM from ._base import Logger, logger, LoggerLoader from .__version__ import __version__ diff --git a/beans_logging/_base.py b/beans_logging/_base.py index 564261a..c364126 100644 --- a/beans_logging/_base.py +++ b/beans_logging/_base.py @@ -5,7 +5,7 @@ import copy import json import logging -from typing import Union +from typing import Union, Dict, Any ## Third-party libraries import yaml @@ -15,12 +15,12 @@ ## Internal modules from ._utils import create_dir, deep_merge -from ._handler import InterceptHandler +from ._handlers import InterceptHandler from .rotation import RotationChecker -from .schema import LoggerConfigPM -from .sink import std_sink -from .format import json_format -from .filter import ( +from .schemas import LoggerConfigPM +from .sinks import std_sink +from .formats import json_format +from .filters import ( use_all_filter, use_std_filter, use_file_filter, @@ -36,9 +36,9 @@ class LoggerLoader: Attributes: _CONFIG_FILE_PATH (str ): Default logger config file path. Defaults to '${PWD}/configs/logger.yml'. - handlers_map (dict ): Registered logger handlers map as dictionary. Defaults to None. - config (LoggerConfigPM): Logger config as . Defaults to None. - config_file_path (str ): Logger config file path. Defaults to `LoggerLoader._CONFIG_FILE_PATH`. + handlers_map (dict ): Registered logger handlers map as dictionary. Defaults to None. + config (LoggerConfigPM): Logger config as . Defaults to None. + config_file_path (str ): Logger config file path. Defaults to `LoggerLoader._CONFIG_FILE_PATH`. Methods: load() : Load logger handlers based on logger config. @@ -61,7 +61,7 @@ class LoggerLoader: @validate_call def __init__( self, - config: Union[LoggerConfigPM, dict, None] = None, + config: Union[LoggerConfigPM, Dict[str, Any], None] = None, config_file_path: str = _CONFIG_FILE_PATH, auto_config_file: bool = True, auto_load: bool = False, @@ -79,6 +79,8 @@ def __init__( self.handlers_map = {"default": 0} self.config = LoggerConfigPM() + if config: + self.update_config(config=config) self.config_file_path = config_file_path self._load_env_vars() @@ -86,9 +88,6 @@ def __init__( if auto_config_file: self._load_config_file() - if config: - self.update_config(config=config) - if auto_load: self.load() @@ -156,7 +155,7 @@ def remove_handler( self.handlers_map.clear() @validate_call - def update_config(self, config: Union[LoggerConfigPM, dict]): + def update_config(self, config: Union[LoggerConfigPM, Dict[str, Any]]): """Update logger config with new config. Args: @@ -166,18 +165,20 @@ def update_config(self, config: Union[LoggerConfigPM, dict]): Exception: Failed to load `config` argument into . """ - try: - if isinstance(config, dict): - _config_dict = self.config.model_dump() - _merged_dict = deep_merge(_config_dict, config) + if isinstance(config, dict): + _config_dict = self.config.model_dump() + _merged_dict = deep_merge(_config_dict, config) + print(_merged_dict) + try: self.config = LoggerConfigPM(**_merged_dict) - elif isinstance(config, LoggerConfigPM): - self.config = config - except Exception: - logger.critical( - "Failed to load `config` argument into ." - ) - raise + except Exception: + logger.critical( + "Failed to load `config` argument into ." + ) + raise + + elif isinstance(config, LoggerConfigPM): + self.config = config def _load_env_vars(self): """Load 'BEANS_LOGGING_CONFIG_PATH' environment variable for logger config file path.""" @@ -436,7 +437,7 @@ def add_custom_handler(self, handler_name: str, **kwargs) -> int: _log_path = _log_path.format(app_name=self.config.app_name) _logs_dir, _ = os.path.split(_log_path) - create_dir(create_dir=_logs_dir) + create_dir(create_dir=_logs_dir, warn_mode="DEBUG") kwargs["sink"] = _log_path if "format" not in kwargs: @@ -516,7 +517,7 @@ def _load_intercept_handlers(self): ### ATTRIBUTES ### ## handlers_map ## @property - def handlers_map(self) -> Union[dict, None]: + def handlers_map(self) -> Dict[str, int]: try: return self.__handlers_map except AttributeError: @@ -525,7 +526,7 @@ def handlers_map(self) -> Union[dict, None]: return self.__handlers_map @handlers_map.setter - def handlers_map(self, handlers_map: dict): + def handlers_map(self, handlers_map: Dict[str, int]): if not isinstance(handlers_map, dict): raise TypeError( f"`handlers_map` attribute type {type(handlers_map)} is invalid, must be !." diff --git a/beans_logging/_handler.py b/beans_logging/_handlers.py similarity index 100% rename from beans_logging/_handler.py rename to beans_logging/_handlers.py diff --git a/beans_logging/_utils.py b/beans_logging/_utils.py index 75c1cee..21d37f7 100644 --- a/beans_logging/_utils.py +++ b/beans_logging/_utils.py @@ -13,20 +13,22 @@ @validate_call -def create_dir(create_dir: str, quiet: bool = True): +def create_dir(create_dir: str, warn_mode: str = "DEBUG"): """Create directory if `create_dir` doesn't exist. Args: - create_dir (str , required): Create directory path. - quiet (bool, optional): If True, don't log anything unless debug is enabled. Defaults to True. + create_dir (str, required): Create directory path. + warn_mode (str, optional): Warning message mode, for example: 'LOG', 'DEBUG', 'QUIET'. Defaults to "QUIET". """ + warn_mode = warn_mode.strip().upper() if not os.path.isdir(create_dir): try: - if quiet: - logger.debug(f"Creaing '{create_dir}' directory...") - else: - logger.info(f"Creaing '{create_dir}' directory...") + _message = f"Creaing '{create_dir}' directory..." + if warn_mode == "LOG": + logger.info(_message) + elif warn_mode == "DEBUG": + logger.debug(_message) os.makedirs(create_dir) except OSError as err: @@ -36,10 +38,11 @@ def create_dir(create_dir: str, quiet: bool = True): logger.error(f"Failed to create '{create_dir}' directory!") raise - if quiet: - logger.debug(f"Successfully created '{create_dir}' directory.") - else: - logger.success(f"Successfully created '{create_dir}' directory.") + _message = f"Successfully created '{create_dir}' directory." + if warn_mode == "LOG": + logger.success(_message) + elif warn_mode == "DEBUG": + logger.debug(_message) @validate_call diff --git a/beans_logging/fastapi/__init__.py b/beans_logging/fastapi/__init__.py index 3682d42..8ecb7e2 100644 --- a/beans_logging/fastapi/__init__.py +++ b/beans_logging/fastapi/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from ._middleware import HttpAccessLogMiddleware -from ._handler import add_file_http_handler, add_file_json_http_handler -from ._filter import use_http_filter -from ._format import file_http_format, file_json_http_format +from ._middlewares import HttpAccessLogMiddleware +from ._handlers import add_http_file_handler, add_http_file_json_handler +from ._filters import use_http_filter +from ._formats import http_file_format, http_file_json_format diff --git a/beans_logging/fastapi/_filter.py b/beans_logging/fastapi/_filters.py similarity index 74% rename from beans_logging/fastapi/_filter.py rename to beans_logging/fastapi/_filters.py index 8375962..42f9c3d 100644 --- a/beans_logging/fastapi/_filter.py +++ b/beans_logging/fastapi/_filters.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from beans_logging.filter import use_all_filter +from beans_logging.filters import use_all_filter def use_http_filter(record: dict) -> bool: @@ -16,7 +16,7 @@ def use_http_filter(record: dict) -> bool: if not use_all_filter(record): return False - if "http_info" in record["extra"]: - return True + if "http_info" not in record["extra"]: + return False - return False + return True diff --git a/beans_logging/fastapi/_format.py b/beans_logging/fastapi/_formats.py similarity index 71% rename from beans_logging/fastapi/_format.py rename to beans_logging/fastapi/_formats.py index 28fa748..b8c8b01 100644 --- a/beans_logging/fastapi/_format.py +++ b/beans_logging/fastapi/_formats.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -def file_http_format(record: dict) -> str: +def http_file_format(record: dict) -> str: """Http access log file format. Args: @@ -11,6 +11,8 @@ def file_http_format(record: dict) -> str: str: Format for http access log record. """ + _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}' + if "http_info" not in record["extra"]: return "" @@ -20,14 +22,17 @@ def file_http_format(record: dict) -> str: _http_info["datetime"] = record["time"].isoformat() record["extra"]["http_info"] = _http_info - _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}' + _msg_format = _MSG_FORMAT + if "http_file_msg_format" in record["extra"]: + _msg_format = record["extra"]["http_file_msg_format"] + _msg = _msg_format.format(**_http_info) record["http_message"] = _msg return "{http_message}\n" -def file_json_http_format(record: dict) -> str: +def http_file_json_format(record: dict) -> str: """Http access json log file format. Args: diff --git a/beans_logging/fastapi/_handler.py b/beans_logging/fastapi/_handlers.py similarity index 80% rename from beans_logging/fastapi/_handler.py rename to beans_logging/fastapi/_handlers.py index f30d45c..2fc9fb4 100644 --- a/beans_logging/fastapi/_handler.py +++ b/beans_logging/fastapi/_handlers.py @@ -4,12 +4,12 @@ from beans_logging import LoggerLoader -from ._filter import use_http_filter -from ._format import file_http_format, file_json_http_format +from ._filters import use_http_filter +from ._formats import http_file_format, http_file_json_format @validate_call(config=dict(arbitrary_types_allowed=True)) -def add_file_http_handler( +def add_http_file_handler( logger_loader: LoggerLoader, log_path: str = "http/{app_name}.http.access.log", err_path: str = "http/{app_name}.http.err.log", @@ -26,7 +26,7 @@ def add_file_http_handler( handler_name="FILE.HTTP", sink=log_path, filter=use_http_filter, - format=file_http_format, + format=http_file_format, ) logger_loader.add_custom_handler( @@ -34,15 +34,15 @@ def add_file_http_handler( sink=err_path, level="WARNING", filter=use_http_filter, - format=file_http_format, + format=http_file_format, ) @validate_call(config=dict(arbitrary_types_allowed=True)) -def add_file_json_http_handler( +def add_http_file_json_handler( logger_loader: LoggerLoader, - log_path: str = "http.json/{app_name}.json.http.access.log", - err_path: str = "http.json/{app_name}.json.http.err.log", + log_path: str = "json.http/{app_name}.json.http.access.log", + err_path: str = "json.http/{app_name}.json.http.err.log", ): """Add http access json log file and json error file handler. @@ -56,7 +56,7 @@ def add_file_json_http_handler( handler_name="FILE.JSON.HTTP", sink=log_path, filter=use_http_filter, - format=file_json_http_format, + format=http_file_json_format, ) logger_loader.add_custom_handler( @@ -64,5 +64,5 @@ def add_file_json_http_handler( sink=err_path, level="WARNING", filter=use_http_filter, - format=file_json_http_format, + format=http_file_json_format, ) diff --git a/beans_logging/fastapi/_middleware.py b/beans_logging/fastapi/_middlewares.py similarity index 83% rename from beans_logging/fastapi/_middleware.py rename to beans_logging/fastapi/_middlewares.py index ecfc2a8..2f686d2 100644 --- a/beans_logging/fastapi/_middleware.py +++ b/beans_logging/fastapi/_middlewares.py @@ -18,25 +18,36 @@ class HttpAccessLogMiddleware(BaseHTTPMiddleware): BaseHTTPMiddleware: Base HTTP middleware class from starlette. Attributes: + _DEBUG_FORMAT (str ): Default http access log debug message format. Defaults to '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}"'. + _MSG_FORMAT (str ): Default http access log message format. Defaults to '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}" {status_code} {content_length}B {response_time}ms'. + _FILE_MSG_FORMAT (str ): Default http access log file message format. Defaults to '{client_host} {request_id} {user_id} [{datetime}] "{method} {url_path} HTTP/{http_version}" {status_code} {content_length} "{h_referer}" "{h_user_agent}" {response_time}'. + has_proxy_headers (bool): If True, use proxy headers to get http request info. Defaults to False. has_cf_headers (bool): If True, use cloudflare headers to get http request info. Defaults to False. - msg_format (str ): Http access log message format. Defaults to '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}" {status_code} {content_length}B {response_time}ms'. - debug_format (str ): Http access log debug message format. Defaults to '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}"'. + debug_format (str ): Http access log debug message format. Defaults to `HttpAccessLogMiddleware._DEBUG_FORMAT`. + msg_format (str ): Http access log message format. Defaults to `HttpAccessLogMiddleware._MSG_FORMAT`. + file_msg_format (str ): Http access log file message format. Defaults to `HttpAccessLogMiddleware._FILE_MSG_FORMAT`. """ + _DEBUG_FORMAT = '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}"' + _MSG_FORMAT = '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}" {status_code} {content_length}B {response_time}ms' + _FILE_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}' + def __init__( self, app, has_proxy_headers: bool = False, has_cf_headers: bool = False, - msg_format: str = '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}" {status_code} {content_length}B {response_time}ms', - debug_format: str = '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}"', + debug_format: str = _DEBUG_FORMAT, + msg_format: str = _MSG_FORMAT, + file_msg_format: str = _FILE_MSG_FORMAT, ): super().__init__(app) self.has_proxy_headers = has_proxy_headers self.has_cf_headers = has_cf_headers - self.msg_format = msg_format self.debug_format = debug_format + self.msg_format = msg_format + self.file_msg_format = file_msg_format async def dispatch(self, request: Request, call_next) -> Response: _logger = logger.opt(colors=True, record=True) @@ -157,7 +168,10 @@ async def dispatch(self, request: Request, call_next) -> Response: _debug_msg = self.debug_format.format(**_http_info) # _logger.debug(_debug_msg) - await run_in_threadpool(_logger.debug, _debug_msg) + await run_in_threadpool( + _logger.debug, + _debug_msg, + ) _start_time = time.time() response = await call_next(request) @@ -221,6 +235,13 @@ async def dispatch(self, request: Request, call_next) -> Response: _msg = _msg_format.format(**_http_info) # _logger.bind(http_info=_http_info).log(_LEVEL, _msg) - await run_in_threadpool(_logger.bind(http_info=_http_info).log, _LEVEL, _msg) + await run_in_threadpool( + _logger.bind( + http_info=_http_info, + http_file_msg_format=self.file_msg_format, + ).log, + _LEVEL, + _msg, + ) return response diff --git a/beans_logging/filter.py b/beans_logging/filters.py similarity index 100% rename from beans_logging/filter.py rename to beans_logging/filters.py diff --git a/beans_logging/format.py b/beans_logging/formats.py similarity index 100% rename from beans_logging/format.py rename to beans_logging/formats.py diff --git a/beans_logging/schema.py b/beans_logging/schemas.py similarity index 66% rename from beans_logging/schema.py rename to beans_logging/schemas.py index f77777c..9127ea1 100644 --- a/beans_logging/schema.py +++ b/beans_logging/schemas.py @@ -16,6 +16,10 @@ ) +class ExtraBaseModel(BaseModel): + model_config = ConfigDict(extra="allow") + + class LevelEnum(str, Enum): TRACE = "TRACE" DEBUG = "DEBUG" @@ -26,35 +30,33 @@ class LevelEnum(str, Enum): CRITICAL = "CRITICAL" -class StdHandlerPM(BaseModel): +class StdHandlerPM(ExtraBaseModel): enabled: bool = Field(default=True) - model_config = ConfigDict(extra="allow") - -class StreamPM(BaseModel): +class StreamPM(ExtraBaseModel): use_color: bool = Field(default=True) use_icon: bool = Field(default=False) format_str: constr(strip_whitespace=True) = Field( default="[{time:YYYY-MM-DD HH:mm:ss.SSS Z} | {level_short:<5} | {name}:{line}]: {message}", - min_length=9, + min_length=3, + max_length=511, ) std_handler: StdHandlerPM = Field(default=StdHandlerPM()) - model_config = ConfigDict(extra="allow") - -class LogHandlersPM(BaseModel): +class LogHandlersPM(ExtraBaseModel): enabled: bool = Field(default=False) format_str: constr(strip_whitespace=True) = Field( default="[{time:YYYY-MM-DD HH:mm:ss.SSS Z} | {level_short:<5} | {name}:{line}]: {message}", - min_length=9, + min_length=4, + max_length=511, ) log_path: constr(strip_whitespace=True) = Field( - default="{app_name}.std.all.log", min_length=5, max_length=255 + default="{app_name}.std.all.log", min_length=4, max_length=4095 ) err_path: constr(strip_whitespace=True) = Field( - default="{app_name}.std.err.log", min_length=5, max_length=255 + default="{app_name}.std.err.log", min_length=4, max_length=4095 ) @model_validator(mode="after") @@ -66,17 +68,15 @@ def _check_log_path(self) -> "LogHandlersPM": return self - model_config = ConfigDict(extra="allow") - -class JsonHandlersPM(BaseModel): +class JsonHandlersPM(ExtraBaseModel): enabled: bool = Field(default=False) use_custom: bool = Field(default=False) log_path: constr(strip_whitespace=True) = Field( - default="{app_name}.json.all.log", min_length=5, max_length=255 + default="{app_name}.json.all.log", min_length=4, max_length=4095 ) err_path: constr(strip_whitespace=True) = Field( - default="{app_name}.json.err.log", min_length=5, max_length=255 + default="{app_name}.json.err.log", min_length=4, max_length=4095 ) @model_validator(mode="after") @@ -88,12 +88,10 @@ def _check_log_path(self) -> "JsonHandlersPM": return self - model_config = ConfigDict(extra="allow") - -class FilePM(BaseModel): +class FilePM(ExtraBaseModel): logs_dir: str = Field( - default=os.path.join(os.getcwd(), "logs"), min_length=2, max_length=4096 + default=os.path.join(os.getcwd(), "logs"), min_length=2, max_length=4095 ) rotate_size: int = Field( default=10_000_000, ge=1_000, lt=1_000_000_000 # 10MB = 10 * 1000 * 1000 @@ -113,30 +111,24 @@ def _check_rotate_time(cls, val): val = datetime.time.fromisoformat(val) return val - model_config = ConfigDict(extra="allow") - -class AutoLoadPM(BaseModel): +class AutoLoadPM(ExtraBaseModel): enabled: bool = Field(default=True) only_base: bool = Field(default=False) ignore_modules: List[str] = Field(default=[]) - model_config = ConfigDict(extra="allow") - -class InterceptPM(BaseModel): +class InterceptPM(ExtraBaseModel): auto_load: AutoLoadPM = Field(default=AutoLoadPM()) include_modules: List[str] = Field(default=[]) mute_modules: List[str] = Field(default=[]) - model_config = ConfigDict(extra="allow") - -class ExtraPM(BaseModel): - model_config = ConfigDict(extra="allow") +class ExtraPM(ExtraBaseModel): + pass -class LoggerConfigPM(BaseModel): +class LoggerConfigPM(ExtraBaseModel): app_name: constr(strip_whitespace=True) = Field( default=os.path.splitext(os.path.basename(sys.argv[0]))[0] .strip() @@ -153,28 +145,26 @@ class LoggerConfigPM(BaseModel): intercept: InterceptPM = Field(default=InterceptPM()) extra: ExtraPM = Field(default=ExtraPM()) - @model_validator(mode="after") - def _check_log_path(self) -> "LoggerConfigPM": - if "{app_name}" in self.file.log_handlers.log_path: - self.file.log_handlers.log_path = self.file.log_handlers.log_path.format( - app_name=self.app_name - ) - - if "{app_name}" in self.file.log_handlers.err_path: - self.file.log_handlers.err_path = self.file.log_handlers.err_path.format( - app_name=self.app_name - ) - - if "{app_name}" in self.file.json_handlers.log_path: - self.file.json_handlers.log_path = self.file.json_handlers.log_path.format( - app_name=self.app_name - ) - - if "{app_name}" in self.file.json_handlers.err_path: - self.file.json_handlers.err_path = self.file.json_handlers.err_path.format( - app_name=self.app_name - ) - - return self - - model_config = ConfigDict(extra="allow") + # @model_validator(mode="after") + # def _check_log_path(self) -> "LoggerConfigPM": + # if "{app_name}" in self.file.log_handlers.log_path: + # self.file.log_handlers.log_path = self.file.log_handlers.log_path.format( + # app_name=self.app_name + # ) + + # if "{app_name}" in self.file.log_handlers.err_path: + # self.file.log_handlers.err_path = self.file.log_handlers.err_path.format( + # app_name=self.app_name + # ) + + # if "{app_name}" in self.file.json_handlers.log_path: + # self.file.json_handlers.log_path = self.file.json_handlers.log_path.format( + # app_name=self.app_name + # ) + + # if "{app_name}" in self.file.json_handlers.err_path: + # self.file.json_handlers.err_path = self.file.json_handlers.err_path.format( + # app_name=self.app_name + # ) + + # return self diff --git a/beans_logging/sink.py b/beans_logging/sinks.py similarity index 100% rename from beans_logging/sink.py rename to beans_logging/sinks.py diff --git a/examples/advanced/app.py b/examples/advanced/app.py index 3e65811..e9269d8 100755 --- a/examples/advanced/app.py +++ b/examples/advanced/app.py @@ -31,8 +31,9 @@ async def lifespan(app: FastAPI): app.add_middleware( HttpAccessLogMiddleware, has_proxy_headers=True, - msg_format=logger_loader.config.extra.http_std_msg_format, debug_format=logger_loader.config.extra.http_std_debug_format, + msg_format=logger_loader.config.extra.http_std_msg_format, + file_msg_format=logger_loader.config.extra.http_file_msg_format, ) diff --git a/examples/advanced/configs/logger.yml b/examples/advanced/configs/logger.yml index e53bcaf..40b94fb 100644 --- a/examples/advanced/configs/logger.yml +++ b/examples/advanced/configs/logger.yml @@ -1,19 +1,42 @@ logger: app_name: "fastapi-app" level: "TRACE" + use_diagnose: false + stream: + use_color: true + use_icon: false + format_str: "[{time:YYYY-MM-DD HH:mm:ss.SSS Z} | {level_short:<5} | {name}:{line}]: {message}" + std_handler: + enabled: true file: + logs_dir: "./logs" + rotate_size: 10000000 # 10MB + rotate_time: "00:00:00" + backup_count: 90 log_handlers: enabled: true + format_str: "[{time:YYYY-MM-DD HH:mm:ss.SSS Z} | {level_short:<5} | {name}:{line}]: {message}" + log_path: "{app_name}.std.all.log" + err_path: "{app_name}.std.err.log" json_handlers: enabled: true + use_custom: false log_path: "json/{app_name}.json.all.log" err_path: "json/{app_name}.json.err.log" intercept: + auto_load: + enabled: true + only_base: false + ignore_modules: [] + include_modules: [] mute_modules: ["uvicorn.access", "uvicorn.error"] extra: - http_std_msg_format: '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}" {status_code} {content_length}B {response_time}ms' http_std_debug_format: '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}"' + http_std_msg_format: '[{request_id}] {client_host} {user_id} "{method} {url_path} HTTP/{http_version}" {status_code} {content_length}B {response_time}ms' + http_file_enabled: true + http_file_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}' http_log_path: "http/{app_name}.http.access.log" http_err_path: "http/{app_name}.http.err.log" - http_json_path: "http.json/{app_name}.json.http.access.log" - http_json_err_path: "http.json/{app_name}.json.http.err.log" + http_json_enabled: true + http_json_path: "json.http/{app_name}.json.http.access.log" + http_json_err_path: "json.http/{app_name}.json.http.err.log" diff --git a/examples/advanced/logger.py b/examples/advanced/logger.py index a12f75c..a5662a3 100644 --- a/examples/advanced/logger.py +++ b/examples/advanced/logger.py @@ -1,19 +1,22 @@ # -*- coding: utf-8 -*- from beans_logging import Logger, LoggerLoader -from beans_logging.fastapi import add_file_http_handler, add_file_json_http_handler +from beans_logging.fastapi import add_http_file_handler, add_http_file_json_handler logger_loader = LoggerLoader() logger: Logger = logger_loader.load() -add_file_http_handler( - logger_loader=logger_loader, - log_path=logger_loader.config.extra.http_log_path, - err_path=logger_loader.config.extra.http_err_path, -) -add_file_json_http_handler( - logger_loader=logger_loader, - log_path=logger_loader.config.extra.http_json_path, - err_path=logger_loader.config.extra.http_json_err_path, -) +if logger_loader.config.extra.http_file_enabled: + add_http_file_handler( + logger_loader=logger_loader, + log_path=logger_loader.config.extra.http_log_path, + err_path=logger_loader.config.extra.http_err_path, + ) + +if logger_loader.config.extra.http_json_enabled: + add_http_file_json_handler( + logger_loader=logger_loader, + log_path=logger_loader.config.extra.http_json_path, + err_path=logger_loader.config.extra.http_json_err_path, + ) diff --git a/tests/test_beans_logging.py b/tests/test_beans_logging.py index 3b94faa..cf7b5c2 100644 --- a/tests/test_beans_logging.py +++ b/tests/test_beans_logging.py @@ -23,7 +23,7 @@ def logger(): del logger -def test_init(logger, logger_loader): +def test_init(logger: Logger, logger_loader: LoggerLoader): logger.info("Testing initialization of 'LoggerLoader'...") assert isinstance(logger_loader, LoggerLoader) @@ -34,7 +34,7 @@ def test_init(logger, logger_loader): logger.success("Done: Initialization of 'LoggerLoader'.\n") -def test_load(logger, logger_loader): +def test_load(logger: Logger, logger_loader: LoggerLoader): logger.info("Testing 'load' method of 'LoggerLoader'...") logger_loader.update_config(config={"level": "TRACE"}) @@ -53,7 +53,7 @@ def test_load(logger, logger_loader): logger.success("Done: 'load' method.\n") -def test_methods(logger): +def test_methods(logger: Logger): logger.info("Testing 'logger' methods...") logger.trace("Tracing...")