如何登录NodeJS,以便院子里的男孩


当您尝试在NodeJS应用程序中组织可读日志时,最让您生气的是什么? 就个人而言,我对创建跟踪ID缺乏任何健全的成熟标准感到非常恼火。 在本文中,我们将讨论创建跟踪ID的选项,让我们看一下本地连续存储或CLS是如何工作的,并借助Proxy的优势使所有记录器都可以使用。


为什么在NodeJS中为每个请求创建跟踪ID会有任何问题?


在过去,过去,过去,当猛mm象仍然步履蹒跚时,所有所有服务器都是多线程的,并为请求创建了新线程。 在此范式的框架内,创建跟踪ID很简单,因为 存在诸如线程本地存储或TLS之类的东西,它允许您将一些可用于此流中的任何功能的数据放入内存中。 在处理请求的开始,您可以赎回随机跟踪ID,将其放入TLS,然后在任何服务中读取它,并对其进行处理。 问题在于这在NodeJS中不起作用。


NodeJS是单线程的(考虑到工作程序的出现,它不是完全是单线程的,但是在具有跟踪ID的问题的框架内,工作程序不会扮演任何角色),因此您可以忽略TLS。 这里的范式是不同的-在同一个线程中处理许多不同的回调,并且一旦函数想要执行异步操作,发送该异步请求,并给处理器时间到队列中的另一个函数(如果您对此事件的运行方式感兴趣,可以骄傲地称为事件循环)在幕后,我建议阅读此系列文章 )。 如果考虑一下NodeJS如何理解何时调用哪个回调,则可以假定它们每个都必须对应于某个ID。 而且,NodeJS甚至具有一个提供对这些ID的访问的API。 我们将使用它。


在远古时代,猛ma象灭绝了,但人们仍然不知道中央污水处理的好处(NodeJS v0.11.11),我们有了addAsyncListener 。 在此基础上, Forrest Norvell创建了本地连续存储或CLS的第一个实现。 但是我们不会再讨论它是如何工作的,因为此API(我正在谈论addAsyncLustener)具有很长的使用寿命。 他已经在NodeJS v0.12中死亡。


在NodeJS 8之前,还没有正式的方法来跟踪异步事件队列。 最后,在版本8中,NodeJS开发人员恢复了正义,并向我们提供了async_hooks API 。 如果您想了解有关async_hooks的更多信息,建议您阅读本文 。 基于async_hooks,重构了以前的CLS实现。 该库称为cls-hooked


引擎盖下的CLS


一般而言,CLS操作方案可以表示为:


CLS概述


让我们再详细一点:


  1. 假设我们有一个典型的Express Web服务器。 首先,创建一个新的CLS命名空间。 在应用程序的整个生命周期中一劳永逸。
  2. 其次,我们将制作中间件,该中间件将为每个请求创建我们自己的CLS上下文。
  3. 当一个新的请求到达时,这个中间件(功能#1)被调用。
  4. 在此函数中,创建一个新的CLS上下文(作为一个选项,可以使用Namespace.run )。 在Namespace.run中,我们传递了一个将在上下文范围内执行的函数。
  5. CLS将新创建的上下文添加到具有当前执行ID密钥的上下文的Map中。
  6. 每个CLS命名空间都有一个active属性。 CLS将此属性分配为对我们上下文的引用。
  7. 在上下文范围内,我们对数据库进行某种异步查询。 我们将回调传递给数据库驱动程序,该请求将在请求完成时被调用。
  8. 异步初始化钩子触发 。 它将当前上下文与带有异步ID(新异步操作的ID)的上下文一起添加到Map中。
  9. 因为 我们的函数不再具有任何其他指令;它完成了执行。
  10. 一个异步的after hook对她有用。 它将active属性分配给undefined名称空间(实际上,并非总是如此,因为我们可以有多个嵌套的上下文,但最简单的情况是)。
  11. 对于我们的第一个异步操作, destroy异步挂钩将触发 。 它通过此操作的异步ID(与第一个回调的当前执行ID相同)从带有上下文的Map中删除上下文。
  12. 数据库中的查询已完成,并调用了第二个回调。
  13. 异步钩之前 。 它的当前执行ID与第二个操作(数据库查询)的异步ID相同。 根据当前执行ID,为名称空间的active属性分配了在Map中找到的上下文以及上下文。 这是我们之前创建的上下文。
  14. 现在执行第二个回调。 某种业务逻辑正在制定中,恶魔在跳舞,伏特加在浇灌。 在此内部,我们可以通过key从上下文中获取任何值 。 CLS将尝试在当前上下文中查找给定的键或返回undefined
  15. 完成此回调后,将触发此回调的异步after钩子。 它将名称空间的active属性设置为undefined
  16. 销毁异步钩子为此操作触发 。 它通过此操作的异步ID从上下文中移除具有上下文的Map(与第二个回调的当前执行ID相同)。
  17. 垃圾收集器(GC)释放与上下文对象关联的内存,因为 在我们的应用程序中,没有更多链接。

这是引擎盖下正在发生的事情的简化视图,但它涵盖了主要阶段和步骤。 如果您想更深入地进行挖掘,建议您熟悉一下这种类型 。 只有500行代码。


创建跟踪ID


因此,在处理了CLS之后,我们将尝试使用此东西造福人类。 让我们创建一个中间件,该中间件为每个请求创建自己的CLS上下文,创建一个随机跟踪ID,并使用键traceID将其添加到上下文中。 然后,在我们的控制器和服务办公室中,我们获得了此跟踪ID。


为了进行快速表达,类似的中间件可能如下所示:


 const cls = require('cls-hooked') const uuidv4 = require('uuid/v4') const clsNamespace = cls.createNamespace('app') const clsMiddleware = (req, res, next) => { // req  res -  event emitters.      CLS     clsNamespace.bind(req) clsNamespace.bind(res) const traceID = uuidv4() clsNamespace.run(() => { clsNamespace.set('traceID', traceID) next() }) } 

在我们的控制器或服务中,我们只需一行代码即可获取此traceID:


 const controller = (req, res, next) => { const traceID = clsNamespace.get('traceID') } 

的确,无需在日志中添加此跟踪ID,它就可以从中受益,例如夏天的吹雪机。


让我们写一个简单的Winston格式化程序,它将自动添加跟踪ID。


 const { createLogger, format, transports } = require('winston') const addTraceId = printf((info) => { let message = info.message const traceID = clsNamespace.get('taceID') if (traceID) { message = `[TraceID: ${traceID}]: ${message}` } return message }) const logger = createLogger({ format: addTraceId, transports: [new transports.Console()], }) 

并且,如果所有记录器都以函数形式支持自定义格式器(其中许多记录器有理由不这样做),那么本文可能就不会了。 那么,如何将跟踪ID添加到受喜爱的Pino的日志中?


我们呼吁Proxy以便与任何记录器和CLS成为朋友


关于Proxy本身的几句话:这是包装原始对象并允许我们在某些情况下重新定义其行为的东西。 在严格定义的有限情况列表中(在科学上,它们称为traps )。 您可以在此处找到完整列表,我们仅对trap get感兴趣。 它使我们有机会在访问对象的属性时覆盖返回值,即 如果我们使用对象const a = { prop: 1 }并将其包装在Proxy中,那么在trap get的帮助下,我们可以返回访问a.propa.prop


对于pino想法是这样的:我们为每个请求创建一个随机跟踪ID,创建一个我们向其传递跟踪ID的pino子实例 ,然后将此子实例放入CLS。 然后,我们将源记录器包装在Proxy中,如果有活动上下文且其中有一个子记录器,则它将使用该子实例进行记录,或者使用原始记录器。


在这种情况下,代理将如下所示:


 const pino = require('pino') const logger = pino() const loggerCls = new Proxy(logger, { get(target, property, receiver) { //    CLS  ,   target = clsNamespace.get('loggerCls') || target return Reflect.get(target, property, receiver) }, }) 

我们的中间件将如下所示:


 const cls = require('cls-hooked') const uuidv4 = require('uuid/v4') const clsMiddleware = (req, res, next) => { // req  res -  event emitters.      CLS     clsNamespace.bind(req) clsNamespace.bind(res) const traceID = uuidv4() const loggerWithTraceId = logger.child({ traceID }) clsNamespace.run(() => { clsNamespace.set('loggerCls', loggerWithTraceId) next() }) } 

我们可以像这样使用记录器:


 const controller = (req, res, next) => { loggerCls.info('Long live rocknroll!') //  // {"level":30,"time":1551385666046,"msg":"Long live rocknroll!","pid":25,"hostname":"eb6a6c70f5c4","traceID":"9ba393f0-ec8c-4396-8092-b7e4b6f375b5","v":1} } 

cls代理


基于上述想法,创建了一个小的cls-proxify库 。 她开箱即用, 快递固定 。 除了为get创建陷阱外,它还会创建其他陷阱以使开发人员拥有更大的自由度。 因此,我们可以使用Proxy包装函数,类等。 有一个有关如何集成pino和固定,pino和表达的现场演示


希望您不要白白浪费时间,并且这篇文章至少对您有用。 请踢并批评。 我们将学习一起更好地编码。

Source: https://habr.com/ru/post/zh-CN442452/


All Articles