从 Node.js 错误中获得的经验
tcqc5700
8年前
<p>有多少次你发现自己在终端或监控系统内查看堆栈轨迹,但并不能看出个所以然来?如果你的回答是“很多次”,那么这篇帖子你应该看看。如果你不经常碰上这种情况也没关系,你也可以看看这篇文章解闷。</p> <p>当处理 Node.js 服务器的复杂数据时,要会从可返回给请求方的错误中总结经验,具备此能力至关重要。在处理一个请求时,一个错误出现会引起链接里另一个错误的出现,于是问题就来了。当此脚本出现时,一旦你生成了新错误,并将它返回到了链接,那你就丢失了与原始错误的所有连接。</p> <p>在 <a href="https://codefresh.io/?utm_medium=DZone&utm_campaign=DzoneNodejsErrors&utm_source=Dzone" rel="nofollow,noindex">Codefresh</a> ,我们花费了大量的时间试图找到最好的模式来处理这些情境。我们真正想要的是能够让一个错误能链接到前一个错误,有能力获取整个链的聚合信息。我们想要这样的接口来做界面将非常简单,也更利于扩展与改进。</p> <p>我们探索已经存在的模块以支持我们的需要。我们发现了唯一满足要求的模块就是 <a href="/misc/goto?guid=4959716600328133195" rel="nofollow,noindex">WError</a> 。</p> <p>‘WError’ 提供给你包装已存错误和新错误的能力。接口是非常酷和简单的,因此我们决定试一试。经过一段时间的密集使用,我们得出了一些做得还不太好的地方:</p> <ul> <li> <p>堆栈跟踪的误差不会超过整个链,而只会生成堆栈跟踪高的错误。</p> </li> <li> <p>它没有能力去简单地创建你自己的错误类型。</p> </li> <li> <p>扩展错误的附加行为,需要扩展他们的代码。</p> </li> </ul> <h2><strong>介绍 CFError</strong></h2> <p>用一个真实的 Express 示例来看看如何使用 CFError。创建一个 Express 应用,它通过一个特定的路由处理某个单独的请求。这个请求需要从 Mongo 数据库中查询一个用户的信息。现在定义一个路由,以及一个负责从 DB 中获取用户信息的函数。</p> <pre> <code class="language-javascript">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}`)); } }) };</code></pre> <p>有几件事情需要注意:</p> <ul> <li> <p>创建错误信息的时候,可以从预定义的 HTTP 错误扩展。</p> </li> <li> <p>创建错误信息的时候可以加入一个 ‘cause’ 属性,用来连接到前一个错误,形成错误链。这样在打印错误相关联的调用栈时,就能得到完整错误链的调用栈信息,使之在阅读时容易识别。</p> </li> <li> <p>你可以在错误信息对象中添加各种字段。稍后我会解释 ‘internalCode’ 和 ‘recognized’ 的用法。</p> </li> <li> <p>可以在你的代码之外定义错误对象,然后在创建错误信息的时候引用它。</p> </li> </ul> <p>下面,给 Express 应用添加一个错误处理中间件。</p> <pre> <code class="language-javascript">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); });</code></pre> <p>注意:</p> <ul> <li> <p>确保将最后出现的的错误输出到日志,以及总是将 ‘CFError’ 对象返回给用户。这样你才能向错误处理中间件中添加其它逻辑。</p> </li> <li> <p>所有预定义的 HTTP 错误都有 ‘statusCode’ 属性和 ‘message’ 属性,它们都是和的属性。</p> </li> <li> <p>扩展错误信息使你可以在一个地方添加处理错误的逻辑。创建错误信息的时候不用考虑去打印每一个错误对象,之后可以一次性打印调用栈的跟踪信息,它包含了完全的执行过程和上下文数据。</p> </li> </ul> <p>现在换个方法向客户端返回错误,返回一个对象来代替顶层的错误消息。</p> <pre> <code class="language-javascript">return response.status(error.statusCode).send({ message: error.message, statusCode: error.statusCode, internalCode: error.internalCode });</code></pre> <p>非常好!现在我们有一个的向客户端返回错误的过程了。</p> <p>向监控器(监控进程)通报错误信息</p> <p>在Codefresh中,我们使用NewRelic作为APM监控器。要注意我们生成并触发到NewRelic的错误信息分为两类:第一类包含了在我们服务器上因不当操作而产生和抛出的各种错误信息。另一类则是我们的服务器正确分析处理产生的异常部分的各种错误信息(业务异常)。</p> <p>向NewRelic报告第二类错误时会造成Apdex积分不可预测地下降,这又导致各种来自我们告警系统的虚假告警消息。</p> <p>所以我们给出了一种新的约定,当我们可将一个生成的错误归纳为系统正确行为的结果时,我们构造一个错误对象并为之附加一个recognized字段。我们想要具备一种能力可在错误链条上的某一错误打上recognized标记,但仍能获取它的值,即使更高层级的错误没有包含这个标记。我们在CFError对象上暴露了一个getFirstValue函数,用来取得它在整个错误链条上碰到的第一个值。我们用下面代码看看在Codefresh中是如何使用的。</p> <pre> <code class="language-javascript">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 }); });</code></pre> <p>注意:</p> <ul> <li> <p>因为我们知道只需要处理 CFError 对象,所以只需要添加两行代码就行。</p> </li> <li> <p>既然已经确定了实际要发送的错误,使用 New Relic 的时候就要手工关闭自动发送错误的选项。为了达到这个目的,需要手工将所有 HTTP 错误状态码加入 ‘newrelic.js’ 中的 ‘ignore_status_codes’ 字段。我们已经向 New Relic 支持团队提出需要一个更简单的办法来做这个事情。</p> </li> </ul> <pre> <code class="language-javascript">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] } };</code></pre> <h2><strong>小结</strong></h2> <p>想很好地处理错误不仅需要一个好的错误处理模块,还需要定义好处理过程:在什么时候、什么位置用什么方法来处理错误。这需要你遵循自己的设计模式,否则就会搞得一团糟。</p> <p>仅向监控系统报告实际的错误是至关重要的,这样你的公司才能专注于检查和处理发生的问题。</p> <p> </p> <p> </p> <p>来自:https://www.oschina.net/translate/getting-the-most-out-of-your-nodejs-errors</p> <p> </p>