Skip to content

Latest commit

 

History

History
870 lines (646 loc) · 32.9 KB

13.md

File metadata and controls

870 lines (646 loc) · 32.9 KB

第13章 维护

本章中包含如下小节:

  • 创建及还原MySQL数据库备份
  • 创建及还原PostgreSQL数据库备份
  • 配置cron job执行定时任务
  • 日志事件供进一步审查
  • 通过email获取详细错误报告

引言

至此,读者应当已经有一个或多个开发及发布的Django项目。对部署环节的最后几步,我们要来学习如何维护项目及监控项目进行优化。一起站好最后一班岗吧!

技术要求

运行本章的代码要求安装最新稳定版的Python 3、MySQL或PostgreSQL数据库以及通过虚拟环境创建的Django项目。

可在GitHub仓库的Chapter13目录中查看本章的代码。

创建及还原MySQL数据库备份

为保证网站的稳定性,能够在硬件出错及黑客攻击后恢复服务非常重要。因此,应当保持进行备份并保证备份有效。代码和静态文件通常处于版本控制之下,可以进行还原,但数据库和媒体应当经常进行备份。

本节中,我们将展示如何为MySQL数据库创建备份。

准备工作

首先保证有一个使用MySQL数据库的可运行的Django项目。部署该项目至远程生产(或预发布)服务器上。

如何实现...

执行如下步骤来备份、还原MySQL数据库:

  1. 在项目目录的commands目录中,创建一个bash脚本:backup_mysql_db.sh。启动带有变量和函数定义的脚本如下:

    /home/myproject/commands/backup_mysql_db.sh
    #!/usr/bin/env bash
    SECONDS=0
    export DJANGO_SETTINGS_MODULE=myproject.settings.production
    PROJECT_PATH=/home/myproject
    REPOSITORY_PATH=${PROJECT_PATH}/src/myproject
    LOG_FILE=${PROJECT_PATH}/logs/backup_mysql_db.log
    DAY_OF_THE_WEEK=$(LC_ALL=en_US.UTF-8 date +"%w-%A")
    DAILY_BACKUP_PATH=${PROJECT_PATH}/db_backups/${DAY_OF_THE_WEEK}.sql
    LATEST_BACKUP_PATH=${PROJECT_PATH}/db_backups/latest.sql
    error_counter=0
    
    echoerr() { echo "$@" 1>&2; }
    
    cd ${PROJECT_PATH}
    mkdir -p logs
    mkdir -p db_backups
    
    source env/bin/activate
    cd ${REPOSITORY_PATH}
    
    DATABASE=$(echo "from django.conf import settings; print(settings.DATABASES['default']['NAME'])" | python manage.py shell -i python)
    USER=$(echo "from django.conf import settings; print(settings.DATABASES['default']['USER'])" | python manage.py shell -i python)
    PASSWORD=$(echo "from django.conf import settings; print(settings.DATABASES['default']['PASSWORD'])" | python manage.py shell -i python)
    
    EXCLUDED_TABLES=(
    django_session
    )
    
    IGNORED_TABLES_STRING=''
    for TABLE in "${EXCLUDED_TABLES[@]}"; do
        IGNORED_TABLES_STRING+=" --ignore-table=${DATABASE}.${TABLE}"
    done
    
  2. 然后添加命令创建数据库结构和数据的导出:

    echo "=== Creating DB Backup ===" > ${LOG_FILE}
    date >> ${LOG_FILE}
    
    echo "- Dump structure" >> ${LOG_FILE}
    mysqldump -u "${USER}" -p"${PASSWORD}" --single-transaction --no-data "${DATABASE}" > "${DAILY_BACKUP_PATH}" 2>> ${LOG_FILE}
    function_exit_code=$?
    if [[ $function_exit_code -ne 0 ]]; then
        {
            echoerr "Command mysqldump for dumping database structure 
             failed with exit code ($function_exit_code)."
            error_counter=$((error_counter + 1))
        } >> "${LOG_FILE}" 2>&1
    fi
    
    echo "- Dump content" >> ${LOG_FILE}
    # shellcheck disable=SC2086
    mysqldump -u "${USER}" -p"${PASSWORD}" "${DATABASE}" ${IGNORED_TABLES_STRING} >> "${DAILY_BACKUP_PATH}" 2>> ${LOG_FILE}
    function_exit_code=$?
    if [[ $function_exit_code -ne 0 ]]; then
        {
            echoerr "Command mysqldump for dumping database content 
             failed with exit code ($function_exit_code)."
            error_counter=$((error_counter + 1))
        } >> "${LOG_FILE}" 2>&1
    fi
    
  3. 添加命令压缩数据库导出文件并创建一个软链接,latest.sql.gz:

    echo "- Create a *.gz archive" >> ${LOG_FILE}
    gzip --force "${DAILY_BACKUP_PATH}"
    function_exit_code=$?
    if [[ $function_exit_code -ne 0 ]]; then
        {
            echoerr "Command gzip failed with exit code 
             ($function_exit_code)."
            error_counter=$((error_counter + 1))
        } >> "${LOG_FILE}" 2>&1
    fi
    
    echo "- Create a symlink latest.sql.gz" >> ${LOG_FILE}
    if [ -e "${LATEST_BACKUP_PATH}.gz" ]; then
        rm "${LATEST_BACKUP_PATH}.gz"
    fi
    ln -s "${DAILY_BACKUP_PATH}.gz" "${LATEST_BACKUP_PATH}.gz"
    function_exit_code=$?
    if [[ $function_exit_code -ne 0 ]]; then
        {
            echoerr "Command ln failed with exit code 
             ($function_exit_code)."
            error_counter=$((error_counter + 1))
        } >> "${LOG_FILE}" 2>&1
    fi
    
  4. 记录下前面命令执行所花费的时间来完成这一脚本:

    duration=$SECONDS
    echo "------------------------------------------" >> ${LOG_FILE}
    echo "The operation took $((duration / 60)) minutes and $((duration % 60)) seconds." >> ${LOG_FILE}
    exit $error_counter
    
  5. 在同一目录中,使用如下内容创建一个bash脚本restore_mysql_db.sh:

    # home/myproject/commands/restore_mysql_db.sh
    #!/usr/bin/env bash
    SECONDS=0
    PROJECT_PATH=/home/myproject
    REPOSITORY_PATH=${PROJECT_PATH}/src/myproject
    LATEST_BACKUP_PATH=${PROJECT_PATH}/db_backups/latest.sql
    export DJANGO_SETTINGS_MODULE=myproject.settings.production
    
    cd "${PROJECT_PATH}"
    source env/bin/activate
    
    echo "=== Restoring DB from a Backup ==="
    
    echo "- Fill the database with schema and data"
    cd "${REPOSITORY_PATH}"
    zcat "${LATEST_BACKUP_PATH}.gz" | python manage.py dbshell
    
    duration=$SECONDS
    echo "------------------------------------------"
    echo "The operation took $((duration / 60)) minutes and $((duration % 60)) seconds."
    
  6. 让这两个脚本可执行:

    $ chmod +x *.sh
    
  7. 运行数据库备份脚本:

    $ ./backup_mysql_db.sh
    
  8. 运行数据库还原脚本(在生产环境请格外小心):

    $ ./restore_mysql_db.sh
    

实现原理...

备份脚本会在/home/myproject/db_backups/之下创建一个备份文件并保存日志至/home/myproject/logs/backup_mysql_db.log,类似下面这样:

=== Creating DB Backup ===
Fri Jan 17 02:12:14 CET 2020
- Dump structure
mysqldump: [Warning] Using a password on the command line interface can be insecure.
- Dump content
mysqldump: [Warning] Using a password on the command line interface can be insecure.
- Create a *.gz archive
- Create a symlink latest.sql.gz
------------------------------------------
The operation took 0 minutes and 2 seconds.

如果操作成功,脚本会返回退出码0;否则退出码为执行脚本的错误数量。并在日志文件中显示错误消息。

在db_backups目录中,有一个以星期压缩的SQL备份,如0-Sunday.sql.gz、1-Monday.sql.gz等,以及另一个文件件,实际上是一个软链接,名为latest.sql.gz。按星期的日期备份让我们可以在定时任务中适当配置后拥有最近7天的备份,以及一个软链接让我们可以通过SSH快速或自动传输最近的一个备份至另一台电脑上。

注意我们从Django配置文件中获取数据库登录信息,然后在bash脚本中进行使用。

我们导出了sessions表以外的其它数据,因为会是临时的并且很耗内存。

在运行restore_mysql_db.sh脚本时,会获取类似下面的输出:

=== Restoring DB from a Backup ===
- Fill the database with schema and data
mysql: [Warning] Using a password on the command line interface can be insecure.
------------------------------------------
The operation took 0 minutes and 2 seconds.

相关内容

  • 第12章 部署中的在生产环境通过mod_wsgi部署Apache一节
  • 第12章 部署中的在生产环境中基于Nginx和Gunicorn部署一节
  • 创建及还原PostgreSQL数据库备份一节
  • 配置cron job执行定时任务一节

创建及还原PostgreSQL数据库备份

本节中我们将学习如何备份PostgreSQL数据库并在硬件出错或黑客攻击时进行还原。

准备工作

需要有一个使用PostgreSQL数据库的有效Django项目。将该项目部署到远程预发布或生产服务器上。

如何实现...

执行如下步骤来备份、还原PostgreSQL数据库:

  1. 在项目家目录的commands目录中,创建一个bash脚本backup_postgresql_db.sh。启动带有变量和函数定义的脚本如下:

    /home/myproject/commands/backup_postgresql_db.sh
    #!/usr/bin/env bash
    SECONDS=0
    PROJECT_PATH=/home/myproject
    REPOSITORY_PATH=${PROJECT_PATH}/src/myproject
    LOG_FILE=${PROJECT_PATH}/logs/backup_postgres_db.log
    DAY_OF_THE_WEEK=$(LC_ALL=en_US.UTF-8 date +"%w-%A")
    DAILY_BACKUP_PATH=${PROJECT_PATH}/db_backups/${DAY_OF_THE_WEEK}.backup
    LATEST_BACKUP_PATH=${PROJECT_PATH}/db_backups/latest.backup
    error_counter=0
    
    echoerr() { echo "$@" 1>&2; }
    
    cd ${PROJECT_PATH}
    mkdir -p logs
    mkdir -p db_backups
    
    source env/bin/activate
    cd ${REPOSITORY_PATH}
    
    DATABASE=$(echo "from django.conf import settings; print(settings.DATABASES['default']['NAME'])" | python manage.py shell -i python)
    
  2. 然后添加一个命令来创建数据库导出文件:

    echo "=== Creating DB Backup ===" > ${LOG_FILE}
    date >> ${LOG_FILE}
    
    echo "- Dump database" >> ${LOG_FILE}
    pg_dump --format=p --file="${DAILY_BACKUP_PATH}" ${DATABASE}
    function_exit_code=$?
    if [[ $function_exit_code -ne 0 ]]; then
        {
            echoerr "Command pg_dump failed with exit code 
             ($function_exit_code)."
            error_counter=$((error_counter + 1))
        } >> "${LOG_FILE}" 2>&1
    fi
    
  3. 添加命令压缩数据库导出文件并对其创建一个软链接latest.backup.gz:

    echo "- Create a *.gz archive" >> ${LOG_FILE}
    gzip --force "${DAILY_BACKUP_PATH}"
    function_exit_code=$?
    if [[ $function_exit_code -ne 0 ]]; then
        {
            echoerr "Command gzip failed with exit code 
             ($function_exit_code)."
            error_counter=$((error_counter + 1))
        } >> "${LOG_FILE}" 2>&1
    fi
    
    echo "- Create a symlink latest.backup.gz" >> ${LOG_FILE}
    if [ -e "${LATEST_BACKUP_PATH}.gz" ]; then
        rm "${LATEST_BACKUP_PATH}.gz"
    fi
    ln -s "${DAILY_BACKUP_PATH}.gz" "${LATEST_BACKUP_PATH}.gz"
    function_exit_code=$?
    if [[ $function_exit_code -ne 0 ]]; then
        {
            echoerr "Command ln failed with exit code 
             ($function_exit_code)."
            error_counter=$((error_counter + 1))
        } >> "${LOG_FILE}" 2>&1
    fi
    
  4. 执行以上命令记录所耗费时间完成脚本:

    duration=$SECONDS
    echo "------------------------------------------" >> ${LOG_FILE}
    echo "The operation took $((duration / 60)) minutes and $((duration % 60)) seconds." >> ${LOG_FILE}
    exit $error_counter
    
  5. 在同一个目录下,使用如下内容创建bash脚本restore_postgresql_db.sh:

    # /home/myproject/commands/restore_postgresql_db.sh
    #!/usr/bin/env bash
    SECONDS=0
    PROJECT_PATH=/home/myproject
    REPOSITORY_PATH=${PROJECT_PATH}/src/myproject
    LATEST_BACKUP_PATH=${PROJECT_PATH}/db_backups/latest.backup
    export DJANGO_SETTINGS_MODULE=myproject.settings.production
    
    cd "${PROJECT_PATH}"
    source env/bin/activate
    
    cd "${REPOSITORY_PATH}"
    
    DATABASE=$(echo "from django.conf import settings; print(settings.DATABASES['default']['NAME'])" | python manage.py shell -i python)
    USER=$(echo "from django.conf import settings; print(settings.DATABASES['default']['USER'])" | python manage.py shell -i python)
    PASSWORD=$(echo "from django.conf import settings; print(settings.DATABASES['default']['PASSWORD'])" | python manage.py shell -i python)
    
    echo "=== Restoring DB from a Backup ==="
    
    echo "- Recreate the database"
    psql --dbname=$DATABASE --command='SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE datname = current_database() AND pid <> pg_backend_pid();'
    
    dropdb $DATABASE
    
    createdb --username=$USER $DATABASE
    
    echo "- Fill the database with schema and data"
    zcat "${LATEST_BACKUP_PATH}.gz" | python manage.py dbshell
    
    duration=$SECONDS
    echo "------------------------------------------"
    echo "The operation took $((duration / 60)) minutes and $((duration % 60)) seconds."
    
  6. 让这两个脚本可执行:

    $ chmod +x *.sh
    
  7. 运行数据库备份脚本:

    $ ./backup_postgresql_db.sh
    
  8. 运行数据库还原脚本(在生产环境中要格外小心):

    $ ./restore_postgresql_db.sh
    

实现原理...

备份脚本会在 /home/myproject/db_backups/下创建备份文件,并将日志保存到/home/myproject/logs/backup_postgresql_db.log中,类似下面这样:

=== Creating DB Backup ===
Fri Jan 17 02:40:55 CET 2020
- Dump database
- Create a *.gz archive
- Create a symlink latest.backup.gz
------------------------------------------
The operation took 0 minutes and 1 seconds.

如果操作成功,脚本会返回退出码0;否则退出码为执行脚本时的错误数量。同时在日志文件中会显示错误消息。

在db_backups目录中,有一个按周几命令的压缩SQL备份文件,如0-Sunday.backup.gz、1-Monday.backup.gz等等,还有另一个文件,实际上是一个软链接,名为latest.backup.gz。基于周几命名的备份让我们在适当配置定时任务的情况下可以有最近7天的备份,以及一个我们可以快速或自动SSH传送最近一份备份到另一台电脑的软链接。

注意我们是从Django配置文件中获取到的数据库登录信息,然后在bash脚本中进行使用。

在运行restore_postgresql_db.sh脚本时,得到的输出如下:

=== Restoring DB from a Backup ===
- Recreate the database
 pg_terminate_backend
----------------------
(0 rows)

- Fill the database with schema and data
SET
SET
SET
SET
SET
 set_config
------------

(1 row)

SET

…

ALTER TABLE
ALTER TABLE
ALTER TABLE
------------------------------------------
The operation took 0 minutes and 2 seconds.

相关内容

 

  • 第12章 部署中的在生产环境通过mod_wsgi部署Apache一节
  • 第12章 部署中的在生产环境中基于Nginx和Gunicorn部署一节
  • 创建及还原MySQL数据库备份一节
  • 配置cron job执行定时任务一节

配置cron job执行定时任务

通常网站有一些管理任务定期在后台执行,如每周一次、每天一次或每小时一次。这可通过使用计划任务来实现,称为cron job。这些脚本在服务端每隔指定时间会执行一次。本节中我们将创建两个定时任务:一个用于从数据库清除会话,另一个用于备份数据库的数据。这两者都会在每天晚上运行。

准备工作

首先将Django项目部署到远程服务器。然后通过SSH链接到服务器上。这些步骤编写时假定你使用了虚拟环境,但可为Docker项目创建一个类似的定时任务,它可以在应用容器内直接运行。代码文件用另一种语法,步骤则大多一致。

如何实现...

通过如下步骤创建两个脚本并定期运行:

  1. 在生产或预发布服务器上,导航至用户的家目录,其中包含env和src目录。

  2. 如尚未创建,请在env目录同级创建commands、db_backups和logs文件夹,如下:

    (env)$ mkdir commands db_backups logs
    
  3. 在commands目录中,创建一个clear_sessions.sh文件。可以通过vim或nano等终端编辑器进行编辑,添加如下内容:

    # /home/myproject/commands/clear_sessions.sh
    #!/usr/bin/env bash
    SECONDS=0
    export DJANGO_SETTINGS_MODULE=myproject.settings.production
    PROJECT_PATH=/home/myproject
    REPOSITORY_PATH=${PROJECT_PATH}/src/myproject
    LOG_FILE=${PROJECT_PATH}/logs/clear_sessions.log
    error_counter=0
    
    echoerr() { echo "$@" 1>&2; }
    
    cd ${PROJECT_PATH}
    mkdir -p logs
    
    echo "=== Clearing up Outdated User Sessions ===" > ${LOG_FILE}
    date >> ${LOG_FILE}
    
    source env/bin/activate
    cd ${REPOSITORY_PATH}
    python manage.py clearsessions >> "${LOG_FILE}" 2>&1
    function_exit_code=$?
    if [[ $function_exit_code -ne 0 ]]; then
        {
            echoerr "Clearing sessions failed with exit code 
             ($function_exit_code)."
            error_counter=$((error_counter + 1))
        } >> "${LOG_FILE}" 2>&1
    fi
    
    duration=$SECONDS
    echo "------------------------------------------" >> ${LOG_FILE}
    echo "The operation took $((duration / 60)) minutes and $((duration % 60)) seconds." >> ${LOG_FILE}
    exit $err
    or_counter
    
  4. 让clear_sessions.sh文件可执行,如下:

    $ chmod +x *.sh
    
  5. 假定项目使用的数据库为PostgreSQL。然后在同一目录中,按照前一小节创建及还原PostgreSQL数据库备份的指示创建一个备份脚本。

  6. 通过运行脚本并查看logs目录中的*.log文件测试该脚本来查看是否正确地进行了执行,如下:

    $ ./clear_sessions.sh
    $ ./backup_postgresql_db.sh
    
  7. 在远程服务器的项目家目录中,创建一个crontab.txt文件,内容如下:

    # /home/myproject/crontab.txt
    MAILTO=""
    HOME=/home/myproject
    PATH=/home/myproject/env/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
    SHELL=/bin/bash
    00 01 * * * /home/myproject/commands/clear_sessions.sh
    00 02 * * * /home/myproject/commands/backup_postgresql_db.sh
    
  8. 使用myproject用户安装该crontab任务,如下:

    (env)$ crontab crontab.txt
    

实现原理...

通过当前的设置,每晚在1:00 A.M时会执行clear_sessions.sh,backup_postgresql_db.sh会在2:00 A.M执行。执行日志保存在~/logs/clear_sessions.sh 和 ~/logs/backup_postgresql_db.log中。如果出现错误,应当查看这两个文件来进行回溯。

每天,clear_sessions.sh会执行clearsessions管理命令,正如其名称一样,它使用默认的数据库配置从数据库中清除过期的会话。

数据库备份脚本有点小复杂。一周中的每天,它使用0-Sunday.backup.gz、1- Monday.backup.gz等的命名模式对该天创建一个备份文件。因此,可以还原7天前或其后的数据。

crontab文件遵循特定的语法。每行包含由一系列数字指定的一天中的具体时间,后接在给定时刻运行的任务。时间由空格分隔的5部分组成,见以下列表:

  • 分钟,0 到 59
  • 小时,0 到 23
  • 日期, 1 到 31
  • 月份,1 到 12
  • 周几,0 到 7,0是星期天,1是周一,以此类推,7也是星期天

星号(*)表示每个时间都会使用。因此,以下任务定义了clear_sessions.sh在每月每天、每周每天的1:00 A.M.执行:

00 01 * * * /home/myproject/commands/clear_sessions.sh

可以通过https://en.wikipedia.org/wiki/Cron学习有关crontab详细知识。

扩展知识...

我定义了定期执行的命令,并且也启动了结果日志,但还不知道定时任务是否成功或失败,除非将其记录至服务器中并且每天手动检查。为解决这一乏味的手工操作问题,可以使用Healthchecks服务自动监控定时任务。

借助于Healthchecks,我们可以修改crontab,这样在每次任务成功执行后ping一个指定的URL。如果脚本失败则使用非0退出码,Healthchecks会知道它是否成功执行。每天可以通过 email获取一个定时任务的总览以及它们的执行状态。

相关内容

  • 第12章 部署中的在生产环境通过mod_wsgi部署Apache一节
  • 第12章 部署中的在生产环境中基于Nginx和Gunicorn部署一节
  • 创建及还原MySQL数据库备份一节
  • 创建及还原PostgreSQL数据库备份一节

日志事件供进一步审查

在前面的小节中,我们可以看到bash脚本的日志是如何运行的。但也可以记录Django网站上的事件,如用户注册、商品添加购物车、购票、银行交易、发送短信、服务器错误等。

ℹ️不应当在日志中记录用户密码或信用卡详情这些敏感信息。另外,使用分析工具来代替Python日志来进行整体网站使用的追踪。

本节中,我们会引导你如何记录网站相关的结构化信息到日志中文件中。

准备工作

我们使用**第4章 模板和JavaScript**中实现Like微件一节中的likes应用。

在Django项目的虚拟环境中安装 django-structlog如下:

(env)$ pip install django-structlog==1.3.5

如何实现...

按照如下步骤在Django网站中配置结构化日志:

  1. 在项目配置文件中添加RequestMiddleware:

    # myproject/settings/_base.py
    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",
        "django.middleware.locale.LocaleMiddleware",
        "django_structlog.middlewares.RequestMiddleware",
    ]
    
  2. 在同一个文件中添加Django日志配置:

    # myproject/settings/_base.py
    LOGGING = {
        "version": 1,
        "disable_existing_loggers": False,
        "formatters": {
            "json_formatter": {
                "()": structlog.stdlib.ProcessorFormatter,
                "processor": structlog.processors.JSONRenderer(),
            },
            "plain_console": {
                "()": structlog.stdlib.ProcessorFormatter,
                "processor": structlog.dev.ConsoleRenderer(),
            },
            "key_value": {
                "()": structlog.stdlib.ProcessorFormatter,
                "processor":  
                 structlog.processors.KeyValueRenderer(key_order=
                 ['timestamp', 'level', 'event', 'logger']),
            },
        },
        "handlers": {
            "console": {
                "class": "logging.StreamHandler",
                "formatter": "plain_console",
            },
            "json_file": {
                "class": "logging.handlers.WatchedFileHandler",
                "filename": os.path.join(BASE_DIR, "tmp", "json.log"),
                "formatter": "json_formatter",
            },
            "flat_line_file": {
                "class": "logging.handlers.WatchedFileHandler",
                "filename": os.path.join(BASE_DIR, "tmp", 
               "flat_line.log"),
                "formatter": "key_value",
            },
        },
        "loggers": {
            "django_structlog": {
                "handlers": ["console", "flat_line_file", "json_file"],
                "level": "INFO",
            },
        }
    }
    
  3. 同时在其中设置structlog的配置:

    # myproject/settings/_base.py
    structlog.configure(
        processors=[
            structlog.stdlib.filter_by_level,
            structlog.processors.TimeStamper(fmt="iso"),
            structlog.stdlib.add_logger_name,
            structlog.stdlib.add_log_level,
            structlog.stdlib.PositionalArgumentsFormatter(),
            structlog.processors.StackInfoRenderer(),
            structlog.processors.format_exc_info,
            structlog.processors.UnicodeDecoder(),
            structlog.processors.ExceptionPrettyPrinter(),
            structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
        ],
        context_class=structlog.threadlocal.wrap_dict(dict),
        logger_factory=structlog.stdlib.LoggerFactory(),
        wrapper_class=structlog.stdlib.BoundLogger,
        cache_logger_on_first_use=True,
    )
    
  4. 在likes应用的views.py文件中,我们用日志记录点赞或取消点赞的对象:

    # myproject/apps/likes/views.py
    import structlog
    
    from django.contrib.contenttypes.models import ContentType
    from django.http import JsonResponse
    from django.views.decorators.cache import never_cache
    from django.views.decorators.csrf import csrf_exempt
    
    from .models import Like
    from .templatetags.likes_tags import liked_count
    
    logger = structlog.get_logger("django_structlog")
    
    
    @never_cache
    @csrf_exempt
    def json_set_like(request, content_type_id, object_id):
        """
        Sets the object as a favorite for the current user
        """
        result = {
            "success": False,
        }
        if request.user.is_authenticated and request.method == "POST":
            content_type = ContentType.objects.get(id=content_type_id)
            obj = content_type.get_object_for_this_type(pk=object_id)
    
            like, is_created = Like.objects.get_or_create(
                content_type=ContentType.objects.get_for_model(obj),
                object_id=obj.pk,
                user=request.user)
            if is_created:
                logger.info("like_created",  
                content_type_id=content_type.pk, 
                object_id=obj.pk)
            else:
                like.delete()
                logger.info("like_deleted",  
                content_type_id=content_type.pk, 
                object_id=obj.pk)     
    
            result = {
                "success": True,
                "action": "add" if is_created else "remove",
                "count": liked_count(obj),
            }
    
        return JsonResponse(result)
    

实现原理...

在访客浏览网站时,具体的事件会记录到tmp/json.log 和 tmp/flat_line.log文件中。django_structlog.middlewares.RequestMiddleware记录HTTP请求处理的开始和结束。此外,我们还记录Like实例于何时被创建或删除。

json.log文件包含JSON格式的日志。这表示可以通过程序解析、查看及分析这些日志。

{"request_id": "ad0ef355-77ef-4474-a91a-2d9549a0e15d", "user_id": 1, "ip": "127.0.0.1", "request": "<WSGIRequest: POST '/en/likes/7/1712dfe4-2e77-405c-aa9b-bfa64a1abe98/'>", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36", "event": "request_started", "timestamp": "2020-01-18T04:27:00.556135Z", "logger": "django_structlog.middlewares.request", "level": "info"}
{"request_id": "ad0ef355-77ef-4474-a91a-2d9549a0e15d", "user_id": 1, "ip": "127.0.0.1", "content_type_id": 7, "object_id": "UUID('1712dfe4-2e77-405c-aa9b-bfa64a1abe98')", "event": "like_created", "timestamp": "2020-01-18T04:27:00.602640Z", "logger": "django_structlog", "level": "info"}
{"request_id": "ad0ef355-77ef-4474-a91a-2d9549a0e15d", "user_id": 1, "ip": "127.0.0.1", "code": 200, "request": "<WSGIRequest: POST '/en/likes/7/1712dfe4-2e77-405c-aa9b-bfa64a1abe98/'>", "event": "request_finished", "timestamp": "2020-01-18T04:27:00.604577Z", "logger": "django_structlog.middlewares.request", "level": "info"}

flat_line.log文件包含更短格式的日志,可能更易于手动读取:

(env)$ tail -3 tmp/flat_line.log
timestamp='2020-01-18T04:27:03.437759Z' level='info' event='request_started' logger='django_structlog.middlewares.request' request_id='a74808ff-c682-4336-aeb9-f043f11a7316' user_id=1 ip='127.0.0.1' request=<WSGIRequest: POST '/en/likes/7/1712dfe4-2e77-405c-aa9b-bfa64a1abe98/'> user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'
timestamp='2020-01-18T04:27:03.489198Z' level='info' event='like_deleted' logger='django_structlog' request_id='a74808ff-c682-4336-aeb9-f043f11a7316' user_id=1 ip='127.0.0.1' content_type_id=7 object_id=UUID('1712dfe4-2e77-405c-aa9b-bfa64a1abe98')
timestamp='2020-01-18T04:27:03.491927Z' level='info' event='request_finished' logger='django_structlog.middlewares.request' request_id='a74808ff-c682-4336-aeb9-f043f11a7316' user_id=1 ip='127.0.0.1' code=200 request=<WSGIRequest: POST '/en/likes/7/1712dfe4-2e77-405c-aa9b-bfa64a1abe98/'>

相关内容

  • 创建及还原MySQL数据库备份一节
  • 创建及还原PostgreSQL数据库备份一节
  • 配置cron job执行定时任务一节

通过email获取详细错误报告

执行系统日志,Django使用Python的内置的日志模块或像前一小节中所提到的structlog模块。默认的Django配置看上去相当复杂。本小节我们学习如何修改它来以完整的HTML发送错误邮件,类似Django在DEBUG模式下发生错误时那样。

准备工作

在虚拟环境中定位到该Django项目。

如何实现...

如下流程会向你发送详细的邮件:

  1. 如果项目还没有配置LOGGING,首先进行配置。查找Django日志工具文件,位于env/lib/python3.7/site-packages/django/utils/log.py。复制DEFAULT_LOGGING字典到项目的配置文件中,依然是字典。

  2. 对mail_admins处理器添加include_html设置。前两步的结果类似下面这样:

    # myproject/settings/production.py
    LOGGING = {
        'version': 1,
        'disable_existing_loggers': False,
        'filters': {
            'require_debug_false': {
                '()': 'django.utils.log.RequireDebugFalse',
            },
            'require_debug_true': {
                '()': 'django.utils.log.RequireDebugTrue',
            },
        },
        'formatters': {
            'django.server': {
                '()': 'django.utils.log.ServerFormatter',
                'format': '[{server_time}] {message}',
                'style': '{',
            }
        },
        'handlers': {
            'console': {
                'level': 'INFO',
                'filters': ['require_debug_true'],
                'class': 'logging.StreamHandler',
            },
            'django.server': {
                'level': 'INFO',
                'class': 'logging.StreamHandler',
                'formatter': 'django.server',
            },
            'mail_admins': {
                'level': 'ERROR',
                'filters': ['require_debug_false'],
                'class': 'django.utils.log.AdminEmailHandler',
                'include_html': True,
            }
        },
        'loggers': {
            'django': {
                'handlers': ['console', 'mail_admins'],
                'level': 'INFO',
            },
            'django.server': {
                'handlers': ['django.server'],
                'level': 'INFO',
                'propagate': False,
            },
        }
    }
    

实现原理...

日志配置由4部分组成:日志器(logger)、 处理器(handler)、 过滤器(filter)和格式化器(formatter)。以下列表进行了描述:

  • 日志器是进入日志系统的入口。每个logger可拥有一个日志级别:DEBUG、INFO、WARNING、ERROR或CRITICAL。在消息写入日志器时,消息的日志级别与日志器的级别进行比对。如果符合或操作日志器的日志级别,它会进行一步的由处理器进行处理。否则,会忽略掉消息。
  • 处理器是定义日志器中每条消息会发生什么的引擎。它们可以写到终端、由邮件发送给管理员、保存至日志文件、发送到Sentry错误日志服务等等。本例中,我们为mail_admins处理器设置了include_html参数,因为我们想要Django项目中所发生错误消息的包含完整回溯信息以及本地变量的HTML。
  • 过滤器对由日志器传递给处理器的消息提供额外的控制。例如,本例中,仅在DEBUG模式设置为false时发送邮件。
  • 格式化器用于定义如何将日志消息渲染为字符串。没有在本例中进行使用,但是,有关日志记录的更多信息,可以参见官方文档

扩展知识...

我们刚刚定义的配置会发送每个网站上所产生的服务端错误。如果流量很大,假设数据崩溃了的话,会迅速发送大量邮件到你的收件箱,甚至会把邮件服务器整挂。

要避免这类问题,可以使用Sentry。它在服务器上追踪所有服务端错误,对每种错误类型会送一封通知邮件。

相关内容

  • 第12章 部署中的在生产环境通过mod_wsgi部署Apache一节
  • 第12章 部署中的在生产环境中基于Nginx和Gunicorn部署一节
  • 日志事件供进一步审查一节