Python log在fastapi中的全局配置


在FastAPI中使用日志功能,實現日志切割。

原由

日志在未實現切割以及回滾時候會將所有的日志記錄寫入同一個地方,這樣就會使日志文件特別大,如果該項目的訪問量很大,然后運行時間長了之后還有可能因為日志文件過大,造成服務器因存儲空間不足而宕機,所以需要將日志進行切割以及回滾。

實現

目錄結構

 注釋:

  • conf文件主要放置項目參數配置文件以及日志配置文件
    • logging.ini為日志的參數配置文件
    • test.ini 為項目的參數配置文件
  • app主要就是項目以及一些相關設置
    • api 項目的接口文件
      • routers文件(使用 APIRouter是更具有層次性)
    • core項目的配置文件
      • defines.py中主要配置各種固定參數,或者文件名
      • setting.py中為項目的配置文件
    • utils項目獨立函數文件
      • parser.py 常用文件或字符串解析函數
    • server.py為項目的服務主入口文件
  • 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) 20202021 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) 20202021 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) 20202021 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) 20202021 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM