跳到主要内容

Python 错误日志

在程序开发过程中,错误难以避免。相比于简单地使用print()语句或让程序在出错时崩溃,专业的开发者会使用日志系统来记录、跟踪和分析程序中的异常和错误。Python提供了强大的logging模块,它是标准库的一部分,专为错误日志记录而设计。

为什么需要错误日志?

当程序运行在生产环境时,我们通常无法实时监控程序的执行情况。此时,日志就成为了了解程序行为的窗口:

  • 记录程序运行过程中的错误和异常
  • 帮助开发者追踪bug的来源
  • 监控应用程序的健康状态
  • 记录用户行为和系统事件
日志 vs print

虽然print()语句可以在调试时显示信息,但日志系统提供了更多功能:不同的日志级别、输出格式控制、输出目标选择(控制台、文件等),以及更好的性能。

Python 的logging模块基础

Python的logging模块是处理日志的标准方式。让我们从简单的例子开始:

python
import logging

# 配置基本的日志格式
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

# 使用不同级别记录日志
logging.debug("这是一条调试信息")
logging.info("程序正在运行")
logging.warning("这是一个警告")
logging.error("发生了一个错误")
logging.critical("程序即将崩溃")

输出结果:

2023-11-10 10:15:30,123 - root - INFO - 程序正在运行
2023-11-10 10:15:30,124 - root - WARNING - 这是一个警告
2023-11-10 10:15:30,124 - root - ERROR - 发生了一个错误
2023-11-10 10:15:30,125 - root - CRITICAL - 程序即将崩溃

注意到debug级别的消息没有显示,这是因为我们将日志级别设置为INFO,低于该级别的日志会被忽略。

日志级别

Python日志系统定义了以下几个级别(从低到高):

  1. DEBUG (10): 详细的调试信息,通常只在诊断问题时使用
  2. INFO (20): 确认程序按预期运行的信息
  3. WARNING (30): 表示可能出现的问题,程序仍在正常工作
  4. ERROR (40): 由于较严重的问题,程序的某些功能无法执行
  5. CRITICAL (50): 严重错误,表明程序可能无法继续运行

级别越高,表示问题越严重。默认情况下,WARNING及以上级别的日志会被输出。

将日志保存到文件

在实际应用中,我们通常需要将日志保存到文件中以便后续分析:

python
import logging

# 配置日志输出到文件
logging.basicConfig(
filename='app.log', # 日志文件名
filemode='a', # 追加模式('w'表示覆盖)
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.DEBUG
)

logging.info("这条信息将被保存到app.log文件中")

记录异常信息

当程序发生异常时,记录完整的异常信息非常重要:

python
import logging

logging.basicConfig(level=logging.DEBUG)

try:
result = 10 / 0
except Exception as e:
# 方法1:使用exc_info参数
logging.error("发生除零错误", exc_info=True)

# 方法2:使用exception方法(自动包含异常信息)
logging.exception("发生除零错误")

输出:

2023-11-10 10:20:15,456 - root - ERROR - 发生除零错误
Traceback (most recent call last):
File "example.py", line 6, in <module>
result = 10 / 0
ZeroDivisionError: division by zero
2023-11-10 10:20:15,457 - root - ERROR - 发生除零错误
Traceback (most recent call last):
File "example.py", line 6, in <module>
result = 10 / 0
ZeroDivisionError: division by zero
注意

logging.exception()始终会记录异常堆栈,等同于调用logging.error(exc_info=True),且日志级别为ERROR。

创建自定义Logger

在大型项目中,使用自定义的Logger可以更好地组织代码:

python
import logging

# 创建一个自定义的logger
logger = logging.getLogger('myapp')

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

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

# 创建处理器:输出到文件
file_handler = logging.FileHandler('myapp.log')
file_handler.setLevel(logging.DEBUG)

# 创建格式化器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

# 将处理器添加到logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)

# 使用logger记录信息
logger.debug("调试信息 - 只会出现在文件中")
logger.info("普通信息 - 将出现在控制台和文件中")
logger.error("错误信息 - 将出现在控制台和文件中")

这种方式的优点是:

  • 更精细的控制(不同的处理器可以有不同的级别)
  • 多目标输出(同时输出到控制台和文件)
  • 可以在大型项目中创建层级关系的logger

日志轮转

当日志文件变得很大时,我们需要日志轮转功能:

python
import logging
from logging.handlers import RotatingFileHandler

# 创建logger
logger = logging.getLogger('myapp')
logger.setLevel(logging.DEBUG)

# 创建一个轮转文件处理器
# 最大文件大小为2MB,保留5个备份文件
handler = RotatingFileHandler(
'app.log', maxBytes=2*1024*1024, backupCount=5
)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

# 记录日志
logger.debug("测试日志轮转")

app.log达到2MB时,它会被重命名为app.log.1,并创建一个新的app.log文件。

日志配置文件

对于复杂的日志需求,可以使用配置文件:

python
import logging
import logging.config

# 从配置文件加载配置
logging.config.fileConfig('logging.conf')

# 获取logger
logger = logging.getLogger('root')
logger.debug('这是来自配置文件的日志')

logging.conf示例:

ini
[loggers]
keys=root

[handlers]
keys=consoleHandler,fileHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler,fileHandler

[handler_consoleHandler]
class=StreamHandler
level=INFO
formatter=simpleFormatter
args=(sys.stdout,)

[handler_fileHandler]
class=FileHandler
level=DEBUG
formatter=simpleFormatter
args=('app.log', 'a')

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s

实际应用案例

让我们创建一个简单的计算器应用,并在其中正确地使用日志:

python
import logging
import sys

# 配置日志系统
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("calculator.log"),
logging.StreamHandler(sys.stdout)
]
)

logger = logging.getLogger("计算器应用")

def divide(a, b):
"""执行除法运算,并记录相关日志"""
logger.info(f"执行除法运算: {a} / {b}")
try:
result = a / b
logger.debug(f"计算结果: {result}")
return result
except ZeroDivisionError:
logger.error("除零错误!不能使用0作为除数", exc_info=True)
return None
except Exception as e:
logger.exception(f"执行除法时发生未知错误: {str(e)}")
return None

def calculator():
"""简单计算器主函数"""
logger.info("计算器应用启动")

try:
num1 = float(input("请输入第一个数: "))
num2 = float(input("请输入第二个数: "))
operation = input("请选择操作 (+, -, *, /): ")

logger.debug(f"用户输入: {num1} {operation} {num2}")

if operation == '+':
result = num1 + num2
elif operation == '-':
result = num1 - num2
elif operation == '*':
result = num1 * num2
elif operation == '/':
result = divide(num1, num2)
if result is None:
print("除法运算失败,请查看日志获取详情")
return
else:
logger.warning(f"用户输入了无效的操作符: {operation}")
print("无效的操作符")
return

logger.info(f"计算完成: {num1} {operation} {num2} = {result}")
print(f"结果: {result}")

except ValueError:
logger.error("用户输入了无效的数字", exc_info=True)
print("请输入有效的数字")
except Exception as e:
logger.critical(f"应用发生致命错误: {str(e)}", exc_info=True)
print("程序遇到错误,已记录到日志")

logger.info("计算器应用退出")

if __name__ == "__main__":
calculator()

在这个示例中,我们:

  1. 配置了日志系统,同时输出到文件和控制台
  2. 对不同类型的操作使用了适当的日志级别
  3. 捕获并记录了不同类型的异常
  4. 提供了详细的上下文信息,便于调试

日志最佳实践

  1. 选择适当的日志级别

    • DEBUG:详细调试信息
    • INFO:常规程序流程信息
    • WARNING:警告但不影响程序运行的问题
    • ERROR:导致功能失败的错误
    • CRITICAL:导致程序崩溃的严重问题
  2. 提供有用的上下文

    • 包含变量值、方法名称和其他相关信息
    • 对于异常,包含完整的堆栈跟踪
  3. 结构化日志

    • 使用一致的格式便于后期分析
    • 考虑使用JSON格式便于机器处理
  4. 保护敏感数据

    • 不要记录密码、信用卡号等敏感信息
    • 如果必要,可以考虑对敏感数据进行脱敏处理
  5. 日志轮转

    • 防止日志文件变得过大
    • 设置合理的保留策略

总结

Python的日志系统是一个强大而灵活的工具,可以帮助开发者有效地跟踪和调试程序中的错误。通过合理地配置和使用日志,我们可以:

  • 更快地定位和修复问题
  • 监控应用程序的健康状态
  • 记录程序的运行历史
  • 在不中断程序的情况下收集信息

从简单的logging.basicConfig()到复杂的自定义Logger配置,Python提供了全面的日志记录解决方案,适应各种规模的项目需求。

练习

  1. 创建一个简单的文件读取程序,使用适当的日志记录可能出现的各种异常(文件不存在、权限问题等)。

  2. 修改上面的计算器示例,添加更多的操作(如平方、平方根等),并为每个操作添加适当的日志。

  3. 创建一个使用配置文件的日志系统,包含两个不同的logger:一个记录用户操作,另一个记录系统错误。

  4. 实现日志轮转,基于文件大小和时间(提示:查看TimedRotatingFileHandler)。

  5. 尝试使用logging.config.dictConfig()从Python字典配置日志系统,而不是使用配置文件。

附加资源

通过掌握Python的日志系统,你将能够更专业地处理程序中的错误和异常,提高代码的可维护性和可靠性。