@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.
Files changed (37) hide show
  1. package/dist/src/handler.js +11 -0
  2. package/dist/src/index.cjs +114 -5
  3. package/dist/src/index.d.ts +2 -0
  4. package/dist/src/index.js +2 -0
  5. package/dist/src/integration.d.ts +1 -0
  6. package/dist/src/integration.js +2 -0
  7. package/dist/src/middlewares/finish.d.ts +0 -1
  8. package/dist/src/middlewares/finish.js +4 -1
  9. package/dist/src/middlewares/start.d.ts +2 -1
  10. package/dist/src/middlewares/start.js +2 -1
  11. package/dist/src/resources/context.d.ts +6 -0
  12. package/dist/src/resources/provider.d.ts +3 -0
  13. package/dist/src/resources/provider.js +15 -3
  14. package/dist/src/resources/requestMetrics.d.ts +24 -0
  15. package/dist/src/resources/requestMetrics.js +33 -0
  16. package/dist/src/resources/tracer.d.ts +16 -0
  17. package/dist/src/resources/tracer.js +45 -0
  18. package/dist/test/middlewares/finish.test.js +5 -4
  19. package/dist/test/middlewares/start.test.js +13 -2
  20. package/dist/test/resources/provider.test.js +95 -0
  21. package/dist/test/resources/requestMetrics.test.d.ts +1 -0
  22. package/dist/test/resources/requestMetrics.test.js +45 -0
  23. package/dist/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +2 -1
  25. package/src/handler.ts +11 -0
  26. package/src/index.ts +2 -0
  27. package/src/integration.ts +3 -0
  28. package/src/middlewares/finish.ts +4 -2
  29. package/src/middlewares/start.ts +3 -2
  30. package/src/resources/context.ts +6 -0
  31. package/src/resources/provider.ts +20 -3
  32. package/src/resources/requestMetrics.ts +37 -0
  33. package/src/resources/tracer.ts +50 -0
  34. package/test/middlewares/finish.test.ts +13 -12
  35. package/test/middlewares/start.test.ts +13 -2
  36. package/test/resources/provider.test.ts +126 -0
  37. package/test/resources/requestMetrics.test.ts +50 -0
@@ -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
  });
@@ -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 durationInNs = Number(process.hrtime.bigint() - res.locals.requestStartTime);
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.requestStartTime = process.hrtime.bigint();
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.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
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.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
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.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
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;
@@ -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 */
@@ -1,3 +1,4 @@
1
+ import './resources/tracer.js';
1
2
  import { HandlersInput } from './handler.js';
2
3
  type Options = {
3
4
  port?: number;
@@ -1,3 +1,5 @@
1
+ // Must be the very first import so dd-trace instruments all subsequent modules.
2
+ import './resources/tracer.js';
1
3
  import express from 'express';
2
4
  import { InvalidHandler } from './errors.js';
3
5
  import { Handler } from './handler.js';
@@ -6,7 +6,6 @@ declare global {
6
6
  interface Locals {
7
7
  logger: Logger;
8
8
  error?: ApiError;
9
- requestStartTime: bigint;
10
9
  }
11
10
  }
12
11
  }
@@ -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 durationInNs = Number(process.hrtime.bigint() - res.locals.requestStartTime);
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,8 +1,9 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
+ import { RequestMetrics } from '../resources/requestMetrics.js';
2
3
  declare global {
3
4
  namespace Express {
4
5
  interface Locals {
5
- requestStartTime: bigint;
6
+ requestMetrics: RequestMetrics;
6
7
  }
7
8
  }
8
9
  }
@@ -1,5 +1,6 @@
1
+ import { RequestMetrics } from '../resources/requestMetrics.js';
1
2
  function start(_, res, next) {
2
- res.locals.requestStartTime = process.hrtime.bigint();
3
+ res.locals.requestMetrics = RequestMetrics.startRequest();
3
4
  next();
4
5
  }
5
6
  export default start;
@@ -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.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
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.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
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.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
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;