gibson
gibson

Reputation: 1086

node.js express request Id

I'm trying to create some sort of a request Id for logging purposes which will be available to me through every function in the request flow. I want to log every step of the request flow with an Id stating which log line is for which request.

I've looked over some ideas and ran into 2 main suggestions:

The first is creating a middleware that will add a field in the 'req' object like so(as suggested here):

var logIdIterator = 0;

app.all('*', function(req, res, next) {
  req.log = {
    id: ++logIdIterator
  }
  return next();
});

And the second is using continuation-local-storage

The problems are:

For the first approach - it means I'll have to pass an extra argument to each function in the flow and this is not an easy solution to do over a mature application with countless APIs and flows.

The second one looks promising but unfortunately it has some issues where the state gets lost(see here for example). Also, it happened a few times when we used our redis library - which is bad because redis requests happen on each of our flows.

I guess if I don't find another solution, I'll have to use the first approach, it's just that I want to avoid passing an extra parameter to thousands of existing functions.

My question is - how do you suggest to maintain a request Id through the request flow?

Upvotes: 17

Views: 31742

Answers (5)

albinjose
albinjose

Reputation: 203

For node >= 16, You can use AsyncLocalStorage provided by nodejs. From nodejs docs

These classes are used to associate state and propagate it throughout callbacks and promise chains. They allow storing data throughout the lifetime of a web request or any other asynchronous duration. It is similar to thread-local storage in other languages

You need to create an asyncLocalStorage instance and export that. This same instace should be used in all files.

// asyncLocalStorage.js
const { AsyncLocalStorage } = require('node:async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();

module.exports = asyncLocalStorage;

Then you can use a middleware to create requestId and associate that state to that particular request.

//middleware.js
const crypto = require('crypto');
const asyncLocalStorage = require('./asyncLocalStorage.js');

function createRequestId(req, res, next) {
  asyncLocalStorage.run(crypto.randomUUID, () => {
    next()
  }
}

module.exports = createRequestId

We are calling next() from inside asyncLocalStorage.run() so that the entire request gets excetuted from inside that function and we can access requestId in all the functions that gets called in that request lifetime.

// service.js
const asyncLocalStorage = require('./asyncLocalStorage.js')
const testfunction() {
  const requestId = asyncLocalStorage.getStore();
}

Upvotes: 2

Zachary
Zachary

Reputation: 1

If you are using express and wanted to import the express-request-id instead of the required approach. You may try this.

import expressRequestId from 'express-request-id'
import express from 'express'

const framework = express()
framework.use(expressRequestId())

Every function would come with a unique ID. Just access it like this (req.id)

Upvotes: 0

Iiridayn
Iiridayn

Reputation: 1821

You have asynchronous communications, and you want to preserve context without either a) closures or b) parameter passing. I'm afraid that your best bet is to pass something to all the functions which need to know it - whether it be the req object, a request id, a curried log function call - something.

Whatever you pass can be easily enough refactored into any of the others - a plain id could look up an object from global storage (not good, but possible) for example. With that in mind - you might already have something being passed around to the methods which uniquely identifies the request; in which case, using that as a key and looking up additional data from a global storage (ie, require a file with an module.exports.cache = new Map(); or something, no reason to pollute the global namespace).

As you've noticed, attempts to do wonky things with the language are often brittle (especially when meeting other wonky things). That said, you could figure out how continuation-local-storage works internally, debug it along with your breaking libraries, and use it or a homebrew solution.

You sound uncomfortable with the cost of maintaining this code. This is a code smell - and adding implicit global continuation local state sounds like something that will only make understanding and maintaining the code harder moving forward instead of simpler. You might take this as a learning opportunity, and ask why you need the request id, and why it wasn't needed when whomever wrote the code. I'm sorry that without knowing the codebase itself, this is the best answer I can give.

Upvotes: 2

vun
vun

Reputation: 1269

You can use this package: https://www.npmjs.com/package/express-request-id

This is the middleware that will append uuid for every request

var app = require('express')();
var addRequestId = require('express-request-id')();
 
app.use(addRequestId);
 
app.get('/', function (req, res, next) {
    res.send(req.id);
    next();
});
 
app.listen(3000, function() {
    console.log('Listening on port %d', server.address().port);
});
 
// curl localhost:3000 
// d7c32387-3feb-452b-8df1-2d8338b3ea22 

Upvotes: 4

Bob Dill
Bob Dill

Reputation: 1010

You could make your original routine a little more sophisticated by (a) storing the information as a server-only cookie and (b) putting more information into the cookie beyond just a counter. For example, I use the following as trace of all calls to my app:

console.log('['+count+'] at: '+format.asString('hh:mm:ss.SSS', new Date())+' Url is: ' + req.url);

where 'count' is incremented from app start (yes, better to use a persistent singleton). This gives me every call to the server (req.url) and exactly when that call was made. This could easily be extended to pick up the sessionID, if your app is doing session level management.

Upvotes: 0

Related Questions