关于python:Python-中更优雅的日志记录方案

3次阅读

共计 7928 个字符,预计需要花费 20 分钟才能阅读完成。

在 Python 中,个别状况下咱们可能间接用自带的 logging 模块来记录日志,包含我之前的时候也是一样。在应用时咱们须要配置一些 Handler、Formatter 来进行一些解决,比方把日志输入到不同的地位,或者设置一个不同的输入格局,或者设置日志分块和备份。但其实个人感觉 logging 用起来其实并不是那么好用,其实次要还是配置较为繁琐。

常见应用
首先看看 logging 常见的解决方案吧,我个别会配置输入到文件、控制台和 Elasticsearch。输入到控制台就仅仅是不便间接查看的;输入到文件是不便间接存储,保留所有历史记录的备份;输入到 Elasticsearch,间接将 Elasticsearch 作为存储和剖析的核心,应用 Kibana 能够十分不便地剖析和查看运行状况。

所以在这里我根本会对 logging 做如下的封装写法:

import logging
import sys
from os import makedirs
from os.path import dirname, exists

from cmreslogging.handlers import CMRESHandler

loggers = {}

LOG_ENABLED = True  # 是否开启日志
LOG_TO_CONSOLE = True  # 是否输入到控制台
LOG_TO_FILE = True  # 是否输入到文件
LOG_TO_ES = True  # 是否输入到 Elasticsearch

LOG_PATH = './runtime.log'  # 日志文件门路
LOG_LEVEL = 'DEBUG'  # 日志级别
LOG_FORMAT = '%(levelname)s - %(asctime)s - process: %(process)d - %(filename)s - %(name)s - %(lineno)d - %(module)s - %(message)s'  # 每条日志输入格局
ELASTIC_SEARCH_HOST = 'eshost'  # Elasticsearch Host
ELASTIC_SEARCH_PORT = 9200  # Elasticsearch Port
ELASTIC_SEARCH_INDEX = 'runtime'  # Elasticsearch Index Name
APP_ENVIRONMENT = 'dev'  # 运行环境,如测试环境还是生产环境

def get_logger(name=None):
    """
    get logger by name
    :param name: name of logger
    :return: logger
    """
    global loggers

    if not name: name = __name__

    if loggers.get(name):
        return loggers.get(name)

    logger = logging.getLogger(name)
    logger.setLevel(LOG_LEVEL)

    # 输入到控制台
    if LOG_ENABLED and LOG_TO_CONSOLE:
        stream_handler = logging.StreamHandler(sys.stdout)
        stream_handler.setLevel(level=LOG_LEVEL)
        formatter = logging.Formatter(LOG_FORMAT)
        stream_handler.setFormatter(formatter)
        logger.addHandler(stream_handler)

    # 输入到文件
    if LOG_ENABLED and LOG_TO_FILE:
        # 如果门路不存在,创立日志文件文件夹
        log_dir = dirname(log_path)
        if not exists(log_dir): makedirs(log_dir)
        # 增加 FileHandler
        file_handler = logging.FileHandler(log_path, encoding='utf-8')
        file_handler.setLevel(level=LOG_LEVEL)
        formatter = logging.Formatter(LOG_FORMAT)
        file_handler.setFormatter(formatter)
        logger.addHandler(file_handler)

    # 输入到 Elasticsearch
    if LOG_ENABLED and LOG_TO_ES:
        # 增加 CMRESHandler
        es_handler = CMRESHandler(hosts=[{'host': ELASTIC_SEARCH_HOST, 'port': ELASTIC_SEARCH_PORT}],
                                  # 能够配置对应的认证权限
                                  auth_type=CMRESHandler.AuthType.NO_AUTH,  
                                  es_index_name=ELASTIC_SEARCH_INDEX,
                                  # 一个月分一个 Index
                                  index_name_frequency=CMRESHandler.IndexNameFrequency.MONTHLY,
                                  # 额定减少环境标识
                                  es_additional_fields={'environment': APP_ENVIRONMENT}  
                                  )
        es_handler.setLevel(level=LOG_LEVEL)
        formatter = logging.Formatter(LOG_FORMAT)
        es_handler.setFormatter(formatter)
        logger.addHandler(es_handler)

    # 保留到全局 loggers
    loggers[name] = logger
    return logger

定义完了怎么应用呢?只须要应用定义的办法获取一个 logger,而后 log 对应的内容即可:

logger = get_logger()
logger.debug('this is a message')

运行后果如下:

DEBUG - 2019-10-11 22:27:35,923 - process: 99490 - logger.py - __main__ - 81 - logger - this is a message

咱们看看这个定义的根本实现吧。首先这里一些常量是用来定义 logging 模块的一些根本属性的,比方 LOG_ENABLED 代表是否开启日志性能,LOG_TO_ES 代表是否将日志输入到 Elasticsearch,另外还有很多其余的日志根本配置,如 LOG_FORMAT 配置了日志每个条目输入的根本格局,另外还有一些连贯的必要信息。这些变量能够和运行时的命令行或环境变量对接起来,能够不便地实现一些开关和配置的更换。

而后定义了这么一个 get_logger 办法,接管一个参数 name。首先该办法拿到 name 之后,会到全局的 loggers 变量外面查找,loggers 变量是一个全局字典,如果有曾经申明过的 logger,间接将其获取返回即可,不必再将其二次初始化。如果 loggers 外面没有找到 name 对应的 logger,那就进行创立即可。创立 logger 之后,能够为其增加各种对应的 Handler,如输入到控制台就用 StreamHandler,输入到文件就用 FileHandler 或 RotatingFileHandler,输入到 Elasticsearch 就用 CMRESHandler,别离配置好对应的信息即可。

最初呢,将新建的 logger 保留到全局的 loggers 外面并返回即可,这样如果有同名的 logger 便能够间接查找 loggers 间接返回了。

在这里依赖了额定的输入到 Elasticsearch 的包,叫做 CMRESHandler,它能够反对将日志输入到 Elasticsearch 外面,如果要应用的话能够装置一下:

pip install CMRESHandler
其 GitHub 地址是:https://github.com/cmanaha/py…,具体的应用形式能够看看它的官网阐明,如配置认证信息,配置 Index 分隔信息等等。

好,下面就是我之前罕用的 logging 配置,通过如上的配置,我就能够实现将 logging 输入到三个地位,并能够实现对应的成果。比方输入到 Elasticsearch 之后,我就能够十分不便地应用 Kibana 来查看以后运行状况,ERROR Log 的比例等等,
也能够在它的根底上做更进一步的统计分析。

loguru
下面的实现形式曾经是一个较为可行的配置计划了。然而,我还是会感觉到有些 Handler 配起来麻烦,尤其是新建一个我的项目的很多时候懒得去写一些配置。即便是不必上文的配置,用最根本的几行 logging 配置,像如下的通用配置:

import logging
logging.basicConfig(level = logging.INFO,format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

我也懒得去写,感觉并不是一个优雅的实现形式。

有需要就有能源啊,这不,就有人实现了这么一个库,叫做 loguru,能够将 log 的配置和应用更加简略和不便。

上面咱们来看看它到底是怎么用的吧。

装置
首先,这个库的装置形式很简略,就用根本的 pip 装置即可,Python 3 版本的装置如下:

pip3 install loguru

装置结束之后,咱们就能够在我的项目里应用这个 loguru 库了。

根本应用
那么这个库怎么来用呢?咱们先用一个实例感触下:

from loguru import logger

logger.debug('this is a debug message')

看到了吧,不须要配置什么货色,间接引入一个 logger,而后调用其 debug 办法即可。

在 loguru 外面有且仅有一个次要对象,那就是 logger,loguru 外面有且仅有一个 logger,而且它曾经被提前配置了一些根底信息,比方比拟敌对的格式化、文本色彩信息等等。

下面的代码运行后果如下:

2019-10-13 22:46:12.367 | DEBUG    | __main__:<module>:4 - this is a debug message

能够看到其默认的输入格局是下面的内容,有工夫、级别、模块名、行号以及日志信息,不须要手动创立 logger,间接应用即可,另外其输入还是黑白的,看起来会更加敌对。

以上的日志信息是间接输入到控制台的,并没有输入到其余的中央,如果想要输入到其余的地位,比方存为文件,咱们只须要应用一行代码申明即可。

例如将后果同时输入到一个 runtime.log 文件外面,能够这么写:

from loguru import logger

logger.add('runtime.log')
logger.debug('this is a debug')

很简略吧,咱们也不须要再申明一个 FileHandler 了,就一行 add 语句搞定,运行之后会发现目录下 runtime.log 外面同样呈现了刚刚控制台输入的 DEBUG 信息。

下面就是一些根本的应用,但这还远远不够,上面咱们来具体理解下它的一些功能模块。

具体应用
既然是日志,那么最常见的就是输入到文件了。loguru 对输入到文件的配置有十分弱小的反对,比方反对输入到多个文件,分级别别离输入,过大创立新文件,过久主动删除等等。

上面咱们别离看看这些怎么来实现,这里基本上就是 add 办法的应用介绍。因为这个 add 办法就相当于给 logger 增加了一个 Handler,它给咱们裸露了许多参数来实现 Handler 的配置,上面咱们来具体介绍下。

首先看看它的办法定义吧:

def add(
        self,
        sink,
        *,
        level=_defaults.LOGURU_LEVEL,
        format=_defaults.LOGURU_FORMAT,
        filter=_defaults.LOGURU_FILTER,
        colorize=_defaults.LOGURU_COLORIZE,
        serialize=_defaults.LOGURU_SERIALIZE,
        backtrace=_defaults.LOGURU_BACKTRACE,
        diagnose=_defaults.LOGURU_DIAGNOSE,
        enqueue=_defaults.LOGURU_ENQUEUE,
        catch=_defaults.LOGURU_CATCH,
        **kwargs
    ):
    pass

看看它的源代码,它反对这么多的参数,如 level、format、filter、color 等等。

sink

另外咱们还留神到它有个十分重要的参数 sink,咱们看看官网文档:https://loguru.readthedocs.io…,能够理解到通过 sink 咱们能够传入多种不同的数据结构,汇总如下:

•sink 能够传入一个 file 对象,例如 sys.stderr 或者 open(‘file.log’, ‘w’) 都能够。
•sink 能够间接传入一个 str 字符串或者 pathlib.Path 对象,其实就是代表文件门路的,如果辨认到是这种类型,它会主动创立对应门路的日志文件并将日志输入进去。
•sink 能够是一个办法,能够自行定义输入实现。
•sink 能够是一个 logging 模块的 Handler,比方 FileHandler、StreamHandler 等等,或者上文中咱们提到的 CMRESHandler 照样也是能够的,这样就能够实现自定义 Handler 的配置。
•sink 还能够是一个自定义的类,具体的实现标准能够参见官网文档。

所以说,方才咱们所演示的输入到文件,仅仅给它传了一个 str 字符串门路,他就给咱们创立了一个日志文件,就是这个原理。

format、filter、level

上面咱们再理解下它的其余参数,例如 format、filter、level 等等。

其实它们的概念和格局和 logging 模块都是根本一样的了,例如这里应用 format、filter、level 来规定输入的格局:

logger.add('runtime.log', format="{time} {level} {message}", filter="my_module", level="INFO")

删除 sink

另外增加 sink 之后咱们也能够对其进行删除,相当于从新刷新并写入新的内容。

删除的时候依据刚刚 add 办法返回的 id 进行删除即可,看上面的例子:

from loguru import logger

trace = logger.add('runtime.log')
logger.debug('this is a debug message')
logger.remove(trace)
logger.debug('this is another debug message')

看这里,咱们首先 add 了一个 sink,而后获取它的返回值,赋值为 trace。随后输入了一条日志,而后将 trace 变量传给 remove 办法,再次输入一条日志,看看后果是怎么的。

控制台输入如下:

2019-10-13 23:18:26.469 | DEBUG    | __main__:<module>:4 - this is a debug message
2019-10-13 23:18:26.469 | DEBUG    | __main__:<module>:6 - this is another debug message

日志文件 runtime.log 内容如下:

2019-10-13 23:18:26.469 | DEBUG    | __main__:<module>:4 - this is a debug message

能够发现,在调用 remove 办法之后,的确将历史 log 删除了。

这样咱们就能够实现日志的刷新从新写入操作。

rotation 配置

用了 loguru 咱们还能够十分不便地应用 rotation 配置,比方咱们想一天输入一个日志文件,或者文件太大了主动分隔日志文件,咱们能够间接应用 add 办法的 rotation 参数进行配置。

咱们看看上面的例子:

logger.add('runtime_{time}.log', rotation="500 MB")

通过这样的配置咱们就能够实现每 500MB 存储一个文件,每个 log 文件过大就会新创建一个 log 文件。咱们在配置 log 名字时加上了一个 time 占位符,这样在生成时能够主动将工夫替换进去,生成一个文件名蕴含工夫的 log 文件。

另外咱们也能够应用 rotation 参数实现定时创立 log 文件,例如:

logger.add('runtime_{time}.log', rotation='00:00')

这样就能够实现每天 0 点新创建一个 log 文件输入了。

另外咱们也能够配置 log 文件的循环工夫,比方每隔一周创立一个 log 文件,写法如下:

logger.add('runtime_{time}.log', rotation='1 week')

这样咱们就能够实现一周创立一个 log 文件了。

retention 配置

很多状况下,一些十分长远的 log 对咱们来说并没有什么用途了,它白白占据了一些存储空间,不革除掉就会十分节约。retention 这个参数能够配置日志的最长保留工夫。

比方咱们想要设置日志文件最长保留 10 天,能够这么来配置:

logger.add('runtime.log', retention='10 days')

这样 log 文件外面就会保留最新 10 天的 log,妈妈再也不必放心 log 沉积的问题啦。

compression 配置

loguru 还能够配置文件的压缩格局,比方应用 zip 文件格式保留,示例如下:

logger.add('runtime.log', compression='zip')

这样能够更加节俭存储空间。

字符串格式化

loguru 在输入 log 的时候还提供了十分敌对的字符串格式化性能,像这样:

logger.info('If you are using Python {}, prefer {feature} of course!', 3.6, feature='f-strings')

这样在增加参数就十分不便了。

Traceback 记录

在很多状况下,如果遇到运行谬误,而咱们在打印输出 log 的时候万一不小心没有配置好 Traceback 的输入,很有可能咱们就没法追踪谬误所在了。

但用了 loguru 之后,咱们用它提供的装璜器就能够间接进行 Traceback 的记录,相似这样的配置即可:

@logger.catch
def my_function(x, y, z):
    # An error? It's caught anyway!
    return 1 / (x + y + z)

咱们做个测试,咱们在调用时三个参数都传入 0,间接引发除以 0 的谬误,看看会呈现什么状况:

my_function(0, 0, 0)
运行结束之后,能够发现 log 外面就呈现了 Traceback 信息,而且给咱们输入了过后的变量值,真的是不能再赞了!后果如下:

> File "run.py", line 15, in <module>
    my_function(0, 0, 0)
    └ <function my_function at 0x1171dd510>

  File "/private/var/py/logurutest/demo5.py", line 13, in my_function
    return 1 / (x + y + z)
                │   │   └ 0
                │   └ 0
                └ 0

ZeroDivisionError: division by zero

因而,用 loguru 能够十分不便地实现日志追踪,debug 效率可能要高上十倍了?

以上就是本次分享的所有内容,想要理解更多 python 常识欢送返回公众号:Python 编程学习圈,发送“J”即可收费获取,每日干货分享

正文完
 0