我有一个log.py
模块,至少用于其他两个模块(server.py
和device.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)
我从LoggingInit
调用server.py
,它初始化文件和控制台记录器。然后我从所有地方拨打WriteLog
,因此多个线程正在访问fileLogger
和consoleLogger
。
我的日志文件需要进一步保护吗?文档声明线程锁由处理程序处理。
答案 0 :(得分:43)
好消息是,您不需要为线程安全做任何额外的事情,并且您不需要额外的任何东西或者几乎无关紧要的干净关闭。我稍后会详细介绍。
坏消息是,在您达到这一点之前,您的代码存在严重问题:fileLogger
和consoleLogger
是同一个对象。来自the documentation for getLogger()
:
返回具有指定名称的记录器,或者,如果未指定名称,则返回记录器,该记录器是层次结构的根记录器。
因此,您将获得根记录器并将其存储为fileLogger
,然后您将获得根记录器并将其存储为consoleLogger
。因此,在LoggingInit
中,您初始化fileLogger
,然后使用不同的值以不同的名称重新初始化相同的对象。
你可以将多个处理程序添加到同一个记录器中,并且由于您实际为每个处理程序执行的唯一初始化是addHandler
,因此您的代码将按预期工作,但仅限于事故。而且只有一点。如果通过print_screen=True
,您将在两个日志中获得每封邮件的两个副本,即使您通过print_screen=False
,也会在控制台中获得副本。
实际上根本没有全局变量的理由; getLogger()
的重点在于,您可以在每次需要时调用它并获取全局根记录器,因此您无需将其存储在任何位置。
更小的问题是您没有转义插入HTML的文本。在某些时候,您将尝试记录字符串"a < b"
并最终遇到麻烦。
不太严肃的是,<p>
内不在<body>
<html>
内的FileHandler
标记序列不是有效的HTML文档。但是大量的观众会自动处理,或者您可以在显示之前轻松地对日志进行后期处理。但是如果你真的希望这是正确的,你需要继承__init__
并让你的close
添加一个标题,如果给出一个空文件并删除一个页脚(如果存在),然后让你的createLock
添加页脚。
回到实际问题:
您不需要任何额外的锁定。如果处理程序正确实现acquire
,release
和StreamHandler
(并且它在带有线程的平台上调用),则日志记录机器将自动确保在需要时获取锁定以确保每条消息都以原子方式记录。
据我所知,文档不直接说FileHandler
和logging.Handler
实现了这些方法,它强烈暗示它(the text you mentioned in the question “记录模块旨在保证线程安全,无需客户完成任何特殊工作”等。您可以查看实施的来源(例如CPython 3.3),看看它们都是从flush
继承正确实现的方法。
同样,如果处理程序正确实现close
和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_switch
没有这样的问题。
将日志发送到两个地方的常规方法是使用带有两个处理程序的根记录程序,每个处理程序都有自己的格式化程序。
此外,即使您确实需要两个记录器,也不需要单独的file_logging_level_switch
和Logger.debug(msg)
地图;调用Logger.log(DEBUG, msg)
与调用debug
完全相同。您仍然需要某种方法将自定义级别名称DEBUG
等映射到标准名称FileHandler
等,但您只需执行一次查找,而不是每个记录器执行一次(另外,如果你的名字只是不同演员的标准名字,你可以作弊。
在Multiple handlers and formatters部分以及其他日志食谱中都对此进行了详细描述。
标准方法的唯一问题是您无法在逐个消息的基础上轻松关闭控制台日志记录。那是因为这不是正常的事情。通常,您只需按级别进行日志记录,并在文件日志中将日志级别设置得更高。
但是,如果您想要更多控制,可以使用过滤器。例如,为您的ConsoleHandler
过滤器提供接受所有内容的过滤器,并为您的console
过滤器提供以'console' if print_screen else ''
开头的内容,然后使用过滤器WriteLog
。这会使WriteLog
减少到几乎一行。
您仍然需要额外的两行来删除换行符 - 但您甚至可以在过滤器中执行 ,或者通过适配器执行此操作(如果需要)。 (再看看食谱。)然后{{1}}真的 是一个单行。
答案 1 :(得分:5)
Python日志记录是线程安全的:
所以你在Python(库)代码中没有问题。
从多个线程(WriteLog
)调用的例程不会写入任何共享状态。所以你的代码没有问题。
所以你没事。