MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python日志记录与异常追踪

2021-05-251.7k 阅读

Python日志记录基础

在Python开发中,日志记录是一项至关重要的功能。它可以帮助开发者在程序运行过程中记录重要信息、调试问题以及监控系统运行状态。Python标准库中的logging模块提供了一个灵活且强大的日志记录工具集。

1. 基本使用

要开始使用日志记录,首先需要导入logging模块。下面是一个简单的示例,展示了如何记录一条简单的日志消息:

import logging

# 配置日志记录的基本设置
logging.basicConfig(level=logging.INFO)

# 记录一条INFO级别的日志
logging.info('这是一条INFO级别的日志消息')

在上述代码中,通过basicConfig函数进行了基本配置,设置日志级别为INFO。这意味着只有INFO级别及以上(如WARNERRORCRITICAL)的日志消息会被记录。

2. 日志级别

Python的logging模块定义了以下几个日志级别:

  • DEBUG:详细的信息,通常只在调试程序时使用。
  • INFO:确认程序按预期工作的一般信息。
  • WARN:表示出现了一些可能的问题,但程序仍能继续运行。
  • ERROR:由于严重问题,程序在某些功能上可能无法正常运行。
  • CRITICAL:严重错误,表明程序可能无法继续运行。

例如,将日志级别设置为DEBUG,就可以看到更详细的调试信息:

import logging

logging.basicConfig(level=logging.DEBUG)

logging.debug('这是一条DEBUG级别的日志消息')
logging.info('这是一条INFO级别的日志消息')
logging.warning('这是一条WARN级别的日志消息')
logging.error('这是一条ERROR级别的日志消息')
logging.critical('这是一条CRITICAL级别的日志消息')

运行上述代码,你会看到所有级别的日志消息都被输出,因为DEBUG是最低级别,包含了其他所有级别。

日志记录的高级配置

1. 日志格式

basicConfig函数允许我们自定义日志消息的格式。通过format参数可以指定消息的格式字符串。例如,我们想要在日志消息中包含时间、日志级别和消息内容:

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

logging.info('这是一条带格式的INFO日志消息')

在上述格式字符串中:

  • %(asctime)s:表示日志记录的时间。
  • %(levelname)s:表示日志级别。
  • %(message)s:表示实际的日志消息内容。

2. 日志输出到文件

除了输出到控制台,我们还可以将日志记录保存到文件中。通过filename参数可以指定日志文件的路径:

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    filename='app.log'
)

logging.info('这是一条写入文件的INFO日志消息')

运行上述代码后,会在当前目录下生成一个名为app.log的文件,其中包含了记录的日志消息。

3. 日志记录器(Logger)

logging模块使用日志记录器来管理日志记录。我们可以通过logging.getLogger(name)方法获取一个日志记录器实例。如果不指定name,则会返回根日志记录器。不同的日志记录器可以有不同的配置,这在大型项目中非常有用,可以对不同模块的日志进行分别管理。

import logging

# 获取一个名为'my_module'的日志记录器
logger = logging.getLogger('my_module')
logger.setLevel(logging.DEBUG)

# 创建一个文件处理器,将日志写入文件
file_handler = logging.FileHandler('my_module.log')
file_handler.setLevel(logging.DEBUG)

# 创建一个格式化器,并将其添加到文件处理器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)

# 将文件处理器添加到日志记录器
logger.addHandler(file_handler)

# 记录日志
logger.debug('这是my_module中的DEBUG日志消息')

在上述代码中,我们创建了一个名为my_module的日志记录器,并为其设置了DEBUG级别。然后创建了一个文件处理器,将日志写入my_module.log文件,并设置了特定的格式。最后将文件处理器添加到日志记录器中。

异常追踪

异常处理是Python编程中不可或缺的一部分。当程序遇到错误时,会引发异常。如果不处理这些异常,程序可能会终止并输出错误信息。通过合理地处理异常,我们可以使程序更加健壮,并提供有用的错误提示。

1. 基本的异常处理

使用try - except语句可以捕获并处理异常。例如,当我们尝试进行一个可能会引发ZeroDivisionError的除法运算时:

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f'捕获到异常: {e}')

在上述代码中,try块中的代码如果引发ZeroDivisionError异常,程序会立即跳转到except块中执行相应的处理代码。as e部分将异常对象赋值给变量e,我们可以通过这个变量获取异常的详细信息。

2. 捕获多个异常

有时候,一段代码可能会引发多种不同类型的异常。我们可以在一个try - except语句中捕获多个异常。例如,在读取文件时,可能会遇到文件不存在(FileNotFoundError)或权限问题(PermissionError):

try:
    with open('nonexistent_file.txt', 'r') as file:
        content = file.read()
except (FileNotFoundError, PermissionError) as e:
    print(f'捕获到异常: {e}')

在上述代码中,except块可以捕获FileNotFoundErrorPermissionError这两种异常,并进行统一处理。

3. 异常的层次结构

Python的异常具有层次结构。例如,OSErrorFileNotFoundErrorPermissionError等许多与操作系统相关异常的基类。如果我们想要捕获所有与操作系统相关的异常,可以捕获OSError

try:
    with open('nonexistent_file.txt', 'r') as file:
        content = file.read()
except OSError as e:
    print(f'捕获到操作系统相关异常: {e}')

这样,无论是文件不存在还是权限问题,都会被except块捕获。

4. 自定义异常

在某些情况下,我们可能需要定义自己的异常类型。这在特定业务逻辑中非常有用,可以使代码更加清晰和易于维护。定义自定义异常很简单,只需继承Exception类或其某个子类:

class MyCustomError(Exception):
    pass

def divide_numbers(a, b):
    if b == 0:
        raise MyCustomError('除数不能为零')
    return a / b

try:
    result = divide_numbers(10, 0)
except MyCustomError as e:
    print(f'捕获到自定义异常: {e}')

在上述代码中,我们定义了一个MyCustomError异常类,并在divide_numbers函数中,当除数为零时,引发这个自定义异常。然后在try - except块中捕获并处理这个异常。

日志记录与异常追踪的结合

在实际开发中,将日志记录与异常追踪结合起来可以大大提高调试效率和系统的可维护性。当捕获到异常时,我们可以将异常信息记录到日志中,以便后续分析。

1. 在异常处理中记录日志

import logging

logging.basicConfig(
    level=logging.ERROR,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

try:
    result = 10 / 0
except ZeroDivisionError as e:
    logging.error(f'发生除零错误: {e}', exc_info=True)

在上述代码中,当捕获到ZeroDivisionError异常时,使用logging.error记录错误信息。exc_info=True参数会将异常的堆栈跟踪信息也记录到日志中,这对于定位问题非常有帮助。

2. 使用日志记录器记录异常

import logging

# 获取日志记录器
logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR)

# 创建文件处理器
file_handler = logging.FileHandler('error.log')
file_handler.setLevel(logging.ERROR)

# 创建格式化器并添加到处理器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)

# 将处理器添加到日志记录器
logger.addHandler(file_handler)

try:
    with open('nonexistent_file.txt', 'r') as file:
        content = file.read()
except FileNotFoundError as e:
    logger.error(f'文件未找到: {e}', exc_info=True)

在这个示例中,我们使用自定义的日志记录器logger,将文件未找到的异常信息记录到error.log文件中,并包含异常的堆栈跟踪信息。

高级日志记录技巧

1. 日志轮转

在实际应用中,日志文件可能会不断增大,占用大量磁盘空间。日志轮转(Log Rotation)可以解决这个问题,它会定期将旧的日志文件归档,并创建新的日志文件。Python的logging.handlers模块提供了RotatingFileHandlerTimedRotatingFileHandler来实现日志轮转。

使用RotatingFileHandler,当日志文件达到一定大小时,会进行轮转:

import logging
from logging.handlers import RotatingFileHandler

# 创建日志记录器
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# 创建RotatingFileHandler
handler = RotatingFileHandler('app.log', maxBytes=1024*1024, backupCount=5)
handler.setLevel(logging.INFO)

# 创建格式化器并添加到处理器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

# 将处理器添加到日志记录器
logger.addHandler(handler)

for i in range(100000):
    logger.info(f'这是第 {i} 条日志消息')

在上述代码中,RotatingFileHandler设置maxBytes为1MB,当app.log文件大小达到1MB时,会将其重命名为app.log.1,并创建一个新的app.log文件继续记录日志。backupCount设置为5,表示最多保留5个旧的日志文件,超过5个时,最早的日志文件会被删除。

使用TimedRotatingFileHandler,可以按时间进行日志轮转,例如每天轮转一次:

import logging
from logging.handlers import TimedRotatingFileHandler

# 创建日志记录器
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# 创建TimedRotatingFileHandler
handler = TimedRotatingFileHandler('app.log', when='D', interval=1, backupCount=7)
handler.setLevel(logging.INFO)

# 创建格式化器并添加到处理器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

# 将处理器添加到日志记录器
logger.addHandler(handler)

for i in range(100000):
    logger.info(f'这是第 {i} 条日志消息')

在上述代码中,when='D'表示按天轮转,interval=1表示每天轮转一次,backupCount=7表示最多保留7个旧的日志文件。

2. 日志过滤

有时候,我们可能只希望记录特定条件下的日志消息。logging模块提供了Filter类来实现日志过滤。例如,我们只想记录特定模块的日志:

import logging

class ModuleFilter(logging.Filter):
    def __init__(self, module_name):
        self.module_name = module_name

    def filter(self, record):
        return record.module == self.module_name

# 创建日志记录器
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# 创建过滤对象
filter_obj = ModuleFilter('__main__')
console_handler.addFilter(filter_obj)

# 创建格式化器并添加到处理器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)

# 将处理器添加到日志记录器
logger.addHandler(console_handler)

logger.info('这是主模块的日志消息')

在上述代码中,我们定义了一个ModuleFilter类,它继承自logging.Filterfilter方法检查日志记录的模块名是否与指定的模块名相同,如果相同则返回True,表示该日志消息应该被记录,否则返回False

日志记录在Web开发中的应用

在Python的Web开发框架(如Flask、Django)中,日志记录同样起着重要作用。

1. Flask中的日志记录

Flask是一个轻量级的Web框架。在Flask应用中,可以通过获取Flask应用实例的日志记录器来进行日志记录。

from flask import Flask
import logging

app = Flask(__name__)

# 获取Flask应用的日志记录器
app.logger.setLevel(logging.INFO)

@app.route('/')
def index():
    app.logger.info('访问了首页')
    return 'Hello, World!'

if __name__ == '__main__':
    app.run(debug=True)

在上述代码中,我们通过app.logger获取Flask应用的日志记录器,并设置日志级别为INFO。在index视图函数中,使用日志记录器记录用户访问首页的信息。

2. Django中的日志记录

Django是一个功能强大的Web框架。Django的日志配置在项目的settings.py文件中进行。例如,我们可以配置将所有ERROR级别及以上的日志记录到文件中:

# settings.py
import logging.config

LOGGING = {
   'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'ERROR',
            'class': 'logging.FileHandler',
            'filename': 'django_errors.log',
            'formatter':'verbose'
        },
    },
    'formatters': {
       'verbose': {
            'format': '%(asctime)s - %(levelname)s - %(message)s'
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'ERROR',
            'propagate': True,
        },
    },
}

logging.config.dictConfig(LOGGING)

在上述配置中,我们定义了一个file处理器,将ERROR级别及以上的日志记录到django_errors.log文件中,并使用verbose格式化器。然后通过logging.config.dictConfig应用这个日志配置。

日志记录在分布式系统中的挑战与解决方案

在分布式系统中,日志记录面临着一些额外的挑战,如多节点日志的统一管理、日志的一致性等。

1. 多节点日志统一管理

在分布式系统中,多个节点都会产生日志。为了便于分析和排查问题,需要将这些日志集中管理。一种常见的解决方案是使用日志聚合工具,如Elasticsearch、Logstash和Kibana(ELK堆栈)。

  • Logstash:负责收集、处理和转发日志。它可以从不同的数据源(如文件、网络端口等)收集日志,并进行过滤、转换等操作,然后将处理后的日志发送到Elasticsearch。
  • Elasticsearch:是一个分布式搜索和分析引擎,用于存储和检索日志数据。它提供了高效的索引和搜索功能,方便快速定位和分析日志。
  • Kibana:是一个可视化工具,与Elasticsearch集成,用于创建仪表板、可视化日志数据,帮助用户直观地了解系统运行状态和问题。

2. 日志一致性

在分布式系统中,由于网络延迟、节点故障等原因,可能会导致日志记录的不一致。为了保证日志的一致性,可以采用以下方法:

  • 使用分布式日志协议:如Apache Kafka,它是一个高吞吐量的分布式发布订阅消息系统,可以作为日志传输的可靠通道,确保日志在各个节点之间的有序传输。
  • 事务性日志:在关键业务操作中,使用事务性日志记录,确保日志的写入与业务操作的原子性,避免部分日志记录成功而部分失败的情况。

通过合理地应用这些技术和方法,可以有效地解决分布式系统中日志记录的挑战,提高系统的可维护性和可靠性。

总结

Python的日志记录和异常追踪是开发健壮、可靠应用程序的重要组成部分。通过logging模块,我们可以灵活地配置日志记录的级别、格式、输出目标等,并且可以结合日志轮转、过滤等高级技巧来更好地管理日志。在异常处理方面,合理地捕获、处理和自定义异常可以使程序更加健壮。将日志记录与异常追踪结合,可以在程序出现问题时快速定位和解决。在不同的应用场景(如Web开发、分布式系统)中,根据具体需求进行相应的日志配置和处理,能够大大提高系统的可维护性和可靠性。希望通过本文的介绍,读者能够深入理解并熟练应用Python的日志记录与异常追踪技术。