从 Node.js 错误中获得的经验

有多少次你发现自己在终端或监控系统内查看堆栈轨迹,但并不能看出个所以然来?如果你的回答是“很多次”,那么这篇帖子你应该看看。如果你不经常碰上这种情况也没关系,你也可以看看这篇文章解闷。

当处理 Node.js 服务器的复杂数据时,要会从可返回给请求方的错误中总结经验,具备此能力至关重要。在处理一个请求时,一个错误出现会引起链接里另一个错误的出现,于是问题就来了。当此脚本出现时,一旦你生成了新错误,并将它返回到了链接,那你就丢失了与原始错误的所有连接。

在 Codefresh,我们花费了大量的时间试图找到最好的模式来处理这些情境。我们真正想要的是能够让一个错误能链接到前一个错误,有能力获取整个链的聚合信息。我们想要这样的接口来做界面将非常简单,也更利于扩展与改进。

我们探索已经存在的模块以支持我们的需要。我们发现了唯一满足要求的模块就是  WError 。

‘WError’ 提供给你包装已存错误和新错误的能力。接口是非常酷和简单的,因此我们决定试一试。经过一段时间的密集使用,我们得出了一些做得还不太好的地方:

介绍 CFError

根据我们丰富的经验,我们会用一个错误处理模块响应所有请求。所有相关的信息和文档都能在这里找到:http://codefresh-io.github.io/cf-errors

用一个真实的 Express 示例来看看如何使用 CFError。创建一个 Express 应用,它通过一个特定的路由处理某个单独的请求。这个请求需要从 Mongo 数据库中查询一个用户的信息。现在定义一个路由,以及一个负责从 DB 中获取用户信息的函数。

var CFError    = require('cf-errors');
var Errors     = CFError.Errors;
var Q          = require('q');
var express    = require('express');

var UserNotFoundError = {
    name: "UserNotFoundError"
};

var app = express();

app.get('/user/:id', function (request, response, next) {
    var userId = request.params.id;
    if (userId !== "coolId") {
        return next(new CFError(Errors.Http.BadRequest, {
            message: "Id must be coolId.",
            internalCode: 04001,
            recognized: true
        }));
    }

    findUserById(userId)
        .done((user) => {
            response.send(user);
        }, (err) => {
            if (err.name === UserNotFoundError.name) {
                next(new CFError(Errors.Http.NotFound, {
                    internalCode: 04041,
                    cause: err,
                    message: `User ${userId} could not be found`,
                    recognized: true
                }));
            }
            else {
                next(new CFError(Errors.Http.InternalServer, {
                    internalCode: 05001,
                    cause: err
                }));
            }
        });
});

var findUserById = function (userId) {
    return User.findOne({_id: userId})
        .exec((user) => {
            if (user) {
                return user;
            }
            else {
                return Q.reject(new CFError(UserNotFoundError, `Failed to retrieve user: ${userId}`));
            }
        })
};

有几件事情需要注意:

下面,给 Express 应用添加一个错误处理中间件。

app.use(function (err, request, response, next) {
    var error;
    if (!(err instanceof CFError)){
        error = new CFError(Errors.Http.InternalServer, {
            cause: err
        }); 
    }
    else {
        if (!err.statusCode){
            error = new CFError(Errors.Http.InternalServer, {
                cause: err
            });
        }
        else {
            error = err;
        }
    }

    console.error(error.stack);
    return response.status(error.statusCode).send(error.message);
});

注意:

现在换个方法向客户端返回错误,返回一个对象来代替顶层的错误消息。

return response.status(error.statusCode).send({
    message: error.message,
    statusCode: error.statusCode,
    internalCode: error.internalCode
});

非常好!现在我们有一个的向客户端返回错误的过程了。

向监控器(监控进程)通报错误信息

 在Codefresh中,我们使用NewRelic作为APM监控器。要注意我们生成并触发到NewRelic的错误信息分为两类:第一类包含了在我们服务器上因不当操作而产生和抛出的各种错误信息。另一类则是我们的服务器正确分析处理产生的异常部分的各种错误信息(业务异常)。

 向NewRelic报告第二类错误时会造成Apdex积分不可预测地下降,这又导致各种来自我们告警系统的虚假告警消息。

 所以我们给出了一种新的约定,当我们可将一个生成的错误归纳为系统正确行为的结果时,我们构造一个错误对象并为之附加一个recognized字段。我们想要具备一种能力可在错误链条上的某一错误打上recognized标记,但仍能获取它的值,即使更高层级的错误没有包含这个标记。我们在CFError对象上暴露了一个getFirstValue函数,用来取得它在整个错误链条上碰到的第一个值。我们用下面代码看看在Codefresh中是如何使用的。

app.use(function (err, request, response, next) {
    var error;
    if (!(err instanceof CFError)){
        error = new CFError(Errors.Http.InternalServer, {
            cause: err
        });
    }
    else {
        if (!err.statusCode){
            error = new CFError(Errors.Http.InternalServer, {
                cause: err
            });
        }
        else {
            error = err;
        }
    }

    if (!error.getFirstValue('recognized')){
        nr.noticeError(error); //report to monitoring systems (newrelic in our case)
    }

    console.error(error.stack);
    return response.status(error.statusCode).send({
        message: error.message,
        statusCode: error.statusCode,
        internalCode: error.internalCode
    });
});

注意:

exports.config = {
  error_collector: {
    ignore_status_codes: [400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511]
  }
};

小结

想很好地处理错误不仅需要一个好的错误处理模块,还需要定义好处理过程:在什么时候、什么位置用什么方法来处理错误。这需要你遵循自己的设计模式,否则就会搞得一团糟。

仅向监控系统报告实际的错误是至关重要的,这样你的公司才能专注于检查和处理发生的问题。