导读:本指南介绍如何设置日志记录、避免常见错误以及应用高级技术来改进您的调试过程,无论您处理的是小脚本还是大型应用程序。
日志记录是 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,并指定日志消息的格式。然后,我们创建一个记录器并使用它来记录不同级别的消息。
logger = logging.getLogger(__name__)使用名称作为记录器名称是一种常见做法。它允许您为应用程序中的每个模块拥有唯一的记录器,这在尝试追踪大型代码库中的问题时非常有用。
logger.setLevel(logging.DEBUG)这将设置记录器以捕获 DEBUG 级别及以上的所有消息。低于此级别的任何消息(如果有)都将被忽略。
DEBUG:详细信息主要用于诊断问题。
信息:确认应用程序按预期运行。
警告:表示意外的事情发生了或者潜在的问题可能很快出现。
错误:表示存在严重问题,导致某些功能无法执行。
CRITICAL:严重错误,表明程序可能无法继续运行。
在编码实践中,你可以这样使用它们:
def divide(x, y):logger.debug(f"Dividing {x} by {y}")if y == 0:logger.error("Attempted to divide by zero!")return Nonereturn 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() 语句对于快速调试来说似乎很方便,但日志记录提供了几个优点:
对输出的精细控制:轻松调整输出的详细程度,而无需更改代码。
轻松禁用或重定向输出:轻松禁用日志记录或将其重定向到文件,无需更改代码。
包括上下文信息:自动在日志中包含时间戳、行号和函数名称等详细信息。
线程安全:与打印语句不同,日志记录是线程安全的,这使其对于多线程应用程序至关重要。
让我们比较一下:
# 使用 printdef some_function(x, y):print(f"some_function called with args: {x}, {y}")result = x + yprint(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 + ylogger.info(f"Result: {result}")return result
日志版本为我们提供了更大的灵活性,并提供了更多开箱即用的上下文。
import loggingdef setup_logger():logger = logging.getLogger(__name__)logger.setLevel(logging.DEBUG)# Create console handler and set level to debugch = logging.StreamHandler()ch.setLevel(logging.DEBUG)# Create formatterformatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')# Add formatter to chch.setFormatter(formatter)# Add ch to loggerlogger.addHandler(ch)return loggerlogger = setup_logger()is a debug message")
此设置为我们提供了一个以特定格式输出到控制台的记录器。
import loggingdef setup_file_logger():logger = logging.getLogger(__name__)logger.setLevel(logging.DEBUG)# Create a file handler that logs even debug messagesfh = logging.FileHandler('spam.log')fh.setLevel(logging.DEBUG)# Create formatterformatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')fh.setFormatter(formatter)# Add the handler to the loggerlogger.addHandler(fh)return loggerlogger = setup_file_logger()logger.debug("This debug message will be written to 'spam.log'")
此设置会将所有日志消息写入名为“spam.log”的文件中。
import loggingclass 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 hereself.logger.info("Something done!")obj = MyClass() # Corrected the instantiationobj.do_something()
此示例展示了如何在类中设置日志记录,这对于面向对象编程非常有用。
基本日志:用于logging.basicConfig()简单设置。
记录器对象:创建和配置Logger实例以获得更好的控制。
处理程序对象:利用不同的处理程序(例如StreamHandler,,FileHandler)来指导日志输出。
格式化程序对象:使用对象自定义日志消息的格式Formatter。
让我们看一个结合了这些内容的例子:
import loggingfrom logging.handlers import RotatingFileHandlerdef setup_logger():logger = logging.getLogger('my_app')logger.setLevel(logging.DEBUG)# Create a rotating file handlerfile_handler = RotatingFileHandler('my_app.log', maxBytes=100000, backupCount=5)file_handler.setLevel(logging.DEBUG)# Create a console handlerconsole_handler = logging.StreamHandler()console_handler.setLevel(logging.INFO)# Create a formatter and add it to the handlersformatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')file_handler.setFormatter(formatter)console_handler.setFormatter(formatter)# Add the handlers to the loggerlogger.addHandler(file_handler)logger.addHandler(console_handler)return loggerlogger = 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进行控制台输出。它还对文件和控制台输出使用不同的日志级别。
导入日志模块:import logs
创建一个记录器:logger = logs.getLogger(__name__)
设置日志级别:logger.setLevel(logging.DEBUG)
配置处理程序和格式化程序(如前面的代码示例所示)
开始记录吧!
请您记住,有效记录的关键是一致性。为您的项目建立记录惯例并坚持执行。
灵活且可定制:从日志级别到格式,定制日志以满足您的需求。
内置于 Python:无需外部库或依赖项。
支持多种输出目的地:日志事件可以被定向到控制台、文件、网络等。
线程安全:适用于多线程应用程序。
分层:按层次结构组织记录器,以便精确控制日志事件。
高级用例的复杂设置:广泛的配置选项可能会导致复杂性。
潜在的性能影响:不正确的配置或过多的日志记录可能会降低应用程序的速度。
数量过多:记录过多的信息会使查找相关的日志事件变得困难。
ELK Stack(Elasticsearch、Logstash、Kibana):用于收集、处理和可视化日志的强大套件。
Splunk:一个用于搜索、监控和分析机器生成数据的高级平台。
Graylog:开源日志管理和分析平台。
Loggly:基于云的日志管理和分析服务。
这些平台可以帮助您聚合来自多个来源的日志、执行高级搜索并创建可视化效果。
记录器:记录操作的入口点。它们公开应用程序代码直接使用的接口。
处理程序:确定日志消息的去向(控制台、文件、电子邮件等)。
格式化程序:定义日志消息的结构和内容。
过滤器:对要输出的日志记录提供细粒度的控制。
下面描述这些概念是如何协同工作的:
自定义过滤器示例:
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”的消息。这演示了如何使用过滤器对日志进行细粒度控制。
import loggingdef 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.stderrconsole = logging.StreamHandler()console.setLevel(logging.INFO)# Set a format that is simpler for console useformatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')console.setFormatter(formatter)# Add the handler to the root loggerlogging.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 loggingdef 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')
这种方法使您可以轻松地在整个应用程序中创建一致的记录器。
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
import loggingimport logging.configimport syslogging.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')
这种方法允许您将日志记录配置与代码分开,从而更容易修改日志记录行为而无需更改应用程序代码。
使用适当的日志级别:在生产中,您可能不需要 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 randomdef 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 logginglogger = 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 datalogger.debug("Request processed", extra={'user': user_id})process_request('12345', {'key': 'value'})
这包括每个日志消息中的用户 ID,从而更容易追踪与特定用户相关的操作。
用于LoggerAdapter添加上下文:对于更复杂的场景,用于LoggerAdapter向日志消息中一致添加上下文。
import loggingclass CustomAdapter(logging.LoggerAdapter):def process(self, msg, kwargs):return '[%s] %s' % (self.extra['ip'], msg), kwargslogger = 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()方法自动在日志消息中包含堆栈跟踪。
保持一致:在整个应用程序中使用相同的日志格式和约定。
以适当的级别记录日志:使用 DEBUG 记录详细信息、使用 INFO 记录一般信息、使用 WARNING 记录意外事件、使用 ERROR 记录严重问题、使用 CRITICAL 记录致命错误。
包括上下文:记录相关详细信息,帮助您了解日志创建时应用程序的状态。
使用结构化日志:考虑对日志使用 JSON 或其他结构化格式,以便更轻松地解析和分析。
不要记录敏感信息:注意不要记录密码、API 密钥或其他敏感数据。
尽早配置日志记录:在应用程序启动时设置日志记录配置,以确保所有模块使用相同的配置。
使用记录器层次结构:利用 Python 的记录器层次结构来控制整个应用程序的日志记录行为。
审查和轮换日志:定期审查您的日志并实施日志轮换来管理日志文件大小。
使用唯一标识符:在日志中包含唯一标识符(如请求 ID)以帮助跟踪跨多个服务的请求。
测试您的日志记录:确保您的日志记录按预期工作,特别是在错误情况下。
请记住,日志记录的目的是让您了解应用程序的操作。良好的日志记录实践可以为您节省N个小时的调试时间,并帮助您在问题变得严重之前发现它们。
随着你继续你的 Python 之旅,请不断完善你的日志记录实践。尝试不同的配置,试用各种日志记录平台,并始终留意使你的日志更具信息性和效率的方法。
祝您使用日志愉快!愿您的日志信息丰富、调试会话简短、生产部署顺利!现在开始,像技术专家一样使用日志吧!
作者:冯迪 
本篇文章为 @ 场长 创作并授权 21CTO 发布,未经许可,请勿转载。
内容授权事宜请您联系 webmaster@21cto.com或关注 21CTO 公众号。
该文观点仅代表作者本人,21CTO 平台仅提供信息存储空间服务。
                    请扫描二维码,使用微信支付哦。