动态更改日志级别而无需重新启动应用程序

120

在 Python 中,是否可以使用 fileConfig 更改日志级别而无需重新启动应用程序?如果不能通过 fileConfig 实现,是否有其他方法可以获得相同的结果?

更新:这是针对运行在服务器上的应用程序,我希望系统管理员可以更改配置文件,该文件将在运行时由应用程序选择并动态更改日志级别。当时我正在使用 gevent,因此我已将我的代码添加为其中一个答案,该代码使用 inotify 来选择更改配置文件。


2
每个人都使用线程和定时器来重置日志级别。这就是信号的发明目的。您可以使用“SIGHUP”(在*nix上是标准)向进程发送信号以重新加载配置。在Python中,您可以安装一个信号处理程序来捕获此事件并重新加载或重置配置。 - Ben DeMott
2
我同意使用信号。建议使用SIGUSR1或SIGUSR2来代替SIGHUP。后者在进程控制终端断开连接时发送。 - Mike Ellis
1
SIGUSR1SIGUSR2仅适用于类Unix系统。您可以使用logging.config.listen()来监听新配置(https://docs.python.org/3/library/logging.config.html#logging.config.listen) - yoonghm
9个回答

173

fileConfig 是一个根据文件配置日志级别的机制;您可以在程序中随时动态更改它。

在要更改日志级别的记录对象上调用 .setLevel()。通常情况下,您会在根记录器上执行此操作:

logging.getLogger().setLevel(logging.DEBUG)

1
如果您只想更改特定处理程序的setLevel,请参阅@sfinkens的答案(以及我的评论)。 - Starman
这是否意味着,如果我在程序已经运行时更改文件中的日志级别,更改将生效?它是否使用inotify来监视更改的conf文件? - 0xc0de
1
@0xc0de:不,文件没有被重新解析。您可以更改正在运行的Python程序中当前使用的“Logger”对象的日志级别。 - Martijn Pieters

57

除了被接受的答案之外:根据您初始化记录器的方式,您可能还需要更新记录器的处理程序:

import logging

level = logging.DEBUG
logger = logging.getLogger()
logger.setLevel(level)
for handler in logger.handlers:
    handler.setLevel(level)

2
有用的提示:如果您使用dictConfig来指定记录器、处理程序等,并且只想在一个特定的处理程序上设置级别,您可以像@sfinkens所示的那样在循环中使用.get_name()函数,并确保您只更改想要的处理程序的级别。 - Starman
2
处理程序为什么需要具有日志级别?此外,为什么它们的级别可以独立于拥有它们的记录器进行设置?这非常令人困惑,是一种糟糕的设计。 - MathKid
6
如果您希望以DEBUG级别记录到文件,但在控制台上以不同的级别记录,您需要连接两个具有不同级别的处理程序吗? - supermitch

15

sfinken 的答案 和 Starman 后来的评论上进行进一步扩展,你还可以检查处理程序的类型以针对特定的输出器进行定位,例如:

import logging
logger = logging.getLogger()
for handler in logger.handlers:
    if isinstance(handler, type(logging.StreamHandler())):
        handler.setLevel(logging.DEBUG)
        logger.debug('Debug logging enabled')

1
这正是我需要的,可以在没有-o标志的情况下自动启用调试日志记录。 - takanuva15
3
谢谢您的反馈!不过我有一个小建议:isinstance(handler, logging.StreamHandler) - modulus0

10
这可能是您正在寻找的内容:
import logging
logging.getLogger().setLevel(logging.INFO)

请注意,没有任何参数调用getLogger()会返回根记录器。

这与Martjin的回答相同,似乎不会影响现有的记录器(至少在Python2.7中)。sfinken的回答会影响现有的记录器。 - undefined

9

fileConfig()可以在程序运行时动态改变日志配置,不过对于简单的更改,采用像Martijn Pieters回答中建议的编程方法可能更适合。日志甚至提供了一个套接字服务器,使用listen() / stopListening() API监听配置更改,如此处所述。要使日志监听特定端口,您可以使用

t = logging.config.listen(PORT_NUMBER)
t.start()

停止监听,请拨打

logging.config.stopListening()

要向服务器发送数据,您可以使用例如:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('localhost', PORT_NUMBER))
with open(CONFIG_FILE) as f:
    data_to_send = f.read()
s.send(struct.pack('>L', len(data_to_send)))
s.send(data_to_send)
s.close()

更新:由于向后兼容性约束,fileConfig() 调用的内部实现意味着您无法在调用中指定 disable_existing_loggers=False,这在某些情况下使此功能不太有用。您可以使用相同的 API 使用 dictConfig 模式发送 JSON 文件,这将允许更好地控制重新配置。这需要 Python 2.7/3.2 或以上版本(其中添加了 dictConfig())。或者,您可以使用 stdlib 代码实现自己的侦听器,其工作方式相同,但会根据您的特定需求进行定制。

1
哇,我今天早些时候看到了这个答案,但没有注意到更新。我也曾为此问题苦苦挣扎。已经提交了一个错误报告以添加该选项。http://bugs.python.org/issue26533现在我用了一种不光彩的方式绕过了这个问题: - Gdogg
def my_fileConfig(fname, defaults=None, disable_existing_loggers=False): """ 这个函数仅仅是将 fileConfig 函数的 disable_existing_loggers 参数设置为 False 而不是 True。 它旨在用于 monkey patch,以便可以在不禁用所有非根记录器的情况下使用配置监听器。 """ orig_fileConfig(fname, defaults, disable_existing_loggers) orig_fileConfig = logging.config.fileConfig # HACK: 用我的版本替换 fileConfig。 logging.config.fileConfig = my_fileConfig logging.config.fileConfig(config_path) - Gdogg

4

最终我决定使用inotify和gevent来检查文件写入操作,一旦我知道文件已经被更改,然后我根据配置为每个记录器设置级别。

import gevent
import gevent_inotifyx as inotify
from gevent.queue import Queue

class FileChangeEventProducer(gevent.Greenlet):
    def __init__(self, fd, queue):
        gevent.Greenlet.__init__(self)
        self.fd = fd
        self.queue = queue

    def _run(self):
        while True:
            events = inotify.get_events(self.fd)
            for event in events:
                self.queue.put(event)
                gevent.sleep(0)


class FileChangeEventConsumer(gevent.Greenlet):
    def __init__(self, queue, callBack):
        gevent.Greenlet.__init__(self)
        self.queue = queue
        self.callback = callBack

    def _run(self):
        while True:
            _ = self.queue.get()
            self.callback()
            gevent.sleep(0)


class GeventManagedFileChangeNotifier:
    def __init__(self, fileLocation, callBack):
        self.fileLocation = fileLocation
        self.callBack = callBack
        self.queue = Queue()
        self.fd = inotify.init()
        self.wd = inotify.add_watch(self.fd, self.fileLocation, inotify.IN_CLOSE_WRITE)


    def start(self):
        producer = FileChangeEventProducer(self.fd, self.queue)
        producer.start()
        consumer = FileChangeEventConsumer(self.queue, self.callBack)
        consumer.start()
        return (producer, consumer)

上述代码用法如下:
    def _setUpLoggingConfigFileChangeNotifier(self):
        loggingFileNameWithFullPath = self._getFullPathForLoggingConfig()
        self.gFsNotifier = GeventManagedFileChangeNotifier(loggingFileNameWithFullPath, self._onLogConfigChanged)
        self.fsEventProducer, self.fsEventConsumer = self.gFsNotifier.start()


    def _onLogConfigChanged(self):
        self.rootLogger.info('Log file config has changed - examining the changes')
        newLoggingConfig = Config(self.resourcesDirectory, [self.loggingConfigFileName]).config.get('LOG')
        self.logHandler.onLoggingConfigChanged(newLoggingConfig)

一旦我有了新的日志文件配置,我就可以从配置中为每个记录器连接正确的记录级别。我只是想分享这个答案,如果有人尝试在Gevent中使用它,这可能会有所帮助。


1
使用武力
例子

    logging.basicConfig(level=logging.DEBUG)

    logger.info('debug')
    logger.info('info')
    logger.warning('warning')
    print()
    logging.basicConfig(level=logging.WARNING, force=True)

    logger.info('debug')
    logger.info('info')
    logger.warning('warning')
    exit()

和输出

INFO:root:debug
INFO:root:info
WARNING:root:warning

WARNING:root:warning

感谢您对新版本的Python(>=3.8)的回答。对于其他人,这里是相关文档的链接:点击这里 - undefined

0
根据您的应用程序,您首先需要找到一种在执行期间重新加载该文件或根据自己的配置文件重置日志级别的方法。
最简单的方法是使用定时器。可以使用线程来实现,也可以让您使用的异步框架来实现(如果有的话,它们通常会实现)。
使用 threading.Timer:
import threading
import time


def reset_level():
    # you can reload your own config file or use logging.config.fileConfig here
    print 'Something else'
    pass


t = threading.Timer(10, reset_level)
t.start()

while True:
    # your app code
    print 'Test'
    time.sleep(2)

输出:

Test
Test
Test
Test
Test
Something else
Test
Test

更新: 请查看Martijn Pieters提出的解决方案。


我认为这并没有完全回答问题。 - Jace Browning
我已经使用了上述解决方案,但我遇到的问题是(在RHEL5上使用Python 2.4.3和ConcurrentLogHandler),每次重新加载配置文件时,它都会添加一个文件句柄,并且运行一段时间后会抛出“打开的文件太多”的异常。因此,我同意Travis Bear的看法,Martijn Pieters的答案应该被接受。 - Fabian76
@Fabian76 你关掉那个文件了吗?使用上下文管理器打开文件,而不是使用独立的 open 方法。 - hafiz031

-3
import logging
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logger.debug('This message should appear on the console')
logger.warning('So should this')
logger.setLevel(logging.WARNING)
logger.debug('This message should NOT appear on the console')
logger.warning('This one should')

你甚至可以设置不同的记录器


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