Python 日志logging模块初探及多线程踩坑(1)

简介: Python 日志logging模块初探及多线程踩坑(1)


目录

1、日志记录的重要性

2、Python官方logging模块

3、logging 模块的基本架构和对象功能

4、打印到控制台

5、利用logging.basicConfig()保存log到文件

6、利用logging模块组件灵活实现需求

7、实现logging中TimedRotatingFileHandler多线程应用


1、日志记录的重要性

在开发过程中,如果程序运行出现了问题,我们是可以使用我们自己的 Debug 工具来检测到到底是哪一步出现了问题,如果出现了问题的话,是很容易排查的。但程序开发完成之后,我们会将它部署到生产环境中去,这时候代码相当于是在一个黑盒环境下运行的,我们只能看到其运行的效果,是不能直接看到代码运行过程中每一步的状态的。在这个环境下,运行过程中难免会在某个地方出现问题,甚至这个问题可能是我们开发过程中未曾遇到的问题,碰到这种情况应该怎么办?

如果我们现在只能得知当前问题的现象,而没有其他任何信息的话,如果我们想要解决掉这个问题的话,那么只能根据问题的现象来试图复现一下,然后再一步步去调试,这恐怕是很难的,很大的概率上我们是无法精准地复现这个问题的,而且 Debug 的过程也会耗费巨多的时间,这样一旦生产环境上出现了问题,修复就会变得非常棘手。但这如果我们当时有做日志记录的话,不论是正常运行还是出现报错,都有相关的时间记录,状态记录,错误记录等,那么这样我们就可以方便地追踪到在当时的运行过程中出现了怎样的状况,从而可以快速排查问题。

因此,日志记录是非常有必要的,任何一款软件如果没有标准的日志记录,都不能算作一个合格的软件。作为开发者,我们需要重视并做好日志记录过程。

2、Python官方logging模块

logging 模块相比 print 有这么几个优点:

    • 可以在 logging 模块中设置日志等级,在不同的版本(如开发环境、生产环境)上通过设置不同的输出等级来记录对应的日志,非常灵活。
    • print 的输出信息都会输出到标准输出流中,而 logging 模块就更加灵活,可以设置输出到任意位置,如写入文件、写入远程服务器等。
    • logging 模块具有灵活的配置和格式化功能,如配置输出当前模块信息、运行时间等,相比 print 的字符串格式化更加方便易用。

    虽然logging模块功能很强大,但还是有坑在里面的,下面我们一一道来。

    3、logging 模块的基本架构和对象功能

      • Logger:即 Logger Main Class,是我们进行日志记录时创建的对象,我们可以调用它的方法传入日志模板和信息,来生成一条条日志记录,称作 Log Record。
      • Log Record:就代指生成的一条条日志记录。
      • Handler:即用来处理日志记录的类,它可以将 Log Record 输出到我们指定的日志位置和存储形式等,如我们可以指定将日志通过 FTP 协议记录到远程的服务器上,Handler 就会帮我们完成这些事情。
      • Formatter:实际上生成的 Log Record 也是一个个对象,那么我们想要把它们保存成一条条我们想要的日志文本的话,就需要有一个格式化的过程,那么这个过程就由 Formatter 来完成,返回的就是日志字符串,然后传回给 Handler 来处理。
      • Filter:另外保存日志的时候我们可能不需要全部保存,我们可能只需要保存我们想要的部分就可以了,所以保存前还需要进行一下过滤,留下我们想要的日志,如只保存某个级别的日志,或只保存包含某个关键字的日志等,那么这个过滤过程就交给 Filter 来完成。
      • Parent Handler:Handler 之间可以存在分层关系,以使得不同 Handler 之间共享相同功能的代码。

      以上就是整个 logging 模块的基本架构和对象功能,了解了之后我们详细来了解一下 logging 模块的用法。

      4、打印到控制台

      import logging
      logging.debug('debug 信息')
      logging.warning('只有这个会输出。。。')
      logging.info('info 信息')

      image.gif

      由于默认设置的等级是warning,所有只有warning的信息会输出到控制台。不推荐使用

      image.gif

      -------------------------------------------------------------------

      利用logging.basicConfig()打印信息到控制台

      import logging
      logging.basicConfig(format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s',
                          level=logging.DEBUG)
      logging.debug('debug 信息')
      logging.info('info 信息')
      logging.warning('warning 信息')
      logging.error('error 信息')
      logging.critical('critial 信息')

      image.gif

      由于在logging.basicConfig()中的level 的值设置为logging.DEBUG, 所有日志级别高于等于debug的log才能打印到控制台。

      日志级别: debug < info < warning < error < critical

      5、利用logging.basicConfig()保存log到文件

      logging.basicConfig(level=logging.DEBUG,
                          filename='new.log',
                          filemode='a',##模式,有w和a,w就是写模式,每次都会重新写日志,覆盖之前的日志
                          #a是追加模式,默认如果不写的话,就是追加模式
                          format=
                          '%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s'
                          #日志格式
                          )注意的是:一旦在logging.basicConfig()设置filename 和filemode,则只会保存log到文件,不会输出到控制台。!!!

      2019/5/29 补充:

       但当某些信息较长,会导致日志输出信息不够美观   我们可以对其的固定长度等进行设置:

      %%(asctime)s %%(name)s %%(levelname)-8s %%(filename)-25s %%(lineno)-4s %%(message)s

      image.gif

      -8 的意思是左对齐,固定长度8 ,默认用空格填充

      下面还有一些常用的设置:

      6、利用logging模块组件灵活实现需求

      咳咳,当正题了~~,推荐使用Logging组件: 记录器(Logger )、处理器(Handler)、过滤器(Filter)和格式化器(Formatter )。

      简单介绍常用的配置一下:

      Logger

      Logger.setLevel()指定logger将会处理的最低的安全等级日志信息,。

      Logger.addHandler()和Logger.removeHandler()从记录器对象中添加和删除处理程序对象。处理器详见Handlers。

      Logger.addFilter()和Logger.removeFilter()从记录器对象添加和删除过滤器对象。

      Handler

      logging.StreamHandler -> 控制台输出

      logging.FileHandler  -> 文件输出

      logging.handlers.RotatingFileHandler -> 按照大小自动分割日志文件,一旦达到指定的大小重新生成文件

      logging.handlers.TimedRotatingFileHandler  -> 按照时间自动分割日志文件 

      Formatters

      Formatter对象设置日志信息最后的规则、结构和内容,默认的时间格式为%Y-%m-%d %H:%M:%S,下面是Formatter常用的一些信息

      %(name)s

      Logger的名字

      %(levelno)s

      数字形式的日志级别

      %(levelname)s

      文本形式的日志级别

      %(pathname)s

      调用日志输出函数的模块的完整路径名,可能没有

      %(filename)s

      调用日志输出函数的模块的文件名

      %(module)s

      调用日志输出函数的模块名

      %(funcName)s

      调用日志输出函数的函数名

      %(lineno)d

      调用日志输出函数的语句所在的代码行

      %(created)f

      当前时间,用UNIX标准的表示时间的浮 点数表示

      %(relativeCreated)d

      输出日志信息时的,自Logger创建以 来的毫秒数

      %(asctime)s

      字符串形式的当前时间。默认格式是 “2003-07-08 16:49:45,896”。逗号后面的是毫秒

      %(thread)d

      线程ID。可能没有

      %(threadName)s

      线程名。可能没有

      %(process)d

      进程ID。可能没有

      %(message)s

      用户输出的消息

      来一个例子,大家就明白了

      import logging
      from logging import handlers
      filename = os.path.join(dirpath+"log")
      fmt='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s')
      log_format = logging.Formatter(fmt) #设置日志格式
      sh = logging.StreamHandler() #往屏幕上输出
      sh.setFormatter(fmt) #设置屏幕上显示的格式
      fh = logging.handlers.TimedRotatingFileHandler(filename=filename , interval=1, when='M', backupCount=2, delay=True) #保存日志到文件
      fh.suffix = "%Y-%m-%d_%H-%M"
      fh.extMatch = re.compile(r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$")
      fh.setFormatter(fmt)
      # log
      logger = logging.getLogger(__name__)
      logger.setLevel("DEBUG") #设置日志等级
      logger.addHandler(sh)
      logger.addHandler(fh)

      image.gif

      TimedRotatingFileHandler 是以时间分割

      filename 是输出日志文件名的前缀,比如log/myapp.log

      when 是一个字符串的定义如下:

      “S”: Seconds

      “M”: Minutes

      “H”: Hours

      “D”: Days

      “W”: Week day (0=Monday)

      “midnight”: Roll over at midnight

      interval 是指等待多少个单位when的时间后,Logger会自动重建文件,当然,这个文件的创建取决于filename+suffix,若这个文件跟之前的文件有重名,则会自动覆盖掉以前的文件,所以有些情况suffix要定义的不能因为when而重复。backupCount 是保留日志个数。默认的0是不会自动删除掉日志。若设2,则在文件的创建过程中库会判断是否有超过这个2,若超过,则会从最先创建的开始删除。

      extMatch会根据when的值进行初始化:

      ‘S’: suffix=”%Y-%m-%d_%H-%M-%S”, extMatch=r”\^d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}”;

      ‘M’:suffix=”%Y-%m-%d_%H-%M”,extMatch=r”^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}”;

      ‘H’:suffix=”%Y-%m-%d_%H”,extMatch=r”^\d{4}-\d{2}-\d{2}_\d{2}”;

      ‘D’:suffxi=”%Y-%m-%d”,extMatch=r”^\d{4}-\d{2}-\d{2}”;

      ‘MIDNIGHT’:”%Y-%m-%d”,extMatch=r”^\d{4}-\d{2}-\d{2}”;

      ‘W’:”%Y-%m-%d”,extMatch=r”^\d{4}-\d{2}-\d{2}”;

       

      这边如果delay=False,表面直接输入文件,不入缓存。会导致‘win32Error :  文件被占用的错误’.这是由于TimedRotatingFileHandler并不是线程安全的。

      而且运行发现,每个进程都保持一个固定的文件句柄,导致在达到条件回滚时,相互之间的rename会相互干扰,还有好多问题

      logging中RotatingFileHandler和TimedRotatingFileHandler对于多进程不支持。

      如果觉得文章对你有帮助,欢迎关注微信公众号:小牛呼噜噜

      7、实现logging中TimedRotatingFileHandler多线程应用

      1、修改源码,但在实际开发过程,特别麻烦。

      2、引用第三方库ConcurrentLogHandler,但它只支持文件大小分割日志,但按时间分割日志,是比较常见的。

      3、继承父类,重写方法。简单易用。下面具体讲解这部分:

      网上重写的方法都多多少少有点问题。github上有现成开源的,写得非常完善,经得起实战。但也有缺点(性能较低)

      https://github.com/kieslee/mlogging

      不过要注意的是,作者虽然说它只支持linux,但windows也可以,不过要做一点修改:

      下面是源码:继承重写了logging的StreamHandler:

      from logging import StreamHandler, FileHandler
      from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
      import fcntl, time, os, codecs, string, re, types, cPickle, struct, shutil
      from stat import ST_DEV, ST_INO, ST_MTIME
      class StreamHandler_MP(StreamHandler):
          """
          A handler class which writes logging records, appropriately formatted,
          to a stream. Use for multiprocess.
          """
          def emit(self, record):
              """
              Emit a record.
                  First seek the end of file for multiprocess to log to the same file
              """
              try:
                  if hasattr(self.stream, "seek"):
                      self.stream.seek(0, os.SEEK_END)
              except IOError, e:
                  pass
              StreamHandler.emit(self, record)
      class FileHandler_MP(FileHandler, StreamHandler_MP):
          """
          A handler class which writes formatted logging records to disk files 
              for multiprocess
          """
          def emit(self, record):
              """
              Emit a record.
              If the stream was not opened because 'delay' was specified in the
              constructor, open it before calling the superclass's emit.
              """
              if self.stream is None:
                  self.stream = self._open()
              StreamHandler_MP.emit(self, record)
      class RotatingFileHandler_MP(RotatingFileHandler, FileHandler_MP):
          """
          Handler for logging to a set of files, which switches from one file
          to the next when the current file reaches a certain size.
          Based on logging.RotatingFileHandler, modified for Multiprocess
          """
          _lock_dir = '.lock'
          if os.path.exists(_lock_dir):
              pass
          else:
              os.mkdir(_lock_dir)
          def doRollover(self):
              """
              Do a rollover, as described in __init__().
              For multiprocess, we use shutil.copy instead of rename.
              """
              self.stream.close()
              if self.backupCount > 0:
                  for i in range(self.backupCount - 1, 0, -1):
                      sfn = "%s.%d" % (self.baseFilename, i)
                      dfn = "%s.%d" % (self.baseFilename, i + 1)
                      if os.path.exists(sfn):
                          if os.path.exists(dfn):
                              os.remove(dfn)
                          shutil.copy(sfn, dfn)
                  dfn = self.baseFilename + ".1"
                  if os.path.exists(dfn):
                      os.remove(dfn)
                  if os.path.exists(self.baseFilename):
                      shutil.copy(self.baseFilename, dfn)
              self.mode = 'w'
              self.stream = self._open()
          def emit(self, record):
              """
              Emit a record.
              Output the record to the file, catering for rollover as described
              in doRollover().
              For multiprocess, we use file lock. Any better method ?
              """
              try:
                  if self.shouldRollover(record):
                      self.doRollover()
                  FileLock = self._lock_dir + '/' + os.path.basename(self.baseFilename) + '.' + record.levelname
                  f = open(FileLock, "w+")
                  fcntl.flock(f.fileno(), fcntl.LOCK_EX)
                  FileHandler_MP.emit(self, record)
                  fcntl.flock(f.fileno(), fcntl.LOCK_UN)
                  f.close()
              except (KeyboardInterrupt, SystemExit):
                  raise
              except:
                  self.handleError(record)
      class TimedRotatingFileHandler_MP(TimedRotatingFileHandler, FileHandler_MP):
          """
          Handler for logging to a file, rotating the log file at certain timed
          intervals.
          If backupCount is > 0, when rollover is done, no more than backupCount
          files are kept - the oldest ones are deleted.
          """
          _lock_dir = '.lock'
          if os.path.exists(_lock_dir):
              pass
          else:
              os.mkdir(_lock_dir)
          def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=0, utc=0):
              FileHandler_MP.__init__(self, filename, 'a', encoding, delay)
              self.encoding = encoding
              self.when = string.upper(when)
              self.backupCount = backupCount
              self.utc = utc
              # Calculate the real rollover interval, which is just the number of
              # seconds between rollovers.  Also set the filename suffix used when
              # a rollover occurs.  Current 'when' events supported:
              # S - Seconds
              # M - Minutes
              # H - Hours
              # D - Days
              # midnight - roll over at midnight
              # W{0-6} - roll over on a certain day; 0 - Monday
              #
              # Case of the 'when' specifier is not important; lower or upper case
              # will work.
              if self.when == 'S':
                  self.suffix = "%Y-%m-%d_%H-%M-%S"
                  self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$"
              elif self.when == 'M':
                  self.suffix = "%Y-%m-%d_%H-%M"
                  self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$"
              elif self.when == 'H':
                  self.suffix = "%Y-%m-%d_%H"
                  self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}$"
              elif self.when == 'D' or self.when == 'MIDNIGHT':
                  self.suffix = "%Y-%m-%d"
                  self.extMatch = r"^\d{4}-\d{2}-\d{2}$"
              elif self.when.startswith('W'):
                  if len(self.when) != 2:
                      raise ValueError("You must specify a day for weekly rollover from 0 to 6 (0 is Monday): %s" % self.when)
                  if self.when[1] < '0' or self.when[1] > '6':
                      raise ValueError("Invalid day specified for weekly rollover: %s" % self.when)
                  self.dayOfWeek = int(self.when[1])
                  self.suffix = "%Y-%m-%d"
                  self.extMatch = r"^\d{4}-\d{2}-\d{2}$"
              else:
                  raise ValueError("Invalid rollover interval specified: %s" % self.when)
              self.extMatch = re.compile(self.extMatch)
              if interval != 1:
                  raise ValueError("Invalid rollover interval, must be 1")
          def shouldRollover(self, record):
              """
              Determine if rollover should occur.
              record is not used, as we are just comparing times, but it is needed so
              the method signatures are the same
              """
              if not os.path.exists(self.baseFilename):
                  #print "file don't exist"  
                  return 0 
              cTime = time.localtime(time.time()) 
              mTime = time.localtime(os.stat(self.baseFilename)[ST_MTIME])
              if self.when == "S" and cTime[5] != mTime[5]:
                  #print "cTime:", cTime[5], "mTime:", mTime[5]
                  return 1
              elif self.when == 'M' and cTime[4] != mTime[4]:  
                  #print "cTime:", cTime[4], "mTime:", mTime[4]
                  return 1  
              elif self.when == 'H' and cTime[3] != mTime[3]: 
                  #print "cTime:", cTime[3], "mTime:", mTime[3]
                  return 1
              elif (self.when == 'MIDNIGHT' or self.when == 'D') and cTime[2] != mTime[2]:
                  #print "cTime:", cTime[2], "mTime:", mTime[2]
                  return 1 
              elif self.when == 'W' and cTime[1] != mTime[1]:
                  #print "cTime:", cTime[1], "mTime:", mTime[1]
                  return 1
              else:  
                  return 0 
          def doRollover(self):
              """
              do a rollover; in this case, a date/time stamp is appended to the filename
              when the rollover happens.  However, you want the file to be named for the
              start of the interval, not the current time.  If there is a backup count,
              then we have to get a list of matching filenames, sort them and remove
              the one with the oldest suffix.
              For multiprocess, we use shutil.copy instead of rename.
              """
              if self.stream:
                  self.stream.close()
              # get the time that this sequence started at and make it a TimeTuple
              #t = self.rolloverAt - self.interval
              t = int(time.time())
              if self.utc:
                  timeTuple = time.gmtime(t)
              else:
                  timeTuple = time.localtime(t)
              dfn = self.baseFilename + "." + time.strftime(self.suffix, timeTuple)
              if os.path.exists(dfn):
                  os.remove(dfn)
              if os.path.exists(self.baseFilename):
                  shutil.copy(self.baseFilename, dfn)
                  #print "%s -> %s" % (self.baseFilename, dfn)
                  #os.rename(self.baseFilename, dfn)
              if self.backupCount > 0:
                  # find the oldest log file and delete it
                  #s = glob.glob(self.baseFilename + ".20*")
                  #if len(s) > self.backupCount:
                  #    s.sort()
                  #    os.remove(s[0])
                  for s in self.getFilesToDelete():
                      os.remove(s)
              self.mode = 'w'
              self.stream = self._open()
          def emit(self, record):
              """
              Emit a record.
              Output the record to the file, catering for rollover as described
              in doRollover().
              For multiprocess, we use file lock. Any better method ?
              """
              try:
                  if self.shouldRollover(record):
                      self.doRollover()
                  FileLock = self._lock_dir + '/' + os.path.basename(self.baseFilename) + '.' + record.levelname
                  f = open(FileLock, "w+")
                  fcntl.flock(f.fileno(), fcntl.LOCK_EX)
                  FileHandler_MP.emit(self, record)
                  fcntl.flock(f.fileno(), fcntl.LOCK_UN)
                  f.close()
              except (KeyboardInterrupt, SystemExit):
                  raise
              except:
                  self.handleError(record)

      image.gif

      修改的地方:

      import fcntl,cPickle

      fcntl windows下载不到,她是用于linux下的用于文件锁的模块。

      except IOError, e:
          pass
      ================ 修改为
      except :
          pass

      image.gif

      关键是这部分代码

      f = open(FileLock, "w+")
      fcntl.flock(f.fileno(), fcntl.LOCK_EX)
      FileHandler_MP.emit(self, record)
      fcntl.flock(f.fileno(), fcntl.LOCK_UN)
      f.close()

      image.gif

      fcntl.flock(f.fileno(), fcntl.LOCK_EX) 意思是给文件加锁

      fcntl.flock(f.fileno(), fcntl.LOCK_UN)意思是给文件解锁

      这边是利用fcntl来实现文件加锁,大家可以从网上随便找一个文件加锁的工具类,替换一下即可。

      我随便从网上找了一个https://my.oschina.net/1123581321/blog/833480

      换一下即可

      多线程情况下日志不在丢失错乱但性能较低(相当于强制单线程,毕竟文件只有一个,加锁的话,只能线程一个一个排队进行修改),我们可以思考一下还有什么方法能解决这个问题?

      参考:

      python logging详解及自动添加上下文信息 - xybaby - 博客园

      Python + logging 输出到屏幕,将log日志写入文件 - nancy05 - 博客园

      读懂掌握 Python logging 模块源码 (附带一些 example) - piperck - 博客园

      https://avaj.iteye.com/blog/246088


      相关实践学习
      日志服务之使用Nginx模式采集日志
      本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
      相关文章
      |
      2天前
      |
      SQL DataWorks 安全
      DataWorks产品使用合集之DataWorks资源里python运行时候,查看中途打印日志如何解决
      DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
      18 0
      |
      2天前
      |
      JSON 数据格式 Python
      Python标准库中包含了json模块,可以帮助你轻松处理JSON数据
      【4月更文挑战第30天】Python的json模块简化了JSON数据与Python对象之间的转换。使用`json.dumps()`可将字典转为JSON字符串,如`{&quot;name&quot;: &quot;John&quot;, &quot;age&quot;: 30, &quot;city&quot;: &quot;New York&quot;}`,而`json.loads()`则能将JSON字符串转回字典。通过`json.load()`从文件读取JSON数据,`json.dump()`则用于将数据写入文件。
      9 1
      |
      3天前
      |
      并行计算 安全 测试技术
      Python多线程
      【4月更文挑战第13天】对比多线程与多进程:多线程适合I/O密集型任务,轻量级但受GIL限制;多进程适用于CPU密集型任务,能实现真正并行。多线程直接共享内存,多进程独立内存,各有优劣。
      6 0
      |
      3天前
      |
      数据采集 安全 Java
      Python的多线程,守护线程,线程安全
      Python的多线程,守护线程,线程安全
      |
      3天前
      |
      并行计算 数据处理 开发者
      Python并发编程:解析异步IO与多线程
      本文探讨了Python中的并发编程技术,着重比较了异步IO和多线程两种常见的并发模型。通过详细分析它们的特点、优劣势以及适用场景,帮助读者更好地理解并选择适合自己项目需求的并发编程方式。
      |
      4天前
      |
      Python 容器
      python内置函数、数学模块、随机模块(二)
      python内置函数、数学模块、随机模块(二)
      |
      4天前
      |
      索引 Python
      python内置函数、数学模块、随机模块(一)
      python内置函数、数学模块、随机模块(一)
      |
      6天前
      |
      人工智能 安全 Java
      Python 多线程编程实战:threading 模块的最佳实践
      Python 多线程编程实战:threading 模块的最佳实践
      123 5
      |
      23天前
      |
      安全 Linux 网络安全
      /var/log/secure日志详解
      Linux系统的 `/var/log/secure` 文件记录安全相关消息,包括身份验证和授权尝试。它涵盖用户登录(成功或失败)、`sudo` 使用、账户锁定解锁及其他安全事件和PAM错误。例如,SSH登录成功会显示&quot;Accepted password&quot;,失败则显示&quot;Failed password&quot;。查看此文件可使用 `tail -f /var/log/secure`,但通常只有root用户有权访问。
      69 4
      |
      2天前
      |
      XML Java Maven
      Springboot整合与使用log4j2日志框架【详解版】
      该文介绍了如何在Spring Boot中切换默认的LogBack日志系统至Log4j2。首先,需要在Maven依赖中排除`spring-boot-starter-logging`并引入`spring-boot-starter-log4j2`。其次,创建`log4j2-spring.xml`配置文件放在`src/main/resources`下,配置包括控制台和文件的日志输出、日志格式和文件切分策略。此外,可通过在不同环境的`application.yml`中指定不同的log4j2配置文件。最后,文章提到通过示例代码解释了日志格式中的各种占位符含义。