能否在node.js中获取当前正在服务的请求?

12

我正在使用express.js。当有人尝试记录消息时,我需要能够记录某些请求数据。为此,我想创建一个帮助方法,如下所示:

function log_message(level, message){
  winston.log(level, req.path + "" + message);
}

那么我会这样使用该方法。

exports.index = function(req, res){
  log_message("info", "I'm here");
}
请注意,我没有将req对象传递给log_message函数。我希望这可以自动完成,这样log_message API用户就不需要知道正在记录的常见数据。
是否有一种方法可以在express.js/node.js中实现这一点?请求对象是否可以从某种全局变量中获取?
6个回答

10

一种有趣的方法是使用新的 Domains 功能。 http://nodejs.org/api/domain.html

Domains 提供了出色的错误恢复功能,同时也可以用作“线程本地存储”的一种方式 - 用于为每个请求存储数据。

创建一些中间件,将每个请求/响应添加到一个域中。

app.use(function(req, res, next) {
  var reqd = domain.create();
  reqd.add(req);
  reqd.add(res);
  reqd._req = req; // Add request object to custom property
  // TODO: hook error event on reqd (see docs)
  next();
});

在log函数中,您现在可以获取当前域并提取请求对象。

function log_message(level, message) {
  // Pull the request from the current domain.
  var request = process.domain._req;

  // TODO: log message
};

域名仍处于实验阶段,但听起来在1.0版本发布之前不会有太多变化。


创建一个域并不会自动将 process.domain 设置为该域。因此,在 log_message 中,process.domain 实际上是未定义的。 - Moiz Raja
@MoizRaja 那么,实际上如何将域名添加到进程中,以便稍后可以使用它? - Christopher Will
@ChristopherWill:在中间件中使用process.domain = reqd;对我很有用。不幸的是,我找不到文件来描述它的行为 :-/ - Timm
1
不要调用 next(),而是调用 reqd.run(next)enter 方法是由 runbindintercept 方法使用的管道,用于设置活动域。它将 domain.activeprocess.domain 设置为该域。http://nodejs.org/api/domain.html#domain_domain_enter - Ricardo Stuven
13
请停止使用它:“该模块待废弃”。 - Matthias M

6

2
当使用CLS时,请注意您将遇到的问题:https://github.com/othiym23/node-continuation-local-storage/issues/59 - Christophe Geers

5
创建一个中间件:
app.use(function(req, res, next) {
       var tid = uuid.v4();
     var cls = require('continuation-local-storage');
       var namespace = cls.createNamespace('com.storage');
      var pre_ip;
          if(get_ip(req))
          { ip_info= get_ip(req).clientIp;
              pre_ip=ip_info
          }
          namespace.bindEmitter(req);
          namespace.bindEmitter(res);
        namespace.run(function() {
               console.log(logobj);
              namespace.set('tid', tid);
              namespace.set('ip',ip_info);
           namespace.set('logobj',logobj);   
              next();
          });
      });

并使用它:

var cls = require('continuation-local-storage');
 var namespace = cls.getNamespace('com.storage');
      namespace.get('ip');

很遗憾,它不能与async/await流程一起使用。您可以在控制器上轻松尝试“等待”3秒钟,然后输出数据。并且快速发送两个请求到您的应用程序 - 只有第二个将显示正确的数据,第一个将是未定义的,因此它不适用于异步方式。但是,在直接请求链调用而没有“线程”竞争条件的情况下,它将起作用。 - Nigrimmist
只是更新一下:找到了线程安全的解决方案,请查看我的帖子回答。 - Nigrimmist

3
log_message是如何向调用者(模块等)公开的,以及在路由之前的管道中你有什么控制权?
您可以在此路由调用之前应用中间件,并使函数log_message从闭包内部调用,或者利用req EventEmitter工具并将调用winston.log的操作包装在req.end的处理程序中,仅记录在请求期间进行的所有消息。这将有效地更改您的log_message成为日志消息的累加器(可能在数组中),并在请求结束时将它们全部记录下来。
但这完全取决于您如何公开这些内容。
这里屠了很多猫 :)

2

以下解决方案对我是可接受的。

这里有一个中间件,它将log_message方法添加到请求对象上。之后,我只需调用req.log_message来记录消息。虽然这与将req对象传递给每个日志记录调用非常相似,但它更加简洁。

function logging_middleware(req, res, next){
    req.log_message = function(level, message){
        winston.log(level, req.path + ":" + message);
    }
    next();
}

1
我找到了一种“线程安全”的解决方案,可以在不修改req对象并通过所有层传递数据的情况下实现。因此,我们有continuation-local-storage包,它允许我们在req的开始绑定req,然后在下一次调用时重复使用。但是当您尝试使用异步调用链时,CLS不是“线程安全的”,如果您尝试在几个并发请求期间调用它,它将失败并丢失“上下文”。因此,我们有cls-hooked包,可以解决此问题,我们可以轻松地捕获当前上下文绑定请求的请求开始。这是一个使用awilix(依赖注入框架)的小例子,我们将创建“RequestContext”类,并能够在需要的任何地方使用它。让我们创建RequestContext类:
module.exports = class RequestContext {

 constructor(req, res, dependencies) {
 const { myService1, myService2 } = dependencies;    
 this.req = req;
 this.res = res;
}

getMyTestHeader() {
 return this.req.headers.testHeader;
}

这是一个简单的“请求包装器”。如果需要,它会消耗请求、响应和其他依赖项,并提供 getMyTestHeader 以在类外使用。
让我们在请求开始时(在所有其他中间件之前)创建中间件:
  //Registering our namespace. creating it one per app
  const clsNamespace = require('cls-hooked').createNamespace('my-per-request-session'); 
  app.use((req, res, next) => {
      // binding continuation-local-storage (cls-hooked) to request and response
      clsNamespace.bind(req);
      clsNamespace.bind(res);
      clsNamespace.run(() => {
      // save here req and res to use it later in requestContext instance, it will alive during request and could be picked up from DI as other dependencies
       clsNamespace.set('req', req);
       clsNamespace.set('res', res);
       next();
      });

  });

现在,让我们使用 awilix 注册 DI 调用:
container.register({  
requestContextProvider: asFunction(dependencies => ({
 getCurrentContext: () => {
  //retrieve res and req on each DI call "from current request"
  //also, clsNamespace should be the same as we registered before, on original code it is also registered using DI container.
  const req = dependencies.clsNamespace.get('req');
  const res = dependencies.clsNamespace.get('res');
  //and return our RequestContext (wrapper for req and res)
  return new RequestContext(req, res, dependencies);
 },
})),

因此,我们将requestContextProvider注册为函数,每次DI依赖项调用时都会在外部上下文(cls)中使用req和res进行实例化。 结果,我们可以这样使用它(例如在控制器中):
module.exports = (dependencies) => ({
 myControllerAction: async (req, res) => {
  const {requestContextProvider} = dependencies;
  const requestContext = requestContextProvider.getCurrentContext();
  //it will return header from req!
  const myHeader = requestContext.getMyTestHeader(); 
  res.status(200).json({ myHeader });
 },    
});

正如您所看到的,现在我们可以在每个层级(控制器/BLL/DAL/helpers等)访问DI时都有“requestContext”。因此,它是“线程安全”的,易于测试,并且不需要将req对象传递到所有“中间”层。

同意,这不是最好和最简单的示例,但希望能对某些人有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接