Python日志方案

一、常用日志方案

  • 自定义封装log类

     rq = time.strftime('%Y%m%d', time.localtime(time.time()))
     class Log(object):    
      '''日志类 ''' 
      def __init__(self, name):    
           self.path = "/User/aaa/log/" # 定义日志存放路径
           self.filename = self.path + rq + '.log'    # 日志文件名称
           self.name = name    # 为%(name)s赋值
           self.logger = logging.getLogger(self.name)    
            #控制日志文件中记录级别    
           self.logger.setLevel(logging.INFO)    
            #控制输出到控制台日志格式、级别    
           self.ch = logging.StreamHandler()    
           gs = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s[line:%(lineno)d] - %(message)s')    
           self.ch.setFormatter(gs)    
          # self.ch.setLevel(logging.NOTSET)    写这个的目的是为了能控制控制台的日志输出级别,但是实际中不生效,不知道为啥,留着待解决
            #日志保留10天,一天保存一个文件    
           self.fh = logging.handlers.TimedRotatingFileHandler(self.filename, 'D', 1, 10)    
            #定义日志文件中格式
           self.formatter = logging.Formatter('%(asctime)s - %(levelname)s -   %(name)s[line:%(lineno)d] - %(message)s')
           self.fh.setFormatter(self.formatter)
           self.logger.addHandler(self.fh)
           self.logger.addHandler(self.ch) 
    
     class customError(Exception):  
     u'''    自定义异常类,用在主动输出异常时使用,用 raise关键字配合使用,例:            
              if True:                
                    pass
              else:
                    raise customError(msg)    '''
         def __init__(self, msg=None):
              self.msg = msg   
         def __str__(self):
               if self.msg:
                      return self.msg
               else:
                      return u"某个不符合条件的语法出问题了"
    

    在其他模块中引用的用法:

    from Function.Log_main_class import *
    try:
       log = Log("casename")
       log.info("msg")
       if True:                
               pass
       else:
               raise customError(msg)
    except BaseException as msg:
       log.exception(msg)
    

    特别说明:def exception(self, msg)方法的使用,可以将控制台中输出的错误日志全部保存到日志文件中,对于根据日志分析问题的场景需求,特别有用。这个函数一般配合try使用:

    try:
    	r = 10/0
    	print r
    except Exception as ex:
    	log.exception(ex)
    
  • Python3彩色日志包

    • 安装

      pip install colorful-logger
      
    • 使用

      • 默认logger

        可以直接使用默认的logger实例输出日志,默认的日志等级是warning

        from colorful_logger.logger import logger
        
        with logger:
          logger.debug("This is a debug message.")
          logger.info("This is a info message.")
          logger.warning("This is a warning message.")
          logger.error("This is a error message.")
          logger.critical("This is a critical message.")
          logger.fatal("This is a fatal message.")
        

        如你所见,logger需要在with语句中执行,因为本包使用的是QueueListener调用日志输出,使用logger输出日志前需要调用start()方法,使用结束后需要调用stop()方法,我将这两个方法封装到了with语句中,非特殊场景下,不需要单独调用start()和stop()方法。

        如果显式调用了start()方法,一定要在调用日志后执行stop()方法

        • 自定义logger

          也可以自定义name,日志等级、是否在终端显示、是否保存到文件:

          from colorful_logger.logger import get_logger, DEBUG
          
          logger = get_logger(name="sample_logger", level=DEBUG, file_path="./test.log")
          
          with get_logger(name="sample_logger", level=DEBUG, file_path="./test.log", file_colorful=True) as logger:
            logger.debug("This is a debug message.")
            logger.info("This is a info message.")
            logger.warning("This is a warning message.")
            logger.error("This is a error message.")
            logger.critical("This is a critical message.")
            logger.fatal("This is a fatal message.")
          

          在with语句外输出日志时可能会有意外情况,达不到预期结果。

          输出到文件的日志默认不是彩色日志

          如果你需要在文件中保存彩色日志,将file_colorful参数设置为True即可,本例中保存的就是彩色日志。

          彩色日志文件的作用特只有一个,就是在终端查看实时日志:

          tailf -f -n 1000 xxx.log
          

          这样查看日志才是彩色的

          FATALCRITICAL本就是影响程序运行的严重错误,而 python 默认的日志管理器中此方法与其他方法没有什么区别,这让我觉得莫名其妙,在本包中,我在fatal方法中加入了sys.exit(1)用来退出程序。如果在程序出现严重错误时不想退出程序,可以调用critical方法。

          get_logger方法:

          def get_logger(
              name: Optional[str] = None,
              level: int = logging.WARNING,
              show: bool = True,
              file_path: Optional[str] = None,
              file_colorful: bool = False,
          ) -> Logger: ...
          
          • name logger实例名,可以在不同的实例对象调用日志时为日志命名
          • level日志等级
          • show是否在终端显示。如果你想用此彩色日志包的话,通常是想在终端显示的吧
          • file_path是否保存到文件。默认是None,当其不是None时,会保存到对应的文件中
          • file_colorful保存到文件的日志是否为彩色,默认为False,以python默认的日志格式保存
      • 子logger

        子logger除了name与父logger不同,其余均相同,也不会输出第三方库的日志

        子logger在父logger的with语句中执行并不意味着一定在with语句中直接调用,在with语句中的某个函数中执行就可以,比如:

        # main.py
        from colorful_logger import get_logger
        from colorful_logger.logger import DEBUG
        
        from other_file import test
        
        # parent logger
        logger = get_logger(name="sample_logger", level=DEBUG, file_path="./test.log")
        
        with logger:
          test()
        # other_file.py
        
        test_logger = child_logger("test_logger", logger)
        
        def test():
          test_logger.error("test error")
        
  • Python中更优雅的记录日志

    • 安装使用

      pip install loguru
      
    • 快速上手

      如图logging一样,loguru也有定义日志等级。不同的日志等级,输出效果也不一样(默认的等级由低到高是DEBUGINFOWARNINGERRORCRITICAL,也可以自己使用level函数定义)

      from loguru  import logger 
      
      logger.debug("That's it, beautiful and simple logging!")
      
      logger.info("this is a info message!")
      
      

      类似logging中的logger.addHandler,loguru统一使用add函数来管理格式、文件输出、过滤等操作,它提供了许多参数来实现logger.addHandler中的配置更加简单方便。

      其中sink是最重要的参数,可以传入不同的数据类型。传入文件路径、文件句柄、sys.stderr、甚至logging模块的HandlerFileHandlerStreamHandler等,这样就可以快速实现自定义的Handler配置。

      通过给remove方法传递add方法返回的对象, 可以删除add方法添加的sink,这里的remove并不是删除test2.log文件,而是停止向该文件输出日志,需要需要继续记录日志则需要重新add日志文件。

      from loguru import logger
      
      obj = logger.add(sink='info.txt', format="{time} {level} {message}", filter="my_module", level="INFO")
      logger.info("That's it, beautiful and simple logging!")
      logger.remove(obj)
      

      rotationretentioncompression进行日志窗口、更新、压缩管理。

      logger.add("file_1.log", rotation="10 MB") # 日志文件的大小是10M,过大就会重新生成一个文件
      logger.add("file_2.log", rotation="00:00") # 每天0点创建新日志文件
      logger.add("file_3.log", rotation="1 week") # 自动更新旧文件
      logger.add("file_X.log", retention="3 days") # 每3天自动清理旧文件
      logger.add("file_Y.log", compression="zip") # zip压缩文件
      

      支持控制台输出添加颜色, 除了基础色,loguru甚至允许16进制、RGB格式的颜色值和加粗、下划线等样式。

      logger.add(sys.stdout, colorize=True, format="<green>{time}</green> <level>{message}</level>") 
      

      使用装饰器@logger.catch可以和logging一样使用logger.exception函数来记录异常信息。

      @logger.catch
      def my_function(x, y, z):
          # An error? It's caught anyway!
          return 1 / (x + y + z)
          
      def func(a, b):
          return a / b
      
      def nested(c):
          try:
              func(5, c)
          except ZeroDivisionError:
              logger.exception("What?!")    
              
      nested(0) 
      

      使用exception方法输出的异常信息包含堆栈信息和当前变量的值,方便问题定位。

      2018-07-17 01:38:43.975 | ERROR    | __main__:nested:10 - What?!
      Traceback (most recent call last):
      
        File "test.py", line 12, in <module>
          nested(0)
          └ <function nested at 0x7f5c755322f0>
      
      > File "test.py", line 8, in nested
          func(5, c)
          │       └ 0
          └ <function func at 0x7f5c79fc2e18>
      
        File "test.py", line 4, in func
          return a / b
                 │   └ 0
                 └ 5
      
      ZeroDivisionError: division by zero
      

      使用serialize 可以将日志转换为JSON格式,enqueue 可以保证多线程、多进程安全。

      logger.add("somefile.log", serialize=True) # 序列化为json 
      logger.add("somefile.log", enqueue=True) 
      

      修改时间格式:

      logger.add("file.log", format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}") 
      
  • Python中logging记录日志,使用@log装饰器

    • 导入依赖包

      import logging #日志
      import time # 时间
      import os # 路径
      
    • 创建一个logger

      logger = logging.getLogger() #获取logger
      logger.setLevel(logging.DEBUG) # 设置记录等级时debug
      
    • 创建日志文件

      rq = time.strftime('%Y-%m-%d-%H', time.localtime(time.time())) #以小时分割记录日志
      log_path = os.getcwd() + '/../Logs/all/' #设置日志路径
      log_name = log_path + rq + '_all.log' #设置日志名称
      logfile = log_name
      if not os.path.exists(log_path):  # 创建路径
          os.makedirs(log_path)
      for root, dirs, files in os.walk(os.path.dirname(log_path)): # 删除空文件
          for i in files:
              fpath = os.path.join(root, i)
              if os.path.getsize(fpath) == 0:
                  os.remove(fpath)
      if not os.path.exists(logfile): # 创建文件
          f = open(logfile, mode='w', encoding="utf-8")
          f.close()
      fh_all = logging.FileHandler(logfile, mode='a', encoding='utf-8')  # 输出到文件
      fh_all.setLevel(logging.DEBUG) 
      # 以上是所有日志,其他分类日志同上
      ch = logging.StreamHandler()
      ch.setLevel(logging.WARNING)  # 控制台输出的日志级别
      
    • 定义输出格式

      formatter = logging.Formatter(
          "%(asctime)s - %(filename)s[line:%(lineno)d](%(funcName)s) - %(levelname)s: %(message)s") 
      fh_debug.setFormatter(formatter) # 添加格式
      logger.addHandler(fh_debug) # 保存
      ch.setFormatter(formatter)
      logger.addHandler(ch)
      
    • 完整代码

         import logging
         import os
         import time
         
         
         logger = logging.getLogger()
         logger.setLevel(logging.DEBUG)  # Log等级总开关
         
         # 第二步,创建一个 file handler,用于写入日志文件
      def log(func):
          @functools.wraps(func)
          def wrapper(*args, **kw):
              dicc = {}
              dinp = {}
              varnames = func.__code__.co_varnames
              deft = func.__defaults__
              if deft is None:
                  deft = ()
      
              for i in range(len(args)):
                  dinp[varnames[i]] = str(args[i])
              for j in range(len(deft)):
                  dinp[varnames[i + j + 1]] = str(deft[j])
              for i, j in kw.items():
                  dinp[i] = str(j)
              # print(str(func.__name__))
              filter = ContextFilter(
                  os.path.basename(str(func.__code__.co_filename)), int(func.__code__.co_firstlineno), str(func.__name__))
      
              try:
                  aa = func(*args, **kw)
              except Exception as e:
                  aa = 'err:' + str(e)
                  if aa is None:
                      dretrun = ''
                  elif isinstance(aa, str):
                      dretrun = aa
                  elif isinstance(aa, tuple):
                      dretrun = list(aa)
                  else:
                      dretrun = str(aa)
                  # dicc['run_info'] = dinfo
                  dicc['run_input'] = dinp
                  dicc['run_return'] = dretrun
                  logger.addFilter(filter)
                  logger.debug(dicc)
                  logger.error(func.__name__ + '运行错误:', exc_info=True)
                  logger.removeFilter(filter)
      
                  raise e
      
              if aa is None:
                  dretrun = ''
              elif isinstance(aa, str):
                  dretrun = aa
              elif isinstance(aa, tuple):
                  dretrun = list(aa)
              else:
                  dretrun = str(aa)
              # dicc['run_info'] = dinfo
              dicc['run_input'] = dinp
              dicc['run_return'] = dretrun
              logger.addFilter(filter)
              logger.debug(dicc)
              logger.removeFilter(filter)
      
              return aa
      
          return wrapper
      
      
    • 使用logger记录日志

      logger.error('err', exc_info=True) #exc_info 记录错误信息
      
    • 使用@log装饰器记录log

      • 导入依赖包

        import functools 
        
      • 完整代码

        def log(func):
         @functools.wraps(func)
         def wrapper(*args, **kw):
             dicc = {}
             dinp = {}
             varnames = func.__code__.co_varnames
             deft = func.__defaults__
             if deft is None:
                 deft = ()
        
             for i in range(len(args)):
                 dinp[varnames[i]] = str(args[i])
             for j in range(len(deft)):
                 dinp[varnames[i + j + 1]] = str(deft[j])
             for i, j in kw.items():
                 dinp[i] = str(j)
             # print(str(func.__name__))
             filter = ContextFilter(
                 os.path.basename(str(func.__code__.co_filename)), int(func.__code__.co_firstlineno), str(func.__name__))
        
             try:
                 aa = func(*args, **kw)
             except Exception as e:
                 aa = 'err:' + str(e)
                 if aa is None:
                     dretrun = ''
                 elif isinstance(aa, str):
                     dretrun = aa
                 elif isinstance(aa, tuple):
                     dretrun = list(aa)
                 else:
                     dretrun = str(aa)
                 # dicc['run_info'] = dinfo
                 dicc['run_input'] = dinp
                 dicc['run_return'] = dretrun
                 logger.addFilter(filter)
                 logger.debug(dicc)
                 logger.error(func.__name__ + '运行错误:', exc_info=True)
                 logger.removeFilter(filter)
        
                 raise e
        
             if aa is None:
                 dretrun = ''
             elif isinstance(aa, str):
                 dretrun = aa
             elif isinstance(aa, tuple):
                 dretrun = list(aa)
             else:
                 dretrun = str(aa)
             # dicc['run_info'] = dinfo
             dicc['run_input'] = dinp
             dicc['run_return'] = dretrun
             logger.addFilter(filter)
             logger.debug(dicc)
             logger.removeFilter(filter)
        
             return aa
        
         return wrapper
        
      • 发现输出文件格式不是喜欢的类型,在最上方添加方法

        class ContextFilter(logging.Filter):
            # filename = 'IP'
            # lineno = 'USER'
            def __init__(self, filename, lineno, funcname):
                self.filename = filename
                self.lineno = lineno
                self.funcname = funcname
        
            def filter(self, record):
                record.filename = self.filename
                record.lineno = self.lineno
                record.funcName = self.funcname
                return True
        

二、原文链接

https://www.jianshu.com/p/a02e013bc935?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

https://www.jianshu.com/p/2898de660cb7

https://www.jianshu.com/p/aff42e5e6099

https://www.jianshu.com/p/843d4189bb42

https://www.jianshu.com/p/3f184414d781

https://loguru.readthedocs.io/en/stable/overview.html

命令行参数解析: https://www.jianshu.com/p/2fc01be2e6e1

posted @ 2021-07-14 16:06  砚台是黑的  阅读(119)  评论(0编辑  收藏  举报