@unito/integration-sdk 5.0.0 → 5.1.1
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 +114 -5
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/integration.d.ts +1 -0
- package/dist/src/integration.js +2 -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/src/resources/tracer.d.ts +16 -0
- package/dist/src/resources/tracer.js +45 -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 +2 -1
- package/src/handler.ts +11 -0
- package/src/index.ts +2 -0
- package/src/integration.ts +3 -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/src/resources/tracer.ts +50 -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
|
@@ -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
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { RequestMetrics } from '../../src/resources/requestMetrics.js';
|
|
4
|
+
describe('RequestMetrics', () => {
|
|
5
|
+
describe('initial state', () => {
|
|
6
|
+
it('starts with zero api call count', () => {
|
|
7
|
+
const metrics = RequestMetrics.startRequest();
|
|
8
|
+
const result = metrics.endRequest();
|
|
9
|
+
assert.equal(result.apiCallCount, 0);
|
|
10
|
+
});
|
|
11
|
+
it('starts with zero total api duration', () => {
|
|
12
|
+
const metrics = RequestMetrics.startRequest();
|
|
13
|
+
const result = metrics.endRequest();
|
|
14
|
+
assert.equal(result.totalApiDurationNs, 0);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('endRequest', () => {
|
|
18
|
+
it('returns elapsed duration since construction', () => {
|
|
19
|
+
const before = process.hrtime.bigint();
|
|
20
|
+
const metrics = RequestMetrics.startRequest();
|
|
21
|
+
const result = metrics.endRequest();
|
|
22
|
+
const after = process.hrtime.bigint();
|
|
23
|
+
assert.ok(result.durationNs >= 0n);
|
|
24
|
+
assert.ok(result.durationNs <= after - before);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe('recordExternalApiCall', () => {
|
|
28
|
+
it('increments api call count', () => {
|
|
29
|
+
const metrics = RequestMetrics.startRequest();
|
|
30
|
+
const start1 = process.hrtime.bigint();
|
|
31
|
+
metrics.recordExternalApiCall(start1);
|
|
32
|
+
const start2 = process.hrtime.bigint();
|
|
33
|
+
metrics.recordExternalApiCall(start2);
|
|
34
|
+
const result = metrics.endRequest();
|
|
35
|
+
assert.equal(result.apiCallCount, 2);
|
|
36
|
+
});
|
|
37
|
+
it('accumulates api duration', () => {
|
|
38
|
+
const metrics = RequestMetrics.startRequest();
|
|
39
|
+
const start = process.hrtime.bigint();
|
|
40
|
+
metrics.recordExternalApiCall(start);
|
|
41
|
+
const result = metrics.endRequest();
|
|
42
|
+
assert.ok(result.totalApiDurationNs >= 0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../src/errors.ts","../src/handler.ts","../src/helpers.ts","../src/httpErrors.ts","../src/index.ts","../src/integration.ts","../src/middlewares/correlationId.ts","../src/middlewares/credentials.ts","../src/middlewares/errors.ts","../src/middlewares/filters.ts","../src/middlewares/finish.ts","../src/middlewares/health.ts","../src/middlewares/logger.ts","../src/middlewares/notFound.ts","../src/middlewares/relations.ts","../src/middlewares/search.ts","../src/middlewares/secrets.ts","../src/middlewares/selects.ts","../src/middlewares/signal.ts","../src/middlewares/start.ts","../src/resources/cache.ts","../src/resources/context.ts","../src/resources/logger.ts","../src/resources/provider.ts","../test/errors.test.ts","../test/handler.test.ts","../test/helpers.test.ts","../test/integration.test.ts","../test/middlewares/correlationId.test.ts","../test/middlewares/credentials.test.ts","../test/middlewares/errors.test.ts","../test/middlewares/filters.test.ts","../test/middlewares/finish.test.ts","../test/middlewares/health.test.ts","../test/middlewares/logger.test.ts","../test/middlewares/notFound.test.ts","../test/middlewares/relations.test.ts","../test/middlewares/search.test.ts","../test/middlewares/secrets.test.ts","../test/middlewares/selects.test.ts","../test/middlewares/signal.test.ts","../test/middlewares/start.test.ts","../test/resources/cache.test.ts","../test/resources/logger.test.ts","../test/resources/provider.test.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["../src/errors.ts","../src/handler.ts","../src/helpers.ts","../src/httpErrors.ts","../src/index.ts","../src/integration.ts","../src/middlewares/correlationId.ts","../src/middlewares/credentials.ts","../src/middlewares/errors.ts","../src/middlewares/filters.ts","../src/middlewares/finish.ts","../src/middlewares/health.ts","../src/middlewares/logger.ts","../src/middlewares/notFound.ts","../src/middlewares/relations.ts","../src/middlewares/search.ts","../src/middlewares/secrets.ts","../src/middlewares/selects.ts","../src/middlewares/signal.ts","../src/middlewares/start.ts","../src/resources/cache.ts","../src/resources/context.ts","../src/resources/logger.ts","../src/resources/provider.ts","../src/resources/requestMetrics.ts","../src/resources/tracer.ts","../test/errors.test.ts","../test/handler.test.ts","../test/helpers.test.ts","../test/integration.test.ts","../test/middlewares/correlationId.test.ts","../test/middlewares/credentials.test.ts","../test/middlewares/errors.test.ts","../test/middlewares/filters.test.ts","../test/middlewares/finish.test.ts","../test/middlewares/health.test.ts","../test/middlewares/logger.test.ts","../test/middlewares/notFound.test.ts","../test/middlewares/relations.test.ts","../test/middlewares/search.test.ts","../test/middlewares/secrets.test.ts","../test/middlewares/selects.test.ts","../test/middlewares/signal.test.ts","../test/middlewares/start.test.ts","../test/resources/cache.test.ts","../test/resources/logger.test.ts","../test/resources/provider.test.ts","../test/resources/requestMetrics.test.ts"],"version":"5.9.3"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unito/integration-sdk",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.1",
|
|
4
4
|
"description": "Integration SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"@unito/integration-api": "7.x",
|
|
57
57
|
"busboy": "^1.6.0",
|
|
58
58
|
"cachette": "4.x",
|
|
59
|
+
"dd-trace": "5.x",
|
|
59
60
|
"express": "^5.1",
|
|
60
61
|
"form-data": "^4.0.0"
|
|
61
62
|
},
|
package/src/handler.ts
CHANGED
|
@@ -308,6 +308,7 @@ export class Handler {
|
|
|
308
308
|
filters: res.locals.filters,
|
|
309
309
|
logger: res.locals.logger,
|
|
310
310
|
signal: res.locals.signal,
|
|
311
|
+
requestMetrics: res.locals.requestMetrics,
|
|
311
312
|
params: req.params,
|
|
312
313
|
query: req.query,
|
|
313
314
|
relations: res.locals.relations,
|
|
@@ -335,6 +336,7 @@ export class Handler {
|
|
|
335
336
|
body: req.body,
|
|
336
337
|
logger: res.locals.logger,
|
|
337
338
|
signal: res.locals.signal,
|
|
339
|
+
requestMetrics: res.locals.requestMetrics,
|
|
338
340
|
params: req.params,
|
|
339
341
|
query: req.query,
|
|
340
342
|
});
|
|
@@ -383,6 +385,7 @@ export class Handler {
|
|
|
383
385
|
body,
|
|
384
386
|
logger: res.locals.logger,
|
|
385
387
|
signal: res.locals.signal,
|
|
388
|
+
requestMetrics: res.locals.requestMetrics,
|
|
386
389
|
params: req.params,
|
|
387
390
|
query: req.query,
|
|
388
391
|
});
|
|
@@ -417,6 +420,7 @@ export class Handler {
|
|
|
417
420
|
secrets: res.locals.secrets,
|
|
418
421
|
logger: res.locals.logger,
|
|
419
422
|
signal: res.locals.signal,
|
|
423
|
+
requestMetrics: res.locals.requestMetrics,
|
|
420
424
|
params: req.params,
|
|
421
425
|
query: req.query,
|
|
422
426
|
});
|
|
@@ -443,6 +447,7 @@ export class Handler {
|
|
|
443
447
|
body: req.body,
|
|
444
448
|
logger: res.locals.logger,
|
|
445
449
|
signal: res.locals.signal,
|
|
450
|
+
requestMetrics: res.locals.requestMetrics,
|
|
446
451
|
params: req.params,
|
|
447
452
|
query: req.query,
|
|
448
453
|
});
|
|
@@ -466,6 +471,7 @@ export class Handler {
|
|
|
466
471
|
secrets: res.locals.secrets,
|
|
467
472
|
logger: res.locals.logger,
|
|
468
473
|
signal: res.locals.signal,
|
|
474
|
+
requestMetrics: res.locals.requestMetrics,
|
|
469
475
|
params: req.params,
|
|
470
476
|
query: req.query,
|
|
471
477
|
});
|
|
@@ -489,6 +495,7 @@ export class Handler {
|
|
|
489
495
|
secrets: res.locals.secrets,
|
|
490
496
|
logger: res.locals.logger,
|
|
491
497
|
signal: res.locals.signal,
|
|
498
|
+
requestMetrics: res.locals.requestMetrics,
|
|
492
499
|
params: req.params,
|
|
493
500
|
query: req.query,
|
|
494
501
|
});
|
|
@@ -533,6 +540,7 @@ export class Handler {
|
|
|
533
540
|
secrets: res.locals.secrets,
|
|
534
541
|
logger: res.locals.logger,
|
|
535
542
|
signal: res.locals.signal,
|
|
543
|
+
requestMetrics: res.locals.requestMetrics,
|
|
536
544
|
params: req.params,
|
|
537
545
|
query: req.query,
|
|
538
546
|
});
|
|
@@ -553,6 +561,7 @@ export class Handler {
|
|
|
553
561
|
secrets: res.locals.secrets,
|
|
554
562
|
logger: res.locals.logger,
|
|
555
563
|
signal: res.locals.signal,
|
|
564
|
+
requestMetrics: res.locals.requestMetrics,
|
|
556
565
|
params: req.params,
|
|
557
566
|
query: req.query,
|
|
558
567
|
body: req.body,
|
|
@@ -574,6 +583,7 @@ export class Handler {
|
|
|
574
583
|
secrets: res.locals.secrets,
|
|
575
584
|
logger: res.locals.logger,
|
|
576
585
|
signal: res.locals.signal,
|
|
586
|
+
requestMetrics: res.locals.requestMetrics,
|
|
577
587
|
params: req.params,
|
|
578
588
|
query: req.query,
|
|
579
589
|
body: req.body,
|
|
@@ -601,6 +611,7 @@ export class Handler {
|
|
|
601
611
|
body: req.body,
|
|
602
612
|
logger: res.locals.logger,
|
|
603
613
|
signal: res.locals.signal,
|
|
614
|
+
requestMetrics: res.locals.requestMetrics,
|
|
604
615
|
params: req.params,
|
|
605
616
|
query: req.query,
|
|
606
617
|
});
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export {
|
|
|
10
10
|
type RequestOptions as ProviderRequestOptions,
|
|
11
11
|
type RateLimiter,
|
|
12
12
|
} from './resources/provider.js';
|
|
13
|
+
export { RequestMetrics } from './resources/requestMetrics.js';
|
|
13
14
|
export type { Secrets } from './middlewares/secrets.js';
|
|
14
15
|
export type { Credentials } from './middlewares/credentials.js';
|
|
15
16
|
export type { Filter } from './middlewares/filters.js';
|
|
@@ -17,4 +18,5 @@ export * as HttpErrors from './httpErrors.js';
|
|
|
17
18
|
export { buildCollectionQueryParams, getApplicableFilters } from './helpers.js';
|
|
18
19
|
export * from './resources/context.js';
|
|
19
20
|
export { type default as Logger, NULL_LOGGER } from './resources/logger.js';
|
|
21
|
+
export { DD_TRACER } from './resources/tracer.js';
|
|
20
22
|
/* c8 ignore stop */
|
package/src/integration.ts
CHANGED
|
@@ -8,7 +8,6 @@ declare global {
|
|
|
8
8
|
interface Locals {
|
|
9
9
|
logger: Logger;
|
|
10
10
|
error?: ApiError;
|
|
11
|
-
requestStartTime: bigint;
|
|
12
11
|
}
|
|
13
12
|
}
|
|
14
13
|
}
|
|
@@ -18,12 +17,15 @@ function onFinish(req: Request, res: Response, next: NextFunction) {
|
|
|
18
17
|
const logger = res.locals.logger ?? new Logger();
|
|
19
18
|
const error = res.locals.error;
|
|
20
19
|
|
|
21
|
-
const
|
|
20
|
+
const endMetrics = res.locals.requestMetrics.endRequest();
|
|
21
|
+
const durationInNs = Number(endMetrics.durationNs);
|
|
22
22
|
const durationInMs = (durationInNs / 1_000_000) | 0;
|
|
23
23
|
|
|
24
24
|
const message = `${req.method} ${req.originalUrl} ${res.statusCode} - ${durationInMs} ms`;
|
|
25
25
|
const metadata: Metadata = {
|
|
26
26
|
duration: durationInNs,
|
|
27
|
+
externalApiCount: endMetrics.apiCallCount,
|
|
28
|
+
externalApiTotalDuration: endMetrics.totalApiDurationNs,
|
|
27
29
|
// Use reserved and standard attributes of Datadog
|
|
28
30
|
// https://app.datadoghq.com/logs/pipelines/standard-attributes
|
|
29
31
|
http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
|
package/src/middlewares/start.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { RequestMetrics } from '../resources/requestMetrics.js';
|
|
2
3
|
|
|
3
4
|
declare global {
|
|
4
5
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
5
6
|
namespace Express {
|
|
6
7
|
interface Locals {
|
|
7
|
-
|
|
8
|
+
requestMetrics: RequestMetrics;
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
function start(_: Request, res: Response, next: NextFunction) {
|
|
13
|
-
res.locals.
|
|
14
|
+
res.locals.requestMetrics = RequestMetrics.startRequest();
|
|
14
15
|
|
|
15
16
|
next();
|
|
16
17
|
}
|
package/src/resources/context.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* c8 ignore start */
|
|
2
2
|
import * as API from '@unito/integration-api';
|
|
3
3
|
import Logger from './logger.js';
|
|
4
|
+
import { RequestMetrics } from './requestMetrics.js';
|
|
4
5
|
import { Credentials } from '../middlewares/credentials.js';
|
|
5
6
|
import { Secrets } from '../middlewares/secrets.js';
|
|
6
7
|
import { Filter } from '../middlewares/filters.js';
|
|
@@ -43,6 +44,11 @@ export type Context<P extends Maybe<Params> = Maybe<Params>, Q extends Maybe<Que
|
|
|
43
44
|
* to be timed out. You can use this signal to abort any operation that would exceed this time frame.
|
|
44
45
|
*/
|
|
45
46
|
signal: AbortSignal | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Per-request metrics accumulator for Provider API calls.
|
|
49
|
+
* Automatically populated by the SDK middleware and used by Provider to track call count and duration.
|
|
50
|
+
*/
|
|
51
|
+
requestMetrics: RequestMetrics | undefined;
|
|
46
52
|
/**
|
|
47
53
|
* The request params.
|
|
48
54
|
*
|
|
@@ -7,6 +7,7 @@ import { buildHttpError } from '../errors.js';
|
|
|
7
7
|
import * as HttpErrors from '../httpErrors.js';
|
|
8
8
|
import { Credentials } from '../middlewares/credentials.js';
|
|
9
9
|
import Logger from '../resources/logger.js';
|
|
10
|
+
import { RequestMetrics } from './requestMetrics.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* RateLimiter is a wrapper function that you can provide to limit the rate of calls to the provider based on the
|
|
@@ -44,6 +45,7 @@ export type RequestOptions = {
|
|
|
44
45
|
queryParams?: { [key: string]: string };
|
|
45
46
|
additionnalheaders?: { [key: string]: string };
|
|
46
47
|
rawBody?: boolean;
|
|
48
|
+
requestMetrics?: RequestMetrics;
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
/**
|
|
@@ -278,7 +280,7 @@ export class Provider {
|
|
|
278
280
|
});
|
|
279
281
|
};
|
|
280
282
|
|
|
281
|
-
return this.
|
|
283
|
+
return this.timedCallToProvider(callToProvider, options);
|
|
282
284
|
}
|
|
283
285
|
|
|
284
286
|
/**
|
|
@@ -406,7 +408,7 @@ export class Provider {
|
|
|
406
408
|
});
|
|
407
409
|
};
|
|
408
410
|
|
|
409
|
-
return this.
|
|
411
|
+
return this.timedCallToProvider(callToProvider, options);
|
|
410
412
|
}
|
|
411
413
|
|
|
412
414
|
/**
|
|
@@ -687,7 +689,7 @@ export class Provider {
|
|
|
687
689
|
return { status: response.status, headers: Object.fromEntries(response.headers.entries()), body };
|
|
688
690
|
};
|
|
689
691
|
|
|
690
|
-
return this.
|
|
692
|
+
return this.timedCallToProvider(callToProvider, options);
|
|
691
693
|
}
|
|
692
694
|
|
|
693
695
|
/**
|
|
@@ -702,6 +704,21 @@ export class Provider {
|
|
|
702
704
|
);
|
|
703
705
|
}
|
|
704
706
|
|
|
707
|
+
private async timedCallToProvider<T>(
|
|
708
|
+
callToProvider: () => Promise<Response<T>>,
|
|
709
|
+
options: RequestOptions,
|
|
710
|
+
): Promise<Response<T>> {
|
|
711
|
+
const timedCall = async (): Promise<Response<T>> => {
|
|
712
|
+
const start = process.hrtime.bigint();
|
|
713
|
+
try {
|
|
714
|
+
return await callToProvider();
|
|
715
|
+
} finally {
|
|
716
|
+
options.requestMetrics?.recordExternalApiCall(start);
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
return this.rateLimiter ? this.rateLimiter(options, timedCall) : timedCall();
|
|
720
|
+
}
|
|
721
|
+
|
|
705
722
|
private handleError(responseStatus: number, message: string, options: RequestOptions): HttpErrors.HttpError {
|
|
706
723
|
const customError = this.customErrorHandler?.(responseStatus, message, options);
|
|
707
724
|
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
private _apiCallCount = 0;
|
|
9
|
+
private _totalApiDurationNs = 0;
|
|
10
|
+
private _requestStartTime: bigint;
|
|
11
|
+
|
|
12
|
+
private constructor() {
|
|
13
|
+
this._requestStartTime = process.hrtime.bigint();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static startRequest(): RequestMetrics {
|
|
17
|
+
return new RequestMetrics();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
endRequest(): { durationNs: bigint; apiCallCount: number; totalApiDurationNs: number } {
|
|
21
|
+
return {
|
|
22
|
+
durationNs: process.hrtime.bigint() - this._requestStartTime,
|
|
23
|
+
apiCallCount: this._apiCallCount,
|
|
24
|
+
totalApiDurationNs: this._totalApiDurationNs,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Record a completed API call.
|
|
30
|
+
* Computes the duration from the given start timestamp, increments the call counter,
|
|
31
|
+
* and accumulates the duration.
|
|
32
|
+
*/
|
|
33
|
+
recordExternalApiCall(startTime: bigint): void {
|
|
34
|
+
this._apiCallCount++;
|
|
35
|
+
this._totalApiDurationNs += Number(process.hrtime.bigint() - startTime);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import tracer, { TracerOptions } from 'dd-trace';
|
|
2
|
+
|
|
3
|
+
if (process.env.NODE_ENV === 'production' && process.env.DD_TRACE_ENABLED === 'true') {
|
|
4
|
+
// List of options available to the tracer: https://datadoghq.dev/dd-trace-js/interfaces/export_.TracerOptions.html
|
|
5
|
+
const apmConfig: TracerOptions = { logInjection: false, profiling: false, sampleRate: 0 };
|
|
6
|
+
|
|
7
|
+
if (process.env.DD_APM_ENABLED == 'true') {
|
|
8
|
+
apmConfig.logInjection = true;
|
|
9
|
+
apmConfig.sampleRate = 1;
|
|
10
|
+
} else {
|
|
11
|
+
// The "enable tracing" boolean is not exposed as part of the TracerOptions so we
|
|
12
|
+
// have to do so through env vars. Setting here instead of in services' config
|
|
13
|
+
// to avoid too many env vars to "flip" when enabling / disabling APM.
|
|
14
|
+
process.env.DD_TRACING_ENABLED = 'false';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Profiling is extra $$$ so we're setting it as an "extra" when needed
|
|
18
|
+
if (process.env.DD_APM_PROFILING_ENABLED == 'true') {
|
|
19
|
+
apmConfig.profiling = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const tags: { [key: string]: any } = {};
|
|
23
|
+
// Using DD_TRACE_AGENT_URL as a hack to know if we're running inside of Kubernetes
|
|
24
|
+
// since this variable is not needed in Elastic Beanstalk of Lambda
|
|
25
|
+
if (process.env.DD_TRACE_AGENT_URL) {
|
|
26
|
+
tags.pod_name = process.env.HOSTNAME;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
tracer.init({
|
|
30
|
+
...apmConfig, // Conditionally enable APM tracing & profiling
|
|
31
|
+
runtimeMetrics: true, // Always enable runtime metrics https://docs.datadoghq.com/tracing/metrics/runtime_metrics/nodejs/?tab=environmentvariables
|
|
32
|
+
tags,
|
|
33
|
+
}); // initialized in a different file to avoid hoisting.
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* WARNING to projects importing this
|
|
38
|
+
* Even if dd-tracer documents that instrumentation happens at **.init() time**
|
|
39
|
+
* (see https://docs.datadoghq.com/tracing/trace_collection/automatic_instrumentation/dd_libraries/nodejs/#import-and-initialize-the-tracer ),
|
|
40
|
+
* 1. dd-tracer has no choice but to do some stuff **at import time**
|
|
41
|
+
* 2. dd-tracer is not exempt from bugs, like https://github.com/DataDog/dd-trace-js/issues/5211
|
|
42
|
+
*
|
|
43
|
+
* So, do not assume that dd-trace is *fully off* by default! Some of it runs!
|
|
44
|
+
* If you want to entirely disable all of dd-trace (e.g. during unit tests),
|
|
45
|
+
* then you must set `DD_TRACE_ENABLED=false`
|
|
46
|
+
*
|
|
47
|
+
* Q: Why not move the dd-trace import inside the `if DD_TRACE_ENABLED` condition, then?
|
|
48
|
+
* A: Because the future is ESM, and ESM imports must be static and top-level.
|
|
49
|
+
*/
|
|
50
|
+
export const DD_TRACER = tracer;
|