Node零基础入门到服务端程序 -- 第七章

第七章 日志及报错

当有请求进入服务端的时候,通常需要把请求记录日志记录下来,这样方便在以后的项目维护过程中查找问题。尤其是线上环境这种可能出现用户反馈等情况,必须通过日志来还原用户可能遇到的场景。

我们希望,至少每一次的api请求都被记录下来,所以我们直接在server.js文件中添加use:

src/server.js

const Koa = require('koa');
const app = new Koa();
const bodyParser = require('koa-bodyparser');
const router = require('./router');
const { mongoConfig } = require('./config');
// 引入一个创建日志的方法
const { createLogger } = require('./lib/logger');
const mongoose = require('mongoose');
mongoose.connect(`mongodb://${mongoConfig.username? mongoConfig.username + ':' + mongoConfig.password + '@': ''}${mongoConfig.host}:${mongoConfig.port}/${mongoConfig.database}`, { useNewUrlParser: true });
app
    .use(bodyParser())
    // 在use中使用这个方法。use是每一次请求都会经过的地方,且每一个use方法中的函数都是有顺序的,可以把它想象成一个管道,自上而下经过每一个use。
    .use(createLogger)
    .use(router.routes())
    .use(router.allowedMethods())
app.listen(3000);
console.log('Web server run on port 3000');

创建上面使用到的那个createLogger文件,这种类似单独完成一种功能的函数,我们把它放在lib中。

src/lib/logger.js

const _ = require('lodash');
// moment是一个用于生成时间的包,npm install moment 安装它
const moment = require('moment');
// 创建并export一个方法,命名为createLogger
// 传入ctx和next,第一个参数ctx写到各种可能要用到的参数,第二个参数next用来离开use,执行管道的下一级
exports.createLogger = async (ctx, next) => {
    // 将http请求的reqeust获取出来
    let requestLog = `[${moment().format('YYYY-MM-DD HH:mm:ss')}]request details: ${JSON.stringify(ctx.request)}`;
    // 如果有body的话(比如一些post请求)也一并记录
    if (!_.isEmpty(ctx.request.body)) {
        requestLog += `, request body: ${JSON.stringify(ctx.request.body)}`;
    }
    // 将http请求打印出来,这里我使用console.log方法直接打印到终端是为了方便。在实际的使用过程中,有非常多的方法记录日志,比如写成文件,通过api发送给第三方记录,直接写入相关数据库等
    console.log(requestLog);
    // 日志记录完成,走向下一步
    await next();
}

tips:
moment文档
记录日志的第三方有非常的多。比如说:sumologic

我们来查看一下效果,启动程序:

node src/server.js

打开浏览器,http://localhost:3000/api/books

图片

查看终端打印:

图片

可以看到,日志已经被打印出来了。

再次说明,在实际生产的使用过程中,记录日志不一定是在终端打印的方式,更多的是通过API发送给第三方记录(如果有安全疑虑,这个第三方也有可能是自己创建的日志系统)。

除了每一次的请求我们希望记录下来之外,报错我们也希望记录下来,并规范格式的返回报错,这样在程序在实际使用的过程中,就可以方便的知道哪个接口除了什么问题,甚至可以做出相关警报,当出现什么报错的时候就发送邮件/信息等方式通知相关人员,保证我们的程序健康运行。

src/server.js

const Koa = require('koa');
const app = new Koa();
const bodyParser = require('koa-bodyparser');
const router = require('./router');
const { mongoConfig } = require('./config');
const { createLogger } = require('./lib/logger');
// 在lib文件夹中创建一个用于报错的方法
const { errorHandler } = require('./lib/error');
const mongoose = require('mongoose');
mongoose.connect(`mongodb://${mongoConfig.username? mongoConfig.username + ':' + mongoConfig.password + '@': ''}${mongoConfig.host}:${mongoConfig.port}/${mongoConfig.database}`, { useNewUrlParser: true });
app
    .use(bodyParser())
    .use(createLogger)
    // 当程序报错时,规范格式地把相关错误报出去并记录日志
    .use(errorHandler)
    .use(router.routes())
    .use(router.allowedMethods())
app.listen(3000);
console.log('Web server run on port 3000');

不同类型的错误,我们应该要有不同的应对方法。有的错误是我们预料中的,比如说我们的数据库中本来就有某本书,然后API请求又要创建同一本,那么我们可以返回失败并报出错误“书本名称已存在”。有的错误是预料之外的,比如说我们代码没有写好导致的报错,那么这种错误我们就不能把相关的真实错误内容返回给API避免暴露我们的系统,增加被黑客攻击的风险。

预料之类的错误我们使用Boom这个包来报错(Boom文档),这是一个方便且格式规范的报错包,使用起来也非常的简单:

图片

Boom的报错会有一个err.isBoom的key,我们可以使用这个来区分boom的报错。

了解了报错的基本情况之后,我们就可以来写这个errorHandler文件了

src/lib/error.js

exports.errorHandler = async (ctx, next) => {
    try {
        // 我们知道,next就是管道的下一个方法,我们使用try catch包裹住它,使得后面路由中任何代码报错都可以被捕捉到
        await next();
    } catch (err) {
        // 区分不同的情况下不同的记录报错方式,Boom的报错会包含一个err.isBoom,我们首先处理Boom报出来的错误
        // 即便是Boom的报错也区分500以上或以下的,500以下的错误可能是一些正常的报错,这里区分也是为了记录日志的时候记录一个warn(警告)级别的错误日志就可以了,也方便检查日志的时候筛选
        if (err.isBoom && err.output.statusCode >= 400 && err.output.statusCode < 500) {
            // 打印warn级别的日志
            console.warn(err.message);
            // 返回API的报错code
            ctx.status = err.output.statusCode;
            // 返回API的报错内容
            ctx.body = err.message;
        }
        // 500以上的boom报错
        else if (err.isBoom && err.output.statusCode > 500) {
            // 500以上表示错误比较严重,打印error级别的日志
            console.error(err);
            // 返回API的报错code和内容
            ctx.status = err.output.statusCode;
            ctx.body = err.message;
        // 还有一种情况是我们前面有过的api请求的入参错误,由于我们在这里catch掉了所有的错误,所以这种类型的错误也需要被分类进来
        // 这种错误的格式中有message的固定句式,我们就用这个区分
        }
        else if (err.message && err.message.includes('validation failed on')) {
            // 这种错误也是警告级别,报400错误就可以了
            console.warn(err);
            ctx.status = 400;
            // 这里如果不想要给外部api太多的信息,我们可以笼统的报一个参数不正确,如果是比较内部的系统(比如说除了部分白名单IP地址之外,其他根本访问不进来的安全级别),那么可以使用err.message报比较具体的错误内容
            ctx.body = '参数不正确';
        }
        else {
            // 这里是预料之外的系统错误,我们希望记录下来并修复它,但是并不希望外界知道我们的系统中发生了什么
            console.error(err);
            // 直接报500,返回笼统的Internal Server Error错误内容
            ctx.status = 500;
            ctx.body = 'Internal Server Error';
        }
        // 使用koa框架方法将报错发送出去
        ctx.app.emit('error', err, ctx);
    }
}

完成好报错相关方法,我们来实践一下。之前我们写好了一个创建书本的API,那么添加一些代码,使得同名书本不被重复创建。

src/services/book-service.js

// 引入boom,这个一个规范报错的包,使用npm install boom安装
const Boom = require('boom');
const bookModel = require('../schemas/book-schema');
class BookService {
// ... 省略一些代码
    async create(name, author) {
        // 创建之前先查询是否有这本书
        const book = await bookModel.findOne({ name });
        // 如果有,就报错
        if (book) {
            // 使用boom进行报错
            throw Boom.badRequest('书本名称已存在');
        }
        const data = {
            name,
            author,
            createdAt: new Date(),
            createdBy: 'default',
        }
        await bookModel.create(data);
    }
}
module.exports = { BookService };

接下来,启动程序

node src/server.js

打开postman发送一个创建书本的请求

图片

书本名称是“流浪地球”,根据上面的内容,这个书本名称已经存在在我们的数据库当中,因此如预期报错了:书本名称已存在。

再次查看终端,创建请求和报错也被打印出来了。

图片

本文已完成电子书《Node零基础入门到服务端程序》电子书(含教程内项目代码)/ 10元,购买链接:https://mianbaoduo.com/o/bread/mbd-Z5WZk5o=
ps:前九章(本书共计十三章)内容会在这里陆续更新。

你可能感兴趣的:(Node零基础入门到服务端程序 -- 第七章)