如何使用Python日志记录器在不同的类和导入中动态更改文件句柄。

73

我无法执行即时的日志文件句柄更改。

例如,我有三个类

one.py

import logging
class One():
    def __init__(self,txt="?"):
        logging.debug("Hey, I'm the class One and I say: %s" % txt)

two.py

import logging
class Two():
    def __init__(self,txt="?"):
        logging.debug("Hey, I'm the class Two and I say: %s" % txt)

config.py

import logging
class Config():
    def __init__(self,logfile=None):
        logging.debug("Reading config")
        self.logfile(logfile)

myapp

from one import One
from two import Two
from config import Config
import logging

#Set default logging
logging.basicConfig( 
    level=logging.getLevelName(DEBUG), 
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    filename=None
)

logging.info("Starting with stdout")

o=One(txt="STDOUT")
c=Config(logfile="/tmp/logfile")

# Here must be the code that change the logging configuration and set the filehandler

t=One(txt="This must be on the file, not STDOUT")

如果我再尝试使用loggin.basicConfig(),它就不起作用了。

我认为通常的做法是为每个类创建一个专用的记录器。这是一个经常使用的模式。我认为切换和切换回来会变得棘手。 - dsz
5个回答

111

实际上,如果已经设置了处理程序,logging.basicConfig不执行 任何操作:

如果根记录器已经配置了处理程序,除非将关键字参数 force 设置为 True,否则此函数将不起作用。

你需要添加 force=True(需要 Python 3.8 或更新版本),或者替换根记录器上的当前处理程序:

import logging

fileh = logging.FileHandler('/tmp/logfile', 'a')
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fileh.setFormatter(formatter)

log = logging.getLogger()  # root logger
for hdlr in log.handlers[:]:  # remove all old handlers
    log.removeHandler(hdlr)
log.addHandler(fileh)      # set the new handler

请查看Python Logging HOWTO中的Configuring Logging章节


24

@Martijn Pieters提供的答案很好。然而,代码片段删除了所有处理程序,并仅放回了文件处理程序。如果您的应用程序具有其他模块添加的处理程序,则这将很麻烦。

因此,下面的代码片段被设计成仅替换文件处理程序。

if isinstance(hdlr,logging.FileHandler)是关键。

import logging

filehandler = logging.FileHandler('/tmp/logfile', 'a')
formatter = logging.Formatter('%(asctime)-15s::%(levelname)s::%(filename)s::%(funcName)s::%(lineno)d::%(message)s')
filehandler.setFormatter(formatter)
log = logging.getLogger()  # root logger - Good to get it only once.
for hdlr in log.handlers[:]:  # remove the existing file handlers
    if isinstance(hdlr,logging.FileHandler):
        log.removeHandler(hdlr)
log.addHandler(filehandler)      # set the new handler
# set the log level to INFO, DEBUG as the default is ERROR
log.setLevel(logging.DEBUG)

12

我发现了一种比上面的“被接受”的答案更简单的方法。如果您有处理程序的引用,您只需要调用close()方法,然后设置baseFilename属性即可。当您分配baseFilename时,请确保使用os.path.abspath()。库源文件中有一个注释指出它是必需的。我将我的配置信息保存在一个全局dict()中,这样就可以轻松地保持FileHandler参考对象。如下所示,只需要两行代码就可以在运行时更改处理程序的日志文件名。

import logging

def setup_logging():
  global config

  if config['LOGGING_SET']:
    config['LOG_FILE_HDL'].close()
    config['LOG_FILE_HDL'].baseFilename = os.path.abspath(config['LOG_FILE'])

    config['DEBUG_LOG_HDL'].close()
    config['DEBUG_LOG_HDL'].baseFilename = os.path.abspath(config['DEBUG_LOG'])
  else:
    format_str = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    formatter = logging.Formatter(format_str)

    log = logging.getLogger()

    log.setLevel(logging.DEBUG)

    # add file mode="w" to overwrite
    config['LOG_FILE_HDL'] = logging.FileHandler(config['LOG_FILE'], mode='a')
    config['LOG_FILE_HDL'].setLevel(logging.INFO)
    config['LOG_FILE_HDL'].setFormatter(formatter)
    log.addHandler(config['LOG_FILE_HDL'])

    # the delay=1 should prevent the file from being opened until used.
    config['DEBUG_LOG_HDL'] = logging.FileHandler(config['DEBUG_LOG'], mode='a', delay=1)
    config['DEBUG_LOG_HDL'].setLevel(logging.DEBUG)
    config['DEBUG_LOG_HDL'].setFormatter(formatter)
    log.addHandler(config['DEBUG_LOG_HDL'])

    ch = logging.StreamHandler()
    ch.setLevel(logging.DEBUG)
    ch.setFormatter(formatter)
    log.addHandler(ch)
    config['LOGGING_SET'] = True

2
即使您没有保存处理程序的引用,如果您的记录器中只有一个处理程序,您仍然可以直接引用它并使用相同的技术:mylogger = logging.getLogger('mylogger') # 获取记录器 然后 mylogger.handlers[0].close() mylogger.handlers[0].baseFilename = os.path.abspath('newfilename.log') - markoan
3
@user2179204,你说“更容易”,却写了四倍的代码行数——这不是Python的方式。 - khex

6

我尝试实现了@Martijn Pieters和@Arun Thundyill Saseendran在此页面上的建议。由于我太新了,无法发表评论,因此必须发布一个调整后的答案。在isinstance调用中,我必须使用“logging”而不是“log”来访问类型(log是实例),然后“FileHander”应该是“FileHandler”。我正在使用Python 3.6。

import logging

filehandler = logging.FileHandler('/tmp/logfile', 'a')
formatter = logging.Formatter('%(asctime)-15s::%(levelname)s::%(filename)s::%(funcName)s::%(lineno)d::%(message)s')
filehandler.setFormatter(formatter)
log = logging.getLogger()  # root logger - Good to get it only once.
for hdlr in log.handlers[:]:  # remove the existing file handlers
    if isinstance(hdlr,logging.FileHandler): #fixed two typos here
        log.removeHandler(hdlr)
log.addHandler(filehandler)      # set the new handler
# set the log level to INFO, DEBUG as the default is ERROR
logging.setLevel(log.DEBUG)      

太棒了!我正在使用Python Flask,它让我将我的消息保存在Flask日志中。谢谢。 - Hilton Fernandes
Arun的回答被编辑了。只有最后一行不同。看起来是Victor编辑了那一行。你知道哪一个是正确的吗? - pauljohn32

3

TL;DR

INDEX = 0

#: change to new file location
logger.handlers[INDEX].setStream( open('/path/to/new/log/file.log', 'a') )

#: change to stdout or stderr
import sys
logger.handlers[INDEX].setStream( sys.stdout ) # or sys.stderr

#: change to another device
logger.handlers[INDEX].setStream( open('/dev/ttys010', 'a') )

注册了一个 logging.FileHandler 到你的记录器之后,你可以进入其内部并在运行时更改它所输出的流。
确保 INDEX 访问记录器中正确的处理程序。如果你只添加了一个 FileHandler,那么它应该是索引 0
首先,根据日志记录文档建议的习惯用法,你需要获取一个以特定包、模块、类或函数的 __name__ 命名的新记录器实例。
#: class
>>> class A:
        def __init__(self):
            self.logger = logging.getLogger(self.__class__.__name__)
>>>A().logger
<Logger A (WARNING)>

#: function
>>> def func():
        logger = logging.getLogger(func.__name__)
        print(logger)
>>> func()
<Logger func (WARNING)>

#: module
>>> logger = logging.getLogger( __name__ )
>>> logger
<Logger __main__ (WARNING)>

#: package (e.g. a package named 'pkg', write this in '__init__.py')
>>> logger = logging.getLogger( __package__ )
>>> logger
<RootLogger pkg (WARNING)>

接下来,如果您为记录器注册了一个logging.FileHandler处理程序,如下所示:
logger.addHandler( logging.FileHandler('/tmp/logfile.log', 'a') )

然后,您可以通过替换输出流来更改它输出的文件:
INDEX = 0  # you will have to find the index position of the `FileHandler` you 
           # registered to this logger. I justed listed them with: `logger.handlers`
           # and picked the one I needed. if you only register one handler
           # then it should be at index 0, i.e the first one

#: change to new file location
logger.handlers[INDEX].setStream( open('/path/to/new/log/file.log', 'a') )

#: change to stdout or stderr
import sys
logger.handlers[INDEX].setStream( sys.stdout ) # or sys.stderr

#: change to another device
logger.handlers[INDEX].setStream( open('/dev/ttys010', 'a') )

If your curious, found this in a few minutes, by doing a bit of digging like so (in ipython and python interpreters):

>>> import logging
>>> logger = logging.getLogger( __name__ )
>>> logger.addHandler( logging.FileHandler('/tmp/logfile', 'a') )

>>> globals()
>>> dir(logger)

#: found that the logger has 'handlers' attribute
>>> dir(logger.handlers)
>>> logger.handlers

#: found that the FileHandler I registered earlier is at index: 0
>>> logger.handlers[0]
>>> dir(logger.handlers[0])

#: found that FileHandler has a dictionary '__dict__'
>>> logger.handlers[0].__dict__

#: found that FileHandler dict has 'baseFilename' attribute with the filename
#: i had set when registering the file handler
>>> logger.handlers[0].__dict__['baseFilename']

#: tried changing the file it points to
>>> logger.handlers[0].__dict__['baseFilename'] = '/tmp/logfile.log'
#: tried logging
>>> logger.info(f'hello world')
#: didn't work

#: found another interesting perhaps relevant attribute 'stream' in the
#: FileHandler dict
>>> logger.handlers[0].__dict__['stream']
>>> dir(logger.handlers[0].__dict__['stream'])
>>> logger.handlers[0].__dict__['stream'].__dict__
#: tried replacing the stream altogether
>>> logger.handlers[0].__dict__['stream'] = open('/tmp/logfile.log','a')
#: tried logging
>>> logger.info(f'hello world again')
#: it worked
>>> logger.info(f'hey it worked')

#: found another interesting perhaps relevant method 'setStream'
>>> logger.handlers[0].setStream( open('/tmp/otherlogfile.log','a') )
#: tried logging
>>> logger.info(f'hello world again')
#: it worked
>>> logger.info(f'hey it worked')
  • you can also change the name of the logger with:

    logger.name = 'bla'
    
  • and more, see: dir(logger)


这似乎是最直接的方法,只要你得到正确的索引。更少的代码==更好的答案。 - pauljohn32
这也适用于Pathlib,它也会再次关闭文件。例如, logger.handlers[0].setStream(Path('your/path').open('a')) - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接