IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在公众号、今日头条持续发布最新文章,助你少走弯路。


前面 24 篇,我们把电商核心功能全部写完,性能优化也做了缓存和异步。但还有个关键模块一直被忽视:日志与异常处理。没有日志,系统就像在黑箱中运行,出了 Bug 只能靠猜;没有统一的异常处理,用户看到的可能就是丑陋的黄色错误页面或者空白页,体验极差。

今天我们就来搭建一套完善的日志体系,涵盖请求追踪、业务日志、异常捕获、邮件告警,并配置好生产环境下的日志轮转。同时,我们会定制友好的错误页面,让项目从“能用”升级到“好用、好维护”。


一、为什么日志和异常处理如此重要

想象一下这些场景:

  • 用户支付成功,但订单状态未更新,你如何排查?

  • 某个接口突然返回 500,却没有留下任何记录,运维一脸茫然。

  • 恶意用户频繁访问不存在的地址,你不知道,服务器资源被白白消耗。

日志 就是系统的“黑匣子”,它记录了系统运行时的一切关键信息。异常处理 则是系统的“安全气囊”,在出错时保护用户体验,并收集必要的诊断信息。两者结合,才能真正做到可观测、可追溯。


二、Django 日志系统基础

Django 使用 Python 标准的 logging 模块,通过 settings.py 中的 LOGGING 字典进行配置。它由四个核心组件组成:

一条日志的流转路径:

代码调用 logger.info('xxx')
  → Logger 根据级别决定是否处理
  → 传递给 Handler
  → Handler 根据级别和 Filter 决定是否输出
  → 使用 Formatter 格式化
  → 输出到目标(控制台/文件/邮件)

三、配置项目日志

打开 django_ecommerce/settings.py,在文件末尾添加完整的 LOGGING 配置。我们将同时满足开发调试和生产运行的需求。

import os

# ==================== Django 日志配置 ====================
LOGS_DIR = BASE_DIR / 'logs'
if not os.path.exists(LOGS_DIR):
    os.makedirs(LOGS_DIR)

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,  # 保留 Django 默认 logger

    # 格式化器
    'formatters': {
        'simple': {
            'format': '[{asctime}] {levelname} [{name}] {message}',
            'style': '{',
        },
        'verbose': {
            'format': '[{asctime}] {levelname} [{name}:{lineno}] {message}',
            'style': '{',
        },
        'request_format': {
            'format': '[{asctime}] {levelname} [{name}] {message} | IP:{client_ip} Path:{path}',
            'style': '{',
        },
    },

    # 过滤器
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse',  # 只在 DEBUG=False 时生效
        },
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',   # 只在 DEBUG=True 时生效
        },
    },

    # 处理器
    'handlers': {
        # 开发环境:控制台输出
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'simple',
            'filters': ['require_debug_true'],
        },
        # 生产环境:所有 INFO 以上日志写入文件
        'file': {
            'level': 'INFO',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': LOGS_DIR / 'django.log',
            'when': 'midnight',       # 每天午夜轮转
            'backupCount': 30,        # 保留 30 天
            'formatter': 'verbose',
            'encoding': 'utf-8',
        },
        # 错误日志单独存放
        'error_file': {
            'level': 'ERROR',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': LOGS_DIR / 'error.log',
            'when': 'midnight',
            'backupCount': 90,
            'formatter': 'verbose',
            'encoding': 'utf-8',
        },
        # 请求日志
        'request_file': {
            'level': 'INFO',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': LOGS_DIR / 'request.log',
            'when': 'midnight',
            'backupCount': 30,
            'formatter': 'request_format',
            'encoding': 'utf-8',
        },
        # 严重错误邮件告警(生产环境配置 EMAIL 后生效)
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
            'filters': ['require_debug_false'],
            'include_html': True,
        },
    },

    # 日志记录器
    'loggers': {
        # Django 主日志
        'django': {
            'handlers': ['console', 'file'],
            'level': 'INFO',
            'propagate': False,
        },
        # 请求日志
        'django.request': {
            'handlers': ['request_file', 'mail_admins'],
            'level': 'INFO',
            'propagate': False,
        },
        # 服务端错误
        'django.server': {
            'handlers': ['error_file'],
            'level': 'ERROR',
            'propagate': False,
        },
        # 支付模块日志
        'payment': {
            'handlers': ['console', 'file', 'error_file'],
            'level': 'INFO',
            'propagate': False,
        },
        # 订单模块日志
        'orders': {
            'handlers': ['console', 'file', 'error_file'],
            'level': 'INFO',
            'propagate': False,
        },
        # 用户模块日志
        'users': {
            'handlers': ['console', 'file'],
            'level': 'INFO',
            'propagate': False,
        },
        # 购物车日志
        'cart': {
            'handlers': ['console', 'file'],
            'level': 'INFO',
            'propagate': False,
        },
    },
}

配置解读:

  • 开发环境(DEBUG=True):日志输出到控制台,便于实时查看。

  • 生产环境(DEBUG=False):INFO 以上日志写入文件并按天轮转;ERROR 单独记录;严重错误发送邮件给管理员。

  • TimedRotatingFileHandler:每天午夜自动归档旧日志,保留指定天数,避免日志文件无限膨胀。

  • AdminEmailHandler:当 DEBUG=False 时,自动把 ERROR 级别的错误邮件发送给 settings.ADMINS 中配置的管理员。


四、在代码中记录日志

4.1 在各模块中获取 Logger

我们已经在第 21 篇的支付视图和订单模型中用了日志,现在统一规范:

import logging

# 在支付 views.py 中
logger = logging.getLogger('payment')

# 在订单 models.py 中
logger = logging.getLogger('orders')

# 在用户 views.py 中
logger = logging.getLogger('users')

# 在购物车 views.py 中
logger = logging.getLogger('cart')
4.2 记录关键业务操作

apps/orders/views.pyorder_submit 中,增加详细日志:

logger.info(f'用户 {user.username} 提交订单,订单号={order.order_no},金额={total_amount},地址ID={address_id}')

支付成功时:

logger.info(f'订单 {out_trade_no} 支付成功,交易号={trade_no}')

支付失败/异常时:

logger.error(f'订单 {out_trade_no} 支付失败:{str(e)}', exc_info=True)

exc_info=True 会把完整的异常堆栈写入日志,对排查问题极其有用。

4.3 记录用户行为

apps/users/views.py 的注册、登录、修改密码等操作中加入日志:

# 注册
logger.info(f'新用户注册:{user.username},手机号={phone},邮箱={email}')

# 登录
logger.info(f'用户 {user.username} 登录成功')

# 密码修改
logger.info(f'用户 {user.username} 修改了密码')
4.4 记录 Celery 任务日志

apps/users/tasks.py 中,任务函数也使用 logging,日志会被 Celery 自动捕获并输出到 Worker 控制台,同时也写入我们在 settings 配置的文件中(如果 Worker 的进程能访问该日志文件)。不过 Celery 日志通常单独配置,这里我们在任务内使用 logger.info 即可。

logger = logging.getLogger('users')

@shared_task
def send_sms_task(phone, code):
    logger.info(f'异步发送短信到 {phone},验证码={code}')
    # ...

五、自定义错误页面

Django 默认在 DEBUG=False 时,404 会返回简陋的页面,500 会显示“服务器内部错误”。我们需要提供友好美观的错误页面。

5.1 创建错误模板

我们之前已经在第 5 篇建了 404.html500.html,现在确认它们存在于 templates/ 目录下,并符合 Bootstrap 风格。如果需要更新,可调整内容。确保它们使用了 base.html 的继承结构(注意 500 页面可能无法加载静态文件,需内联样式)。

500 页面建议内联 CSS(因为服务器错误时静态文件可能无法访问):

templates/500.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>服务器错误 - 500</title>
    <style>
        body { padding-top: 100px; text-align: center; font-family: sans-serif; background-color: #f8f9fa; }
        h1 { font-size: 80px; color: #dc3545; } p { color: #6c757d; }
    </style>
</head>
<body>
    <h1>500</h1>
    <p>服务器开小差了,请稍后重试。</p>
    <a href="/">返回首页</a>
</body>
</html>
5.2 在视图中触发 404/500

Django 会自动调用 handler404handler500 视图,默认就是根据 DEBUG 显示模板。我们已经在 django_ecommerce/urls.py 中配置了 handler404handler500(如果没有,可添加):

handler404 = 'django.views.defaults.page_not_found'
handler500 = 'django.views.defaults.server_error'

Django 会自动查找 404.html500.html,无需额外配置。

5.3 在视图中主动记录异常

对于可预期的错误(如库存不足、支付失败),我们使用 logger.warninglogger.error,并给用户友好提示。对于不可预期的异常,使用 try/except 并记录:

try:
    # 一些可能出错的操作
    result = some_service.call()
except Exception as e:
    logger.exception(f'操作失败:{request.user} - {request.path}')
    messages.error(request, '系统繁忙,请稍后重试。')
    return redirect('home')

logger.exception() 会自动附带异常堆栈,等同于 logger.error(exc_info=True)


六、生产环境日志轮转与持久化

我们已经在 handlers 中使用了 TimedRotatingFileHandler,它是生产环境最常用的方案。配置文件中的 when='midnight' 表示每天零点轮转,backupCount=30 表示保留最近 30 天的日志,旧文件自动删除。

生成的文件名类似:

django.log
django.log.2026-05-25
error.log
error.log.2026-05-24

这样可以避免单一日志文件过大,也方便按日期检索问题。


七、邮件告警配置

DEBUG=False 且发生服务器错误(HTTP 500)时,Django 会自动通过 AdminEmailHandler 发送邮件给 ADMINS 配置中的人员。我们需要在 settings.py 中配置:

# 管理员列表,用于接收错误报告
ADMINS = [('IT策士', 'admin@example.com')]

# 生产环境邮件配置(使用真实 SMTP)
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.example.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'noreply@example.com'
EMAIL_HOST_PASSWORD = 'your-email-password'
EMAIL_USE_TLS = True

当发生 500 错误时,管理员会收到一封包含详细 Traceback 的邮件,便于第一时间发现问题。


八、集成 Celery 日志

Celery 本身的日志可以通过配置 CELERYD_HIJACK_ROOT_LOGGER = False 来与 Django 日志系统集成,避免 Celery 接管根日志。在 settings.py 中添加:

CELERYD_HIJACK_ROOT_LOGGER = False
CELERY_WORKER_LOG_FORMAT = '[%(asctime)s] %(levelname)s [%(name)s] %(message)s'
CELERY_TASK_LOG_FORMAT = '[%(asctime)s] %(levelname)s [%(task_name)s] %(message)s'

这样 Celery Worker 的日志也会使用我们配置的格式,并可以写入同一个文件(或单独的 Celery 日志文件)。我们可以再添加一个 celery_file handler 单独存储 Celery 日志:

'celery_file': {
    'level': 'INFO',
    'class': 'logging.handlers.TimedRotatingFileHandler',
    'filename': LOGS_DIR / 'celery.log',
    'when': 'midnight',
    'backupCount': 30,
    'formatter': 'verbose',
},

并在 loggers 中加入:

'celery': {
    'handlers': ['celery_file'],
    'level': 'INFO',
    'propagate': False,
},

九、测试日志输出

9.1 开发环境测试

启动开发服务器(DEBUG=True),访问任意页面,控制台会输出类似:

[2026-05-28 10:20:15] INFO [django.request] GET /products/list/ 200
[2026-05-28 10:20:18] INFO [users] 用户 13800138000 登录成功

执行一个会触发错误操作(如访问不存在的订单),观察 error.log 文件(如果配置了文件输出,开发环境也生效)或控制台的错误输出。

9.2 模拟生产环境

临时将 DEBUG=False,并配置好 EMAIL,然后访问一个不存在的页面,会触发 404 页面并记录到 request.log;触发 500 错误(例如在视图中故意 raise Exception),则 error.log 会记录,管理员会收到邮件。

查看日志文件:

cat logs/django.log
cat logs/error.log

内容示例:

[2026-05-28 10:30:22] ERROR [payment:85] 订单 20260528103022X7K9M2 支付失败:签名验证失败
Traceback (most recent call last):
  File "/app/apps/payment/views.py", line 82, in payment_return
    ...
AlipaySignError: Sign verification failed

十、总结与下集预告

今天我们为项目搭建了一套完整的日志和异常处理体系:

  • 使用 Django LOGGING 配置了控制台、文件、邮件等多种处理器;

  • 按模块划分了 Logger(payment、orders、users、cart),实现业务级追踪;

  • 通过 TimedRotatingFileHandler 实现了日志的自动轮转和留存;

  • 配置了友好的 404/500 错误页面,捕获异常并记录;

  • 集成了 Celery 日志,统一管理。

日志到位后,项目就有了“诊断能力”。但还有一处性能隐患——数据库查询。频繁的 ORM 查询可能成为瓶颈。第 26 篇,我将带大家深度优化 数据库查询与索引,从 select_relatedprefetch_related 到数据库索引设计,让每一步数据库访问都精确高效。

想了解更多还可以去公众号、今日头条搜索「IT策士」,一起升级 IT 思维 !


*本文为《Django 从 0 到 1 打造完整电商平台》系列第 25 篇。
*

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐