Python中日誌的處理示例,根據不同的日誌級別輸出至日誌文件,以及多進程中日誌輸出解決方案

前言

在工作中有好幾個小夥伴問我關於Python日誌模塊的使用和日誌文件這塊我通常是怎麼處理的,所以我打算總結一下我在Python普通程序(非中大型項目)中的日誌常見處理方法。

  • 操作環境:Python3.6MacOS 10.14
  • 日誌級別:DEBUG < INFO < WARNING < ERROR< CRITICAL

1. 最快捷的使用

直接從loggingbasicConfig函數直接配置,適用於對日誌管理要求不高的小腳本程序。

更多詳細的basicConfig配置,可以看看logging.basicConfig函數的源碼。

# base_logger.py
import logging

logging.basicConfig(level=logging.INFO,  # 最低輸出級別
                    format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',  # 日誌輸出格式
                    datefmt='%Y-%m-%d %H:%M:%S')  # 日期格式

logging.debug('this is a debug message ~')
logging.info('this is a info message.')
logging.error('this is a error message!')

日誌輸出效果

可以看到debug信息是沒有打印出來的,因爲上面示例程序中設置的日誌輸出最低級別爲INFO

2019-05-25 11:53:32 base_logger.py[line:8] INFO this is a info message.
2019-05-25 11:53:32 base_logger.py[line:9] ERROR this is a error message!

2. 控制檯能輸出,還能寫入日誌文件

在日常使用中,除了期望控制檯能打印日誌外,還期望能將日誌信息按照我們的要求寫入指定的日誌文件中,可以幫助我們掌握程序的運行情況。

根據日期保存日誌文件

在下列的示例程序中可以通過get_my_logger函數獲取一個logger來輸出日誌,示例程序中可以將INFO級別以上的信息輸出至控制檯,而WARNING以上級別的信息輸出至日誌文件,並且每天會創建一個新的日誌文件,並且會保留最近10天的日誌文件。

# date_logger.py
import logging
from logging.handlers import TimedRotatingFileHandler


def get_my_logger(log_path, logger_name=__name__, **kwargs):
    """
    獲取日誌記錄器
    :param log_path: 日誌保存位置
    :param logger_name:
    :param kwargs:
    :return:
    """
    logger = logging.getLogger(logger_name)
    # 設置全局的日誌輸出最低級別
    logger.setLevel(level=logging.INFO)

    # 設置輸出控制檯
    stream_handler = logging.StreamHandler()
    # 控制檯輸出的日誌級別INFO
    stream_handler.setLevel(level=logging.INFO)
    # 控制檯輸出日誌格式
    stream_formatter = logging.Formatter(fmt='%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s %(message)s',
                                         datefmt='%Y-%m-%d %H:%M:%S')
    stream_handler.setFormatter(stream_formatter)
    logger.addHandler(stream_handler)

    # 以設置日期記錄日誌文件,當前設置爲日誌輸出路徑爲log_path,每天[D]生成一個日誌文件,並且備份最近的10個日誌文件
    file_tr_handler = TimedRotatingFileHandler(filename=log_path, when='D', backupCount=10, encoding='utf-8')
    # 將warning級別以上的日誌記錄下來
    file_tr_handler.setLevel(level=logging.WARNING)
    # 設置日誌文件中單條日誌輸出格式
    file_tr_formatter = logging.Formatter(fmt='%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s %(message)s',
                                          datefmt='%Y-%m-%d %H:%M:%S')
    file_tr_handler.setFormatter(file_tr_formatter)
    logger.addHandler(file_tr_handler)
    return logger


my_logger = get_my_logger('test.log')
my_logger.debug('this is a debug message ~')
my_logger.info('this is a info message.')
my_logger.error('this is a error message !')

程序運行效果

日誌文件中只保留了WARNING以上級別的日誌信息。

# 控制檯效果
2019-05-25 12:24:11 date_logger.py [line:44] INFO this is a info message.
2019-05-25 12:24:11 date_logger.py [line:45] ERROR this is a error message !

# 日誌文件內容
2019-05-25 12:24:11 date_logger.py [line:45] ERROR this is a error message !

根據日誌文件大小保存日誌文件

因爲並非所有程序日誌我們都期望以日期來保存,並且由於有些程序會輸出大量日誌從而有擠爆硬盤的風險。例如一次性的爬蟲程序,普遍會持續輸出較多日誌信息,並且由於是一次性的爬蟲,我們並不期望持續觀察他的日誌信息,只需要瞭解它最新的採集情況就足夠了,所以這個時候按照日誌文件大小來保存就是更優的選擇。

在下面的示例程序中可以將單個日誌文件大小最大爲100MB,並且備份最近5個日誌文件,這樣既能記錄最新的日誌信息,並且日誌文件累計不會超過600MB,避免了撐爆硬盤的風險。

# size_logger.py
import logging
from logging.handlers import RotatingFileHandler


def get_my_logger(log_path, logger_name=__name__, **kwargs):
    """
    獲取日誌記錄器
    :param log_path: 日誌保存位置
    :param logger_name:
    :param kwargs:
    :return:
    """
    logger = logging.getLogger(logger_name)
    # 設置全局的日誌輸出最低級別
    logger.setLevel(level=logging.INFO)

    # 設置輸出控制檯
    stream_handler = logging.StreamHandler()
    # 控制檯輸出的日誌級別INFO
    stream_handler.setLevel(level=logging.INFO)
    # 控制檯輸出日誌格式
    stream_formatter = logging.Formatter(fmt='%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s %(message)s',
                                         datefmt='%Y-%m-%d %H:%M:%S')
    stream_handler.setFormatter(stream_formatter)
    logger.addHandler(stream_handler)

    # 設置日誌文件輸出
    # 以限制日誌文件大小輸出,單個日誌文件最大爲100MB,並且備份最近5個日誌文件
    file_rt_handler = RotatingFileHandler(filename=log_path, mode='a', maxBytes=100 * 1024 * 1024, backupCount=5,
                                          encoding='utf-8')
    # 設置日誌文件記錄等級
    file_rt_handler.setLevel(level=logging.WARN)
    file_rt_formatter = logging.Formatter(fmt='%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s %(message)s',
                                          datefmt='%Y-%m-%d %H:%M:%S')
    file_rt_handler.setFormatter(file_rt_formatter)
    logger.addHandler(file_rt_handler)
    return logger


my_logger = get_my_logger('test.log')
my_logger.debug('this is a debug message ~')
my_logger.info('this is a info message.')
my_logger.error('this is a error message !')

運行效果展示

# 控制檯效果
2019-05-25 13:01:27 size_logger.py [line:45] INFO this is a info message.
2019-05-25 13:01:27 size_logger.py [line:46] ERROR this is a error message !

# 日誌文件內容
2019-05-25 13:01:27 size_logger.py [line:46] ERROR this is a error message !

將不同級別的日誌寫入不用的日誌文件

程序運行中,我們一方面希望將程序運行的常規信息保存下來,同時也希望將程序的WARNINGERROR甚至更高級別的信息記錄下來,以方便改進程序。如果直接將所有日誌寫入同一個日誌文件,常規的日誌信息過多很可能會掩蓋掉WARNING以上級別的日誌信息,所以這個時候最好將重要級別日誌信息額外保存。

下列的logger會在保存INFO級別以上的日誌的同時,將ERROR以上級別的日誌信息保存至另外的日誌文件

# mul_logger.py
import logging
from logging.handlers import RotatingFileHandler


def get_my_logger(log_path, logger_name=__name__, **kwargs):
    """
    獲取日誌記錄器
    :param log_path: 日誌保存位置
    :param logger_name:
    :param kwargs:
    :return:
    """
    logger = logging.getLogger(logger_name)
    # 設置全局的日誌輸出最低級別
    logger.setLevel(level=logging.INFO)

    # 設置INFO級別的日誌文件輸出
    # 以限制日誌文件大小輸出,單個日誌文件最大爲10MB,並且備份最近5個日誌文件
    file_rt_info_handler = RotatingFileHandler(filename=log_path, mode='a', maxBytes=10 * 1024 * 1024, backupCount=5,
                                               encoding='utf-8')
    # 設置日誌文件記錄等級
    file_rt_info_handler.setLevel(level=logging.INFO)
    file_rt_formatter = logging.Formatter(fmt='%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s %(message)s',
                                          datefmt='%Y-%m-%d %H:%M:%S')
    file_rt_info_handler.setFormatter(file_rt_formatter)
    logger.addHandler(file_rt_info_handler)

    # 設置ERROR級別的日誌文件輸出
    # 以限制日誌文件大小輸出,單個日誌文件最大爲10MB,並且備份最近5個日誌文件
    file_rt_error_handler = RotatingFileHandler(filename=log_path + '.error', mode='a', maxBytes=10 * 1024 * 1024,
                                                backupCount=5,
                                                encoding='utf-8')
    # 設置日誌文件記錄等級
    file_rt_error_handler.setLevel(level=logging.ERROR)
    file_rt_formatter = logging.Formatter(fmt='%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s %(message)s',
                                          datefmt='%Y-%m-%d %H:%M:%S')
    file_rt_error_handler.setFormatter(file_rt_formatter)
    logger.addHandler(file_rt_error_handler)

    return logger


my_logger = get_my_logger('test.log')
my_logger.debug('this is a debug message ~')
my_logger.info('this is a info message.')
my_logger.warning('this is a warning message !')
my_logger.error('this is a error message !')

運行效果展示

# test.log 內容
2019-05-25 13:09:07 mul_logger.py [line:48] INFO this is a info message.
2019-05-25 13:09:07 mul_logger.py [line:49] WARNING this is a warning message !
2019-05-25 13:09:07 mul_logger.py [line:50] ERROR this is a error message !

# test.log.error 內容
2019-05-25 13:09:07 mul_logger.py [line:50] ERROR this is a error message !


3. 多進程中日誌記錄的處理方式

有時爲了提高程序運行效率,通常會採用開啓多線程或多進程的方式來並行處理相關邏輯。

  • 多線程:在開啓多線程時,由於在線程中是操作的是同一片內存區域,所以上面的日誌logger在多線程中是安全的,不過不同線程的日誌寫入順序可能會互相交叉混合;
  • 多進程:多進程在當某一子進程操作寫入日誌文件時或未及時關閉,另一子進程同時寫入時,就會出現文件被另一程序使用的錯誤。

爲解決這個問題,可以使用這個concurrent-log-handler模塊來解決多進程同時寫入日誌的問題。

# 模塊信息URL: https://pypi.org/project/concurrent-log-handler/
# concurrent-log-handler 安裝方式
pip install concurrent-log-handler
或
python3 -m pip install concurrent-log-handler

我去看了一下源碼,似乎這個模塊針對上面說到的根據日期寫入日誌和按照文件大小進行寫入日誌並不完善,這個模塊只提供ConcurrentRotatingFileHandler來多進程下按照日誌文件大小進行寫入。

程序示例

# mulprocess_logger.py
import logging
from concurrent_log_handler import ConcurrentRotatingFileHandler


def get_my_logger(log_path, logger_name=__name__, **kwargs):
    """
    支持多進程的logger設置
    :param log_path: 日誌保存位置
    :param logger_name:
    :param kwargs:
    :return:
    """
    logger = logging.getLogger(logger_name)
    # 設置全局的日誌輸出最低級別
    logger.setLevel(level=logging.INFO)

    # 設置INFO級別的日誌文件輸出
    # 以限制日誌文件大小輸出,單個日誌文件最大爲10MB,並且備份最近5個日誌文件
    file_rt_info_handler = ConcurrentRotatingFileHandler(filename=log_path, mode='a', maxBytes=10 * 1024 * 1024,
                                                         backupCount=5, encoding='utf-8')
    # 設置日誌文件記錄等級
    file_rt_info_handler.setLevel(level=logging.INFO)
    file_rt_formatter = logging.Formatter(fmt='%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s %(message)s',
                                          datefmt='%Y-%m-%d %H:%M:%S')
    file_rt_info_handler.setFormatter(file_rt_formatter)
    logger.addHandler(file_rt_info_handler)

    return logger


my_logger = get_my_logger('test.log')
my_logger.debug('this is a debug message ~')
my_logger.info('this is a info message.')
my_logger.warning('this is a warning message !')
my_logger.error('this is a error message !')

運行效果展示

會生成一個.lock文件,這個文件相當於正式日誌文件的緩存文件,.lock文件的緩存大小也可以進行設置

2019-05-25 14:12:07 mulprocess_logger.py [line:33] INFO this is a info message.
2019-05-25 14:12:07 mulprocess_logger.py [line:34] WARNING this is a warning message !
2019-05-25 14:12:07 mulprocess_logger.py [line:35] ERROR this is a error message !

4. 使用logger記錄詳細的異常信息

創建的logger可以使用logger.exception()方法獲取詳細的代碼異常信息,並且記錄至日誌文件中。

# exception_logger.py
# 忽略get_my_logger函數的具體設置
def get_my_logger(log_path: str, logger_name=__name__, **kwargs):
  	# ....
    return logger
  
my_logger = get_my_logger('test.log')
try:
    a = 1 / 0
except Exception as err:
    my_logger.exception(f'捕捉到的詳細異常信息: {err}')

運行效果展示

2019-05-25 14:24:02 exception_logger.py [line:54] ERROR 捕捉到的詳細異常信息: division by zero
Traceback (most recent call last):
  File "/Users/leowoo/PycharmProjects/PythonDemo/LoggerDemo/exception_logger.py", line 52, in <module>
    a = 1 / 0
ZeroDivisionError: division by zero

大致的關於小型項目中的創建日誌管理的方式就差不多了,當然這只是一種配置方式而已;
雖然logging只是作爲一個日誌模塊,但是支持各種自定義的配置方式,例如另外支持logging.config.fileConfiglogging.config.dictConfig來讀取配置信息。
望各位大佬多多指教!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章