在FastAPI中使用日志功能,實現日志切割。
原由
日志在未實現切割以及回滾時候會將所有的日志記錄寫入同一個地方,這樣就會使日志文件特別大,如果該項目的訪問量很大,然后運行時間長了之后還有可能因為日志文件過大,造成服務器因存儲空間不足而宕機,所以需要將日志進行切割以及回滾。
實現
目錄結構
注釋:
- conf文件主要放置項目參數配置文件以及日志配置文件
- logging.ini為日志的參數配置文件
- test.ini 為項目的參數配置文件
- app主要就是項目以及一些相關設置
- api 項目的接口文件
- routers文件(使用 APIRouter是更具有層次性)
- core項目的配置文件
- defines.py中主要配置各種固定參數,或者文件名
- setting.py中為項目的配置文件
- utils項目獨立函數文件
- parser.py 常用文件或字符串解析函數
- server.py為項目的服務主入口文件
- api 項目的接口文件
- logs為日志輸出文件
- default.log 訪問日志
- error.log 錯誤日志
- main.py為項目的啟動文件
部分代碼以及注釋
-
按照時間切割
cinf/logging.ini
[loggers] keys=root,test [handlers] keys=rotatingFileHandler,streamHandler,errorHandler [formatters] keys=simpleFmt, errorFmt, consoleFmt [logger_root] level=DEBUG handlers=rotatingFileHandler,streamHandler,errorHandler [logger_test] level=DEBUG qualname=test handlers=rotatingFileHandler,streamHandler,errorHandler [handler_rotatingFileHandler] class=handlers.TimedRotatingFileHandler level=INFO formatter=simpleFmt args=(os.path.join(sys.path[0], "logs/default.log"),"M", 1, 6,'utf-8') [handler_errorHandler] class=handlers.TimedRotatingFileHandler level=ERROR formatter=errorFmt args=(os.path.join(sys.path[0], "logs/error.log"), "M", 1, 6,'utf-8') [handler_streamHandler] level=INFO class=StreamHandler formatter=consoleFmt args=(sys.stdout,) [formatter_consoleFmt] format=%(asctime)s.%(msecs)03d [%(levelname)s] [%(name)s] %(message)s [formatter_simpleFmt] format=%(asctime)s %(pathname)s(%(lineno)d): [%(levelname)s] [%(name)s] %(message)s [formatter_errorFmt] format=%(asctime)s %(pathname)s(%(lineno)d): [%(levelname)s] [%(name)s] %(message)s
args參數(filename, when, interval, backupCount, encoding) filename:日志文件的地址+文件名 when: 參數是一個字符串。表示時間間隔的單位,不區分大小寫 S 秒 M 分 H 小時 D 天 W 每星期(interval==0時代表星期一) midnight 每天凌晨 interval: 時間間隔 eg:args=(os.path.join(sys.path[0], "logs/error.log"), "M", 1, 6,'utf-8') 1分鍾切割一次
-
按照大小切割
cinf/logging.ini
[loggers] keys=root,test [handlers] keys=rotatingFileHandler,streamHandler,errorHandler [formatters] keys=simpleFmt, errorFmt, consoleFmt [logger_root] level=DEBUG handlers=rotatingFileHandler,streamHandler,errorHandler [logger_test] level=DEBUG qualname=test handlers=rotatingFileHandler,streamHandler,errorHandler [handler_rotatingFileHandler] class=handlers.RotatingFileHandler level=INFO formatter=simpleFmt args=(os.path.join(sys.path[0], "logs/default.log"), 'a', 2*1024*10, 5, 'utf-8') [handler_errorHandler] class=handlers.RotatingFileHandler level=ERROR formatter=errorFmt args=(os.path.join(sys.path[0], "logs/error.log"), 'a', 2*1024*10, 5, 'utf-8') [handler_streamHandler] level=INFO class=StreamHandler formatter=consoleFmt args=(sys.stdout,) [formatter_consoleFmt] format=%(asctime)s.%(msecs)03d [%(levelname)s] [%(name)s] %(message)s [formatter_simpleFmt] format=%(asctime)s %(pathname)s(%(lineno)d): [%(levelname)s] [%(name)s] %(message)s [formatter_errorFmt] format=%(asctime)s %(pathname)s(%(lineno)d): [%(levelname)s] [%(name)s] %(message)s
args參數(filename, mode, maxBytes, backupCount, encoding) filename:日志文件的地址+文件名 mode: 對日志文件的操作方式
maxBytes:日志文件的最大存儲
backupCount:回滾數量
encoding:編碼格式
eg:args=(os.path.join(sys.path[0], "logs/error.log"), "a", 10*2*1024, 6,'utf-8') 20KB切割一次
- 公用注釋
日志等級:使用范圍 FATAL:致命錯誤 CRITICAL:特別糟糕的事情,如內存耗盡、磁盤空間為空,一般很少使用 ERROR:發生錯誤時,如IO操作失敗或者連接問題 WARNING:發生很重要的事件,但是並不是錯誤時,如用戶登錄密碼錯誤 INFO:處理請求或者狀態變化等日常事務 DEBUG:調試過程中使用DEBUG等級,如算法中每個循環的中間狀態format參數中可能用到的格式化串: %(name)s Logger的名字 %(levelno)s 數字形式的日志級別 %(levelname)s 文本形式的日志級別 %(pathname)s 調用日志輸出函數的模塊的完整路徑名,可能沒有 %(filename)s 調用日志輸出函數的模塊的文件名 %(module)s 調用日志輸出函數的模塊名 %(funcName)s 調用日志輸出函數的函數名 %(lineno)d 調用日志輸出函數的語句所在的代碼行 %(created)f 當前時間,用UNIX標准的表示時間的浮 點數表示 %(relativeCreated)d 輸出日志信息時的,自Logger創建以 來的毫秒數 %(asctime)s 字符串形式的當前時間。默認格式是 “2003-07-08 16:49:45,896”。逗號后面的是毫秒 %(thread)d 線程ID。可能沒有 %(threadName)s 線程名。可能沒有 %(process)d 進程ID。可能沒有 %(message)s用戶輸出的消息
注解:日志切割的時候,在win下會出現文件被占用的情況,從而使用切割是會出現錯誤
在centos下就能規避該問題,從而正常的使用切割功能
conf/test.ini
[SERVER] API_PREFIX=/api SERVER_HOST=0.0.0.0 SERVER_PORT=8080 WORKERS_COUNT=4
routers/test.py
# Copyright (c) 2020—2021 Toplinker Corporation. All rights reserved. """ 用戶管理相關路由 """ from fastapi import APIRouter router = APIRouter() @router.get('/error') def error( a: int = None, b: int = None ): # if b == 0: # raise TimpHTTPException(status_code=status.HTTP_400_BAD_REQUEST,err_code=400,err_msg='b can`t be 0') return a/b
core/defines.py
# Copyright (c) 2020—2021 Toplinker Corporation. All rights reserved. """ 全局靜態變量定義 """ # 配置文件路徑環境變量名 ENV_TIMPSTACK_CONFIG_DIR = "SYCC_CONFIG_DIR" # 服務配置文件名 SERVER_CONFIG_FILENAME = "test.ini" # 日志配置文件名 LOGGING_CONFIG_FILENAME = "logging.ini"
core/setting.py
# Copyright (c) 2020—2021 Toplinker Corporation. All rights reserved. """ 管理TIMP-STACK后台服務配置信息 功能特點: * 基於pydantic進行有效性驗證. * 全局單例模式,各模塊共享配置. * 可從文件載入配置信息 """ import os from pathlib import Path from typing import List from pydantic import BaseModel, AnyHttpUrl from app.core.defines import ENV_TIMPSTACK_CONFIG_DIR, LOGGING_CONFIG_FILENAME class Settings(BaseModel): # HTTP服務的主機地址, 默認為任意地址 server_host: str = "0.0.0.0" # HTTP服務端口, 默認為8000 server_port: int = 8000 # Uvicorn進程數量 workers_count: int = 4 # 數據表前綴 table_prefix: str = "" # API路由前綴 api_prefix: str = "/api" # 跨域設置, 兼容json格式list字符串 # 如: '["http://localhost", "http://localhost:4200", "http://localhost:3000"]' backend_cors_origins: List[AnyHttpUrl] = [] # 日志配置文件 logging_config_file: str = None settings = Settings() def load_settings(): """ 初始化配置信息 由於uvicorn在開啟reload模式或開啟多個workers時會重新開啟新的進程, 需重新載入配置信息。 要想多個進程之前用的是同一套配置文件。因此通過'TIMPSTACK_CONFIG_DIR'環境變量來傳遞配置文件路徑。 """ app_root_path = Path(__file__).parent.parent.parent.absolute() if not os.getenv(ENV_TIMPSTACK_CONFIG_DIR): conf_dir = app_root_path.joinpath('conf') else: conf_dir = Path(os.getenv(ENV_TIMPSTACK_CONFIG_DIR)).absolute() # 日志配置文件路徑 settings.logging_config_file = str(conf_dir.joinpath(LOGGING_CONFIG_FILENAME))
utils/parser.py
# Copyright (c) 2020—2021 Toplinker Corporation. All rights reserved. """ Parser - 包含常用文件或字符串解析函數 功能特點: * read_ini_file - 讀取ini文件函數, 並返回為dict結構. """ from configparser import ConfigParser, ExtendedInterpolation def read_ini_file(file: str, pre_sections: dict = None, optionxform: callable = None, default_section_name: str = "DEFAULT" ) -> dict: """ 讀取ini文件, 轉化為dict結構; 並且可預傳入一系列值初始值, 用將ini文件中`%{SomeSection:somekey}` 替換為實際傳入的值。如下文件:: ``` [LOG] LOG_PATH=${APP:path}/${APP:name}/logs/log.txt ``` res = read_ini_file(file, {'APP':{'path': '/opt', 'name': 'timpstack'}}) 相當於把ini文件修改為如下: ``` [APP] root_path=/opt name=timpstack [LOG] PATH=${APP:root_path}/${APP:name}/logs/log.txt ``` #=> ress返回值為 {'LOG': {'path': '/opt/timpstack/logs/log.txg'}} :param file: ini文件路徑 :param pre_sections: 預傳入的Section dict對象 :param optionxform: key值轉換函數, 默認為str.lower; 即將所有key轉為小寫 :param default_section_name: 默認Section名稱, 缺省為DEFAULT :return: 返回解析后的字典對象 """ parser = ConfigParser(interpolation=ExtendedInterpolation(), default_section=default_section_name) if optionxform: parser.optionxform = optionxform ini_lines = [] if pre_sections and isinstance(pre_sections, dict): for sec_key, sec_values in pre_sections.items(): if sec_values and isinstance(sec_values, dict): ini_lines.append(f'[{sec_key}]\n') for k, v in sec_values.items(): ini_lines.append(f'{k}={v}\n') with open(file) as f: for ln in f.readlines(): ini_lines.append(ln) parser.read_string("".join(ini_lines)) result = {} for sn in parser.sections(): section = parser[sn] section_values = {} for k, v in section.items(): if v == "": v = None section_values[k] = v result[sn] = section_values return result
main.py
import fire import uvicorn import os from pathlib import Path from app.core import settings, load_settings from app.core.defines import ENV_TIMPSTACK_CONFIG_DIR class Command(object): def start(self, confdir: str = None, reload: bool = False) -> None: if not confdir: conf_directory = Path(__file__).parent.absolute().joinpath('conf') else: conf_directory = Path(confdir).absolute() os.putenv(ENV_TIMPSTACK_CONFIG_DIR, str(conf_directory)) load_settings() uvicorn.run('app.server:app', host=settings.server_host, port=settings.server_port, workers=settings.workers_count, log_config=settings.logging_config_file, reload=reload) if __name__ == '__main__': command = Command() fire.Fire(command) command.start()
異常及原因
--- Logging error --- Traceback (most recent call last): File "C:\Program Files (x86)\Python37-32\lib\logging\handlers.py", line 70, in emit self.doRollover() File "C:\Program Files (x86)\Python37-32\lib\logging\handlers.py", line 394, in doRollover self.rotate(self.baseFilename, dfn) File "C:\Program Files (x86)\Python37-32\lib\logging\handlers.py", line 111, in rotate os.rename(source, dest) PermissionError: [WinError 32] 另一個程序正在使用此文件,進程無法訪問。: 'G:\\python_object\\test6_22\\logs\\default.log' -> 'G:\\python_object\\test6_22\\logs\\default.log.2021-06-22_11-26' Call stack: File "<string>", line 1, in <module> File "C:\Program Files (x86)\Python37-32\lib\multiprocessing\spawn.py", line 105, in spawn_main exitcode = _main(fd) File "C:\Program Files (x86)\Python37-32\lib\multiprocessing\spawn.py", line 118, in _main return self._bootstrap() File "C:\Program Files (x86)\Python37-32\lib\multiprocessing\process.py", line 297, in _bootstrap self.run() File "C:\Program Files (x86)\Python37-32\lib\multiprocessing\process.py", line 99, in run self._target(*self._args, **self._kwargs) File "G:\python_object\tset_log\backend\venv\lib\site-packages\uvicorn\subprocess.py", line 61, in subprocess_started target(sockets=sockets) File "G:\python_object\tset_log\backend\venv\lib\site-packages\uvicorn\server.py", line 49, in run loop.run_until_complete(self.serve(sockets=sockets)) File "C:\Program Files (x86)\Python37-32\lib\asyncio\base_events.py", line 566, in run_until_complete self.run_forever() File "C:\Program Files (x86)\Python37-32\lib\asyncio\base_events.py", line 534, in run_forever self._run_once() File "C:\Program Files (x86)\Python37-32\lib\asyncio\base_events.py", line 1771, in _run_once handle._run() File "C:\Program Files (x86)\Python37-32\lib\asyncio\events.py", line 88, in _run self._context.run(self._callback, *self._args) File "G:\python_object\tset_log\backend\venv\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 396, in run_asgi result = await app(self.scope, self.receive, self.send) File "G:\python_object\tset_log\backend\venv\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 45, in __call__ return await self.app(scope, receive, send) File "G:\python_object\tset_log\backend\venv\lib\site-packages\uvicorn\middleware\message_logger.py", line 61, in __call__ await self.app(scope, inner_receive, inner_send) File "G:\python_object\tset_log\backend\venv\lib\site-packages\fastapi\applications.py", line 199, in __call__ await super().__call__(scope, receive, send) File "G:\python_object\tset_log\backend\venv\lib\site-packages\starlette\applications.py", line 111, in __call__ await self.middleware_stack(scope, receive, send) File "G:\python_object\tset_log\backend\venv\lib\site-packages\starlette\middleware\errors.py", line 159, in __call__ await self.app(scope, receive, _send) File "G:\python_object\tset_log\backend\venv\lib\site-packages\starlette\middleware\cors.py", line 78, in __call__ await self.app(scope, receive, send) File "G:\python_object\tset_log\backend\venv\lib\site-packages\starlette\exceptions.py", line 71, in __call__ await self.app(scope, receive, sender) File "G:\python_object\tset_log\backend\venv\lib\site-packages\starlette\routing.py", line 566, in __call__ await route.handle(scope, receive, send) File "G:\python_object\tset_log\backend\venv\lib\site-packages\starlette\routing.py", line 227, in handle await self.app(scope, receive, send) File "G:\python_object\tset_log\backend\venv\lib\site-packages\starlette\routing.py", line 44, in app await response(scope, receive, send) File "G:\python_object\tset_log\backend\venv\lib\site-packages\starlette\responses.py", line 136, in __call__ "headers": self.raw_headers, File "G:\python_object\tset_log\backend\venv\lib\site-packages\starlette\exceptions.py", line 68, in sender await send(message) File "G:\python_object\tset_log\backend\venv\lib\site-packages\starlette\middleware\errors.py", line 156, in _send await send(message) File "G:\python_object\tset_log\backend\venv\lib\site-packages\uvicorn\middleware\message_logger.py", line 55, in inner_send await send(message) File "G:\python_object\tset_log\backend\venv\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 467, in send status_code, Message: '%s - "%s %s HTTP/%s" %d' Arguments: ('127.0.0.1:58497', 'GET', '/api/openapi.json', '1.1', 200) 2021-06-22 13:34:37,344.344 [INFO] [uvicorn.access] 127.0.0.1:58497 - "GET /api/openapi.json HTTP/1.1" 200
原因:logs文件夾里面的文件日志文件一直在被監控着,當寫的時候發現大小或者時間滿足的切割的時候,文件已經被打開,進入了寫入狀態,然后將該文件備份的事就就會造成無法完成該動作的錯誤,在centos中就不會出現該問題,可以正常的切割以及回滾
樣板代碼離線包,百度盤地址
鏈接:https://pan.baidu.com/s/1Fr6AHYISyPqWZUrf-LKdEw
提取碼:k6n8