跟踪node.js应用程序中的错误
#node

生产错误降低了速度,并且通常会影响释放路线图的完全轨迹。如果您有强大的错误跟踪设置可以依靠。

在本文中,我们将研究如何使Node.js应用程序中的错误更方便,自动化和安全。

让我们开始!

两个错误类型

我们构建的每个应用程序都有一个最终目标。该代码反映了该目标,并允许用户执行预期的操作。由于现实世界很复杂,因此可能出现的情况无法通过应用程序来处理。这导致了意外的和不必要的输出,击败了该应用程序的最终目标。与happy path分歧称为错误。

错误可以放入两个存储桶中:

  1. 操作错误
  2. 编程错误

操作错误在主动使用应用程序期间会出现。他们也称为运行时错误,它们可以帮助我们发现开发过程中错过的边缘案例。例如,用户添加了负年龄,在食品交付应用程序上给出负面提示,或在输入字段中输入恶意脚本等。开发人员应正确处理这些信息。

编程错误,另一方面,由于代码中缺少错误处理程序而导致错误滑倒时会出现。示例包括:

  • 将字符串分配给一个数字。
  • 没有抓住拒绝的承诺。
  • 访问对象的不存在属性。

什么是错误跟踪?

错误跟踪是一个过程,在该过程中,观察并监视应用程序的异常行为,以在影响最终用户之前对其进行修复。错误跟踪需要根据历史使用模式进行记录,监视,创建警报和异常检测。

主动跟踪错误并解决这些错误提供了平稳的用户体验,并有助于建立信任。依靠用户报告错误不仅不可靠,而且对您的业务和品牌不利。

如何跟踪node.js中的错误

跟踪错误涉及预期错误和编写代码来处理意外/不必要的方案 - 换句话说,当您的应用程序无法正常工作时,情况。这是唯一的(正确的)操纵正确的课程并确保您的应用程序在正确的轨道上。

有多种方法可以识别node.js应用程序中的错误:

  1. 回调
  2. 承诺
  3. try-catch

我们将在本文中使用Try-Catch。基本想法是运行一块代码并将其包裹在TRY块中,而错误处理程序则位于捕获块中。如果您的应用程序的行为正常,则捕获块将永远不会运行。

这是一个示例片段,使用try-catch处理这种情况:

try {
  operationThatMightFail();
} catch (err) {
  log.error(err);
  throw err;
}

现在,我们已经通过Vanilla错误处理了,让我们继续进入令人兴奋的部分。上述错误处理程序是一个不错的起点,但是我们可以做得更好。您可能已经注意到,它并没有为我们提供太多价值。查看该错误消息不一定会在事件中对我们有所帮助。有更好的方法。让我们探索。

创建自定义错误处理程序

我们希望每个错误都具有足够的上下文以使其可行。例如,如果叠加错误的堆栈轨迹,调试可能会花费不合理的时间。平衡丰富错误报告与简明语法是关键。错误处理程序不应分散我们的主要应用程序逻辑。

为了解决这个问题,我们可以设置受过教育的默认值,以避免在每个错误处理程序中传递某些细节。可以抽象各种普通配置,例如:

  • 将错误上下文传递给APM(应用程序性能监视)工具
  • 记录错误
  • 设置在分析和监视过程中有助于帮助的自定义标志
  • 将HTTP状态代码返回客户端

为此,我们可以创建一个扩展Error并添加一些不错的默认值的自定义错误类。让我们看看一个:

const { sendError } = require("@appsignal/nodejs");
const { HTTP_STATUS_CODES } = require("../../constants");

class AppError extends Error {
  constructor(message, statusCode, isOperational = true) {
    super(message);

    this.name = "AppError";
    this.statusCode = statusCode || HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR;
    this.isOperational = isOperational;
    Error.captureStackTrace(this);

    // send to APM
    sendError(this);
  }
}

现在,我们可以从技术上开始,将此自定义错误处理程序插入到处的代码中。它会起作用。但这不是必需的。我们将仅在控制器层中使用它。在服务/utils/helpers/etc。中发生的任何错误都将报告给控制器层的错误处理程序。

这利用event bubbling并避免冗余错误处理程序。此外,它有助于避免偶然出现错误堆栈痕迹。

node.js应用中的更好错误消息

虽然“某物出了问题”错误消息可能对您的最终用户足够(提示:不是),但它不能帮助开发人员从事您的应用程序。

作为开发人员,您需要一条错误消息,以至少给您根本原因(如果不绘制完整的图片)。同样,最终目标是使您的错误可行。

这是写错误消息的一些最佳实践:

  1. 使它们简洁
  2. 避免歧义
  3. 避免首字母缩写/语/技术术语
  4. 使它们成为人类可读
  5. 确保它们是可行的,并帮助开发人员找到根本原因
  6. 避免重复错误消息

良好错误消息的一些示例可能是:

  1. “实例没有SES:sendemail许可”。
  2. “无法建立数据库连接”。
  3. “无法上传文件,大小大于10 MB”。

处理编程错误

现在,我们可以借助我们新创建的自定义错误类跟踪各种操作错误。现在该处理编程错误了。

两个这样的错误是uncaughtExceptionunhandledRejection

process.on("uncaughtException", (err) => {
  logger.fatal(`uncaughtException error has occurred: ${err}`);
  process.exit(1);
});

process.on("unhandledRejection", (err) => {
  logger.fatal(`uncaughtRejection error has occurred: ${err}`);
  process.exit(1);
});

这些处理程序确保即使没有操控性的错误也会被跟踪,并引起您急需的注意。这些错误必须以紧迫感(P0)采取行动,因为如果忽略了这些错误,您的服务可能会陷入出乎意料且较不可预测的状态。

如何在node.js中选择错误的日志级别

现在,我们的应用程序看起来更具弹性。本节是一个进修和一般指南,可帮助您为(错误)日志选择日志级别。每个日志级别都表示不同的目的。

痕迹

“我在这里”的日志。使用跟踪日志,您通常不想跟踪变量的状态,而是想知道特定请求的控制流。

调试

调试日志比跟踪日志更进一步:您热衷于跟踪变量的状态。例如,经过身份验证的用户详细信息,传递到服务的参数,数据库查询中设置的标志等等。

信息

这些日志是用于不需要立即采取行动或注意力的一般信息。它们不代表不必要的代码状态,因此在正常情况下可以忽略。信息日志的一些示例可以是成功初始化的装载机,成功的事件摄入和排水日志等。

警告

这些不是错误,而是不需要的状态。如果可以通过不适当的有效载荷导致请求失败,则可以使用警告日志级别。这是一个不必要的状态,但是一个预期的状态。例如,由于失败锁而引起的请求故障日志是预期的错误。

错误

与警告相反,错误日志表示意外的,不需要的代码状态。但是,如果发生这种错误,服务可以继续运行。例如,如果请求由于服务主机暂时无法到达而失败,则是错误日志。错误日志是可操作的。

致命的

这些将P0优先考虑。致命错误必须立即承认并采取行动,因为他们(通常)将服务降低。

但是,等等,我应该确切跟踪什么错误?

记录错误时有一个重要规则:仅包括基本信息。提供足够的背景很重要,它也是开发人员避免日志中噪声的责任。为什么?

每个日志都需要存储在磁盘上,并且它具有成本。处理日志需要磁盘IOPS,如果您的服务受到爆发或高持续负载,则很容易成为瓶颈。同样的IOPS配额也用于服务请求。

这是您应该跟踪的一些重要事情:

  1. 请求元数据
  2. 请求有效负载(如果太大,可以唯一标识有效载荷的钥匙)
  3. 在检查点上的成功/失败(在必要的上下文中)
  4. 最终回应

在需要时优雅地重新启动

在某些情况下,关键错误发生,没有其他选择,只需关闭并重新启动应用程序(例如,您的应用程序的内存越过其阈值)。

这样做的一种直接的(且潜在危险的方法)是直接process.exit(1)并进行新的重新启动。但这可能会导致问题,如果您的存储层或任何其他状态组件处于尴尬境地。

更好的方法是通过关闭所有开路,优雅地重新启动。仅当每个组件准备就绪时才重新启动应用程序。

我们将在该过程中收听SIGTERMSIGINT,并致电我们的实用程序以优雅地停止服务器:

process.on("SIGTERM", (signal) => loaders.close(server));
process.on("SIGINT", (signal) => loaders.close(server));

这是加载器模块:

module.exports = {
  run: () => {
    return new Promise((resolve, reject) => {
      const mongoClient = mongoose.getConnection();
      // other dependencies init goes here...

      resolve({ mongoClient });
    });
  },
  // runs before app crash/shutdown
  close: async (server) => {
    if (!server) {
      logger.warn("Cannot stop server, it's not running");
      process.exit(0);
    }

    // Fetching DB conn from loader as it is idempotent
    const { mongoClient } = await module.exports.run();

    server.close(async (err) => {
      // closing connection(s)
      await mongoClient.close();

      if (err) process.exit(1);
      process.exit(0);
    });
  },
};

根据您的特定用例,可以不退出该过程。另外,如果您的应用程序崩溃,则像PM2这样的过程经理允许automatic restarts

通过appsignal从node.js错误中学习

从整体上查看您的错误可以提供更好的视角,并帮助您观察代码中的重复行为,否则可能不会引起您的注意。

AppSignal使错误跟踪主动且方便,您可以sign up for a 30-day free trial(无需信用卡详细信息)。

例如,我可以在此图上看到一群错误。我可以简单地缩小并检查这些错误是否与当天发布的发布 /供应商的停机 /其他任何可能帮助我解决根本原因并决定进一步措施的释放。< / p>

AppSignal issues graph showing error details

您甚至可以设置警报以通知您是否检测到任何异常:

AppSignal alerts & triggers to be sus in the moment

AppSignal for Node.js还提供记录,主机和正常运行时间监控以及性能指标。

减少node.js中错误的最佳实践

跟踪和监视错误是好的,最好防止它们。

这是一些可以消除琐碎错误的最佳实践:

  1. 使用静态类型检查在编译阶段捕获琐碎错误(例如TypeError:无法将编号分配给字符串)。
  2. 在您的CI级别中添加预加入钩子和测试,以确保破坏构建不会熄灭。
  3. 负载测试您的服务,以观察您的应用程序在重负载下的行为。正常负载可能没有错误,但是您可以在爆发流量模式下观察到应用程序内存峰值。
  4. 定期审查并删除没有任何特定目的并添加噪声的日志。这也节省了磁盘空间,并在有限/小型IOPS的情况下有助于。

包起来

在本文中,我们首先定义了错误跟踪,并查看了不同类型的错误。然后,我们探索了如何以正确的方式跟踪您的node.js错误,包括创建自定义错误处理程序。我们还看到了如何避免跟踪微不足道的错误以减少警报中的噪音。

希望您学到了一些可以立即应用于项目的知识。您可能还会发现我们的Node.js Error Handling: Tips and Tricks帖子有用。

快乐编码!

P.S。如果您喜欢这篇文章,subscribe to our JavaScript Sorcery list每月深入研究更神奇的JavaScript技巧和技巧。

P.P.S。如果您需要node.js应用程序的APM,则GO和check out the AppSignal APM for Node.js