【问题标题】:Ensuring Python logging in multiple threads is thread-safe确保 Python 在多个线程中的日志记录是线程安全的
【发布时间】:2013-05-31 13:06:07
【问题描述】:

我有一个log.py 模块,它至少用于其他两个模块(server.pydevice.py)。

它有这些全局变量:

fileLogger = logging.getLogger()
fileLogger.setLevel(logging.DEBUG)
consoleLogger = logging.getLogger()
consoleLogger.setLevel(logging.DEBUG)

file_logging_level_switch = {
    'debug':    fileLogger.debug,
    'info':     fileLogger.info,
    'warning':  fileLogger.warning,
    'error':    fileLogger.error,
    'critical': fileLogger.critical
}

console_logging_level_switch = {
    'debug':    consoleLogger.debug,
    'info':     consoleLogger.info,
    'warning':  consoleLogger.warning,
    'error':    consoleLogger.error,
    'critical': consoleLogger.critical
}

它有两个功能:

def LoggingInit( logPath, logFile, html=True ):
    global fileLogger
    global consoleLogger

    logFormatStr = "[%(asctime)s %(threadName)s, %(levelname)s] %(message)s"
    consoleFormatStr = "[%(threadName)s, %(levelname)s] %(message)s"

    if html:
        logFormatStr = "<p>" + logFormatStr + "</p>"

    # File Handler for log file
    logFormatter = logging.Formatter(logFormatStr)
    fileHandler = logging.FileHandler( 
        "{0}{1}.html".format( logPath, logFile ))
    fileHandler.setFormatter( logFormatter )
    fileLogger.addHandler( fileHandler )

    # Stream Handler for stdout, stderr
    consoleFormatter = logging.Formatter(consoleFormatStr)
    consoleHandler = logging.StreamHandler() 
    consoleHandler.setFormatter( consoleFormatter )
    consoleLogger.addHandler( consoleHandler )

还有:

def WriteLog( string, print_screen=True, remove_newlines=True, 
        level='debug' ):

    if remove_newlines:
        string = string.replace('\r', '').replace('\n', ' ')

    if print_screen:
        console_logging_level_switch[level](string)

    file_logging_level_switch[level](string)

我从server.py 调用LoggingInit,它初始化文件和控制台记录器。然后我从各处调用WriteLog,因此多个线程正在访问fileLoggerconsoleLogger

我的日志文件是否需要任何进一步的保护?文档指出线程锁由处理程序处理。

【问题讨论】:

  • 如果你想写的时候没有加锁,你的日志记录可能会混在一起,造成一个不可读的日志。
  • @AliBZ - 什么日志记录?在日志记录级别? python日志记录服务不涵盖它们吗? stackoverflow.com/questions/2973900/…
  • 当您在两个不同的线程中使用“fileLogger.info('1 2 3 4')”时,您的最终日志可能是它们的混合,类似于“1 2 1 2 3 3 4 4"
  • 我今天遇到了同样的问题,你应该在登录前后加锁以防止这种情况。
  • 这不是直接与 python 文档相矛盾(见我的回答)吗?你提出过错误吗?还是我理解错了?

标签: python multithreading logging


【解决方案1】:

好消息是您不需要为线程安全做任何额外的事情,而且您不需要任何额外的东西或几乎微不足道的东西来干净关闭。稍后我会详细介绍。

坏消息是你的代码甚至在你到达那个点之前就有一个严重的问题:fileLoggerconsoleLogger 是同一个对象。来自the documentation for getLogger()

返回具有指定名称的记录器,或者,如果未指定名称,则返回作为层次结构的根记录器的记录器。

因此,您将获取根记录器并将其存储为fileLogger,然后您将获取根记录器并将其存储为consoleLogger。因此,在LoggingInit 中,您初始化fileLogger,然后以不同的名称使用不同的值重新初始化同一个对象。

可以将多个处理程序添加到同一个记录器——而且,由于您实际为每个记录器执行的唯一初始化是addHandler,因此您的代码将按预期工作,但只是偶然。而且只是一种。如果您通过print_screen=True,您将在两个日志中获得每条消息的两个副本,即使您通过print_screen=False,您也会在控制台中获得副本。

实际上根本没有理由使用全局变量; getLogger() 的全部意义在于您可以在每次需要时调用它并获取全局根记录器,因此您无需将其存储在任何地方。


一个更小的问题是您没有转义插入到 HTML 中的文本。在某些时候,您将尝试记录字符串 "a &lt; b" 并最终遇到麻烦。

不太严重的是,不在&lt;body&gt; 内的&lt;p&gt; 标记序列在&lt;html&gt; 内不是有效的HTML 文档。但是很多观众会自动处理这些问题,或者您可以在显示之前对日志进行简单的后期处理。但如果你真的希望这是正确的,你需要继承 FileHandler 并让你的 __init__ 添加一个标题(如果给定一个空文件)并删除一个页脚(如果存在),然后让你的 close 添加一个页脚。


回到你的实际问题:

您不需要任何额外的锁定。如果处理程序正确实现了createLockacquirerelease(并且在具有线程的平台上调用),则日志记录机制将在需要时自动确保获取锁,以确保以原子方式记录每条消息。

据我所知,文档并没有直接StreamHandlerFileHandler 实现了这些方法,而是强烈暗示它(the text you mentioned in the question 说“日志模块是旨在实现线程安全,无需其客户完成任何特殊工作”等)。您可以查看实现的源代码(例如,CPython 3.3)并查看它们都从 logging.Handler 继承了正确实现的方法。


同样,如果处理程序正确实现了flushclose,日志记录机制将确保它在正常关闭期间正确完成。

这里,文档确实解释了 StreamHandler.flush()FileHandler.flush()FileHandler.close() 的含义。它们大多是您所期望的,除了StreamHandler.close() 是无操作的,这意味着控制台的最终日志消息可能会丢失。来自文档:

请注意,close() 方法继承自 Handler,因此没有输出,因此有时可能需要显式调用 flush()

如果这对您很重要,并且您想解决它,您需要执行以下操作:

class ClosingStreamHandler(logging.StreamHandler):
    def close(self):
        self.flush()
        super().close()

然后使用ClosingStreamHandler() 代替StreamHandler()

FileHandler没有这个问题。


将日志发送到两个地方的正常方法是仅使用带有两个处理程序的根记录器,每个处理程序都有自己的格式化程序。

此外,即使您确实需要两个记录器,也不需要单独的 console_logging_level_switchfile_logging_level_switch 映射;调用Logger.debug(msg) 与调用Logger.log(DEBUG, msg) 完全相同。您仍然需要一些方法将您的自定义级别名称debug 等映射到标准名称DEBUG 等,但是您可以只进行一次查找,而不是每个记录器执行一次(另外,如果您的名称只是具有不同演员的标准名称,您可以作弊。

这在 `Multiple handlers and formatters 部分以及日志记录手册的其余部分中都有很好的描述。

执行此操作的标准方式的唯一问题是,您无法轻松地逐条关闭控制台日志记录。那是因为这不是一件正常的事情。通常,您只需按级别记录,并在文件日志上设置更高的日志级别。

但是,如果您想要更多控制,可以使用过滤器。例如,给您的FileHandler 一个接受所有内容的过滤器,给您的ConsoleHandler 一个需要以console 开头的过滤器,然后使用过滤器'console' if print_screen else ''。这将WriteLog 减少到几乎是单线。

您仍然需要额外的两行来删除换行符,但您甚至可以在过滤器中执行 ,如果您愿意,也可以通过适配器执行。 (再次,请参阅食谱。)然后WriteLog 真的单线。

【讨论】:

  • 感谢其他提示。这是否意味着我需要做的就是在getLogger() 中指定名称,它就会按预期工作?
  • 而且,@abarnert 如果我确实想继续登录到两个不同的地方(通过在getLogger() 中指定名称,我仍然需要全局变量,对吗?
  • @Raskol:嗯,这取决于“预期”的含义,但这可能不是您真正想要的。 getLogger 背后的想法是您应该将它用于某种分层日志结构;请注意,“Logger Objects”的文档讨论了“推荐的构造 logging.getLogger(__name__) 以使您的日志记录层次结构与您的模块层次结构相匹配。您可以颠覆它以让两个记录器并排,但它是奇怪的事情。
  • @rbp:发布您的单独问题。 (以及您正在使用哪个平台,以及您如何配置logginggunicorn。)问题很可能是gunicorn 是一个分叉服务器,这意味着您有多个单独的进程 i>,所以threading.RLock 没有任何好处。但绿叶也可能是相关的。
  • @rbp:为了将来任何不明白为什么这足以解决您的问题的人的利益:我假设您只是将您的Handler 子类化,并覆盖了它的@987654383 @到self.lock = multiprocessing.RLock(),对吗?
【解决方案2】:

Python 日志记录是线程安全的:

所以你在 Python(库)代码中没有问题。

您从多个线程 (WriteLog) 调用的例程不会写入任何共享状态。所以你的代码没有问题。

所以你没事。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-07-11
    • 1970-01-01
    • 1970-01-01
    • 2020-12-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多