@unito/integration-sdk 5.0.0 → 5.1.0
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.
- package/dist/src/handler.js +11 -0
- package/dist/src/index.cjs +66 -5
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/middlewares/finish.d.ts +0 -1
- package/dist/src/middlewares/finish.js +4 -1
- package/dist/src/middlewares/start.d.ts +2 -1
- package/dist/src/middlewares/start.js +2 -1
- package/dist/src/resources/context.d.ts +6 -0
- package/dist/src/resources/provider.d.ts +3 -0
- package/dist/src/resources/provider.js +15 -3
- package/dist/src/resources/requestMetrics.d.ts +24 -0
- package/dist/src/resources/requestMetrics.js +33 -0
- package/dist/test/middlewares/finish.test.js +5 -4
- package/dist/test/middlewares/start.test.js +13 -2
- package/dist/test/resources/provider.test.js +95 -0
- package/dist/test/resources/requestMetrics.test.d.ts +1 -0
- package/dist/test/resources/requestMetrics.test.js +45 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/handler.ts +11 -0
- package/src/index.ts +1 -0
- package/src/middlewares/finish.ts +4 -2
- package/src/middlewares/start.ts +3 -2
- package/src/resources/context.ts +6 -0
- package/src/resources/provider.ts +20 -3
- package/src/resources/requestMetrics.ts +37 -0
- package/test/middlewares/finish.test.ts +13 -12
- package/test/middlewares/start.test.ts +13 -2
- package/test/resources/provider.test.ts +126 -0
- package/test/resources/requestMetrics.test.ts +50 -0
package/dist/src/handler.js
CHANGED
|
@@ -103,6 +103,7 @@ export class Handler {
|
|
|
103
103
|
filters: res.locals.filters,
|
|
104
104
|
logger: res.locals.logger,
|
|
105
105
|
signal: res.locals.signal,
|
|
106
|
+
requestMetrics: res.locals.requestMetrics,
|
|
106
107
|
params: req.params,
|
|
107
108
|
query: req.query,
|
|
108
109
|
relations: res.locals.relations,
|
|
@@ -124,6 +125,7 @@ export class Handler {
|
|
|
124
125
|
body: req.body,
|
|
125
126
|
logger: res.locals.logger,
|
|
126
127
|
signal: res.locals.signal,
|
|
128
|
+
requestMetrics: res.locals.requestMetrics,
|
|
127
129
|
params: req.params,
|
|
128
130
|
query: req.query,
|
|
129
131
|
});
|
|
@@ -164,6 +166,7 @@ export class Handler {
|
|
|
164
166
|
body,
|
|
165
167
|
logger: res.locals.logger,
|
|
166
168
|
signal: res.locals.signal,
|
|
169
|
+
requestMetrics: res.locals.requestMetrics,
|
|
167
170
|
params: req.params,
|
|
168
171
|
query: req.query,
|
|
169
172
|
});
|
|
@@ -196,6 +199,7 @@ export class Handler {
|
|
|
196
199
|
secrets: res.locals.secrets,
|
|
197
200
|
logger: res.locals.logger,
|
|
198
201
|
signal: res.locals.signal,
|
|
202
|
+
requestMetrics: res.locals.requestMetrics,
|
|
199
203
|
params: req.params,
|
|
200
204
|
query: req.query,
|
|
201
205
|
});
|
|
@@ -216,6 +220,7 @@ export class Handler {
|
|
|
216
220
|
body: req.body,
|
|
217
221
|
logger: res.locals.logger,
|
|
218
222
|
signal: res.locals.signal,
|
|
223
|
+
requestMetrics: res.locals.requestMetrics,
|
|
219
224
|
params: req.params,
|
|
220
225
|
query: req.query,
|
|
221
226
|
});
|
|
@@ -234,6 +239,7 @@ export class Handler {
|
|
|
234
239
|
secrets: res.locals.secrets,
|
|
235
240
|
logger: res.locals.logger,
|
|
236
241
|
signal: res.locals.signal,
|
|
242
|
+
requestMetrics: res.locals.requestMetrics,
|
|
237
243
|
params: req.params,
|
|
238
244
|
query: req.query,
|
|
239
245
|
});
|
|
@@ -252,6 +258,7 @@ export class Handler {
|
|
|
252
258
|
secrets: res.locals.secrets,
|
|
253
259
|
logger: res.locals.logger,
|
|
254
260
|
signal: res.locals.signal,
|
|
261
|
+
requestMetrics: res.locals.requestMetrics,
|
|
255
262
|
params: req.params,
|
|
256
263
|
query: req.query,
|
|
257
264
|
});
|
|
@@ -287,6 +294,7 @@ export class Handler {
|
|
|
287
294
|
secrets: res.locals.secrets,
|
|
288
295
|
logger: res.locals.logger,
|
|
289
296
|
signal: res.locals.signal,
|
|
297
|
+
requestMetrics: res.locals.requestMetrics,
|
|
290
298
|
params: req.params,
|
|
291
299
|
query: req.query,
|
|
292
300
|
});
|
|
@@ -302,6 +310,7 @@ export class Handler {
|
|
|
302
310
|
secrets: res.locals.secrets,
|
|
303
311
|
logger: res.locals.logger,
|
|
304
312
|
signal: res.locals.signal,
|
|
313
|
+
requestMetrics: res.locals.requestMetrics,
|
|
305
314
|
params: req.params,
|
|
306
315
|
query: req.query,
|
|
307
316
|
body: req.body,
|
|
@@ -318,6 +327,7 @@ export class Handler {
|
|
|
318
327
|
secrets: res.locals.secrets,
|
|
319
328
|
logger: res.locals.logger,
|
|
320
329
|
signal: res.locals.signal,
|
|
330
|
+
requestMetrics: res.locals.requestMetrics,
|
|
321
331
|
params: req.params,
|
|
322
332
|
query: req.query,
|
|
323
333
|
body: req.body,
|
|
@@ -339,6 +349,7 @@ export class Handler {
|
|
|
339
349
|
body: req.body,
|
|
340
350
|
logger: res.locals.logger,
|
|
341
351
|
signal: res.locals.signal,
|
|
352
|
+
requestMetrics: res.locals.requestMetrics,
|
|
342
353
|
params: req.params,
|
|
343
354
|
query: req.query,
|
|
344
355
|
});
|
package/dist/src/index.cjs
CHANGED
|
@@ -552,6 +552,7 @@ class Handler {
|
|
|
552
552
|
filters: res.locals.filters,
|
|
553
553
|
logger: res.locals.logger,
|
|
554
554
|
signal: res.locals.signal,
|
|
555
|
+
requestMetrics: res.locals.requestMetrics,
|
|
555
556
|
params: req.params,
|
|
556
557
|
query: req.query,
|
|
557
558
|
relations: res.locals.relations,
|
|
@@ -573,6 +574,7 @@ class Handler {
|
|
|
573
574
|
body: req.body,
|
|
574
575
|
logger: res.locals.logger,
|
|
575
576
|
signal: res.locals.signal,
|
|
577
|
+
requestMetrics: res.locals.requestMetrics,
|
|
576
578
|
params: req.params,
|
|
577
579
|
query: req.query,
|
|
578
580
|
});
|
|
@@ -613,6 +615,7 @@ class Handler {
|
|
|
613
615
|
body,
|
|
614
616
|
logger: res.locals.logger,
|
|
615
617
|
signal: res.locals.signal,
|
|
618
|
+
requestMetrics: res.locals.requestMetrics,
|
|
616
619
|
params: req.params,
|
|
617
620
|
query: req.query,
|
|
618
621
|
});
|
|
@@ -645,6 +648,7 @@ class Handler {
|
|
|
645
648
|
secrets: res.locals.secrets,
|
|
646
649
|
logger: res.locals.logger,
|
|
647
650
|
signal: res.locals.signal,
|
|
651
|
+
requestMetrics: res.locals.requestMetrics,
|
|
648
652
|
params: req.params,
|
|
649
653
|
query: req.query,
|
|
650
654
|
});
|
|
@@ -665,6 +669,7 @@ class Handler {
|
|
|
665
669
|
body: req.body,
|
|
666
670
|
logger: res.locals.logger,
|
|
667
671
|
signal: res.locals.signal,
|
|
672
|
+
requestMetrics: res.locals.requestMetrics,
|
|
668
673
|
params: req.params,
|
|
669
674
|
query: req.query,
|
|
670
675
|
});
|
|
@@ -683,6 +688,7 @@ class Handler {
|
|
|
683
688
|
secrets: res.locals.secrets,
|
|
684
689
|
logger: res.locals.logger,
|
|
685
690
|
signal: res.locals.signal,
|
|
691
|
+
requestMetrics: res.locals.requestMetrics,
|
|
686
692
|
params: req.params,
|
|
687
693
|
query: req.query,
|
|
688
694
|
});
|
|
@@ -701,6 +707,7 @@ class Handler {
|
|
|
701
707
|
secrets: res.locals.secrets,
|
|
702
708
|
logger: res.locals.logger,
|
|
703
709
|
signal: res.locals.signal,
|
|
710
|
+
requestMetrics: res.locals.requestMetrics,
|
|
704
711
|
params: req.params,
|
|
705
712
|
query: req.query,
|
|
706
713
|
});
|
|
@@ -736,6 +743,7 @@ class Handler {
|
|
|
736
743
|
secrets: res.locals.secrets,
|
|
737
744
|
logger: res.locals.logger,
|
|
738
745
|
signal: res.locals.signal,
|
|
746
|
+
requestMetrics: res.locals.requestMetrics,
|
|
739
747
|
params: req.params,
|
|
740
748
|
query: req.query,
|
|
741
749
|
});
|
|
@@ -751,6 +759,7 @@ class Handler {
|
|
|
751
759
|
secrets: res.locals.secrets,
|
|
752
760
|
logger: res.locals.logger,
|
|
753
761
|
signal: res.locals.signal,
|
|
762
|
+
requestMetrics: res.locals.requestMetrics,
|
|
754
763
|
params: req.params,
|
|
755
764
|
query: req.query,
|
|
756
765
|
body: req.body,
|
|
@@ -767,6 +776,7 @@ class Handler {
|
|
|
767
776
|
secrets: res.locals.secrets,
|
|
768
777
|
logger: res.locals.logger,
|
|
769
778
|
signal: res.locals.signal,
|
|
779
|
+
requestMetrics: res.locals.requestMetrics,
|
|
770
780
|
params: req.params,
|
|
771
781
|
query: req.query,
|
|
772
782
|
body: req.body,
|
|
@@ -788,6 +798,7 @@ class Handler {
|
|
|
788
798
|
body: req.body,
|
|
789
799
|
logger: res.locals.logger,
|
|
790
800
|
signal: res.locals.signal,
|
|
801
|
+
requestMetrics: res.locals.requestMetrics,
|
|
791
802
|
params: req.params,
|
|
792
803
|
query: req.query,
|
|
793
804
|
});
|
|
@@ -887,11 +898,14 @@ function onFinish(req, res, next) {
|
|
|
887
898
|
res.on('finish', function () {
|
|
888
899
|
const logger = res.locals.logger ?? new Logger();
|
|
889
900
|
const error = res.locals.error;
|
|
890
|
-
const
|
|
901
|
+
const endMetrics = res.locals.requestMetrics.endRequest();
|
|
902
|
+
const durationInNs = Number(endMetrics.durationNs);
|
|
891
903
|
const durationInMs = (durationInNs / 1_000_000) | 0;
|
|
892
904
|
const message = `${req.method} ${req.originalUrl} ${res.statusCode} - ${durationInMs} ms`;
|
|
893
905
|
const metadata = {
|
|
894
906
|
duration: durationInNs,
|
|
907
|
+
externalApiCount: endMetrics.apiCallCount,
|
|
908
|
+
externalApiTotalDuration: endMetrics.totalApiDurationNs,
|
|
895
909
|
// Use reserved and standard attributes of Datadog
|
|
896
910
|
// https://app.datadoghq.com/logs/pipelines/standard-attributes
|
|
897
911
|
http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
|
|
@@ -944,8 +958,42 @@ function injectLogger(req, res, next) {
|
|
|
944
958
|
next();
|
|
945
959
|
}
|
|
946
960
|
|
|
961
|
+
/**
|
|
962
|
+
* Accumulates per-request metrics for Provider API calls.
|
|
963
|
+
*
|
|
964
|
+
* Created once per incoming request (in the start middleware), threaded through Context and RequestOptions,
|
|
965
|
+
* incremented by Provider on each API call, and logged by the finish middleware.
|
|
966
|
+
*/
|
|
967
|
+
class RequestMetrics {
|
|
968
|
+
_apiCallCount = 0;
|
|
969
|
+
_totalApiDurationNs = 0;
|
|
970
|
+
_requestStartTime;
|
|
971
|
+
constructor() {
|
|
972
|
+
this._requestStartTime = process.hrtime.bigint();
|
|
973
|
+
}
|
|
974
|
+
static startRequest() {
|
|
975
|
+
return new RequestMetrics();
|
|
976
|
+
}
|
|
977
|
+
endRequest() {
|
|
978
|
+
return {
|
|
979
|
+
durationNs: process.hrtime.bigint() - this._requestStartTime,
|
|
980
|
+
apiCallCount: this._apiCallCount,
|
|
981
|
+
totalApiDurationNs: this._totalApiDurationNs,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Record a completed API call.
|
|
986
|
+
* Computes the duration from the given start timestamp, increments the call counter,
|
|
987
|
+
* and accumulates the duration.
|
|
988
|
+
*/
|
|
989
|
+
recordExternalApiCall(startTime) {
|
|
990
|
+
this._apiCallCount++;
|
|
991
|
+
this._totalApiDurationNs += Number(process.hrtime.bigint() - startTime);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
947
995
|
function start(_, res, next) {
|
|
948
|
-
res.locals.
|
|
996
|
+
res.locals.requestMetrics = RequestMetrics.startRequest();
|
|
949
997
|
next();
|
|
950
998
|
}
|
|
951
999
|
|
|
@@ -1352,7 +1400,7 @@ class Provider {
|
|
|
1352
1400
|
}
|
|
1353
1401
|
});
|
|
1354
1402
|
};
|
|
1355
|
-
return this.
|
|
1403
|
+
return this.timedCallToProvider(callToProvider, options);
|
|
1356
1404
|
}
|
|
1357
1405
|
/**
|
|
1358
1406
|
* Performs a POST request to the provider streaming a Readable directly without loading it into memory.
|
|
@@ -1462,7 +1510,7 @@ class Provider {
|
|
|
1462
1510
|
}
|
|
1463
1511
|
});
|
|
1464
1512
|
};
|
|
1465
|
-
return this.
|
|
1513
|
+
return this.timedCallToProvider(callToProvider, options);
|
|
1466
1514
|
}
|
|
1467
1515
|
/**
|
|
1468
1516
|
* Performs a PUT request to the provider.
|
|
@@ -1702,7 +1750,7 @@ class Provider {
|
|
|
1702
1750
|
}
|
|
1703
1751
|
return { status: response.status, headers: Object.fromEntries(response.headers.entries()), body };
|
|
1704
1752
|
};
|
|
1705
|
-
return this.
|
|
1753
|
+
return this.timedCallToProvider(callToProvider, options);
|
|
1706
1754
|
}
|
|
1707
1755
|
/**
|
|
1708
1756
|
* Normalizes Node.js IncomingHttpHeaders (which may have undefined or string[] values)
|
|
@@ -1713,6 +1761,18 @@ class Provider {
|
|
|
1713
1761
|
.filter((entry) => entry[1] !== undefined)
|
|
1714
1762
|
.map(([key, value]) => [key, Array.isArray(value) ? value.join(',') : value]));
|
|
1715
1763
|
}
|
|
1764
|
+
async timedCallToProvider(callToProvider, options) {
|
|
1765
|
+
const timedCall = async () => {
|
|
1766
|
+
const start = process.hrtime.bigint();
|
|
1767
|
+
try {
|
|
1768
|
+
return await callToProvider();
|
|
1769
|
+
}
|
|
1770
|
+
finally {
|
|
1771
|
+
options.requestMetrics?.recordExternalApiCall(start);
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
return this.rateLimiter ? this.rateLimiter(options, timedCall) : timedCall();
|
|
1775
|
+
}
|
|
1716
1776
|
handleError(responseStatus, message, options) {
|
|
1717
1777
|
const customError = this.customErrorHandler?.(responseStatus, message, options);
|
|
1718
1778
|
return customError ?? buildHttpError(responseStatus, message);
|
|
@@ -1775,5 +1835,6 @@ exports.HttpErrors = httpErrors;
|
|
|
1775
1835
|
exports.Integration = Integration;
|
|
1776
1836
|
exports.NULL_LOGGER = NULL_LOGGER;
|
|
1777
1837
|
exports.Provider = Provider;
|
|
1838
|
+
exports.RequestMetrics = RequestMetrics;
|
|
1778
1839
|
exports.buildCollectionQueryParams = buildCollectionQueryParams;
|
|
1779
1840
|
exports.getApplicableFilters = getApplicableFilters;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { Cache, type CacheInstance } from './resources/cache.js';
|
|
|
3
3
|
export { default as Integration } from './integration.js';
|
|
4
4
|
export * from './handler.js';
|
|
5
5
|
export { Provider, type Response as ProviderResponse, type RequestOptions as ProviderRequestOptions, type RateLimiter, } from './resources/provider.js';
|
|
6
|
+
export { RequestMetrics } from './resources/requestMetrics.js';
|
|
6
7
|
export type { Secrets } from './middlewares/secrets.js';
|
|
7
8
|
export type { Credentials } from './middlewares/credentials.js';
|
|
8
9
|
export type { Filter } from './middlewares/filters.js';
|
package/dist/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { Cache } from './resources/cache.js';
|
|
|
4
4
|
export { default as Integration } from './integration.js';
|
|
5
5
|
export * from './handler.js';
|
|
6
6
|
export { Provider, } from './resources/provider.js';
|
|
7
|
+
export { RequestMetrics } from './resources/requestMetrics.js';
|
|
7
8
|
export * as HttpErrors from './httpErrors.js';
|
|
8
9
|
export { buildCollectionQueryParams, getApplicableFilters } from './helpers.js';
|
|
9
10
|
export * from './resources/context.js';
|
|
@@ -3,11 +3,14 @@ function onFinish(req, res, next) {
|
|
|
3
3
|
res.on('finish', function () {
|
|
4
4
|
const logger = res.locals.logger ?? new Logger();
|
|
5
5
|
const error = res.locals.error;
|
|
6
|
-
const
|
|
6
|
+
const endMetrics = res.locals.requestMetrics.endRequest();
|
|
7
|
+
const durationInNs = Number(endMetrics.durationNs);
|
|
7
8
|
const durationInMs = (durationInNs / 1_000_000) | 0;
|
|
8
9
|
const message = `${req.method} ${req.originalUrl} ${res.statusCode} - ${durationInMs} ms`;
|
|
9
10
|
const metadata = {
|
|
10
11
|
duration: durationInNs,
|
|
12
|
+
externalApiCount: endMetrics.apiCallCount,
|
|
13
|
+
externalApiTotalDuration: endMetrics.totalApiDurationNs,
|
|
11
14
|
// Use reserved and standard attributes of Datadog
|
|
12
15
|
// https://app.datadoghq.com/logs/pipelines/standard-attributes
|
|
13
16
|
http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as API from '@unito/integration-api';
|
|
2
2
|
import Logger from './logger.js';
|
|
3
|
+
import { RequestMetrics } from './requestMetrics.js';
|
|
3
4
|
import { Credentials } from '../middlewares/credentials.js';
|
|
4
5
|
import { Secrets } from '../middlewares/secrets.js';
|
|
5
6
|
import { Filter } from '../middlewares/filters.js';
|
|
@@ -40,6 +41,11 @@ export type Context<P extends Maybe<Params> = Maybe<Params>, Q extends Maybe<Que
|
|
|
40
41
|
* to be timed out. You can use this signal to abort any operation that would exceed this time frame.
|
|
41
42
|
*/
|
|
42
43
|
signal: AbortSignal | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Per-request metrics accumulator for Provider API calls.
|
|
46
|
+
* Automatically populated by the SDK middleware and used by Provider to track call count and duration.
|
|
47
|
+
*/
|
|
48
|
+
requestMetrics: RequestMetrics | undefined;
|
|
43
49
|
/**
|
|
44
50
|
* The request params.
|
|
45
51
|
*
|
|
@@ -3,6 +3,7 @@ import * as stream from 'stream';
|
|
|
3
3
|
import * as HttpErrors from '../httpErrors.js';
|
|
4
4
|
import { Credentials } from '../middlewares/credentials.js';
|
|
5
5
|
import Logger from '../resources/logger.js';
|
|
6
|
+
import { RequestMetrics } from './requestMetrics.js';
|
|
6
7
|
/**
|
|
7
8
|
* RateLimiter is a wrapper function that you can provide to limit the rate of calls to the provider based on the
|
|
8
9
|
* caller's credentials.
|
|
@@ -42,6 +43,7 @@ export type RequestOptions = {
|
|
|
42
43
|
[key: string]: string;
|
|
43
44
|
};
|
|
44
45
|
rawBody?: boolean;
|
|
46
|
+
requestMetrics?: RequestMetrics;
|
|
45
47
|
};
|
|
46
48
|
/**
|
|
47
49
|
* Response object returned by the Provider's method.
|
|
@@ -226,5 +228,6 @@ export declare class Provider {
|
|
|
226
228
|
* into a flat Record<string, string> by joining array values and dropping undefined entries.
|
|
227
229
|
*/
|
|
228
230
|
private static normalizeNodeHeaders;
|
|
231
|
+
private timedCallToProvider;
|
|
229
232
|
private handleError;
|
|
230
233
|
}
|
|
@@ -181,7 +181,7 @@ export class Provider {
|
|
|
181
181
|
}
|
|
182
182
|
});
|
|
183
183
|
};
|
|
184
|
-
return this.
|
|
184
|
+
return this.timedCallToProvider(callToProvider, options);
|
|
185
185
|
}
|
|
186
186
|
/**
|
|
187
187
|
* Performs a POST request to the provider streaming a Readable directly without loading it into memory.
|
|
@@ -291,7 +291,7 @@ export class Provider {
|
|
|
291
291
|
}
|
|
292
292
|
});
|
|
293
293
|
};
|
|
294
|
-
return this.
|
|
294
|
+
return this.timedCallToProvider(callToProvider, options);
|
|
295
295
|
}
|
|
296
296
|
/**
|
|
297
297
|
* Performs a PUT request to the provider.
|
|
@@ -531,7 +531,7 @@ export class Provider {
|
|
|
531
531
|
}
|
|
532
532
|
return { status: response.status, headers: Object.fromEntries(response.headers.entries()), body };
|
|
533
533
|
};
|
|
534
|
-
return this.
|
|
534
|
+
return this.timedCallToProvider(callToProvider, options);
|
|
535
535
|
}
|
|
536
536
|
/**
|
|
537
537
|
* Normalizes Node.js IncomingHttpHeaders (which may have undefined or string[] values)
|
|
@@ -542,6 +542,18 @@ export class Provider {
|
|
|
542
542
|
.filter((entry) => entry[1] !== undefined)
|
|
543
543
|
.map(([key, value]) => [key, Array.isArray(value) ? value.join(',') : value]));
|
|
544
544
|
}
|
|
545
|
+
async timedCallToProvider(callToProvider, options) {
|
|
546
|
+
const timedCall = async () => {
|
|
547
|
+
const start = process.hrtime.bigint();
|
|
548
|
+
try {
|
|
549
|
+
return await callToProvider();
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
options.requestMetrics?.recordExternalApiCall(start);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
return this.rateLimiter ? this.rateLimiter(options, timedCall) : timedCall();
|
|
556
|
+
}
|
|
545
557
|
handleError(responseStatus, message, options) {
|
|
546
558
|
const customError = this.customErrorHandler?.(responseStatus, message, options);
|
|
547
559
|
return customError ?? buildHttpError(responseStatus, message);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accumulates per-request metrics for Provider API calls.
|
|
3
|
+
*
|
|
4
|
+
* Created once per incoming request (in the start middleware), threaded through Context and RequestOptions,
|
|
5
|
+
* incremented by Provider on each API call, and logged by the finish middleware.
|
|
6
|
+
*/
|
|
7
|
+
export declare class RequestMetrics {
|
|
8
|
+
private _apiCallCount;
|
|
9
|
+
private _totalApiDurationNs;
|
|
10
|
+
private _requestStartTime;
|
|
11
|
+
private constructor();
|
|
12
|
+
static startRequest(): RequestMetrics;
|
|
13
|
+
endRequest(): {
|
|
14
|
+
durationNs: bigint;
|
|
15
|
+
apiCallCount: number;
|
|
16
|
+
totalApiDurationNs: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Record a completed API call.
|
|
20
|
+
* Computes the duration from the given start timestamp, increments the call counter,
|
|
21
|
+
* and accumulates the duration.
|
|
22
|
+
*/
|
|
23
|
+
recordExternalApiCall(startTime: bigint): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accumulates per-request metrics for Provider API calls.
|
|
3
|
+
*
|
|
4
|
+
* Created once per incoming request (in the start middleware), threaded through Context and RequestOptions,
|
|
5
|
+
* incremented by Provider on each API call, and logged by the finish middleware.
|
|
6
|
+
*/
|
|
7
|
+
export class RequestMetrics {
|
|
8
|
+
_apiCallCount = 0;
|
|
9
|
+
_totalApiDurationNs = 0;
|
|
10
|
+
_requestStartTime;
|
|
11
|
+
constructor() {
|
|
12
|
+
this._requestStartTime = process.hrtime.bigint();
|
|
13
|
+
}
|
|
14
|
+
static startRequest() {
|
|
15
|
+
return new RequestMetrics();
|
|
16
|
+
}
|
|
17
|
+
endRequest() {
|
|
18
|
+
return {
|
|
19
|
+
durationNs: process.hrtime.bigint() - this._requestStartTime,
|
|
20
|
+
apiCallCount: this._apiCallCount,
|
|
21
|
+
totalApiDurationNs: this._totalApiDurationNs,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Record a completed API call.
|
|
26
|
+
* Computes the duration from the given start timestamp, increments the call counter,
|
|
27
|
+
* and accumulates the duration.
|
|
28
|
+
*/
|
|
29
|
+
recordExternalApiCall(startTime) {
|
|
30
|
+
this._apiCallCount++;
|
|
31
|
+
this._totalApiDurationNs += Number(process.hrtime.bigint() - startTime);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import onFinish from '../../src/middlewares/finish.js';
|
|
4
|
+
import { RequestMetrics } from '../../src/resources/requestMetrics.js';
|
|
4
5
|
describe('finish middleware', () => {
|
|
5
6
|
it('logs info', () => {
|
|
6
7
|
let expected = '';
|
|
@@ -11,7 +12,7 @@ describe('finish middleware', () => {
|
|
|
11
12
|
eventHandler = func;
|
|
12
13
|
},
|
|
13
14
|
locals: {
|
|
14
|
-
|
|
15
|
+
requestMetrics: RequestMetrics.startRequest(),
|
|
15
16
|
logger: {
|
|
16
17
|
info: (_message) => {
|
|
17
18
|
expected = 'works!';
|
|
@@ -33,7 +34,7 @@ describe('finish middleware', () => {
|
|
|
33
34
|
eventHandler = func;
|
|
34
35
|
},
|
|
35
36
|
locals: {
|
|
36
|
-
|
|
37
|
+
requestMetrics: RequestMetrics.startRequest(),
|
|
37
38
|
logger: {
|
|
38
39
|
error: (_message) => {
|
|
39
40
|
expected = 'works!';
|
|
@@ -55,7 +56,7 @@ describe('finish middleware', () => {
|
|
|
55
56
|
eventHandler = func;
|
|
56
57
|
},
|
|
57
58
|
locals: {
|
|
58
|
-
|
|
59
|
+
requestMetrics: RequestMetrics.startRequest(),
|
|
59
60
|
logger: {
|
|
60
61
|
info: (_message) => {
|
|
61
62
|
expected = 'ohoh!';
|
|
@@ -77,7 +78,7 @@ describe('finish middleware', () => {
|
|
|
77
78
|
eventHandler = func;
|
|
78
79
|
},
|
|
79
80
|
locals: {
|
|
80
|
-
|
|
81
|
+
requestMetrics: RequestMetrics.startRequest(),
|
|
81
82
|
logger: {
|
|
82
83
|
error: (_message) => {
|
|
83
84
|
expected = 'works!';
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import start from '../../src/middlewares/start.js';
|
|
4
|
+
import { RequestMetrics } from '../../src/resources/requestMetrics.js';
|
|
4
5
|
describe('start middleware', () => {
|
|
5
|
-
it('
|
|
6
|
+
it('initializes request metrics on locals', () => {
|
|
6
7
|
const request = {};
|
|
7
8
|
const response = { locals: {} };
|
|
8
9
|
start(request, response, () => { });
|
|
9
|
-
assert.ok(response.locals.
|
|
10
|
+
assert.ok(response.locals.requestMetrics instanceof RequestMetrics);
|
|
11
|
+
});
|
|
12
|
+
it('records request start time in metrics', () => {
|
|
13
|
+
const request = {};
|
|
14
|
+
const response = { locals: {} };
|
|
15
|
+
const before = process.hrtime.bigint();
|
|
16
|
+
start(request, response, () => { });
|
|
17
|
+
const result = response.locals.requestMetrics.endRequest();
|
|
18
|
+
const after = process.hrtime.bigint();
|
|
19
|
+
assert.ok(result.durationNs >= 0n);
|
|
20
|
+
assert.ok(result.durationNs <= after - before);
|
|
10
21
|
});
|
|
11
22
|
});
|
|
@@ -7,6 +7,7 @@ import https from 'https';
|
|
|
7
7
|
import { Provider } from '../../src/resources/provider.js';
|
|
8
8
|
import * as HttpErrors from '../../src/httpErrors.js';
|
|
9
9
|
import Logger from '../../src/resources/logger.js';
|
|
10
|
+
import { RequestMetrics } from '../../src/resources/requestMetrics.js';
|
|
10
11
|
// There is currently an issue with node 20.12 and fetch mocking. A quick fix is to first call fetch so it's getter
|
|
11
12
|
// get properly instantiated, which allow it to be mocked properly.
|
|
12
13
|
// Issue: https://github.com/nodejs/node/issues/52015
|
|
@@ -1336,4 +1337,98 @@ describe('Provider', () => {
|
|
|
1336
1337
|
}
|
|
1337
1338
|
});
|
|
1338
1339
|
});
|
|
1340
|
+
describe('request metrics', () => {
|
|
1341
|
+
it('records api call duration in requestMetrics via fetchWrapper', async (context) => {
|
|
1342
|
+
const metrics = RequestMetrics.startRequest();
|
|
1343
|
+
const response = new Response('{"data": "value"}', {
|
|
1344
|
+
status: 200,
|
|
1345
|
+
headers: new Headers({ 'Content-Type': 'application/json' }),
|
|
1346
|
+
});
|
|
1347
|
+
context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
1348
|
+
await provider.get('/endpoint', {
|
|
1349
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1350
|
+
logger,
|
|
1351
|
+
requestMetrics: metrics,
|
|
1352
|
+
});
|
|
1353
|
+
const result = metrics.endRequest();
|
|
1354
|
+
assert.equal(result.apiCallCount, 1);
|
|
1355
|
+
assert.ok(result.totalApiDurationNs >= 0, 'Duration should be recorded');
|
|
1356
|
+
});
|
|
1357
|
+
it('records multiple api calls in the same requestMetrics', async (context) => {
|
|
1358
|
+
const metrics = RequestMetrics.startRequest();
|
|
1359
|
+
context.mock.method(global, 'fetch', () => Promise.resolve(new Response('{"data": "value"}', {
|
|
1360
|
+
status: 200,
|
|
1361
|
+
headers: new Headers({ 'Content-Type': 'application/json' }),
|
|
1362
|
+
})));
|
|
1363
|
+
await provider.get('/endpoint1', {
|
|
1364
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1365
|
+
logger,
|
|
1366
|
+
requestMetrics: metrics,
|
|
1367
|
+
});
|
|
1368
|
+
await provider.post('/endpoint2', { key: 'value' }, {
|
|
1369
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1370
|
+
logger,
|
|
1371
|
+
requestMetrics: metrics,
|
|
1372
|
+
});
|
|
1373
|
+
const result = metrics.endRequest();
|
|
1374
|
+
assert.equal(result.apiCallCount, 2);
|
|
1375
|
+
assert.ok(result.totalApiDurationNs >= 0);
|
|
1376
|
+
});
|
|
1377
|
+
it('works without requestMetrics (backward compatible)', async (context) => {
|
|
1378
|
+
const response = new Response('{"data": "value"}', {
|
|
1379
|
+
status: 200,
|
|
1380
|
+
headers: new Headers({ 'Content-Type': 'application/json' }),
|
|
1381
|
+
});
|
|
1382
|
+
context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
1383
|
+
// No requestMetrics passed — should not throw
|
|
1384
|
+
const result = await provider.get('/endpoint', {
|
|
1385
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1386
|
+
logger,
|
|
1387
|
+
});
|
|
1388
|
+
assert.equal(result.status, 200);
|
|
1389
|
+
});
|
|
1390
|
+
it('records api call duration from postForm', async () => {
|
|
1391
|
+
const metrics = RequestMetrics.startRequest();
|
|
1392
|
+
const httpsProvider = new Provider({
|
|
1393
|
+
prepareRequest: () => ({
|
|
1394
|
+
url: 'https://www.myApi.com',
|
|
1395
|
+
headers: {},
|
|
1396
|
+
}),
|
|
1397
|
+
});
|
|
1398
|
+
const scope = nock('https://www.myApi.com').post('/upload').reply(201, { success: true });
|
|
1399
|
+
const FormData = (await import('form-data')).default;
|
|
1400
|
+
const form = new FormData();
|
|
1401
|
+
form.append('file', Buffer.from('test data'), 'test.txt');
|
|
1402
|
+
await httpsProvider.postForm('/upload', form, {
|
|
1403
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1404
|
+
logger,
|
|
1405
|
+
requestMetrics: metrics,
|
|
1406
|
+
});
|
|
1407
|
+
const postFormResult = metrics.endRequest();
|
|
1408
|
+
assert.equal(postFormResult.apiCallCount, 1);
|
|
1409
|
+
assert.ok(postFormResult.totalApiDurationNs > 0);
|
|
1410
|
+
scope.isDone();
|
|
1411
|
+
});
|
|
1412
|
+
it('records api call duration from postStream', async () => {
|
|
1413
|
+
const metrics = RequestMetrics.startRequest();
|
|
1414
|
+
const testData = 'binary stream data';
|
|
1415
|
+
const httpsProvider = new Provider({
|
|
1416
|
+
prepareRequest: () => ({
|
|
1417
|
+
url: 'https://www.myApi.com',
|
|
1418
|
+
headers: {},
|
|
1419
|
+
}),
|
|
1420
|
+
});
|
|
1421
|
+
const scope = nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
|
|
1422
|
+
const stream = Readable.from(Buffer.from(testData));
|
|
1423
|
+
await httpsProvider.postStream('/upload', stream, {
|
|
1424
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1425
|
+
logger,
|
|
1426
|
+
requestMetrics: metrics,
|
|
1427
|
+
});
|
|
1428
|
+
const postStreamResult = metrics.endRequest();
|
|
1429
|
+
assert.equal(postStreamResult.apiCallCount, 1);
|
|
1430
|
+
assert.ok(postStreamResult.totalApiDurationNs > 0);
|
|
1431
|
+
scope.isDone();
|
|
1432
|
+
});
|
|
1433
|
+
});
|
|
1339
1434
|
});
|