Python2/Python3自定義日志類教程


一、說明

1.1 背景說明

Python的logging功能是比較豐富的支持不同層次的日志輸出,但或是我們想在日志前輸出時間、或是我們想要將日志輸入到文件,我們還是想要自定義日志類。

之前自己也嘗試寫過但感覺文檔太亂看不懂怎么寫,今天有人拿個半成品來問為什么代碼報錯,在其基礎上改造了一下。

 

1.2 logging級別說明

logging日志級別及對應值如下,默認情況下直接運行只有INFO及以上級別才會輸出(本質上是大於等於20才會輸出),調試模式運行DEBUG日志才會輸出。

可以通過自定義輸出日志級別,指定直接運行輸出什么級別的日志;不過調試模式打印的日志應該是不可以修改的。

Level

Numeric value

CRITICAL

50

ERROR

40

WARNING

30

INFO

20

DEBUG

10

NOTSET

0

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

二、實現代碼

2.1 Python2實現代碼

# -*- coding: utf-8 -*-
import os
import datetime
import logging

class LogConfig:
    def __init__(self,log_type="console"):
        # 指定日志輸出到控制台時的初始化
        if log_type == "console":
            logging.basicConfig(level=logging.INFO,
                                format='%(asctime)s %(levelname)s %(message)s',
                                datefmt='%Y-%m-%d %H:%M:%S',
                                )
        # 指定日志輸出到文件的初始化
        elif log_type == "file":
            # 創建存放日志的目錄
            if not os.path.exists('./log'):
                os.mkdir('./log')

            # 操作系統本身不允許文件名包含:等特殊字符,所以這里也不要用,不然賦給filename時會報錯
            nowTime = datetime.datetime.now().strftime('%Y-%m-%d')
            file_name = './log/%s.log' % nowTime

            # python2.7也有logging.basicConfig(),但只直接用logging.basicConfig(),寫中文時會報錯
            # 所以為風格統一,我們這里不使用logging.basicConfig(),全通過set設置
            root_logger = logging.getLogger()
            root_logger.setLevel(logging.INFO)
            handler = logging.FileHandler(filename=file_name, encoding='utf-8', mode='a')
            formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
            handler.setFormatter(formatter)
            root_logger.addHandler(handler)

    def getLogger(self):
        logger = logging.getLogger()
        return logger

if __name__ == "__main__":
    # log_type = "console"
    log_type = "file"
    logger = LogConfig(log_type).getLogger()
    logger.debug('print by debug')
    logger.info('print by info')
    logger.warning('print by warning')

 

2.2 Python3實現代碼

python3.3 之后logging.basicConfig()中提供了handlers參數,我們可借助handlers參數來指定編碼。

python3.3之前的python3版本寫法得和python2一樣。另外python3.9之后logging.basicConfig()會直接提供encoding參數,到時可以更方便。

import os
import datetime
import logging

class LogConfig:
    def __init__(self,log_type="console"):
        # 指定日志輸出到控制台時的初始化
        if log_type == "console":
            logging.basicConfig(level=logging.INFO,
                                format='%(asctime)s %(levelname)s %(message)s',
                                datefmt='%Y-%m-%d %H:%M:%S',
                                )
        # 指定日志輸出到文件的初始化
        elif log_type == "file":
            # 創建存放日志的目錄
            if not os.path.exists('./log'):
                os.mkdir('./log')

            # 操作系統本身不允許文件名包含:等特殊字符,所以這里也不要用,不然賦給filename時會報錯
            nowTime = datetime.datetime.now().strftime('%Y-%m-%d')

            file_name = './log/%s.log' % nowTime
            file_handler = logging.FileHandler(filename=file_name,encoding='utf-8', mode='a')
            # level----指定打印的日志等級;默認為WARNING;可為NOTSET、DEBUG、INFO、WARNING、ERROR、CRITICAL
            # format----指定整條日志的格式;這里設置為“時間-等級-日志內容”
            # datefmt----format中時間的格式;
            # filename----日志輸出到的文件;默認打印到控制台
            # filemode----日志文件讀寫形式;默認為“a”;配合filename使用,如果不用filename該參數也可不用
            # 本來輸出到文件使用filename和filemode兩個參數就可以了,不需要handlers
            # 但是logging將日志輸出到文件時中文會亂碼,而logging.basicConfig又沒有提供指定編碼的參數(python3.9之后才提供有直接的encoding參數)
            # 要指定編碼只能使用handlers。另外handlers還是python3.3 之后才提供的參數,在此之前的版本請參考python2的寫法
            logging.basicConfig(level=logging.INFO,
                                format='%(asctime)s %(levelname)s %(message)s',
                                datefmt='%Y-%m-%d %H:%M:%S',
                                # filename=file_name,
                                # filemode='a',
                                handlers=[file_handler],
                                )

    def getLogger(self):
        logger = logging.getLogger()
        return logger

if __name__ == "__main__":
    # log_type = "console"
    log_type = "file"
    logger = LogConfig(log_type).getLogger()
    logger.debug('print by debug')
    logger.info('print by info')
    logger.warning('print by warning')

 

三、日志截圖

 

四、更科學的日志定義方式(20200310更新)

通過近段時間的使用發現原先的方法就自己用用沒問題,但與別人產生調用及上生產時就會存在幾個問題:

第一個問題是,直接通過logging.basicConfig()進行配置,會同時影響被調用庫的日志設置。

第二個問題是,原現的文件日志形式只能輸出到一個給定的文件,不能實現不同的日志類型輸出到不同的日志文件。

第三個問題是,原現的文件日志形式使用"w"模式則上次日志會被清除,使用"a"模式則日志又會無限增長需要注意清理。

前兩個問題通過getLogger時給定一個名稱而不是直接獲取根logger進行處理;第三個問題通過使用RotatingFileHandler等替換FileHandler進行處理。更新代碼如下:

import inspect
import logging
import logging.handlers
import os


class LogConfig:
    def __init__(self, log_level=logging.INFO):
        if __name__ != "__main__":
            self.logger = self.get_file_logger(log_level=log_level)
        pass

    def get_console_logger(self):
        def _gen_file_logger_handler():
            _handler = logging.StreamHandler()
            formatter = logging.Formatter(
                "[%(asctime)s %(msecs)03d][%(process)d][tid=%(thread)d][%(name)s][%(levelname)s] %(message)s [%(filename)s"
                " %(funcName)s %(lineno)s] ", datefmt="%Y-%m-%d %H:%M:%S")
            _handler.setLevel(logging.INFO)
            _handler.setFormatter(formatter)
            return _handler
        def _gen_console_logger():
            _console_logger = logging.getLogger("console_logger")
            _console_logger.addHandler(handler)
            return _console_logger

        handler = _gen_file_logger_handler()
        console_logger = _gen_console_logger()
        return console_logger

    # 允許同時存在不同的logger,即可以傳入不同log_file_name實例化不同logger即可實現不同類型日志寫入不同文件
    # logger自帶了線程鎖,是線程安全的,所以多線程不用自己實現日志文件鎖
    def get_file_logger(self, logger_name=None, log_file_name=None, log_level=logging.INFO):
        def _get_log_file_name():
            # 如果已定義有日志文件則直接原樣返回
            if log_file_name:
                return log_file_name
            # 如果是直接運行的,那取當前文件名
            if __name__ == "__main__":
                caller_file_name = __file__
            # 如果是被調用的,則取上層調用文件文件名
            else:
                frame = inspect.stack()[3]
                module = inspect.getmodule(frame[0])
                caller_file_name = module.__file__
            inner_log_file_name = f"{os.path.basename(caller_file_name)[:-3]}.log"
            return inner_log_file_name
        def _make_sure_log_dir_exist():
            if not os.path.isdir(log_file_dir):
                os.mkdir(log_file_dir)
        def _gen_file_logger_handler():
            # 操作系統本身不允許文件名包含:等特殊字符,所以這里也不要用,不然賦給filename時會報錯
            # nowTime = datetime.datetime.now().strftime('%Y-%m-%d')
            file_path = f'{log_file_dir}/{log_file_name}'
            # formatter = logging.Formatter(
            #     "[%(asctime)s %(msecs)03d][%(process)d][tid=%(thread)d][%(name)s][%(levelname)s] %(message)s [%(filename)s"
            #     " %(funcName)s %(lineno)s] ", datefmt="%Y-%m-%d %H:%M:%S")
            formatter = logging.Formatter(
                "[%(asctime)s %(msecs)03d][%(name)s][%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
            # filename----日志文件
            # when----更換日志文件的時間單位
            # interval----更換日志文件的時間單位個數;這里是7天換一個文件
            # backupCount----保存的舊日志文件個數;這里即只保留上一個日志文件
            # encoding----日志文件編碼
            # 實際使用感覺,如果程序是crontab這樣定時運行而不是一直運行着,那這個按時間滾動並不生效
            # _handler = logging.handlers.TimedRotatingFileHandler(
            #     filename=file_path,
            #     when='D',
            #     interval=7,
            #     backupCount=1,
            #     encoding='utf-8',
            # )
            # filename--日志文件
            # mode--日志文件打開模式
            # maxBytes--日志文件最大大小。每次調用打印日志時logging去檢測日志大小是否達到設定的上限,如果達到則更換日志文件
            # backupCount--保存的舊日志文件個數。xxx表示當前日志文件,則xxx.1表示上一份日志文件、xxx.2表示上上份日志文件...
            # encoding----日志文件編碼
            _handler = logging.handlers.RotatingFileHandler(
                filename=file_path,
                mode='a',
                maxBytes=1024 * 1024 * 100,
                backupCount=1,
                encoding='utf-8',
            )
            # 實際發現這里setLevel並不起作用
            # _handler.setLevel(logging.DEBUG)
            _handler.setFormatter(formatter)
            return _handler
        def _gen_file_logger():
            # 如果指定了logger_name那直接采用,如果沒有使用日志文件名為logger_name
            nonlocal logger_name
            if not logger_name:
                logger_name = log_file_name
            _file_logger = logging.getLogger(logger_name)
            _file_logger.addHandler(handler)
            _file_logger.setLevel(log_level)
            return _file_logger
        log_file_name = _get_log_file_name()
        log_file_dir = "log"
        _make_sure_log_dir_exist()
        handler = _gen_file_logger_handler()
        file_logger = _gen_file_logger()
        return file_logger

if __name__ == "__main__":
    # logger = LogConfig().get_console_logger()
    # log_file_name不同,返回的是不同的logger,這樣就可以方便地定義多個logger
    log_file_name = "random_file_name.log"
    logger = LogConfig().get_file_logger(log_file_name=log_file_name)
    logger.debug('print by debug')
    logger.info('print by info')
    logger.warning('print by warning')
View Code

 

參考:

https://docs.python.org/3/library/logging.html#levels

https://www.jianshu.com/p/2ec5187a953e

https://docs.python.org/2/library/logging.html#logging.basicConfig

https://docs.python.org/3/library/logging.html#logging.basicConfig

https://stackoverflow.com/questions/10706547/add-encoding-parameter-to-logging-basicconfig


免責聲明!

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



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