@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
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
|
@@ -4,6 +4,7 @@ var integrationApi = require('@unito/integration-api');
|
|
|
4
4
|
var cachette = require('cachette');
|
|
5
5
|
var crypto = require('crypto');
|
|
6
6
|
var util = require('util');
|
|
7
|
+
var tracer = require('dd-trace');
|
|
7
8
|
var express = require('express');
|
|
8
9
|
var busboy = require('busboy');
|
|
9
10
|
var fs = require('fs');
|
|
@@ -299,6 +300,51 @@ function create(redisUrl) {
|
|
|
299
300
|
}
|
|
300
301
|
const Cache = { create };
|
|
301
302
|
|
|
303
|
+
if (process.env.NODE_ENV === 'production' && process.env.DD_TRACE_ENABLED === 'true') {
|
|
304
|
+
// List of options available to the tracer: https://datadoghq.dev/dd-trace-js/interfaces/export_.TracerOptions.html
|
|
305
|
+
const apmConfig = { logInjection: false, profiling: false, sampleRate: 0 };
|
|
306
|
+
if (process.env.DD_APM_ENABLED == 'true') {
|
|
307
|
+
apmConfig.logInjection = true;
|
|
308
|
+
apmConfig.sampleRate = 1;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
// The "enable tracing" boolean is not exposed as part of the TracerOptions so we
|
|
312
|
+
// have to do so through env vars. Setting here instead of in services' config
|
|
313
|
+
// to avoid too many env vars to "flip" when enabling / disabling APM.
|
|
314
|
+
process.env.DD_TRACING_ENABLED = 'false';
|
|
315
|
+
}
|
|
316
|
+
// Profiling is extra $$$ so we're setting it as an "extra" when needed
|
|
317
|
+
if (process.env.DD_APM_PROFILING_ENABLED == 'true') {
|
|
318
|
+
apmConfig.profiling = true;
|
|
319
|
+
}
|
|
320
|
+
const tags = {};
|
|
321
|
+
// Using DD_TRACE_AGENT_URL as a hack to know if we're running inside of Kubernetes
|
|
322
|
+
// since this variable is not needed in Elastic Beanstalk of Lambda
|
|
323
|
+
if (process.env.DD_TRACE_AGENT_URL) {
|
|
324
|
+
tags.pod_name = process.env.HOSTNAME;
|
|
325
|
+
}
|
|
326
|
+
tracer.init({
|
|
327
|
+
...apmConfig, // Conditionally enable APM tracing & profiling
|
|
328
|
+
runtimeMetrics: true, // Always enable runtime metrics https://docs.datadoghq.com/tracing/metrics/runtime_metrics/nodejs/?tab=environmentvariables
|
|
329
|
+
tags,
|
|
330
|
+
}); // initialized in a different file to avoid hoisting.
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* WARNING to projects importing this
|
|
334
|
+
* Even if dd-tracer documents that instrumentation happens at **.init() time**
|
|
335
|
+
* (see https://docs.datadoghq.com/tracing/trace_collection/automatic_instrumentation/dd_libraries/nodejs/#import-and-initialize-the-tracer ),
|
|
336
|
+
* 1. dd-tracer has no choice but to do some stuff **at import time**
|
|
337
|
+
* 2. dd-tracer is not exempt from bugs, like https://github.com/DataDog/dd-trace-js/issues/5211
|
|
338
|
+
*
|
|
339
|
+
* So, do not assume that dd-trace is *fully off* by default! Some of it runs!
|
|
340
|
+
* If you want to entirely disable all of dd-trace (e.g. during unit tests),
|
|
341
|
+
* then you must set `DD_TRACE_ENABLED=false`
|
|
342
|
+
*
|
|
343
|
+
* Q: Why not move the dd-trace import inside the `if DD_TRACE_ENABLED` condition, then?
|
|
344
|
+
* A: Because the future is ESM, and ESM imports must be static and top-level.
|
|
345
|
+
*/
|
|
346
|
+
const DD_TRACER = tracer;
|
|
347
|
+
|
|
302
348
|
/**
|
|
303
349
|
* Error class meant to be returned by integrations in case of exceptions. These errors will be caught and handled
|
|
304
350
|
* appropriately. Any other error would result in an unhandled server error accompanied by a 500 status code.
|
|
@@ -552,6 +598,7 @@ class Handler {
|
|
|
552
598
|
filters: res.locals.filters,
|
|
553
599
|
logger: res.locals.logger,
|
|
554
600
|
signal: res.locals.signal,
|
|
601
|
+
requestMetrics: res.locals.requestMetrics,
|
|
555
602
|
params: req.params,
|
|
556
603
|
query: req.query,
|
|
557
604
|
relations: res.locals.relations,
|
|
@@ -573,6 +620,7 @@ class Handler {
|
|
|
573
620
|
body: req.body,
|
|
574
621
|
logger: res.locals.logger,
|
|
575
622
|
signal: res.locals.signal,
|
|
623
|
+
requestMetrics: res.locals.requestMetrics,
|
|
576
624
|
params: req.params,
|
|
577
625
|
query: req.query,
|
|
578
626
|
});
|
|
@@ -613,6 +661,7 @@ class Handler {
|
|
|
613
661
|
body,
|
|
614
662
|
logger: res.locals.logger,
|
|
615
663
|
signal: res.locals.signal,
|
|
664
|
+
requestMetrics: res.locals.requestMetrics,
|
|
616
665
|
params: req.params,
|
|
617
666
|
query: req.query,
|
|
618
667
|
});
|
|
@@ -645,6 +694,7 @@ class Handler {
|
|
|
645
694
|
secrets: res.locals.secrets,
|
|
646
695
|
logger: res.locals.logger,
|
|
647
696
|
signal: res.locals.signal,
|
|
697
|
+
requestMetrics: res.locals.requestMetrics,
|
|
648
698
|
params: req.params,
|
|
649
699
|
query: req.query,
|
|
650
700
|
});
|
|
@@ -665,6 +715,7 @@ class Handler {
|
|
|
665
715
|
body: req.body,
|
|
666
716
|
logger: res.locals.logger,
|
|
667
717
|
signal: res.locals.signal,
|
|
718
|
+
requestMetrics: res.locals.requestMetrics,
|
|
668
719
|
params: req.params,
|
|
669
720
|
query: req.query,
|
|
670
721
|
});
|
|
@@ -683,6 +734,7 @@ class Handler {
|
|
|
683
734
|
secrets: res.locals.secrets,
|
|
684
735
|
logger: res.locals.logger,
|
|
685
736
|
signal: res.locals.signal,
|
|
737
|
+
requestMetrics: res.locals.requestMetrics,
|
|
686
738
|
params: req.params,
|
|
687
739
|
query: req.query,
|
|
688
740
|
});
|
|
@@ -701,6 +753,7 @@ class Handler {
|
|
|
701
753
|
secrets: res.locals.secrets,
|
|
702
754
|
logger: res.locals.logger,
|
|
703
755
|
signal: res.locals.signal,
|
|
756
|
+
requestMetrics: res.locals.requestMetrics,
|
|
704
757
|
params: req.params,
|
|
705
758
|
query: req.query,
|
|
706
759
|
});
|
|
@@ -736,6 +789,7 @@ class Handler {
|
|
|
736
789
|
secrets: res.locals.secrets,
|
|
737
790
|
logger: res.locals.logger,
|
|
738
791
|
signal: res.locals.signal,
|
|
792
|
+
requestMetrics: res.locals.requestMetrics,
|
|
739
793
|
params: req.params,
|
|
740
794
|
query: req.query,
|
|
741
795
|
});
|
|
@@ -751,6 +805,7 @@ class Handler {
|
|
|
751
805
|
secrets: res.locals.secrets,
|
|
752
806
|
logger: res.locals.logger,
|
|
753
807
|
signal: res.locals.signal,
|
|
808
|
+
requestMetrics: res.locals.requestMetrics,
|
|
754
809
|
params: req.params,
|
|
755
810
|
query: req.query,
|
|
756
811
|
body: req.body,
|
|
@@ -767,6 +822,7 @@ class Handler {
|
|
|
767
822
|
secrets: res.locals.secrets,
|
|
768
823
|
logger: res.locals.logger,
|
|
769
824
|
signal: res.locals.signal,
|
|
825
|
+
requestMetrics: res.locals.requestMetrics,
|
|
770
826
|
params: req.params,
|
|
771
827
|
query: req.query,
|
|
772
828
|
body: req.body,
|
|
@@ -788,6 +844,7 @@ class Handler {
|
|
|
788
844
|
body: req.body,
|
|
789
845
|
logger: res.locals.logger,
|
|
790
846
|
signal: res.locals.signal,
|
|
847
|
+
requestMetrics: res.locals.requestMetrics,
|
|
791
848
|
params: req.params,
|
|
792
849
|
query: req.query,
|
|
793
850
|
});
|
|
@@ -887,11 +944,14 @@ function onFinish(req, res, next) {
|
|
|
887
944
|
res.on('finish', function () {
|
|
888
945
|
const logger = res.locals.logger ?? new Logger();
|
|
889
946
|
const error = res.locals.error;
|
|
890
|
-
const
|
|
947
|
+
const endMetrics = res.locals.requestMetrics.endRequest();
|
|
948
|
+
const durationInNs = Number(endMetrics.durationNs);
|
|
891
949
|
const durationInMs = (durationInNs / 1_000_000) | 0;
|
|
892
950
|
const message = `${req.method} ${req.originalUrl} ${res.statusCode} - ${durationInMs} ms`;
|
|
893
951
|
const metadata = {
|
|
894
952
|
duration: durationInNs,
|
|
953
|
+
externalApiCount: endMetrics.apiCallCount,
|
|
954
|
+
externalApiTotalDuration: endMetrics.totalApiDurationNs,
|
|
895
955
|
// Use reserved and standard attributes of Datadog
|
|
896
956
|
// https://app.datadoghq.com/logs/pipelines/standard-attributes
|
|
897
957
|
http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
|
|
@@ -944,8 +1004,42 @@ function injectLogger(req, res, next) {
|
|
|
944
1004
|
next();
|
|
945
1005
|
}
|
|
946
1006
|
|
|
1007
|
+
/**
|
|
1008
|
+
* Accumulates per-request metrics for Provider API calls.
|
|
1009
|
+
*
|
|
1010
|
+
* Created once per incoming request (in the start middleware), threaded through Context and RequestOptions,
|
|
1011
|
+
* incremented by Provider on each API call, and logged by the finish middleware.
|
|
1012
|
+
*/
|
|
1013
|
+
class RequestMetrics {
|
|
1014
|
+
_apiCallCount = 0;
|
|
1015
|
+
_totalApiDurationNs = 0;
|
|
1016
|
+
_requestStartTime;
|
|
1017
|
+
constructor() {
|
|
1018
|
+
this._requestStartTime = process.hrtime.bigint();
|
|
1019
|
+
}
|
|
1020
|
+
static startRequest() {
|
|
1021
|
+
return new RequestMetrics();
|
|
1022
|
+
}
|
|
1023
|
+
endRequest() {
|
|
1024
|
+
return {
|
|
1025
|
+
durationNs: process.hrtime.bigint() - this._requestStartTime,
|
|
1026
|
+
apiCallCount: this._apiCallCount,
|
|
1027
|
+
totalApiDurationNs: this._totalApiDurationNs,
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Record a completed API call.
|
|
1032
|
+
* Computes the duration from the given start timestamp, increments the call counter,
|
|
1033
|
+
* and accumulates the duration.
|
|
1034
|
+
*/
|
|
1035
|
+
recordExternalApiCall(startTime) {
|
|
1036
|
+
this._apiCallCount++;
|
|
1037
|
+
this._totalApiDurationNs += Number(process.hrtime.bigint() - startTime);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
947
1041
|
function start(_, res, next) {
|
|
948
|
-
res.locals.
|
|
1042
|
+
res.locals.requestMetrics = RequestMetrics.startRequest();
|
|
949
1043
|
next();
|
|
950
1044
|
}
|
|
951
1045
|
|
|
@@ -1014,6 +1108,7 @@ function onHealthCheck(_req, res) {
|
|
|
1014
1108
|
res.status(200).json({});
|
|
1015
1109
|
}
|
|
1016
1110
|
|
|
1111
|
+
// Must be the very first import so dd-trace instruments all subsequent modules.
|
|
1017
1112
|
function printErrorMessage(message) {
|
|
1018
1113
|
console.error();
|
|
1019
1114
|
console.error(`\x1b[31m Oops! Something went wrong! \x1b[0m`);
|
|
@@ -1352,7 +1447,7 @@ class Provider {
|
|
|
1352
1447
|
}
|
|
1353
1448
|
});
|
|
1354
1449
|
};
|
|
1355
|
-
return this.
|
|
1450
|
+
return this.timedCallToProvider(callToProvider, options);
|
|
1356
1451
|
}
|
|
1357
1452
|
/**
|
|
1358
1453
|
* Performs a POST request to the provider streaming a Readable directly without loading it into memory.
|
|
@@ -1462,7 +1557,7 @@ class Provider {
|
|
|
1462
1557
|
}
|
|
1463
1558
|
});
|
|
1464
1559
|
};
|
|
1465
|
-
return this.
|
|
1560
|
+
return this.timedCallToProvider(callToProvider, options);
|
|
1466
1561
|
}
|
|
1467
1562
|
/**
|
|
1468
1563
|
* Performs a PUT request to the provider.
|
|
@@ -1702,7 +1797,7 @@ class Provider {
|
|
|
1702
1797
|
}
|
|
1703
1798
|
return { status: response.status, headers: Object.fromEntries(response.headers.entries()), body };
|
|
1704
1799
|
};
|
|
1705
|
-
return this.
|
|
1800
|
+
return this.timedCallToProvider(callToProvider, options);
|
|
1706
1801
|
}
|
|
1707
1802
|
/**
|
|
1708
1803
|
* Normalizes Node.js IncomingHttpHeaders (which may have undefined or string[] values)
|
|
@@ -1713,6 +1808,18 @@ class Provider {
|
|
|
1713
1808
|
.filter((entry) => entry[1] !== undefined)
|
|
1714
1809
|
.map(([key, value]) => [key, Array.isArray(value) ? value.join(',') : value]));
|
|
1715
1810
|
}
|
|
1811
|
+
async timedCallToProvider(callToProvider, options) {
|
|
1812
|
+
const timedCall = async () => {
|
|
1813
|
+
const start = process.hrtime.bigint();
|
|
1814
|
+
try {
|
|
1815
|
+
return await callToProvider();
|
|
1816
|
+
}
|
|
1817
|
+
finally {
|
|
1818
|
+
options.requestMetrics?.recordExternalApiCall(start);
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
return this.rateLimiter ? this.rateLimiter(options, timedCall) : timedCall();
|
|
1822
|
+
}
|
|
1716
1823
|
handleError(responseStatus, message, options) {
|
|
1717
1824
|
const customError = this.customErrorHandler?.(responseStatus, message, options);
|
|
1718
1825
|
return customError ?? buildHttpError(responseStatus, message);
|
|
@@ -1770,10 +1877,12 @@ function buildCollectionQueryParams(params) {
|
|
|
1770
1877
|
|
|
1771
1878
|
exports.Api = integrationApi__namespace;
|
|
1772
1879
|
exports.Cache = Cache;
|
|
1880
|
+
exports.DD_TRACER = DD_TRACER;
|
|
1773
1881
|
exports.Handler = Handler;
|
|
1774
1882
|
exports.HttpErrors = httpErrors;
|
|
1775
1883
|
exports.Integration = Integration;
|
|
1776
1884
|
exports.NULL_LOGGER = NULL_LOGGER;
|
|
1777
1885
|
exports.Provider = Provider;
|
|
1886
|
+
exports.RequestMetrics = RequestMetrics;
|
|
1778
1887
|
exports.buildCollectionQueryParams = buildCollectionQueryParams;
|
|
1779
1888
|
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';
|
|
@@ -10,3 +11,4 @@ export * as HttpErrors from './httpErrors.js';
|
|
|
10
11
|
export { buildCollectionQueryParams, getApplicableFilters } from './helpers.js';
|
|
11
12
|
export * from './resources/context.js';
|
|
12
13
|
export { type default as Logger, NULL_LOGGER } from './resources/logger.js';
|
|
14
|
+
export { DD_TRACER } from './resources/tracer.js';
|
package/dist/src/index.js
CHANGED
|
@@ -4,8 +4,10 @@ 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';
|
|
10
11
|
export { NULL_LOGGER } from './resources/logger.js';
|
|
12
|
+
export { DD_TRACER } from './resources/tracer.js';
|
|
11
13
|
/* c8 ignore stop */
|
package/dist/src/integration.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import tracer from 'dd-trace';
|
|
2
|
+
/**
|
|
3
|
+
* WARNING to projects importing this
|
|
4
|
+
* Even if dd-tracer documents that instrumentation happens at **.init() time**
|
|
5
|
+
* (see https://docs.datadoghq.com/tracing/trace_collection/automatic_instrumentation/dd_libraries/nodejs/#import-and-initialize-the-tracer ),
|
|
6
|
+
* 1. dd-tracer has no choice but to do some stuff **at import time**
|
|
7
|
+
* 2. dd-tracer is not exempt from bugs, like https://github.com/DataDog/dd-trace-js/issues/5211
|
|
8
|
+
*
|
|
9
|
+
* So, do not assume that dd-trace is *fully off* by default! Some of it runs!
|
|
10
|
+
* If you want to entirely disable all of dd-trace (e.g. during unit tests),
|
|
11
|
+
* then you must set `DD_TRACE_ENABLED=false`
|
|
12
|
+
*
|
|
13
|
+
* Q: Why not move the dd-trace import inside the `if DD_TRACE_ENABLED` condition, then?
|
|
14
|
+
* A: Because the future is ESM, and ESM imports must be static and top-level.
|
|
15
|
+
*/
|
|
16
|
+
export declare const DD_TRACER: tracer.Tracer;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import tracer from 'dd-trace';
|
|
2
|
+
if (process.env.NODE_ENV === 'production' && process.env.DD_TRACE_ENABLED === 'true') {
|
|
3
|
+
// List of options available to the tracer: https://datadoghq.dev/dd-trace-js/interfaces/export_.TracerOptions.html
|
|
4
|
+
const apmConfig = { logInjection: false, profiling: false, sampleRate: 0 };
|
|
5
|
+
if (process.env.DD_APM_ENABLED == 'true') {
|
|
6
|
+
apmConfig.logInjection = true;
|
|
7
|
+
apmConfig.sampleRate = 1;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
// The "enable tracing" boolean is not exposed as part of the TracerOptions so we
|
|
11
|
+
// have to do so through env vars. Setting here instead of in services' config
|
|
12
|
+
// to avoid too many env vars to "flip" when enabling / disabling APM.
|
|
13
|
+
process.env.DD_TRACING_ENABLED = 'false';
|
|
14
|
+
}
|
|
15
|
+
// Profiling is extra $$$ so we're setting it as an "extra" when needed
|
|
16
|
+
if (process.env.DD_APM_PROFILING_ENABLED == 'true') {
|
|
17
|
+
apmConfig.profiling = true;
|
|
18
|
+
}
|
|
19
|
+
const tags = {};
|
|
20
|
+
// Using DD_TRACE_AGENT_URL as a hack to know if we're running inside of Kubernetes
|
|
21
|
+
// since this variable is not needed in Elastic Beanstalk of Lambda
|
|
22
|
+
if (process.env.DD_TRACE_AGENT_URL) {
|
|
23
|
+
tags.pod_name = process.env.HOSTNAME;
|
|
24
|
+
}
|
|
25
|
+
tracer.init({
|
|
26
|
+
...apmConfig, // Conditionally enable APM tracing & profiling
|
|
27
|
+
runtimeMetrics: true, // Always enable runtime metrics https://docs.datadoghq.com/tracing/metrics/runtime_metrics/nodejs/?tab=environmentvariables
|
|
28
|
+
tags,
|
|
29
|
+
}); // initialized in a different file to avoid hoisting.
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* WARNING to projects importing this
|
|
33
|
+
* Even if dd-tracer documents that instrumentation happens at **.init() time**
|
|
34
|
+
* (see https://docs.datadoghq.com/tracing/trace_collection/automatic_instrumentation/dd_libraries/nodejs/#import-and-initialize-the-tracer ),
|
|
35
|
+
* 1. dd-tracer has no choice but to do some stuff **at import time**
|
|
36
|
+
* 2. dd-tracer is not exempt from bugs, like https://github.com/DataDog/dd-trace-js/issues/5211
|
|
37
|
+
*
|
|
38
|
+
* So, do not assume that dd-trace is *fully off* by default! Some of it runs!
|
|
39
|
+
* If you want to entirely disable all of dd-trace (e.g. during unit tests),
|
|
40
|
+
* then you must set `DD_TRACE_ENABLED=false`
|
|
41
|
+
*
|
|
42
|
+
* Q: Why not move the dd-trace import inside the `if DD_TRACE_ENABLED` condition, then?
|
|
43
|
+
* A: Because the future is ESM, and ESM imports must be static and top-level.
|
|
44
|
+
*/
|
|
45
|
+
export const DD_TRACER = tracer;
|