17611538698
webmaster@21cto.com

Python 日志最佳实践指南

编程语言 0 547 2024-09-15 09:53:03

图片

导读:本指南介绍如何设置日志记录、避免常见错误以及应用高级技术来改进您的调试过程,无论您处理的是小脚本还是大型应用程序。

日志记录是 Python 开发中必不可少的一部分。它可以帮助开发人员跟踪应用程序行为并解决问题。本指南介绍了关键的日志记录实践,以提高代码的可观察性并使调试更容易。

我们将探讨如何设置日志记录、应避免的常见陷阱以及在大型项目中处理日志的高级技术。无论是构建小型脚本还是复杂的应用程序,您都可以找到实用的技巧来增强您的日志记录方法。

什么是 Python 日志?


Python 日志记录就像软件开发的瑞士军刀。它是 Python 标准库中的一个强大功能,可让您跟踪事件、调试问题并监控应用程序的运行状况。


可以将其视为您的应用程序的日记,但它不是青少年的焦虑,而是充满了宝贵的见解,可以让您免于无数小时的头脑风暴和键盘敲击。


为什么日志如此重要?


想象一下在没有任何线索的情况下尝试解决谋杀谜团 - 这就是没有适当日志记录的调试或故障排除的感觉。


良好的日志记录实践可以:

  1. 帮助你掌握应用程序的流程

  2. 提供调试的关键信息

  3. 在问题升级之前通知您潜在问题

  4. 提供对用户行为和应用程序性能的洞察


Python 日志模块


日志模块是 Python 日志系统的核心。它就像所有日志操作的指挥中心,为从 Python 程序生成日志消息提供了一个灵活的框架。

让我们了解如何有效地使用它。

import logging
# Basic configurationlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# Creating a loggerlogger = logging.getLogger(__name__)

# Logging messageslogger.debug("This is a debug message")logger.info("This is an info message")logger.warning("This is a warning message")logger.error("This is an error message")logger.critical("This is a critical message")


在此示例中,我们使用basicConfig()为日志记录设置基本配置。我们将默认级别设置为 INFO,并指定日志消息的格式。然后,我们创建一个记录器并使用它来记录不同级别的消息。

Python 记录器


记录器是应用程序的叙述者。它们负责捕获事件并将其路由到适当的处理程序。将它们视为代码的记者,随时准备报道正在发生的事情。


创建新的记录器


创建记录器很简单:

logger = logging.getLogger(__name__)

使用名称作为记录器名称是一种常见做法。它允许您为应用程序中的每个模块拥有唯一的记录器,这在尝试追踪大型代码库中的问题时非常有用。

日志记录阈值级别


阈值级别决定了哪些消息会被记录。它就像是日志的保镖,决定哪些消息足够重要,可以进入 VIP 部分(您的日志文件或控制台)。


logger.setLevel(logging.DEBUG)


这将设置记录器以捕获 DEBUG 级别及以上的所有消息。低于此级别的任何消息(如果有)都将被忽略。

日志的级别


Python 提供了几个日志级别,每个级别都有不同的用途:

  • DEBUG:详细信息主要用于诊断问题。

  • 信息:确认应用程序按预期运行。

  • 警告:表示意外的事情发生了或者潜在的问题可能很快出现。

  • 错误:表示存在严重问题,导致某些功能无法执行。

  • CRITICAL:严重错误,表明程序可能无法继续运行。


在编码实践中,你可以这样使用它们:

def divide(x, y):
logger.debug(f"Dividing {x} by {y}") if y == 0: logger.error("Attempted to divide by zero!") return None return x / yresult = divide(10, 2)logger.info(f"Division result: {result}")result = divide(10, 0)logger.warning("Division operation failed")


在上面的例子中,我们使用不同的日志级别来提供有关我们的功能中发生的事情的背景信息。

DEBUG消息为我们提供了有关操作的详细信息,ERROR消息提醒我们出现了严重问题,INFO消息确认操作成功,WARNING消息表明出现了问题,但不一定是严重错误。

print函数 vs日志记录

虽然 print() 语句对于快速调试来说似乎很方便,但日志记录提供了几个优点:

  • 对输出的精细控制:轻松调整输出的详细程度,而无需更改代码。

  • 轻松禁用或重定向输出:轻松禁用日志记录或将其重定向到文件,无需更改代码。

  • 包括上下文信息:自动在日志中包含时间戳、行号和函数名称等详细信息。

  • 线程安全:与打印语句不同,日志记录是线程安全的,这使其对于多线程应用程序至关重要。


让我们比较一下:

# 使用 print
def some_function(x, y): print(f"some_function called with args: {x}, {y}") result = x + y print(f"Result: {result}") return result
# 使用 loggingimport logginglogger = logging.getLogger(__name__)def some_function(x, y): logger.debug(f"some_function called with args: {x}, {y}") result = x + y logger.info(f"Result: {result}") return result

日志版本为我们提供了更大的灵活性,并提供了更多开箱即用的上下文。

日志记录示例


让我们看一些更高级的示例,以锻炼我们的记录能力。


片段 1:创建一个带有处理程序和格式化的日志。


import logging

def setup_logger():
logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG)

# Create console handler and set level to debug ch = logging.StreamHandler() ch.setLevel(logging.DEBUG)


# Create formatter formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # Add formatter to ch ch.setFormatter(formatter) # Add ch to logger logger.addHandler(ch) return logger
logger = setup_logger()logger.debug("This is a debug message")

此设置为我们提供了一个以特定格式输出到控制台的记录器。

代码片段 2:记录到文件

import logging
def setup_file_logger(): logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG)
# Create a file handler that logs even debug messages fh = logging.FileHandler('spam.log') fh.setLevel(logging.DEBUG)
# Create formatter formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') fh.setFormatter(formatter)
# Add the handler to the logger logger.addHandler(fh) return logger
logger = setup_file_logger()logger.debug("This debug message will be written to 'spam.log'")

此设置会将所有日志消息写入名为“spam.log”的文件中。

代码片段 3:在类中使用日志记录


import logging
class MyClass: def __init__(self): # Corrected from 'init' to '__init__' self.logger = logging.getLogger(self.__class__.__name__) self.logger.setLevel(logging.DEBUG) handler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) self.logger.addHandler(handler)
def do_something(self): self.logger.debug("Doing something...") # Do something here self.logger.info("Something done!")
obj = MyClass() # Corrected the instantiationobj.do_something()


此示例展示了如何在类中设置日志记录,这对于面向对象编程非常有用。

Python 日志记录方法的类型


Python 的日志模块提供了各种方法来满足不同的需求:

  • 基本日志:用于logging.basicConfig()简单设置。

  • 记录器对象:创建和配置Logger实例以获得更好的控制。

  • 处理程序对象:利用不同的处理程序(例如StreamHandler,,FileHandler)来指导日志输出。

  • 格式化程序对象:使用对象自定义日志消息的格式Formatter。


让我们看一个结合了这些内容的例子:

import logging
from logging.handlers import RotatingFileHandler
def setup_logger(): logger = logging.getLogger('my_app') logger.setLevel(logging.DEBUG)
# Create a rotating file handler file_handler = RotatingFileHandler('my_app.log', maxBytes=100000, backupCount=5) file_handler.setLevel(logging.DEBUG)
# Create a console handler console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO)
# Create a formatter and add it to the handlers formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler.setFormatter(formatter) console_handler.setFormatter(formatter)
# Add the handlers to the logger logger.addHandler(file_handler) logger.addHandler(console_handler)
return logger
logger = setup_logger()
# Now we can use the loggerlogger.debug("This will go to the log file")logger.info("This will go to both the log file and console")logger.warning("This is a warning!")


此设置将使用RotatingFileHandler来管理日志文件(当前文件太大时创建新文件)并使用StreamHandler进行控制台输出。它还对文件和控制台输出使用不同的日志级别。

如何开始使用 Python 日志记录?


  • 导入日志模块:import logs

  • 创建一个记录器:logger = logs.getLogger(__name__)

  • 设置日志级别:logger.setLevel(logging.DEBUG)

  • 配置处理程序和格式化程序(如前面的代码示例所示)

  • 开始记录吧!


请您记住,有效记录的关键是一致性。为您的项目建立记录惯例并坚持执行。

优点:

  • 灵活且可定制:从日志级别到格式,定制日志以满足您的需求。

  • 内置于 Python:无需外部库或依赖项。

  • 支持多种输出目的地:日志事件可以被定向到控制台、文件、网络等。

  • 线程安全:适用于多线程应用程序。

  • 分层:按层次结构组织记录器,以便精确控制日志事件。

缺点:

  • 高级用例的复杂设置:广泛的配置选项可能会导致复杂性。

  • 潜在的性能影响:不正确的配置或过多的日志记录可能会降低应用程序的速度。

  • 数量过多:记录过多的信息会使查找相关的日志事件变得困难。


Python 日志平台


尽管 Python 的内置日志功能本身就非常强大,但有时您还需要更多功能。以下是一些流行的日志平台:

  • ELK Stack(Elasticsearch、Logstash、Kibana):用于收集、处理和可视化日志的强大套件。

  • Splunk:一个用于搜索、监控和分析机器生成数据的高级平台。

  • Graylog:开源日志管理和分析平台。

  • Loggly:基于云的日志管理和分析服务。


这些平台可以帮助您聚合来自多个来源的日志、执行高级搜索并创建可视化效果。

基本 Python 日志概念

  • 记录器:记录操作的入口点。它们公开应用程序代码直接使用的接口。

  • 处理程序:确定日志消息的去向(控制台、文件、电子邮件等)。

  • 格式化程序:定义日志消息的结构和内容。

  • 过滤器:对要输出的日志记录提供细粒度的控制。


下面描述这些概念是如何协同工作的:

自定义过滤器示例:

import logging

# Create a custom filterclass MyFilter(logging.Filter): def filter(self, record): return 'important' in record.msg.lower()
# Set up the loggerlogger = logging.getLogger(__name__)logger.setLevel(logging.DEBUG)
# Create a handlerhandler = logging.StreamHandler()handler.setLevel(logging.DEBUG)
# Create a formatterformatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')handler.setFormatter(formatter)
# Add the filter to the handlerhandler.addFilter(MyFilter())
# Add the handler to the loggerlogger.addHandler(handler)
# Now let's log some messageslogger.debug("This is a debug message") # This won't be loggedlogger.info("This is an important message") # This will be loggedlogger.warning("Another message") # This won't be logged

在此示例中,我们创建了一个自定义过滤器,仅允许包含单词“important”的消息。这演示了如何使用过滤器对日志进行细粒度控制。

Python 日志配置


请看如下的代码。


import logging
def configure_logging(): logging.basicConfig( level=logging.DEBUG, format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M', filename='myapp.log', filemode='w' )
# Define a Handler which writes INFO messages or higher to the sys.stderr console = logging.StreamHandler() console.setLevel(logging.INFO)
# Set a format that is simpler for console use formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') console.setFormatter(formatter)
# Add the handler to the root logger logging.getLogger('').addHandler(console)
configure_logging()
# Now, we can log to the root logger, or any other logger. The root logger is the parent of all loggers.logging.info('Jackdaws love my big sphinx of quartz.')
# Log messages to child loggerslogger1 = logging.getLogger('myapp.area1')logger2 = logging.getLogger('myapp.area2')
logger1.debug('Quick zephyrs blow, vexing daft Jim.')logger1.info('How quickly daft jumping zebras vex.')logger2.warning('Jail zesty vixen who grabbed pay from quack.')logger2.error('The five boxing wizards jump quickly.')

此配置设置了文件和控制台的日志记录,每种记录都有不同的格式和级别。

自定义记录器工厂函数


import logging
def create_logger(name, log_file, level=logging.INFO): formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
handler = logging.FileHandler(log_file) handler.setFormatter(formatter)
logger = logging.getLogger(name) logger.setLevel(level) logger.addHandler(handler) return logger
# Usagelogger = create_logger('my_module', 'my_module.log', logging.DEBUG)logger.debug('This is a debug message')


这种方法使您可以轻松地在整个应用程序中创建一致的记录器。

使用 Configparse 格式的文件进行配置:

logging.conf

[loggers]
keys=root,simpleExample
[handlers]keys=consoleHandler
[formatters]keys=simpleFormatter
[logger_root]level=DEBUGhandlers=consoleHandler
[logger_simpleExample]level=DEBUGhandlers=consoleHandlerqualname=simpleExamplepropagate=0
[handler_consoleHandler]class=StreamHandlerlevel=DEBUGformatter=simpleFormatterargs=(sys.stdout,)
[formatter_simpleFormatter]format=%(asctime)s - %(name)s - %(levelname)s - %(message)s


用于加载配置的 Python 代码



import loggingimport logging.configimport sys
logging.config.fileConfig('logging.conf')
# Create loggerlogger = logging.getLogger('simpleExample')
# Use the loggerlogger.debug('This is a debug message')logger.info('This is an info message')logger.warning('This is a warning message')

这种方法允许您将日志记录配置与代码分开,从而更容易修改日志记录行为而无需更改应用程序代码。

Python 日志记录性能 


虽然日志记录对于理解和调试应用程序至关重要,但以不会显著影响应用程序性能的方式实现它也很重要。以下是一些保持日志记录高效的技巧:


基于配置的考虑

  • 使用适当的日志级别:在生产中,您可能不需要 DEBUG 级别的日志。适当设置日志级别以减少生成的日志数据量。


if app.config['ENV'] == 'production':
logging.getLogger().setLevel(logging.ERROR)else: logging.getLogger().setLevel(logging.DEBUG)
  • 实现日志轮换:使用RotatingFileHandler或TimedRotatingFileHandler管理日志文件大小,防止它们占用过多的磁盘空间。


from logging.handlers import RotatingFileHandlerhandler = RotatingFileHandler('app.log', maxBytes=10000, backupCount=3)logger.addHandler(handler)

  • 考虑对大容量场景使用异步日志记录:类似的库concurrent-log-handler可以帮助管理高吞吐量应用程序中的日志记录。


from concurrent_log_handler import ConcurrentRotatingFileHandlerhandler = ConcurrentRotatingFileHandler('app.log', 'a', 512*1024, 5)logger.addHandler(handler)

基于代码的考虑

  • 不要执行非活动日志记录语句:使用延迟日志记录来避免不必要的性能损失。

if logger.isEnabledFor(logging.DEBUG):    logger.debug(f"User {get_user_name(user_id)} logged in from {get_ip_address()}")

这样,只有在实际启用调试级别时才会进行昂贵的函数调用。

  • 使用%格式化或str.format()代替 f 字符串来记录日志消息:虽然 f 字符串很方便,但即使未启用日志级别,它们也总是会被评估# Instead of this:


logger.debug(f"Processing item {item} with options {options}")
# Do this:logger.debug("Processing item %s with options %s", item, options)
  • 在关键路径中批量记录调用:如果您有一个在每次迭代中记录的循环,请考虑批量记录调用日志。# Instead of this:


for item in items:    process(item)    logger.debug(f"Processed {item}")
# Do this:processed = []for item in items: process(item) processed.append(item)logger.debug("Processed items: %s", processed)

  • 对大容量日志使用采样:在某些情况下,您可能希望仅记录事件样本以减少日志量


import random

def log_sample(message, sample_rate=0.1): if random.random() < sample_rate: logger.debug(message)
for item in large_list_of_items: process(item) log_sample(f"Processed {item}")

这将记录大约 10% 已处理的项目。

高级日志记录技术


  • extra使用参数进行上下文日志记录:向您的日志添加额外的上下文信息。


import logging

logger = logging.getLogger(__name__)logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(user)s')handler.setFormatter(formatter)logger.addHandler(handler)
def process_request(user_id, data): logger.info("Processing request", extra={'user': user_id}) # Process the data logger.debug("Request processed", extra={'user': user_id})
process_request('12345', {'key': 'value'})


这包括每个日志消息中的用户 ID,从而更容易追踪与特定用户相关的操作。

  • 用于LoggerAdapter添加上下文:对于更复杂的场景,用于LoggerAdapter向日志消息中一致添加上下文。



import logging

class CustomAdapter(logging.LoggerAdapter): def process(self, msg, kwargs): return '[%s] %s' % (self.extra['ip'], msg), kwargs
logger = logging.getLogger(__name__)logger.setLevel(logging.DEBUG)handler = logging.StreamHandler()logger.addHandler(handler)
adapter = CustomAdapter(logger, {'ip': '123.45.67.89'})adapter.debug('This is a debug message')adapter.info('This is an info message')


这会在每个日志消息前面加上 IP 地址作为前缀。

  • 记录异常:记录异常时包含回溯。import logging

logger = logging.getLogger(__name__)
try: 1 / 0except Exception: logger.exception("An error occurred")

该exception()方法自动在日志消息中包含堆栈跟踪。

Python 日志记录的最佳实践


  • 保持一致:在整个应用程序中使用相同的日志格式和约定。

  • 以适当的级别记录日志:使用 DEBUG 记录详细信息、使用 INFO 记录一般信息、使用 WARNING 记录意外事件、使用 ERROR 记录严重问题、使用 CRITICAL 记录致命错误。

  • 包括上下文:记录相关详细信息,帮助您了解日志创建时应用程序的状态。

  • 使用结构化日志:考虑对日志使用 JSON 或其他结构化格式,以便更轻松地解析和分析。

  • 不要记录敏感信息:注意不要记录密码、API 密钥或其他敏感数据。

  • 尽早配置日志记录:在应用程序启动时设置日志记录配置,以确保所有模块使用相同的配置。

  • 使用记录器层次结构:利用 Python 的记录器层次结构来控制整个应用程序的日志记录行为。

  • 审查和轮换日志:定期审查您的日志并实施日志轮换来管理日志文件大小。

  • 使用唯一标识符:在日志中包含唯一标识符(如请求 ID)以帮助跟踪跨多个服务的请求。

  • 测试您的日志记录:确保您的日志记录按预期工作,特别是在错误情况下。


结论


日志记录是任何强大的 Python 应用程序的重要组成部分。如果能够正确使用,它可以成为您了解应用程序行为、调试问题和保持开发人员理智的最佳得力助手。

请记住,日志记录的目的是让您了解应用程序的操作。良好的日志记录实践可以为您节省N个小时的调试时间,并帮助您在问题变得严重之前发现它们。

随着你继续你的 Python 之旅,请不断完善你的日志记录实践。尝试不同的配置,试用各种日志记录平台,并始终留意使你的日志更具信息性和效率的方法。

祝您使用日志愉快!愿您的日志信息丰富、调试会话简短、生产部署顺利!现在开始,像技术专家一样使用日志吧!

作者:冯迪

评论