日志、健康检查与错误处理
学习目标
建立统一控制台日志、数据库就绪检查和友好错误页,能够根据症状找到下一条证据。
为什么需要可观察的失败
用户只看到“打不开”,原因却可能是数据库未启动、请求参数错误、模板异常或代理连接失败。日志提供时间、级别和模块;健康检查告诉部署系统依赖是否可用;用户错误页负责给出可理解的反馈,三者面向不同对象。
本章 health 执行 SELECT 1,仅表明当前可以访问数据库,不保证所有迁移和业务表都正确。数据库不可用返回 503,日志记录事件名,不把连接密码、异常细节或 SQL 参数公开给访客。真正上线还需要日志轮转、告警和可定位请求的关联标识。
具体怎么做
- 新建 config/health.py,根路由增加
/health/。 - 配置标准日志格式,输出到控制台,Docker 会收集标准输出。
- 创建独立的 404.html 和 500.html,不依赖数据库和复杂模板,避免错误页自身再次报错。
- 本地 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