@villedemontreal/correlation-id
Version:
Express middleware to set a correlation in Express. The correlation id will be consistent across async calls within the handling of a request.
166 lines • 5.73 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.correlationIdService = void 0;
const async_hooks_1 = require("async_hooks");
const cls = require("cls-hooked");
const events_1 = require("events");
const semver = require("semver");
const uuid_1 = require("uuid");
const constants_1 = require("../config/constants");
const oldEmitSlot = Symbol('kOriginalEmit');
const cidSlot = Symbol('kCorrelationId');
const storeSlot = Symbol('kCidStore');
/**
* CorrelationId service
*/
class CorrelationIdServiceWithClsHooked {
constructor() {
this.store = cls.createNamespace('343c9880-fa2b-4212-a9f3-15f3cc09581d');
}
createNewId() {
return (0, uuid_1.v4)();
}
withId(work, cid) {
let cidClean = cid;
if (!cidClean) {
cidClean = this.createNewId();
}
return this.store.runAndReturn(() => {
this.store.set('correlator', cidClean);
return work();
});
}
async withIdAsync(work, cid) {
return new Promise((resolve, reject) => {
this.withId(() => {
try {
work().then(resolve).catch(reject);
}
catch (err) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
reject(err);
}
}, cid);
});
}
bind(target) {
if (target instanceof events_1.EventEmitter) {
return this.bindEmitter(target);
}
if (typeof target === 'function') {
return this.bindFunction(target);
}
return target;
}
getId() {
return this.store.get('correlator');
}
getCidInfo(req) {
return {
current: this.getId(),
receivedInRequest: req[constants_1.constants.requestExtraVariables.cidReceivedInRequest],
generated: req[constants_1.constants.requestExtraVariables.cidNew],
};
}
bindEmitter(emitter) {
// Note that we can't use the following line:
// this.store.bindEmitter(emitter);
// because this works only if bindEmitter is called before any
// call to the "on" method of the emitter, and I don't want to
// risk having ordering issues.
// Note however that patching an emitter might not work in 100% cases.
// patch emit method only once!
const emitterObj = emitter;
if (!emitterObj[oldEmitSlot]) {
emitterObj[oldEmitSlot] = emitter.emit;
emitter.emit = (...args) => {
// wrap the emit call within a new correlation context
// with the bound cid.
this.withId(() => {
// invoke original emit method
emitterObj[oldEmitSlot].apply(emitter, args);
}, emitterObj[cidSlot]);
};
}
// update the cid bound to the emitter
emitterObj[cidSlot] = this.getId();
return emitter;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
bindFunction(target) {
return this.store.bind(target);
}
}
class CorrelationIdServiceWithAsyncLocalStorage {
constructor() {
this.storage = new async_hooks_1.AsyncLocalStorage();
}
createNewId() {
return (0, uuid_1.v4)();
}
withId(work, cid) {
const correlationId = cid || this.createNewId();
return this.storage.run({ correlationId }, work);
}
async withIdAsync(work, cid) {
return this.withId(work, cid);
}
bind(target) {
if (target instanceof events_1.EventEmitter) {
return this.bindEmitter(target);
}
if (typeof target === 'function') {
return this.bindFunction(target);
}
return target;
}
getId() {
const store = this.storage.getStore();
if (store) {
return store.correlationId;
}
return undefined;
}
getCidInfo(req) {
return {
current: this.getId(),
receivedInRequest: req[constants_1.constants.requestExtraVariables.cidReceivedInRequest],
generated: req[constants_1.constants.requestExtraVariables.cidNew],
};
}
bindEmitter(emitter) {
// patch emit method only once!
const emitterObj = emitter;
if (!emitterObj[oldEmitSlot]) {
emitterObj[oldEmitSlot] = emitter.emit;
emitterObj.emit = (...args) => {
// use the store that was bound to this emitter
const store = emitterObj[storeSlot];
if (store) {
this.storage.enterWith(store);
}
// invoke original emit method
emitterObj[oldEmitSlot].call(emitter, ...args);
};
}
// update the store bound to the emitter
emitterObj[storeSlot] = this.storage.getStore();
return emitter;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
bindFunction(target) {
const storage = this.storage;
const store = this.storage.getStore();
return function (...args) {
storage.enterWith(store);
return target.call(this, ...args);
};
}
}
function canUseAsyncLocalStorage() {
return semver.satisfies(process.versions.node, '>=13.10.0') && !!async_hooks_1.AsyncLocalStorage;
}
exports.correlationIdService = canUseAsyncLocalStorage()
? new CorrelationIdServiceWithAsyncLocalStorage()
: new CorrelationIdServiceWithClsHooked();
//# sourceMappingURL=correlationIdService.js.map