Django 从入门到精通:知行博客实战(6.1) · 第 25 / 36 节

日志、健康检查与错误处理

吴秀峰 3 次阅读 0 条评论 ↓

学习目标

建立统一控制台日志、数据库就绪检查和友好错误页,能够根据症状找到下一条证据。

为什么需要可观察的失败

用户只看到“打不开”,原因却可能是数据库未启动、请求参数错误、模板异常或代理连接失败。日志提供时间、级别和模块;健康检查告诉部署系统依赖是否可用;用户错误页负责给出可理解的反馈,三者面向不同对象。

本章 health 执行 SELECT 1,仅表明当前可以访问数据库,不保证所有迁移和业务表都正确。数据库不可用返回 503,日志记录事件名,不把连接密码、异常细节或 SQL 参数公开给访客。真正上线还需要日志轮转、告警和可定位请求的关联标识。

具体怎么做

  1. 新建 config/health.py,根路由增加 /health/。
  2. 配置标准日志格式,输出到控制台,Docker 会收集标准输出。
  3. 创建独立的 404.html 和 500.html,不依赖数据库和复杂模板,避免错误页自身再次报错。
  4. 本地 DEBUG=True 会显示调试页面;验证正式错误页时使用生产设置或测试中的 override_settings,不要为了给用户看报错长期打开公网 DEBUG。

本章新增或替换的完整文件

以下路径均相对于练习项目根目录。替换同名文件的全部内容;未列出的文件保持上一章版本。

config/health.py

import logging
from django.db import connection, DatabaseError
from django.http import JsonResponse
from django.views.decorators.http import require_GET

logger = logging.getLogger(__name__)

@require_GET
def health(request):
    try:
        with connection.cursor() as cursor:
            cursor.execute("SELECT 1")
            cursor.fetchone()
    except DatabaseError:
        logger.error("health_database_unavailable")
        return JsonResponse({"status": "unavailable"}, status=503)
    return JsonResponse({"status": "ok"})

config/settings.py

import os
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured

BASE_DIR = Path(__file__).resolve().parent.parent
DEBUG = os.environ.get("DJANGO_DEBUG", "1") == "1"
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "development-only-never-use-this-key-in-production")
if not DEBUG and (SECRET_KEY.startswith("development-") or len(SECRET_KEY) < 50):
    raise ImproperlyConfigured("生产环境必须设置至少 50 字符的独立 DJANGO_SECRET_KEY")
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1,[::1]").split(",")
INSTALLED_APPS = [
    "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes",
    "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles",
    "accounts.apps.AccountsConfig", "blog.apps.BlogConfig",
]
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "config.urls"
TEMPLATES = [{
    "BACKEND": "django.template.backends.django.DjangoTemplates",
    "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True,
    "OPTIONS": {"context_processors": [
        "django.template.context_processors.request",
        "django.contrib.auth.context_processors.auth",
        "django.contrib.messages.context_processors.messages",
    ]},
}]
WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application"
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3"}}
AUTH_PASSWORD_VALIDATORS = [
    {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
LANGUAGE_CODE = "zh-hans"
TIME_ZONE = "Asia/Shanghai"
USE_I18N = True
USE_TZ = True
STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
MAILERS = {"default": {"BACKEND": "django.core.mail.backends.console.EmailBackend"}}

AUTH_USER_MODEL = "accounts.User"

LOGIN_URL = "login"
LOGIN_REDIRECT_URL = "blog:home"
LOGOUT_REDIRECT_URL = "blog:home"

MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
DATA_UPLOAD_MAX_MEMORY_SIZE = 3 * 1024 * 1024
FILE_UPLOAD_MAX_MEMORY_SIZE = 2 * 1024 * 1024

# SMTP 可选;没有配置时只打印到开发终端。
if os.environ.get("EMAIL_HOST"):
    MAILERS = {"default": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {
            "host": os.environ["EMAIL_HOST"],
            "port": int(os.environ.get("EMAIL_PORT", "587")),
            "username": os.environ.get("EMAIL_HOST_USER", ""),
            "password": os.environ.get("EMAIL_HOST_PASSWORD", ""),
            "use_tls": True,
            "timeout": 10,
        },
    }}
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "noreply@example.invalid")
PASSWORD_RESET_TIMEOUT = 3600

# 入门采用进程内缓存;多进程间不是共享缓存。
CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "zhixing"}}

LOGGING = {
    "version": 1, "disable_existing_loggers": False,
    "formatters": {"standard": {"format": "{asctime} {levelname} {name} {message}", "style": "{"}},
    "handlers": {"console": {"class": "logging.StreamHandler", "formatter": "standard"}},
    "root": {"handlers": ["console"], "level": "INFO"},
}

config/urls.py

from django.contrib import admin
from django.urls import path, include
from .health import health

urlpatterns = [path("health/", health, name="health"), path("accounts/", include("accounts.urls")), path("admin/", admin.site.urls), path("", include("blog.urls"))]

from django.conf import settings
from django.conf.urls.static import static
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

templates/404.html

<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>找不到页面</title><h1>找不到页面</h1><p>请稍后再试或返回首页。</p><a href="/">返回首页</a></html>

templates/500.html

<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>服务暂时不可用</title><h1>服务暂时不可用</h1><p>请稍后再试或返回首页。</p><a href="/">返回首页</a></html>

如何确认成功

curl -i http://127.0.0.1:8000/health/
python manage.py check

返回 200 和 {"status":"ok"}(空格可能不同)。开发日志中可以看见请求和业务发布事件,但没有密码、Cookie 或重置令牌。不要在终端录屏时打开包含重置链接的开发邮件输出。

练习:在隔离环境中停止数据库观察 503,再恢复服务;不要在有访客的本站数据库上做此实验。给一个模拟异常指定时间,尝试从日志定位到对应请求而不是只搜索最后一行。

常见问题

健康检查通过为什么首页仍报错? SELECT 1 不检查业务表和模板,发布时还需要 migrate --check 和页面冒烟测试。所有异常都 catch 后返回 200 可以吗? 不可以,会掩盖真实失败并误导监控。日志越详细越好? 也不对,应过滤私人内容、设置保留期并限制访问,生产不要记录完整请求体和认证头。

官方参考

把学习连起来

登录后可保存阅读位置、标记章节完成,并在其他设备继续学习。

登录并保存学习进度 →
联系站长

微信:VIPdjango。技术问题建议先在 社区讨论 中交流,方便更多人参与和查阅。

章节讨论 0