日志记录是程序员工具箱中非常有用的工具。它可以帮助您更好地理解程序流程并发现您在开发时甚至可能没有想到的场景。
日志为开发人员提供了一组额外的眼睛,可以不断查看应用程序正在经历的流程。它们可以存储信息,例如哪个用户或 IP 访问了应用程序。如果发生错误,那么他们可以通过告诉您程序到达发生错误的代码行之前的状态,提供比堆栈跟踪更多的洞察力。
通过从正确的位置记录有用的数据,您不仅可以轻松调试错误,还可以使用数据来分析应用程序的性能以计划扩展或查看使用模式以计划营销。
Python 提供了一个日志系统作为其标准库的一部分,因此您可以快速将日志添加到您的应用程序中。在本文中,您将了解为什么使用此模块是向应用程序添加日志记录的最佳方式以及如何快速入门,并且您将了解一些可用的高级功能。
日志模块
Python 中的日志模块是一个随时可用且功能强大的模块,旨在满足初学者和企业团队的需求。大多数第三方 Python 库都使用它,因此您可以将日志消息与来自这些库的日志消息集成,为您的应用程序生成同构日志。
向 Python 程序添加日志记录就像这样简单:
import logging
导入日志模块后,您可以使用称为“记录器”的东西来记录您想要查看的消息。默认情况下,有 5 个标准级别指示事件的严重性。每个都有相应的方法,可用于记录该严重性级别的事件。定义的级别,按照严重性递增的顺序,如下所示:
- DEBUG
- INFO
- WARNING
- ERROR
- CRITICAL
logging 模块为您提供了一个默认的记录器,让您无需进行太多配置即可开始使用。可以调用每个级别的相应方法,如下例所示:
import logging
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')
上述程序的输出如下所示:
WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message
输出显示每条消息之前的严重性级别以及root
,这是日志记录模块为其默认记录器提供的名称。(记录器将在后面的部分中详细讨论。)这种格式显示了由冒号 ( :
)分隔的级别、名称和消息,是默认的输出格式,可以配置为包括时间戳、行号和其他细节。
请注意,没有记录debug()
和info()
消息。这是因为默认情况下,日志记录模块会记录严重性级别为WARNING
或以上的消息。如果需要,您可以通过将日志记录模块配置为记录所有级别的事件来更改它。您还可以通过更改配置来定义自己的严重性级别,但通常不建议这样做,因为它可能会导致与您可能正在使用的某些第三方库的日志混淆。
基本配置
您可以使用该方法来配置日志记录:basicConfig(**
kwargs
)
“你会注意到日志模块打破了 PEP8 风格指南并使用了
camelCase
命名约定。这是因为它是从 Java 中的日志实用程序 Log4j 中采用的。这是包中的一个已知问题,但在决定将其添加到标准库时,它已经被用户采用,更改它以满足 PEP8 要求会导致向后兼容性问题。” (来源)
一些常用的参数basicConfig()
如下:
level
:根记录器将设置为指定的严重性级别。filename
: 这指定了文件。filemode
: 如果filename
给定,则以这种模式打开文件。默认为a
,表示追加。format
:这是日志消息的格式。
通过使用该level
参数,您可以设置要记录的日志消息级别。这可以通过传递类中可用的常量之一来完成,这将允许记录该级别或更高级别的所有日志记录调用。下面是一个例子:
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug('This will get logged')
DEBUG:root:This will get logged
DEBUG
现在将记录级别或以上级别的所有事件。
同样,用于记录到文件而不是控制台,filename
并且filemode
可以使用,并且您可以使用format
. 下面的例子展示了这三者的用法:
import logging
logging.basicConfig(filename='app.log', filemode='w', format='%(name)s - %(levelname)s - %(message)s')
logging.warning('This will get logged to a file')
root - ERROR - This will get logged to a file
该消息看起来像这样,但将写入一个名为app.log
而不是控制台的文件中。filemode 设置为w
,这意味着每次basicConfig()
调用日志文件都以“写入模式”打开,程序每次运行都会重写该文件。filemode 的默认配置a
是 append。
您可以通过使用更多参数 for 来进一步自定义根记录器basicConfig()
,可在此处找到。
需要注意的是,basicConfig()
只有在之前没有配置过根记录器的情况下,才可以调用配置根记录器。基本上,这个函数只能被调用一次。
debug()
, info()
, warning()
, error()
, 并且如果之前没有被调用过,critical()
也会basicConfig()
自动不带参数调用。这意味着在第一次调用上述函数之一后,您不能再配置根记录器,因为它们会在basicConfig()
内部调用该函数。
中的默认设置basicConfig()
是将记录器设置为以以下格式写入控制台:
ERROR:root:This is an error message
格式化输出
虽然您可以将任何可以表示为程序中的字符串的变量作为消息传递给日志,但有一些基本元素已经是 的一部分LogRecord
并且可以轻松添加到输出格式中。如果要记录进程 ID 以及级别和消息,可以执行以下操作:
import logging
logging.basicConfig(format='%(process)d-%(levelname)s-%(message)s')
logging.warning('This is a Warning')
18472-WARNING-This is a Warning
format
可以按照LogRecord
您喜欢的任何排列方式获取具有属性的字符串。可以在此处找到可用属性的完整列表。
这是您可以添加日期和时间信息的另一个示例:
import logging
logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO)
logging.info('Admin logged in')
2018-07-11 20:12:06,288 - Admin logged in
%(asctime)s
增加了创建的时间LogRecord
。可以使用datefmt
属性更改格式,该属性使用与 datetime 模块中的格式化函数相同的格式化语言,例如time.strftime()
:
import logging
logging.basicConfig(format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S')
logging.warning('Admin logged out')
12-Jul-18 20:53:19 - Admin logged out
您可以在此处找到指南。
记录变量数据
在大多数情况下,您希望在日志中包含来自应用程序的动态信息。您已经看到日志记录方法将字符串作为参数,将带有可变数据的字符串格式化在单独的行中并将其传递给日志方法似乎很自然。但这实际上可以通过使用消息的格式字符串并附加变量数据作为参数来直接完成。下面是一个例子:
import logging
name = 'John'
logging.error('%s raised an error', name)
ERROR:root:John raised an error
传递给该方法的参数将作为变量数据包含在消息中。
虽然您可以使用任何格式样式,但Python 3.6 中引入的f-strings是一种很棒的格式化字符串的方式,因为它们可以帮助保持格式简短且易于阅读:
import logging
name = 'John'
logging.error(f'{name} raised an error')
ERROR:root:John raised an error
捕获堆栈跟踪
日志模块还允许您捕获应用程序中的完整堆栈跟踪。如果参数传递为,则可以捕获异常信息,并且日志记录函数的调用方式如下:exc_info
True
import logging
a = 5
b = 0
try:
c = a / b
except Exception as e:
logging.error("Exception occurred", exc_info=True)
ERROR:root:Exception occurred
Traceback (most recent call last):
File "exceptions.py", line 6, in <module>
c = a / b
ZeroDivisionError: division by zero
[Finished in 0.2s]
如果exc_info
未设置为True
,则上述程序的输出不会告诉我们有关异常的任何信息,在实际场景中,这可能不像ZeroDivisionError
. 想象一下,尝试使用仅显示以下内容的日志来调试复杂代码库中的错误:
ERROR:root:Exception occurred
这是一个快速提示:如果您从异常处理程序进行记录,请使用logging.exception()
方法,该方法记录带有级别的消息ERROR
并将异常信息添加到消息中。更简单地说,调用logging.exception()
就像调用logging.error(exc_info=True)
. 但由于此方法总是转储异常信息,因此只能从异常处理程序中调用它。看看这个例子:
import logging
a = 5
b = 0
try:
c = a / b
except Exception as e:
logging.exception("Exception occurred")
ERROR:root:Exception occurred
Traceback (most recent call last):
File "exceptions.py", line 6, in <module>
c = a / b
ZeroDivisionError: division by zero
[Finished in 0.2s]
使用logging.exception()
将显示级别的日志ERROR
。如果您不希望那样,您可以从debug()
to调用任何其他日志记录方法critical()
并将exc_info
参数作为True
.
类和函数
到目前为止,我们已经看到了一个名为默认记录器root
,用于通过日志模块,只要其功能被直接称为是这样的:logging.debug()
。您可以(并且应该)通过创建Logger
类的对象来定义自己的记录器,尤其是在您的应用程序具有多个模块的情况下。让我们来看看模块中的一些类和函数。
日志模块中定义的最常用的类如下:
-
Logger
:这是一个类,其对象将在应用程序代码中直接用于调用函数。 -
LogRecord
:记录器会自动创建LogRecord
对象,其中包含与正在记录的事件相关的所有信息,例如记录器的名称、函数、行号、消息等。 -
Handler
:处理程序将 发送LogRecord
到所需的输出目的地,如控制台或文件。Handler
对于像的子类的碱StreamHandler
,FileHandler
,SMTPHandler
,HTTPHandler
,等等。这些子类将日志输出发送到相应的目的地,如sys.stdout
磁盘文件。 -
Formatter
:这是您通过指定列出输出应包含的属性的字符串格式来指定输出格式的地方。
其中,我们主要处理Logger
类的对象,这些对象是使用模块级函数实例化的logging.getLogger(name)
。getLogger()
使用相同的多次调用name
将返回对同一Logger
对象的引用,这使我们无需将记录器对象传递到需要它的每个部分。下面是一个例子:
import logging
logger = logging.getLogger('example_logger')
logger.warning('This is a warning')
This is a warning
这将创建一个名为 的自定义记录器example_logger
,但与根记录器不同,自定义记录器的名称不是默认输出格式的一部分,必须添加到配置中。将其配置为显示记录器名称的格式将提供如下输出:
WARNING:example_logger:This is a warning
同样,与根记录器不同,自定义记录器不能使用basicConfig()
. 您必须使用处理程序和格式化程序对其进行配置:
“建议我们使用模块级记录器,将其
__name__
作为 name 参数传递给getLogger()
来创建记录器对象,因为记录器本身的名称会告诉我们事件的记录位置。__name__
是 Python 中的一个特殊内置变量,其计算结果为当前模块的名称。” (来源)
使用处理程序
当您想配置自己的记录器并在生成日志时将日志发送到多个地方时,处理程序就会出现。处理程序将日志消息发送到配置的目的地,如标准输出流或文件,或通过 HTTP 或通过 SMTP 发送到您的电子邮件。
您创建的记录器可以有多个处理程序,这意味着您可以将其设置为保存到日志文件中,也可以通过电子邮件发送。
与记录器一样,您也可以在处理程序中设置严重性级别。如果您想为同一个记录器设置多个处理程序但希望每个处理程序具有不同的严重性级别,这将非常有用。例如,您可能希望将级别WARNING
及以上的日志记录到控制台,但级别ERROR
及以上的所有内容也应保存到文件中。这是一个执行此操作的程序:
# logging_example.py
import logging
# Create a custom logger
logger = logging.getLogger(__name__)
# Create handlers
c_handler = logging.StreamHandler()
f_handler = logging.FileHandler('file.log')
c_handler.setLevel(logging.WARNING)
f_handler.setLevel(logging.ERROR)
# Create formatters and add it to handlers
c_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
f_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
c_handler.setFormatter(c_format)
f_handler.setFormatter(f_format)
# Add handlers to the logger
logger.addHandler(c_handler)
logger.addHandler(f_handler)
logger.warning('This is a warning')
logger.error('This is an error')
__main__ - WARNING - This is a warning
__main__ - ERROR - This is an error
在这里,logger.warning()
正在创建一个LogRecord
保存事件的所有信息并将其传递给它拥有的所有处理程序的 :c_handler
和f_handler
。
c_handler
是一个StreamHandler
with 级别WARNING
并从 中获取信息LogRecord
以生成指定格式的输出并将其打印到控制台。f_handler
是一个FileHandler
with 级别ERROR
,它会忽略它,LogRecord
因为它的级别是WARNING
。
当logger.error()
被称为c_handler
行为完全像以前一样,并f_handler
得到了LogRecord
在水平ERROR
,所以它继续生成输出,就像c_handler
,但不是将它打印到控制台,它把它写到此格式指定的文件:
2018-08-03 16:12:21,723 - __main__ - ERROR - This is an error
与__name__
变量对应的记录器的名称记录为__main__
,这是 Python 分配给执行开始的模块的名称。如果此文件由其他某个模块导入,则该__name__
变量将对应于其名称logging_example。这是它的外观:
# run.py
import logging_example
logging_example - WARNING - This is a warning
logging_example - ERROR - This is an error
其他配置方式
您可以使用模块和类函数或通过创建配置文件或字典并分别使用fileConfig()
或加载它来配置日志记录,如上所示dictConfig()
。如果您想在正在运行的应用程序中更改日志记录配置,这些非常有用。
这是一个示例文件配置:
[loggers]
keys=root,sampleLogger
[handlers]
keys=consoleHandler
[formatters]
keys=sampleFormatter
[logger_root]
level=DEBUG
handlers=consoleHandler
[logger_sampleLogger]
level=DEBUG
handlers=consoleHandler
qualname=sampleLogger
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=sampleFormatter
args=(sys.stdout,)
[formatter_sampleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
在上面的文件中,有两个记录器,一个处理程序和一个格式化程序。定义它们的名称后,通过在它们的名称之前添加单词 logger、handler 和 formatter 来配置它们,并用下划线分隔。
要加载此配置文件,您必须使用fileConfig()
:
import logging
import logging.config
logging.config.fileConfig(fname='file.conf', disable_existing_loggers=False)
# Get the logger specified in the file
logger = logging.getLogger(__name__)
logger.debug('This is a debug message')
2018-07-13 13:57:45,467 - __main__ - DEBUG - This is a debug message
配置文件的路径作为参数传递给该fileConfig()
方法,该disable_existing_loggers
参数用于保留或禁用调用该函数时存在的记录器。True
如果未提及,则默认为。
以下是字典方法的 YAML 格式的相同配置:
version: 1
formatters:
simple:
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
console:
class: logging.StreamHandler
level: DEBUG
formatter: simple
stream: ext://sys.stdout
loggers:
sampleLogger:
level: DEBUG
handlers: [console]
propagate: no
root:
level: DEBUG
handlers: [console]
这是一个显示如何从yaml
文件加载配置的示例:
import logging
import logging.config
import yaml
with open('config.yaml', 'r') as f:
config = yaml.safe_load(f.read())
logging.config.dictConfig(config)
logger = logging.getLogger(__name__)
logger.debug('This is a debug message')
2018-07-13 14:05:03,766 - __main__ - DEBUG - This is a debug message
保持冷静并阅读日志
日志模块被认为是非常灵活的。它的设计非常实用,应该适合您开箱即用的用例。您可以向小型项目添加基本日志记录,或者如果您正在处理大型项目,则可以创建自己的自定义日志级别、处理程序类等。
如果您还没有在应用程序中使用日志记录,那么现在是开始的好时机。如果做得好,日志记录肯定会消除您的开发过程中的许多摩擦,并帮助您找到机会将您的应用程序提升到一个新的水平。