发布于 2026-01-06 2 阅读
0

Node.js 并发日志记录终极指南

Node.js 并发日志记录终极指南

这是一个每个人都会遇到的问题:如果没有启动上下文,并发任务生成的日志就毫无用处。

如果您需要一个例子,可以考虑这样一个程序:它在接收到事件后执行一些代码并生成日志。事件可以是传入的 HTTP 请求、作业队列等等。

process.on('task', (task) => {
  executeTask(task);
});

const executeTask = async (task) => {
  try {
    // [..]

    console.log('task completed');
  } catch (error) {
    console.log('task failed');
  }
};

Enter fullscreen mode Exit fullscreen mode

目前,该程序会生成如下所示的日志:

task completed
task completed
task failed
task completed
task failed
Enter fullscreen mode Exit fullscreen mode

关键在于,如果我们想要生成有意义的日志,那么我们必须以某种方式将task每条日志消息关联起来。

const executeTask = async (task) => {
  try {
    await download(task.url);

    console.log({task}, 'task completed');
  } catch (error) {
    console.log({task}, 'task failed');
  }
};

Enter fullscreen mode Exit fullscreen mode

问题在于,为了实现这一点,你必须将对象向下传递给每个生成日志的函数。在我们的示例中,添加task日志很容易,但通常情况下,生成代码的函数嵌套很深,或者它们是第三方模块,无法将额外的上下文传递给日志记录器。taskconsole.log

const download = (url) => {
  if ([..]) {
    return console.error('invalid url');
  }

  if ([..]) {
    return console.error('unsupported protocol');
  }

  // [..]
};

Enter fullscreen mode Exit fullscreen mode

由于没有传递日志记录器上下文的约定,最终会得到一个列出随机事件的日志跟踪,而没有提供将这些事件与其所属的异步任务关联起来所需的必要上下文。

invalid url
task completed {id:6, url: [..]}
task completed {id:4, url: [..]}
unsupported protocol
task completed {id:5, url: [..]}
task completed {id:3, url: [..]}
task failed {id:2, url: [..]}
task completed
task failed {id:1, url: [..]}
Enter fullscreen mode Exit fullscreen mode

上述日志不足以确定哪个任务失败以及失败的原因。幸运的是,有解决办法。

使用Node.js域名

使用 Node.js 的Domain,我们可以向异步调用链添加上下文,而无需显式地传递它们(可以将其视为React 中的闭包或Context ),例如:

import domain from 'domain';

process.on('task', (task) => {
  domain
    .create()
    .run(() => {
      process.domain.context = {
        task,
      };

      executeTask(task);
    });
});

const download = (url) => {
  if ([..]) {
    return console.error(process.domain.context, 'invalid url');
  }

  if ([..]) {
    return console.error(process.domain.context, 'unsupported protocol');
  }

  // [..]
};

const executeTask = async (task) => {
  try {
    await download(task.url);

    console.log({task}, 'task completed');
  } catch (error) {
    console.log({task}, 'task failed');
  }
};

Enter fullscreen mode Exit fullscreen mode

这样,每条日志消息都与发起异步调用链的异步上下文相关联。

invalid url {id:1, url: [..]}
task completed {id:6, url: [..]}
task completed {id:4, url: [..]}
unsupported protocol {id:2, url: [..]}
task completed {id:5, url: [..]}
task completed {id:3, url: [..]}
task failed {id:2, url: [..]}
task completed {id:2, url: [..]}
task failed {id:1, url: [..]}
Enter fullscreen mode Exit fullscreen mode

理论上,域甚至可以嵌套,也就是说,一个了解父域的域可以用来将日志消息与导致该日志消息的整个异步调用链关联起来。Node.js 本身并不提供此功能。但是,可以通过猴子补丁(monkey-patcher)将父域显式绑定到活动域,例如:

const domain = require('domain');

const originalCreate = domain.create;

domain.create = (...args) => {
  const parentDomain = process.domain || null;

  const nextDomain = originalCreate(...args);

  nextDomain.parentDomain = parentDomain;

  return nextDomain;
};

Enter fullscreen mode Exit fullscreen mode

之后便parentDomain成为对父域的引用:

const d0 = domain.create();

d0.run(() => {
  const d1 = domain.create();

  d1.run(() => {
    d1.parentDomain === d0;
  });
});

Enter fullscreen mode Exit fullscreen mode

为了使用我即将介绍的日志记录器,您需要使用以下方法修补 Node.js domain-parent

域名弃用

大家都在指出明显的弃用通知。

域模块使用异步钩子实现。自 2015 年 2 月 28 日起,域模块已被列入待弃用名单,目前其弃用状态为“仅文档弃用”。然而,许多流行的 NPM 模块和 Node.js 内部机制都高度依赖域模块。它们距离运行时弃用还有很长一段时间(甚至可能永远不会),距离生命周期结束弃用也需要很长时间。在此之前,使用域模块是安全的。

咆哮

最后,这一切都为大家介绍了一个已经开发完成并可供我们使用的、基于约定、上下文感知的日志记录器:Roarr

Roarradopt方法会创建一个域,该域能够高效地将用户定义的上下文绑定到异步调用期间产生的所有日志消息。要将上下文与异步代码关联起来,只需使用 Roarradopt方法执行您的例程即可,例如:

import Logger from 'roarr';

process.on('task', (task) => {
  Logger
    .adopt(
      () => {
        return executeTask(task);
      },
      // Here we define the context that we want to associate with
      // all the Roarr loggers that will be called when executing
      // `executeTask` with the current `task` value. 
      {
        task,
      }
    );
});

const download = (url) => {
  if ([..]) {
    return log.error('invalid url');
  }

  if ([..]) {
    return log.error('unsupported protocol');
  }

  // [..]
};

const executeTask = async (task) => {
  try {
    await download(task.url);

    log.info('task completed');
  } catch (error) {
    log.info('task failed');
  }
};

Enter fullscreen mode Exit fullscreen mode

上述结果将产生等价于:

invalid url {id:1, url: [..]}
task completed {id:6, url: [..]}
task completed {id:4, url: [..]}
unsupported protocol {id:2, url: [..]}
task completed {id:5, url: [..]}
task completed {id:3, url: [..]}
task failed {id:2, url: [..]}
task completed {id:2, url: [..]}
task failed {id:1, url: [..]}

Enter fullscreen mode Exit fullscreen mode

当然,为了让所有日志都包含异步上下文,所有依赖项都需要使用 Roarr 日志记录器(或者需要从其他库读取日志记录器上下文process.domain.roarr.context)。然而,与其他日志记录器不同,Roarr 无需配置,既可用于可分发的软件包,也可用于顶级程序。因此,目前已有数千个软件包在使用 Roarr。

要开始使用 Roarr,请阅读Roarr 为何是 Node.js 中完美的日志记录器,并查看实现该日志记录器的示例库(Slonik是一个不错的起点)。

总结一下:

  1. 在并发执行环境中,缺乏上下文信息的日志是没有意义的。
  2. 域可用于将上下文与异步调用链关联起来。
  3. Roarr利用域来继承描述导致日志消息的异步调用链的上下文。这使得 Roarr 即使在多个任务并发执行的情况下,也能生成描述完整执行上下文的日志。

不妨试试Roarr。它甚至还有一个简洁的命令行程序,内置了美化输出和过滤功能。

文章来源:https://dev.to/gajus/ultimate-guide-to-concurrent-logging-in-node-js-18h7