UNPKG

@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
"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