日志对于一个应用程序而言,不可获取;特别是在问题定位,链路分析往往是最直观,最有效的富足工具;所以在python中可以怎样便捷的使用日志模块呢
I. logging 模块
我们这里使用logging来记录我们的日志,一般来说,我们常用的日志无非以下几个点
- 日志落盘,存文件,支持定期生成一个备份文件;支持自动删除过期日志
 
- 异常时,可以输出完整的堆栈,帮助定位出问题的代码行
 
- 不同的日志level
 
1. 基本使用姿势
首先我们来看一下日志模块的基本使用姿势,默认的场景下,日志输出在控制台;可以通过指定文件名的方式来讲日志输出到对应的文件中
1 2 3 4 5 6 7 8 9 10 11
   | import logging
 
  logging.basicConfig(filename='logger.log', level=logging.INFO)
 
  logging.debug('debug message') logging.info('info message') logging.warning('warn message') logging.error('error message') logging.critical('critical message')
   | 
 
因为指定了日志的级别为INFO,所以debug日志将不会显示在日志文件中,具体输出如下
1 2 3 4
   | INFO:root:info message WARNING:root:warn message ERROR:root:warn message CRITICAL:root:critical message
   | 
 
上面的使用虽说实现了日志记录的功能,但是相对原始;通常我们希望日志的输出包括一些基本信息,如输出日志的时间
这个时候就可以考虑通过format来设置输出日志的格式
| 格式 | 
描述 | 
| %(levelno)s | 
打印日志级别的数值 | 
| %(levelname)s | 
打印日志级别名称 | 
| %(pathname)s | 
打印当前执行程序的路径 | 
| %(filename)s | 
打印当前执行程序名称 | 
| %(funcName)s | 
打印日志的当前函数 | 
| %(lineno)d | 
打印日志的当前行号 | 
| %(asctime)s | 
打印日志的时间 | 
| %(thread)d | 
打印线程id | 
| %(threadName)s | 
打印线程名称 | 
| %(process)d | 
打印进程ID | 
| %(message)s | 
打印日志信息 | 
下面我们在日志的输出中,添加上时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14
   | import logging
 
  logger = logging.getLogger('test')
 
  fh = logging.FileHandler(filename='logger.log') fh.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
 
  logger.addHandler(fh)
  logger.setLevel(logging.INFO) logger.info('new info message')
   | 
 
实际输出结果如
1
   | 2019-11-04 18:25:57,519 - INFO - new info message
   | 
 
3. Handler
一下几个说明,主要来自博文: Python模块之Logging(四)——常用handlers的使用
a. StreamHandler
流handler——包含在logging模块中的三个handler之一。
能够将日志信息输出到sys.stdout, sys.stderr 或者类文件对象(更确切点,就是能够支持write()和flush()方法的对象)
1
   | logging.StreamHandler(stream=None)
   | 
 
b. FileHandler
继承自StreamHandler。将日志信息输出到磁盘文件上
1
   | logging.FileHandler(filename, mode='a', encoding=None, delay=False)
   | 
 
c. NullHandler
什么都不干,相当于吃掉日志
d. WatchedFileHandler
位于logging.handlers模块中。用于监视文件的状态,如果文件被改变了,那么就关闭当前流,重新打开文件,创建一个新的流。由于newsyslog或者logrotate的使用会导致文件改变。这个handler是专门为linux/unix系统设计的,因为在windows系统下,正在被打开的文件是不会被改变的
e. RotatingFileHandler
位于logging.handlers支持循环日志文件。
1
   | logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=0)
   | 
 
参数maxBytes和backupCount允许日志文件在达到maxBytes时rollover.当文件大小达到或者超过maxBytes时,就会新创建一个日志文件。上述的这两个参数任一一个为0时,rollover都不会发生。也就是就文件没有maxBytes限制。backupcount是备份数目,也就是最多能有多少个备份。命名会在日志的base_name后面加上.0-.n的后缀,如example.log.1,example.log.1,…,example.log.10。当前使用的日志文件为base_name.log。
f. TimedRotatingFileHandler
定时循环日志handler,位于logging.handlers,支持定时生成新日志文件。
1
   | logging.handlers.TimedRotatingFileHandler(filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False)
   | 
 
参数when决定了时间间隔的类型,参数interval决定了多少的时间间隔。如when=‘D’,interval=2,就是指两天的时间间隔,backupCount决定了能留几个日志文件。超过数量就会丢弃掉老的日志文件
‘S’        |  秒
‘M’        |  分
‘H’        |  时
‘D’        |  天
‘W0’-‘W6’  |  周一至周日
‘midnight’ |  每天的凌晨
I. 日志模块封装
接下来我们的目标是封装一个简单好用的日志工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
   |  import logging import os from logging.handlers import TimedRotatingFileHandler
  class LoggerWrapper:     def __init__(self):         self._logger = {}
      @staticmethod     def _get_path(action, path=""):         """         根据日志名,创建对应的日志路径         :param path:         :return:         """         if action != 'logs':             action = "logs/" + action + "/"
          path = action + path         if not os.path.exists(path):                          os.makedirs(path)
          return path
      def _gen_logger(self, action='logs', log_name='Crawler'):         base_logger = logging.getLogger(log_name)         base_logger.setLevel(logging.INFO)
          log_file = self._get_path(action, log_name) + "/" + log_name + ".log"         ch = TimedRotatingFileHandler(log_file, when='D', encoding="utf-8", backupCount=10)         ch.setLevel(logging.INFO)         formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')         ch.setFormatter(formatter)         base_logger.addHandler(ch)         base_logger.propagate = 0         return base_logger
      def get_logger(self, name=None):         if name is None:             key = env_wrapper.get_current_task_name()         else:             key = name
          if key not in self._logger:             action, path = key, env_wrapper.get_current_task_name()             self._logger[key] = self._gen_logger(action, path)
          return self._logger[key]
 
  | 
 
上面是一个简单的按天回滚日志,且只保存最近10天;但是在我们的测试环境下,我们可能希望日志也同时输出一份到控制台
因此我们可以稍微改造一下,拿一个EnvWrapper类,来保存项目根路径,是否输出一份到控制台
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | class EnvWrapper:     def __init__(self):         self._module_path = None         self._log_console = False              def init_env(xxx):                  pass
      def get_module_path(self):         return self._module_path              def console_log_enable(self):         return self._log_console          env_wrapper = EnvWrapper() `
   | 
 
然后我们的日志工具类可以改造成下面的case
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
   | 
  import logging import os from logging.handlers import TimedRotatingFileHandler
  from src.env.EnvWrapper import env_wrapper
  class LoggerWrapper:     def __init__(self):         self._logger = {}         self._console_init = False
      @staticmethod     def _get_path(action, path=""):         """         根据日志名,创建对应的日志路径         :param path:         :return:         """         if action != 'logs':             action = "logs/" + action + "/"
          path = env_wrapper.get_module_path() + "/" + action + path         if not os.path.exists(path):                          os.makedirs(path)
          return path
      def _gen_logger(self, action='logs', log_name='Crawler'):         base_logger = logging.getLogger(log_name)         base_logger.setLevel(logging.INFO)
          log_file = self._get_path(action, log_name) + "/" + log_name + ".log"         ch = TimedRotatingFileHandler(log_file, when='D', encoding="utf-8")         ch.setLevel(logging.INFO)         formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')         ch.setFormatter(formatter)         base_logger.addHandler(ch)         base_logger.propagate = 0
          if env_wrapper.console_log_enable() and not self._console_init:             console = logging.StreamHandler()             console.setLevel(logging.DEBUG)             console.setFormatter(formatter)             base_logger.addHandler(console)             self._console_init = True
          return base_logger
      def get_logger(self, name=None):         if name is None:             key = env_wrapper.get_current_task_name()         else:             key = name
          if key not in self._logger:             action, path = key, env_wrapper.get_current_task_name()             self._logger[key] = self._gen_logger(action, path)
          return self._logger[key]
      def error(self, msg, name=None):         log = self.get_logger(name)         log.error(msg)
      def warn(self, msg, name=None):         log = self.get_logger(name)         log.warning(msg)
      def info(self, msg, name=None):         log = self.get_logger(name)         log.info(msg)
      def debug(self, msg, name=None):         log = self.get_logger(name)         log.debug(msg)
      def exception(self, msg, name=None):         """         打印堆栈信息         :param msg:         :param name:         :return:         """         log = self.get_logger(name)         log.exception(msg)
 
  SpiderLogger = LoggerWrapper() logger = SpiderLogger.get_logger debug = SpiderLogger.debug info = SpiderLogger.info error = SpiderLogger.error warning = SpiderLogger.warn exception = SpiderLogger.exception
 
  | 
 
III. 其他
一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
2. 声明
尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
3. 扫描关注
一灰灰blog

知识星球
