@villedemontreal/correlation-id 5.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/dist/src/config/configs.d.ts +16 -0
  4. package/dist/src/config/configs.js +26 -0
  5. package/dist/src/config/configs.js.map +1 -0
  6. package/dist/src/config/constants.d.ts +27 -0
  7. package/dist/src/config/constants.js +27 -0
  8. package/dist/src/config/constants.js.map +1 -0
  9. package/dist/src/config/init.d.ts +15 -0
  10. package/dist/src/config/init.js +34 -0
  11. package/dist/src/config/init.js.map +1 -0
  12. package/dist/src/index.d.ts +3 -0
  13. package/dist/src/index.js +25 -0
  14. package/dist/src/index.js.map +1 -0
  15. package/dist/src/middleware/correlationIdMiddleware.d.ts +8 -0
  16. package/dist/src/middleware/correlationIdMiddleware.js +43 -0
  17. package/dist/src/middleware/correlationIdMiddleware.js.map +1 -0
  18. package/dist/src/middleware/correlationIdMiddleware.test.d.ts +1 -0
  19. package/dist/src/middleware/correlationIdMiddleware.test.js +306 -0
  20. package/dist/src/middleware/correlationIdMiddleware.test.js.map +1 -0
  21. package/dist/src/services/correlationIdService.d.ts +68 -0
  22. package/dist/src/services/correlationIdService.js +166 -0
  23. package/dist/src/services/correlationIdService.js.map +1 -0
  24. package/dist/src/services/correlationIdService.test.d.ts +1 -0
  25. package/dist/src/services/correlationIdService.test.js +215 -0
  26. package/dist/src/services/correlationIdService.test.js.map +1 -0
  27. package/dist/src/utils/logger.d.ts +11 -0
  28. package/dist/src/utils/logger.js +54 -0
  29. package/dist/src/utils/logger.js.map +1 -0
  30. package/dist/src/utils/testingConfigurations.d.ts +9 -0
  31. package/dist/src/utils/testingConfigurations.js +18 -0
  32. package/dist/src/utils/testingConfigurations.js.map +1 -0
  33. package/package.json +62 -0
  34. package/src/config/configs.ts +26 -0
  35. package/src/config/constants.ts +40 -0
  36. package/src/config/init.ts +33 -0
  37. package/src/index.ts +9 -0
  38. package/src/middleware/correlationIdMiddleware.test.ts +335 -0
  39. package/src/middleware/correlationIdMiddleware.ts +45 -0
  40. package/src/services/correlationIdService.test.ts +255 -0
  41. package/src/services/correlationIdService.ts +255 -0
  42. package/src/utils/logger.ts +53 -0
  43. package/src/utils/testingConfigurations.ts +14 -0
@@ -0,0 +1,255 @@
1
+ const { AsyncLocalStorage } = require('async_hooks');
2
+ import * as cls from 'cls-hooked';
3
+ import { EventEmitter } from 'events';
4
+ import * as express from 'express';
5
+ import * as semver from 'semver';
6
+ import { v4 as uuid } from 'uuid';
7
+ import { constants } from '../config/constants';
8
+ const oldEmitSlot = Symbol('kOriginalEmit');
9
+ const cidSlot = Symbol('kCorrelationId');
10
+ const storeSlot = Symbol('kCidStore');
11
+
12
+ /**
13
+ * CorrelationId type
14
+ */
15
+ export type CorrelationId = string;
16
+
17
+ /**
18
+ * Informations about the correlation ID.
19
+ */
20
+ export interface ICidInfo {
21
+ /**
22
+ * Current cid
23
+ */
24
+ current: string;
25
+
26
+ /**
27
+ * Cid received in the request (may be undefined)
28
+ */
29
+ receivedInRequest: string;
30
+
31
+ /**
32
+ * Cid generated (may be undefined)
33
+ */
34
+ generated: string;
35
+ }
36
+
37
+ /**
38
+ * CorrelationId service
39
+ */
40
+ export interface ICorrelationIdService {
41
+ /**
42
+ * Creates a new correlation ID that can then be passed to the
43
+ * "withId()" function.
44
+ */
45
+ createNewId(): CorrelationId;
46
+
47
+ /**
48
+ * Executes a function inside a context where the correlation ID is defined.
49
+ *
50
+ * @param work the function to run within the cid context.
51
+ * @param cid the correlation ID to use.
52
+ *
53
+ */
54
+ withId<T>(work: () => T, cid?: CorrelationId): T;
55
+
56
+ /**
57
+ * Executes a function inside a context where the correlation ID is defined.
58
+ * This is the promisified version of the `withId` method.
59
+ * @param work a callback to invoke with the submitted correlation ID
60
+ * @param cid the correlation ID to install be before invoking the submitted callback
61
+ *
62
+ * @deprecated `#withId` is preferable instead: if the wrapped operation
63
+ * is asynchronous it will still be properly scoped (correlation context) and
64
+ * can safely be awaited for outside of `#withId`.
65
+ */
66
+ withIdAsync<T>(work: () => Promise<T>, cid?: string): Promise<T>;
67
+
68
+ /**
69
+ * binds the current correlation context to the target
70
+ * @param target the target to bind to
71
+ * @returns either the submitted target (if it is an emitter) or a wrapped target (for a function)
72
+ * @remarks you might have to bind to an emitter in order to maitain
73
+ * the correlation context.
74
+ */
75
+ bind<T>(target: T): T;
76
+
77
+ /**
78
+ * Returns the correlation ID from the current context
79
+ */
80
+ getId(): CorrelationId;
81
+
82
+ /**
83
+ * Returns all correlation ID info
84
+ */
85
+ getCidInfo(req: express.Request): ICidInfo;
86
+ }
87
+
88
+ /**
89
+ * CorrelationId service
90
+ */
91
+ class CorrelationIdServiceWithClsHooked implements ICorrelationIdService {
92
+ private store: cls.Namespace = cls.createNamespace('343c9880-fa2b-4212-a9f3-15f3cc09581d');
93
+
94
+ public createNewId(): CorrelationId {
95
+ return uuid();
96
+ }
97
+
98
+ public withId<T>(work: () => T, cid?: CorrelationId): T {
99
+ let cidClean = cid;
100
+ if (!cidClean) {
101
+ cidClean = this.createNewId();
102
+ }
103
+
104
+ return this.store.runAndReturn(() => {
105
+ this.store.set('correlator', cidClean);
106
+ return work();
107
+ });
108
+ }
109
+
110
+ public async withIdAsync<T>(work: () => Promise<T>, cid?: string): Promise<T> {
111
+ return new Promise<T>((resolve, reject) => {
112
+ this.withId(() => {
113
+ try {
114
+ work()
115
+ .then(resolve)
116
+ .catch(reject);
117
+ } catch (err) {
118
+ reject(err);
119
+ }
120
+ }, cid);
121
+ });
122
+ }
123
+
124
+ public bind<T>(target: T): T {
125
+ if (target instanceof EventEmitter) {
126
+ return this.bindEmitter(target);
127
+ }
128
+ if (typeof target === 'function') {
129
+ return this.bindFunction(target);
130
+ }
131
+ return target;
132
+ }
133
+
134
+ public getId() {
135
+ return this.store.get('correlator');
136
+ }
137
+
138
+ public getCidInfo(req: express.Request): ICidInfo {
139
+ return {
140
+ current: this.getId(),
141
+ receivedInRequest: req[constants.requestExtraVariables.cidReceivedInRequest],
142
+ generated: req[constants.requestExtraVariables.cidNew]
143
+ };
144
+ }
145
+
146
+ private bindEmitter<T extends EventEmitter>(emitter: T): T {
147
+ // Note that we can't use the following line:
148
+ // this.store.bindEmitter(emitter);
149
+ // because this works only if bindEmitter is called before any
150
+ // call to the "on" method of the emitter, and I don't want to
151
+ // risk having ordering issues.
152
+ // Note however that patching an emitter might not work in 100% cases.
153
+ // patch emit method only once!
154
+ if (!emitter[oldEmitSlot]) {
155
+ emitter[oldEmitSlot] = emitter.emit;
156
+ (emitter as any).emit = (...args: any[]) => {
157
+ // wrap the emit call within a new correlation context
158
+ // with the bound cid.
159
+ this.withId(() => {
160
+ // invoke original emit method
161
+ emitter[oldEmitSlot].apply(emitter, args);
162
+ }, emitter[cidSlot]);
163
+ };
164
+ }
165
+ // update the cid bound to the emitter
166
+ emitter[cidSlot] = this.getId();
167
+ return emitter;
168
+ }
169
+
170
+ // tslint:disable-next-line: ban-types
171
+ private bindFunction<T extends Function>(target: T): T {
172
+ return this.store.bind(target);
173
+ }
174
+ }
175
+
176
+ // tslint:disable-next-line: max-classes-per-file
177
+ class CorrelationIdServiceWithAsyncLocalStorage implements ICorrelationIdService {
178
+ private storage = new AsyncLocalStorage();
179
+
180
+ public createNewId(): CorrelationId {
181
+ return uuid();
182
+ }
183
+
184
+ public withId<T>(work: () => T, cid?: CorrelationId): T {
185
+ const correlationId = cid || this.createNewId();
186
+ return this.storage.run({ correlationId }, work);
187
+ }
188
+
189
+ public async withIdAsync<T>(work: () => Promise<T>, cid?: string): Promise<T> {
190
+ return this.withId(work, cid);
191
+ }
192
+
193
+ public bind<T>(target: T): T {
194
+ if (target instanceof EventEmitter) {
195
+ return this.bindEmitter(target);
196
+ }
197
+ if (typeof target === 'function') {
198
+ return this.bindFunction(target);
199
+ }
200
+ return target;
201
+ }
202
+
203
+ public getId() {
204
+ const store = this.storage.getStore();
205
+ if (store) {
206
+ return store.correlationId;
207
+ }
208
+ return undefined;
209
+ }
210
+
211
+ public getCidInfo(req: express.Request): ICidInfo {
212
+ return {
213
+ current: this.getId(),
214
+ receivedInRequest: req[constants.requestExtraVariables.cidReceivedInRequest],
215
+ generated: req[constants.requestExtraVariables.cidNew]
216
+ };
217
+ }
218
+
219
+ private bindEmitter<T extends EventEmitter>(emitter: T): T {
220
+ // patch emit method only once!
221
+ if (!emitter[oldEmitSlot]) {
222
+ emitter[oldEmitSlot] = emitter.emit;
223
+ (emitter as any).emit = (...args: any[]) => {
224
+ // use the store that was bound to this emitter
225
+ const store = emitter[storeSlot];
226
+ if (store) {
227
+ this.storage.enterWith(store);
228
+ }
229
+ // invoke original emit method
230
+ emitter[oldEmitSlot].call(emitter, ...args);
231
+ };
232
+ }
233
+ // update the store bound to the emitter
234
+ emitter[storeSlot] = this.storage.getStore();
235
+ return emitter;
236
+ }
237
+
238
+ // tslint:disable-next-line: ban-types
239
+ private bindFunction<T extends Function>(target: T): T {
240
+ const storage = this.storage;
241
+ const store = this.storage.getStore();
242
+ return function(...args: any[]) {
243
+ storage.enterWith(store);
244
+ return target.call(this, ...args);
245
+ } as any;
246
+ }
247
+ }
248
+
249
+ function canUseAsyncLocalStorage() {
250
+ return semver.satisfies(process.versions.node, '>=13.10.0') && !!AsyncLocalStorage;
251
+ }
252
+
253
+ export let correlationIdService: ICorrelationIdService = canUseAsyncLocalStorage()
254
+ ? new CorrelationIdServiceWithAsyncLocalStorage()
255
+ : new CorrelationIdServiceWithClsHooked();
@@ -0,0 +1,53 @@
1
+ import { ILogger, initLogger, LazyLogger, Logger, LoggerConfigs, LogLevel } from '@villedemontreal/logger';
2
+ import { configs } from '../config/configs';
3
+
4
+ let testingLoggerLibInitialised = false;
5
+
6
+ /**
7
+ * Creates a Logger.
8
+ */
9
+ export function createLogger(name: string): ILogger {
10
+ // ==========================================
11
+ // We use a LazyLogger so the real Logger
12
+ // is only created when the first
13
+ // log is actually performed... At that point,
14
+ // our "configs.loggerCreator" configuration
15
+ // must have been set by the code using our library!
16
+ //
17
+ // This pattern allows calling code to import
18
+ // modules from us in which a logger is
19
+ // created in the global scope :
20
+ //
21
+ // let logger = createLogger('someName');
22
+ //
23
+ // Without a Lazy Logger, the library configurations
24
+ // would at that moment *not* have been set yet
25
+ // (by the calling code) and an Error would be thrown
26
+ // because the "configs.loggerCreator" is required.
27
+ // ==========================================
28
+ return new LazyLogger(name, (nameArg: string) => {
29
+ return configs.loggerCreator(nameArg);
30
+ });
31
+ }
32
+
33
+ function initTestingLoggerConfigs() {
34
+ const loggerConfig: LoggerConfigs = new LoggerConfigs(() => 'test-cid');
35
+ loggerConfig.setLogLevel(LogLevel.DEBUG);
36
+ initLogger(loggerConfig);
37
+ }
38
+
39
+ /**
40
+ * A Logger that uses a dummy cid provider.
41
+ *
42
+ * Only use this when running the tests!
43
+ */
44
+ export function getTestingLoggerCreator(): (name: string) => ILogger {
45
+ return (name: string): ILogger => {
46
+ if (!testingLoggerLibInitialised) {
47
+ initTestingLoggerConfigs();
48
+ testingLoggerLibInitialised = true;
49
+ }
50
+
51
+ return new Logger(name);
52
+ };
53
+ }
@@ -0,0 +1,14 @@
1
+ import { init } from '../config/init';
2
+ import { getTestingLoggerCreator } from '../utils/logger';
3
+
4
+ /**
5
+ * Call this when your need to set
6
+ * *Testing* configurations to the current
7
+ * library, without the need for a calling code
8
+ * to do so.
9
+ *
10
+ * A test Correlation Id will be used!
11
+ */
12
+ export function setTestingConfigurations(): void {
13
+ init(getTestingLoggerCreator());
14
+ }