アーティファクトロギング、本当に理解していますか?

loggingは、プログラムでログをログに記録して処理するためのPython標準モジュールです。

機能は非常に強力で、公式文書は非常に詳細であり、インターネット上には多くの指示とチュートリアルがありますが、それに慣れていない多くの学生にとって、いくつかの障害があります。

1つは、標準ライブラリのドキュメントが煩雑で、高度な理論的基盤が必要なことです。急いで使用すると、ドキュメントに混乱することがよくあります。

第二に、説明資料のほとんどは、公式文書のリストまたは簡単なアプリケーションのいずれかであり、実際のアプリケーションではほとんど役に立ちません。

今日は、いくつかのアプリケーションの問題から始めて、ロギングアーティファクトロギングの背後にある原理について説明します。これにより、本当に役立つようになります。

logging.debugまたはlogger.debugを使用する必要がありますか?
デバッグは、ログレベルがDEBUGで、情報、警告、エラー、評論家が含まれるログモジュールでのログ生成方法です。ここでは、説明の代表としてデバッグを使用します。

logging.debugを使用してログをしばらく記録し、logger.debugを使用してログをしばらく記録することがよくあります。何を使用すればよいですか。

最初にコードを見てください:

import logging

logging.debug('调试信息')

logger = logging.getLogger()

logger.debug('调试信息')
复制代码

最初のロギングはモジュールとして導入されました。logging.debugは、ロギングモジュールのモジュールメソッドを使用します。

ロガーはログオブジェクトであるlogging.getLogger()によって生成され、logger.debugはロガーのログオブジェクトのメソッドを呼び出します。

上記のコードのlogging.debugとlogger.debugの効果はまったく同じです。

これは、開発者が使いやすくするために、ロギングモジュールがデバッグなどの一連のモジュールメソッドを提供し、モジュールが導入された直後に使用できるためです。このように、開発者はログモジュールの詳細を気にする必要がなく、印刷のようにログを出力します。

ログをファイルに出力したり、特定のレベルのログをフィルタリングしたりするなど、ログ出力をカスタマイズする必要がある場合は、上記のgetLoggerメソッドによって取得されたログオブジェクトなど、処理用の実際のログオブジェクトを作成または取得する必要があります。コード。

我们知道,程序设计里要避免重复的设计,如果模块方法采用一套机制,日志对象上的方法采用另一套机制,就会出现重复造轮子的问题。

所以在使用模块方法,logging 其实创建了一个日志对象 —— root logger。

也就是 logging.debug 这个调用,实质上是调用 root logger 的日志方法。

相当于默认情况下 root logger 会作为日志处理对象。

如何获得 root logger 对象呢?

通过不带参数的 logging.getLogger() 方法获得。

那么 logging.debug 和 rootLogger.debug 是一会事,可以理解(但不严谨)为 logging.debug 是 rootlogger.debug 的快捷方式。

日志树
稍加留意就会观察到,程序是有层次结构的,通过相互引用,调用形成一个树状结构。

程序加载的地方是树根,比如 python 中要运行的代码文件,我们称之为 main。从树根开始长出其他枝叶。对于一个模块来说,又会形成一个自己的树。

如何用日志清楚地记录层次结构呢?

虽然直接打印出调用堆栈也可以看到调用结构,不过不太直观,缺乏业务逻辑描述。

而用 print 来打印出层次结构,需要编写大量的代码才能反射出(通过运行状态获取代码状态的一种方式)调用环境。

logging 提供了完毕的解决方案。

前面提到的 root logger 就是整个日志树的根,其他所有的 logger 都是从 root logger 伸展出来的枝叶。只要通过 getLogger(loggername) 方法获得的 logger 对象,都是伸展自 root logger 的。

如何向下伸展呢?

很简单,就像引用模块的层次关系一样,用 . 分隔层次就好了,例如:

logger = logging.getLogger('mod1.mod2.mod3')

logger.debug("调试信息")
复制代码

语句 logging.getLogger('mod1.mod2.mod3') 实际上创建了三个 logger,名称分别是 mod1、mod1.mod2 和 mod1.mod2.mod3

mod1 为根,mod1.mod2 为子,mod1.mod2.mod3 为孙。

如果在 mod1 上设置了日志处理器(handler),那么其他两个的日志对象都会用到这个处理器。

这样不但记录的日志更清晰而且,可以为同一个根的日志对象设置可以共享的日志处理方式。

这样感觉也不方便,需要些那么多层次,如何才能更方便呢?在下面的 实践参考 里会有说明。

logging.basicConfig 的功与过
说完了日志模块的树状结构,来看看一个很常用的设置方法 basicConfig。

它可以方便的设置日志处理和记录方式,如没必要,不用为每个日志对象单独设置。

根据第一节的分析,我们知道,直接使用模块方法,用的其实是 root logger,那么就能明白 basicConfig 设置了 root logger 的日志处理方式。

这就意味着:

一旦设置了通过 logging.basicConfig 设置了日志处理方式,其他所有日志都很受到影响。

另外 basicConfig 是个一次性方法,即:

只有第一次设置有效,其后设置无效

本来是个一劳永逸的方法。

但用错了地方,就会很麻烦。

看下例子:

__all__ = ['Connection', 'ConnectionPool', 'logger']

warnings.filterwarnings('error', category=pymysql.err.Warning)
# use logging module for easy debug
logging.basicConfig(format='%(asctime)s %(levelname)8s: %(message)s', datefmt='%m-%d %H:%M:%S')
logger = logging.getLogger(__name__)
logger.setLevel('WARNING')
复制代码

这段代码中,用 logging.basicConfig 对日志做了设置,意思是后面的日志都按照这样的方式输出。

但它是一个底层模块 —— pymysqlpool。

pymysqlpool 封装了 pymysql 模块,提供了链接池特性,在多线程处理数据库场景下很有用。

也就是说,pymysqlpool 只会被引用加载,不会作为 main 被加载,这就比较尴尬了,因为 main 中对日志的设置就没有效果。

作为一个服务类模块(相对于业务的底层模块),不要通过 basicConfig 来设置日志模式,要么通过自己专属的日志对象来设置,要么不去设置,统一交给 main 去设置,例如:

logger = logging.getLogger(__name__)

fmt = logging.Formatter("%(asctime)s %(levelname)8s: %(message)s", datefmt='%m-%d %H:%M:%S')
hdl = logging.StreamHandler()
hdl.setFormatter(fmt)
logger.addHandler(hdl)

logger.setLevel('WARNING')
复制代码

如果为了测试,可以在测试的初始化方法中,使用 basicConfig 来设置,因为测试时,模块往往是被作为程序入库加载的。

实践参考
了解了日志模块的一下特性,和其中的原理之后,这里有几条实践参考。

  • 不要再子模块中使用 logging.basicConfig 设置日志模式

  • 强烈建议在任何模块中通过 logger = logging.getLogger(__name__) 来创建日志对象 因为 name 代表的就是模板被加载的引用名称。

  • 例如 from a.b.c import b 模块 c 中的 name 值就为 a.b.c。

  • 而且这个引用名称刚好符合 logger 定义的层次结构。

  • 通过命令行参数设置不同类型的日志,见代码:

    import logging
    import argparse
    logger = logging.getLogger(__name__)
    
    def create_args_parse():
      parser = argparse.ArgumentParser(description="参数列表")
      parser.add_argument('-d', '--debug', action='store_true', help='调试模式')
      # 加入其他命令行参数
          
      return parser
    
    def set_logger(debug):
      formatter = logging.Formatter('%(asctime)s - %(levelname)8s - %(name)s - %(filename)s:%(lineno)d - %(thread)d- %(funcName)s:\t%(message)s')
      if debug:
          hd = logging.StreamHandler()
          logger.setLevel(logging.DEBUG)
          hd.setFormatter(formatter)
      else:
          hd = logging.FileHandler(f'{__name__}.log', 'a', encoding='utf-8')
          logger.setLevel(logging.INFO)
          hd.setFormatter(formatter)
      logger.addHandler(hd)
    
    if __name__ == '__main__':
     parser = create_args_parse()
     args = parser.parse_args()
     debug = args.debug
     set_logger(debug)
     ...
    复制代码

    代码有点长,但不难懂。

  • create_args_parse 方法用于解析命令行参数,其中定义了一个 debug 参数,表示开启调试模式

  • set_logger 方法接收一个是否为调试模式的参数,根据是否为调试模式,设置不同的日志模式

  • main 中,首先调用 create_args_parse 获得命令行参数对象,然后从中解析出参数,提取 debug 模式,传送给 set_logger 方法,设置日志模式

  • 这样只需要在运行程序时,加上参数 -d 就可以让日志打印到终端上,不加,日志就会自动去 __main__.log 日志文件中去了。

总结
python 为我们提供了很多便利的功能,有些需要真的用到才能有所体会,所以在遇到问题时,需要多研究一下,找到其中的特点和内在的原理或机制,这样就能更好的应用了。

在我理解了 logging 的原理之后,已经在我的很多项目中发挥了巨大作用,而且再也不必纠结于怎么用,如何更合理等这些问题了。

以上就是本次分享的所有内容,想要了解更多 python 知识欢迎前往公众号:Python 编程学习圈,每日干货分享

おすすめ

転載: juejin.im/post/7105600396537626654