apitally 0.10.0 → 0.11.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 (85) hide show
  1. package/README.md +1 -2
  2. package/dist/common/client.cjs +408 -12
  3. package/dist/common/client.cjs.map +1 -1
  4. package/dist/common/client.d.cts +8 -1
  5. package/dist/common/client.d.ts +8 -1
  6. package/dist/common/client.js +404 -8
  7. package/dist/common/client.js.map +1 -1
  8. package/dist/common/consumerRegistry.d.cts +4 -0
  9. package/dist/common/consumerRegistry.d.ts +4 -0
  10. package/dist/common/requestCounter.d.cts +4 -0
  11. package/dist/common/requestCounter.d.ts +4 -0
  12. package/dist/common/requestLogger.cjs +429 -0
  13. package/dist/common/requestLogger.cjs.map +1 -0
  14. package/dist/common/requestLogger.d.cts +64 -0
  15. package/dist/common/requestLogger.d.ts +64 -0
  16. package/dist/common/requestLogger.js +405 -0
  17. package/dist/common/requestLogger.js.map +1 -0
  18. package/dist/common/serverErrorCounter.d.cts +4 -0
  19. package/dist/common/serverErrorCounter.d.ts +4 -0
  20. package/dist/common/tempGzipFile.cjs +93 -0
  21. package/dist/common/tempGzipFile.cjs.map +1 -0
  22. package/dist/common/tempGzipFile.d.cts +17 -0
  23. package/dist/common/tempGzipFile.d.ts +17 -0
  24. package/dist/common/tempGzipFile.js +74 -0
  25. package/dist/common/tempGzipFile.js.map +1 -0
  26. package/dist/common/types.cjs.map +1 -1
  27. package/dist/common/types.d.cts +5 -0
  28. package/dist/common/types.d.ts +5 -0
  29. package/dist/common/validationErrorCounter.d.cts +4 -0
  30. package/dist/common/validationErrorCounter.d.ts +4 -0
  31. package/dist/express/index.cjs +510 -34
  32. package/dist/express/index.cjs.map +1 -1
  33. package/dist/express/index.d.cts +4 -0
  34. package/dist/express/index.d.ts +4 -0
  35. package/dist/express/index.js +506 -30
  36. package/dist/express/index.js.map +1 -1
  37. package/dist/express/middleware.cjs +510 -34
  38. package/dist/express/middleware.cjs.map +1 -1
  39. package/dist/express/middleware.d.cts +4 -0
  40. package/dist/express/middleware.d.ts +4 -0
  41. package/dist/express/middleware.js +506 -30
  42. package/dist/express/middleware.js.map +1 -1
  43. package/dist/fastify/index.cjs +501 -27
  44. package/dist/fastify/index.cjs.map +1 -1
  45. package/dist/fastify/index.d.cts +4 -0
  46. package/dist/fastify/index.d.ts +4 -0
  47. package/dist/fastify/index.js +497 -23
  48. package/dist/fastify/index.js.map +1 -1
  49. package/dist/fastify/plugin.cjs +501 -27
  50. package/dist/fastify/plugin.cjs.map +1 -1
  51. package/dist/fastify/plugin.d.cts +4 -0
  52. package/dist/fastify/plugin.d.ts +4 -0
  53. package/dist/fastify/plugin.js +497 -23
  54. package/dist/fastify/plugin.js.map +1 -1
  55. package/dist/hono/index.cjs +470 -15
  56. package/dist/hono/index.cjs.map +1 -1
  57. package/dist/hono/index.d.cts +4 -0
  58. package/dist/hono/index.d.ts +4 -0
  59. package/dist/hono/index.js +466 -11
  60. package/dist/hono/index.js.map +1 -1
  61. package/dist/hono/middleware.cjs +470 -15
  62. package/dist/hono/middleware.cjs.map +1 -1
  63. package/dist/hono/middleware.d.cts +4 -0
  64. package/dist/hono/middleware.d.ts +4 -0
  65. package/dist/hono/middleware.js +466 -11
  66. package/dist/hono/middleware.js.map +1 -1
  67. package/dist/koa/index.cjs +485 -15
  68. package/dist/koa/index.cjs.map +1 -1
  69. package/dist/koa/index.d.cts +4 -0
  70. package/dist/koa/index.d.ts +4 -0
  71. package/dist/koa/index.js +481 -11
  72. package/dist/koa/index.js.map +1 -1
  73. package/dist/koa/middleware.cjs +485 -15
  74. package/dist/koa/middleware.cjs.map +1 -1
  75. package/dist/koa/middleware.d.cts +4 -0
  76. package/dist/koa/middleware.d.ts +4 -0
  77. package/dist/koa/middleware.js +481 -11
  78. package/dist/koa/middleware.js.map +1 -1
  79. package/dist/nestjs/index.cjs +510 -34
  80. package/dist/nestjs/index.cjs.map +1 -1
  81. package/dist/nestjs/index.d.cts +4 -0
  82. package/dist/nestjs/index.d.ts +4 -0
  83. package/dist/nestjs/index.js +506 -30
  84. package/dist/nestjs/index.js.map +1 -1
  85. package/package.json +8 -2
@@ -41,7 +41,7 @@ module.exports = __toCommonJS(express_exports);
41
41
  var import_perf_hooks = require("perf_hooks");
42
42
 
43
43
  // src/common/client.ts
44
- var import_crypto3 = require("crypto");
44
+ var import_crypto5 = require("crypto");
45
45
  var import_fetch_retry = __toESM(require("fetch-retry"), 1);
46
46
 
47
47
  // src/common/consumerRegistry.ts
@@ -210,8 +210,405 @@ var _RequestCounter = class _RequestCounter {
210
210
  __name(_RequestCounter, "RequestCounter");
211
211
  var RequestCounter = _RequestCounter;
212
212
 
213
- // src/common/serverErrorCounter.ts
213
+ // src/common/requestLogger.ts
214
+ var import_buffer2 = require("buffer");
215
+ var import_crypto2 = require("crypto");
216
+ var import_fs2 = require("fs");
217
+ var import_os2 = require("os");
218
+ var import_path2 = require("path");
219
+
220
+ // src/common/tempGzipFile.ts
221
+ var import_buffer = require("buffer");
214
222
  var import_crypto = require("crypto");
223
+ var import_fs = require("fs");
224
+ var import_os = require("os");
225
+ var import_path = require("path");
226
+ var import_zlib = require("zlib");
227
+ var _TempGzipFile = class _TempGzipFile {
228
+ uuid;
229
+ filePath;
230
+ gzip;
231
+ writeStream;
232
+ readyPromise;
233
+ constructor() {
234
+ this.uuid = (0, import_crypto.randomUUID)();
235
+ this.filePath = (0, import_path.join)((0, import_os.tmpdir)(), `apitally-${this.uuid}.gz`);
236
+ this.writeStream = (0, import_fs.createWriteStream)(this.filePath);
237
+ this.readyPromise = new Promise((resolve, reject) => {
238
+ this.writeStream.once("ready", resolve);
239
+ this.writeStream.once("error", reject);
240
+ });
241
+ this.gzip = (0, import_zlib.createGzip)();
242
+ this.gzip.pipe(this.writeStream);
243
+ }
244
+ get size() {
245
+ return this.writeStream.bytesWritten;
246
+ }
247
+ async writeLine(data) {
248
+ await this.readyPromise;
249
+ return new Promise((resolve, reject) => {
250
+ this.gzip.write(import_buffer.Buffer.concat([
251
+ data,
252
+ import_buffer.Buffer.from("\n")
253
+ ]), (error) => {
254
+ if (error) {
255
+ reject(error);
256
+ } else {
257
+ resolve();
258
+ }
259
+ });
260
+ });
261
+ }
262
+ async getContent() {
263
+ return new Promise((resolve, reject) => {
264
+ (0, import_fs.readFile)(this.filePath, (error, data) => {
265
+ if (error) {
266
+ reject(error);
267
+ } else {
268
+ resolve(data);
269
+ }
270
+ });
271
+ });
272
+ }
273
+ async close() {
274
+ return new Promise((resolve) => {
275
+ this.gzip.end(() => {
276
+ resolve();
277
+ });
278
+ });
279
+ }
280
+ async delete() {
281
+ await this.close();
282
+ (0, import_fs.unlinkSync)(this.filePath);
283
+ }
284
+ };
285
+ __name(_TempGzipFile, "TempGzipFile");
286
+ var TempGzipFile = _TempGzipFile;
287
+
288
+ // src/common/requestLogger.ts
289
+ var MAX_BODY_SIZE = 5e4;
290
+ var MAX_FILE_SIZE = 1e6;
291
+ var MAX_FILES = 50;
292
+ var MAX_PENDING_WRITES = 100;
293
+ var BODY_TOO_LARGE = import_buffer2.Buffer.from("<body too large>");
294
+ var BODY_MASKED = import_buffer2.Buffer.from("<masked>");
295
+ var MASKED = "******";
296
+ var ALLOWED_CONTENT_TYPES = [
297
+ "application/json",
298
+ "text/plain"
299
+ ];
300
+ var EXCLUDE_PATH_PATTERNS = [
301
+ /\/_?healthz?$/i,
302
+ /\/_?health[_-]?checks?$/i,
303
+ /\/_?heart[_-]?beats?$/i,
304
+ /\/ping$/i,
305
+ /\/ready$/i,
306
+ /\/live$/i
307
+ ];
308
+ var EXCLUDE_USER_AGENT_PATTERNS = [
309
+ /health[-_ ]?check/i,
310
+ /microsoft-azure-application-lb/i,
311
+ /googlehc/i,
312
+ /kube-probe/i
313
+ ];
314
+ var MASK_QUERY_PARAM_PATTERNS = [
315
+ /auth/i,
316
+ /api-?key/i,
317
+ /secret/i,
318
+ /token/i,
319
+ /password/i,
320
+ /pwd/i
321
+ ];
322
+ var MASK_HEADER_PATTERNS = [
323
+ /auth/i,
324
+ /api-?key/i,
325
+ /secret/i,
326
+ /token/i,
327
+ /cookie/i
328
+ ];
329
+ var DEFAULT_CONFIG = {
330
+ enabled: false,
331
+ logQueryParams: true,
332
+ logRequestHeaders: false,
333
+ logRequestBody: false,
334
+ logResponseHeaders: true,
335
+ logResponseBody: false,
336
+ maskQueryParams: [],
337
+ maskHeaders: [],
338
+ excludePaths: []
339
+ };
340
+ var _RequestLogger = class _RequestLogger {
341
+ config;
342
+ enabled;
343
+ suspendUntil = null;
344
+ pendingWrites = [];
345
+ currentFile = null;
346
+ files = [];
347
+ maintainIntervalId;
348
+ constructor(config) {
349
+ this.config = {
350
+ ...DEFAULT_CONFIG,
351
+ ...config
352
+ };
353
+ this.enabled = this.config.enabled && checkWritableFs();
354
+ if (this.enabled) {
355
+ this.maintainIntervalId = setInterval(() => {
356
+ this.maintain();
357
+ }, 1e3);
358
+ }
359
+ }
360
+ shouldExcludePath(urlPath) {
361
+ const patterns = [
362
+ ...this.config.excludePaths,
363
+ ...EXCLUDE_PATH_PATTERNS
364
+ ];
365
+ return matchPatterns(urlPath, patterns);
366
+ }
367
+ shouldExcludeUserAgent(userAgent) {
368
+ return userAgent ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS) : false;
369
+ }
370
+ shouldMaskQueryParam(name) {
371
+ const patterns = [
372
+ ...this.config.maskQueryParams,
373
+ ...MASK_QUERY_PARAM_PATTERNS
374
+ ];
375
+ return matchPatterns(name, patterns);
376
+ }
377
+ shouldMaskHeader(name) {
378
+ const patterns = [
379
+ ...this.config.maskHeaders,
380
+ ...MASK_HEADER_PATTERNS
381
+ ];
382
+ return matchPatterns(name, patterns);
383
+ }
384
+ hasSupportedContentType(headers) {
385
+ var _a2;
386
+ const contentType = (_a2 = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a2[1];
387
+ return contentType !== void 0 && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
388
+ }
389
+ maskQueryParams(search) {
390
+ const params = new URLSearchParams(search);
391
+ for (const [key] of params) {
392
+ if (this.shouldMaskQueryParam(key)) {
393
+ params.set(key, MASKED);
394
+ }
395
+ }
396
+ return params.toString();
397
+ }
398
+ maskHeaders(headers) {
399
+ return headers.map(([k, v]) => [
400
+ k,
401
+ this.shouldMaskHeader(k) ? MASKED : v
402
+ ]);
403
+ }
404
+ logRequest(request, response) {
405
+ var _a2, _b, _c;
406
+ if (!this.enabled || this.suspendUntil !== null) return;
407
+ const url = new URL(request.url);
408
+ const path = request.path ?? url.pathname;
409
+ const userAgent = (_a2 = request.headers.find(([k]) => k.toLowerCase() === "user-agent")) == null ? void 0 : _a2[1];
410
+ if (this.shouldExcludePath(path) || this.shouldExcludeUserAgent(userAgent) || (((_c = (_b = this.config).excludeCallback) == null ? void 0 : _c.call(_b, request, response)) ?? false)) {
411
+ return;
412
+ }
413
+ url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
414
+ request.url = url.toString();
415
+ request.headers = this.config.logRequestHeaders ? this.maskHeaders(request.headers) : [];
416
+ response.headers = this.config.logResponseHeaders ? this.maskHeaders(response.headers) : [];
417
+ if (!this.config.logRequestBody || !this.hasSupportedContentType(request.headers)) {
418
+ request.body = void 0;
419
+ } else if (request.body) {
420
+ if (request.body.length > MAX_BODY_SIZE) {
421
+ request.body = BODY_TOO_LARGE;
422
+ } else if (this.config.maskRequestBodyCallback) {
423
+ try {
424
+ request.body = this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;
425
+ if (request.body.length > MAX_BODY_SIZE) {
426
+ request.body = BODY_TOO_LARGE;
427
+ }
428
+ } catch {
429
+ request.body = void 0;
430
+ }
431
+ }
432
+ }
433
+ if (!this.config.logResponseBody || !this.hasSupportedContentType(response.headers)) {
434
+ response.body = void 0;
435
+ } else if (response.body) {
436
+ if (response.body.length > MAX_BODY_SIZE) {
437
+ response.body = BODY_TOO_LARGE;
438
+ } else if (this.config.maskResponseBodyCallback) {
439
+ try {
440
+ response.body = this.config.maskResponseBodyCallback(request, response) ?? BODY_MASKED;
441
+ if (response.body.length > MAX_BODY_SIZE) {
442
+ response.body = BODY_TOO_LARGE;
443
+ }
444
+ } catch {
445
+ response.body = void 0;
446
+ }
447
+ }
448
+ }
449
+ const item = {
450
+ uuid: (0, import_crypto2.randomUUID)(),
451
+ request: skipEmptyValues(request),
452
+ response: skipEmptyValues(response)
453
+ };
454
+ [
455
+ item.request.body,
456
+ item.response.body
457
+ ].forEach((body) => {
458
+ if (body) {
459
+ body.toJSON = function() {
460
+ return this.toString("base64");
461
+ };
462
+ }
463
+ });
464
+ this.pendingWrites.push(JSON.stringify(item));
465
+ if (this.pendingWrites.length > MAX_PENDING_WRITES) {
466
+ this.pendingWrites.shift();
467
+ }
468
+ }
469
+ async writeToFile() {
470
+ if (!this.enabled || this.pendingWrites.length === 0) {
471
+ return;
472
+ }
473
+ if (!this.currentFile) {
474
+ this.currentFile = new TempGzipFile();
475
+ }
476
+ while (this.pendingWrites.length > 0) {
477
+ const item = this.pendingWrites.shift();
478
+ if (item) {
479
+ await this.currentFile.writeLine(import_buffer2.Buffer.from(item));
480
+ }
481
+ }
482
+ }
483
+ getFile() {
484
+ return this.files.shift();
485
+ }
486
+ retryFileLater(file) {
487
+ this.files.unshift(file);
488
+ }
489
+ async rotateFile() {
490
+ if (this.currentFile) {
491
+ await this.currentFile.close();
492
+ this.files.push(this.currentFile);
493
+ this.currentFile = null;
494
+ }
495
+ }
496
+ async maintain() {
497
+ await this.writeToFile();
498
+ if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {
499
+ await this.rotateFile();
500
+ }
501
+ while (this.files.length > MAX_FILES) {
502
+ const file = this.files.shift();
503
+ file == null ? void 0 : file.delete();
504
+ }
505
+ if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {
506
+ this.suspendUntil = null;
507
+ }
508
+ }
509
+ async clear() {
510
+ this.pendingWrites = [];
511
+ await this.rotateFile();
512
+ this.files.forEach((file) => {
513
+ file.delete();
514
+ });
515
+ this.files = [];
516
+ }
517
+ async close() {
518
+ this.enabled = false;
519
+ await this.clear();
520
+ if (this.maintainIntervalId) {
521
+ clearInterval(this.maintainIntervalId);
522
+ }
523
+ }
524
+ };
525
+ __name(_RequestLogger, "RequestLogger");
526
+ var RequestLogger = _RequestLogger;
527
+ function convertHeaders(headers) {
528
+ if (headers instanceof Headers) {
529
+ return Array.from(headers.entries());
530
+ }
531
+ return Object.entries(headers).flatMap(([key, value]) => {
532
+ if (value === void 0) {
533
+ return [];
534
+ }
535
+ if (Array.isArray(value)) {
536
+ return value.map((v) => [
537
+ key,
538
+ v
539
+ ]);
540
+ }
541
+ return [
542
+ [
543
+ key,
544
+ value.toString()
545
+ ]
546
+ ];
547
+ });
548
+ }
549
+ __name(convertHeaders, "convertHeaders");
550
+ function convertBody(body, contentType) {
551
+ if (!body || !contentType) {
552
+ return;
553
+ }
554
+ try {
555
+ if (contentType.startsWith("application/json")) {
556
+ if (isValidJsonString(body)) {
557
+ return import_buffer2.Buffer.from(body);
558
+ } else {
559
+ return import_buffer2.Buffer.from(JSON.stringify(body));
560
+ }
561
+ }
562
+ if (contentType.startsWith("text/") && typeof body === "string") {
563
+ return import_buffer2.Buffer.from(body);
564
+ }
565
+ } catch (error) {
566
+ return;
567
+ }
568
+ }
569
+ __name(convertBody, "convertBody");
570
+ function isValidJsonString(body) {
571
+ if (typeof body !== "string") {
572
+ return false;
573
+ }
574
+ try {
575
+ JSON.parse(body);
576
+ return true;
577
+ } catch {
578
+ return false;
579
+ }
580
+ }
581
+ __name(isValidJsonString, "isValidJsonString");
582
+ function matchPatterns(value, patterns) {
583
+ return patterns.some((pattern) => {
584
+ return pattern.test(value);
585
+ });
586
+ }
587
+ __name(matchPatterns, "matchPatterns");
588
+ function skipEmptyValues(data) {
589
+ return Object.fromEntries(Object.entries(data).filter(([_, v]) => {
590
+ if (v == null || Number.isNaN(v)) return false;
591
+ if (Array.isArray(v) || import_buffer2.Buffer.isBuffer(v) || typeof v === "string") {
592
+ return v.length > 0;
593
+ }
594
+ return true;
595
+ }));
596
+ }
597
+ __name(skipEmptyValues, "skipEmptyValues");
598
+ function checkWritableFs() {
599
+ try {
600
+ const testPath = (0, import_path2.join)((0, import_os2.tmpdir)(), `apitally-${(0, import_crypto2.randomUUID)()}`);
601
+ (0, import_fs2.writeFileSync)(testPath, "test");
602
+ (0, import_fs2.unlinkSync)(testPath);
603
+ return true;
604
+ } catch (error) {
605
+ return false;
606
+ }
607
+ }
608
+ __name(checkWritableFs, "checkWritableFs");
609
+
610
+ // src/common/serverErrorCounter.ts
611
+ var import_crypto3 = require("crypto");
215
612
  var MAX_MSG_LENGTH = 2048;
216
613
  var MAX_STACKTRACE_LENGTH = 65536;
217
614
  var _ServerErrorCounter = class _ServerErrorCounter {
@@ -263,7 +660,7 @@ var _ServerErrorCounter = class _ServerErrorCounter {
263
660
  serverError.msg.trim(),
264
661
  serverError.traceback.trim()
265
662
  ].join("|");
266
- return (0, import_crypto.createHash)("md5").update(hashInput).digest("hex");
663
+ return (0, import_crypto3.createHash)("md5").update(hashInput).digest("hex");
267
664
  }
268
665
  getTruncatedMessage(msg) {
269
666
  msg = msg.trim();
@@ -309,7 +706,7 @@ __name(_ServerErrorCounter, "ServerErrorCounter");
309
706
  var ServerErrorCounter = _ServerErrorCounter;
310
707
 
311
708
  // src/common/validationErrorCounter.ts
312
- var import_crypto2 = require("crypto");
709
+ var import_crypto4 = require("crypto");
313
710
  var _ValidationErrorCounter = class _ValidationErrorCounter {
314
711
  errorCounts;
315
712
  errorDetails;
@@ -353,7 +750,7 @@ var _ValidationErrorCounter = class _ValidationErrorCounter {
353
750
  validationError.msg.trim(),
354
751
  validationError.type
355
752
  ].join("|");
356
- return (0, import_crypto2.createHash)("md5").update(hashInput).digest("hex");
753
+ return (0, import_crypto4.createHash)("md5").update(hashInput).digest("hex");
357
754
  }
358
755
  };
359
756
  __name(_ValidationErrorCounter, "ValidationErrorCounter");
@@ -382,11 +779,12 @@ var _ApitallyClient = class _ApitallyClient {
382
779
  startupData;
383
780
  startupDataSent = false;
384
781
  requestCounter;
782
+ requestLogger;
385
783
  validationErrorCounter;
386
784
  serverErrorCounter;
387
785
  consumerRegistry;
388
786
  logger;
389
- constructor({ clientId, env = "dev", logger }) {
787
+ constructor({ clientId, env = "dev", requestLoggingConfig, logger }) {
390
788
  if (_ApitallyClient.instance) {
391
789
  throw new Error("Apitally client is already initialized");
392
790
  }
@@ -399,9 +797,10 @@ var _ApitallyClient = class _ApitallyClient {
399
797
  _ApitallyClient.instance = this;
400
798
  this.clientId = clientId;
401
799
  this.env = env;
402
- this.instanceUuid = (0, import_crypto3.randomUUID)();
800
+ this.instanceUuid = (0, import_crypto5.randomUUID)();
403
801
  this.syncDataQueue = [];
404
802
  this.requestCounter = new RequestCounter();
803
+ this.requestLogger = new RequestLogger(requestLoggingConfig);
405
804
  this.validationErrorCounter = new ValidationErrorCounter();
406
805
  this.serverErrorCounter = new ServerErrorCounter();
407
806
  this.consumerRegistry = new ConsumerRegistry();
@@ -423,6 +822,8 @@ var _ApitallyClient = class _ApitallyClient {
423
822
  async handleShutdown() {
424
823
  this.stopSync();
425
824
  await this.sendSyncData();
825
+ await this.sendLogData();
826
+ await this.requestLogger.close();
426
827
  _ApitallyClient.instance = void 0;
427
828
  }
428
829
  getHubUrlPrefix() {
@@ -469,7 +870,8 @@ var _ApitallyClient = class _ApitallyClient {
469
870
  async sync() {
470
871
  try {
471
872
  const promises = [
472
- this.sendSyncData()
873
+ this.sendSyncData(),
874
+ this.sendLogData()
473
875
  ];
474
876
  if (!this.startupDataSent) {
475
877
  promises.push(this.sendStartupData());
@@ -497,7 +899,7 @@ var _ApitallyClient = class _ApitallyClient {
497
899
  this.logger.debug("Sending startup data to Apitally Hub");
498
900
  const payload = {
499
901
  instance_uuid: this.instanceUuid,
500
- message_uuid: (0, import_crypto3.randomUUID)(),
902
+ message_uuid: (0, import_crypto5.randomUUID)(),
501
903
  ...this.startupData
502
904
  };
503
905
  try {
@@ -519,7 +921,7 @@ var _ApitallyClient = class _ApitallyClient {
519
921
  const newPayload = {
520
922
  time_offset: 0,
521
923
  instance_uuid: this.instanceUuid,
522
- message_uuid: (0, import_crypto3.randomUUID)(),
924
+ message_uuid: (0, import_crypto5.randomUUID)(),
523
925
  requests: this.requestCounter.getAndResetRequests(),
524
926
  validation_errors: this.validationErrorCounter.getAndResetValidationErrors(),
525
927
  server_errors: this.serverErrorCounter.getAndResetServerErrors(),
@@ -538,8 +940,7 @@ var _ApitallyClient = class _ApitallyClient {
538
940
  const timeOffset = Date.now() - time;
539
941
  if (timeOffset <= MAX_QUEUE_TIME) {
540
942
  if (i > 0) {
541
- const waitMs = 100 + Math.random() * 200;
542
- await new Promise((resolve) => setTimeout(resolve, waitMs));
943
+ await this.randomDelay();
543
944
  }
544
945
  payload.time_offset = timeOffset / 1e3;
545
946
  await this.sendData("sync", payload);
@@ -558,6 +959,52 @@ var _ApitallyClient = class _ApitallyClient {
558
959
  }
559
960
  }
560
961
  }
962
+ async sendLogData() {
963
+ this.logger.debug("Sending request log data to Apitally Hub");
964
+ await this.requestLogger.rotateFile();
965
+ const fetchWithRetry = (0, import_fetch_retry.default)(fetch, {
966
+ retries: 3,
967
+ retryDelay: 1e3,
968
+ retryOn: [
969
+ 408,
970
+ 429,
971
+ 500,
972
+ 502,
973
+ 503,
974
+ 504
975
+ ]
976
+ });
977
+ let i = 0;
978
+ let logFile;
979
+ while (logFile = this.requestLogger.getFile()) {
980
+ if (i > 0) {
981
+ await this.randomDelay();
982
+ }
983
+ try {
984
+ const response = await fetchWithRetry(`${this.getHubUrlPrefix()}log?uuid=${logFile.uuid}`, {
985
+ method: "POST",
986
+ body: await logFile.getContent()
987
+ });
988
+ if (response.status === 402 && response.headers.has("Retry-After")) {
989
+ const retryAfter = parseInt(response.headers.get("Retry-After") ?? "0");
990
+ if (retryAfter > 0) {
991
+ this.requestLogger.suspendUntil = Date.now() + retryAfter * 1e3;
992
+ this.requestLogger.clear();
993
+ return;
994
+ }
995
+ }
996
+ if (!response.ok) {
997
+ throw new HTTPError(response);
998
+ }
999
+ logFile.delete();
1000
+ } catch (error) {
1001
+ this.requestLogger.retryFileLater(logFile);
1002
+ break;
1003
+ }
1004
+ i++;
1005
+ if (i >= 10) break;
1006
+ }
1007
+ }
561
1008
  handleHubError(error) {
562
1009
  if (error instanceof HTTPError) {
563
1010
  if (error.response.status === 404) {
@@ -572,6 +1019,10 @@ var _ApitallyClient = class _ApitallyClient {
572
1019
  }
573
1020
  return false;
574
1021
  }
1022
+ async randomDelay() {
1023
+ const delay = 100 + Math.random() * 200;
1024
+ await new Promise((resolve) => setTimeout(resolve, delay));
1025
+ }
575
1026
  };
576
1027
  __name(_ApitallyClient, "ApitallyClient");
577
1028
  __publicField(_ApitallyClient, "instance");
@@ -742,18 +1193,18 @@ var getMiddleware = /* @__PURE__ */ __name((app, client) => {
742
1193
  }
743
1194
  try {
744
1195
  const startTime = import_perf_hooks.performance.now();
745
- const originalJson = res.json;
746
- res.json = (body) => {
1196
+ const originalSend = res.send;
1197
+ res.send = (body) => {
747
1198
  res.locals.body = body;
748
- return originalJson.call(res, body);
1199
+ return originalSend.call(res, body);
749
1200
  };
750
1201
  res.on("finish", () => {
751
1202
  try {
1203
+ const responseTime = import_perf_hooks.performance.now() - startTime;
752
1204
  const path = getRoutePath(req);
1205
+ const consumer = getConsumer(req);
1206
+ client.consumerRegistry.addOrUpdateConsumer(consumer);
753
1207
  if (path) {
754
- const responseTime = import_perf_hooks.performance.now() - startTime;
755
- const consumer = getConsumer(req);
756
- client.consumerRegistry.addOrUpdateConsumer(consumer);
757
1208
  client.requestCounter.addRequest({
758
1209
  consumer: consumer == null ? void 0 : consumer.identifier,
759
1210
  method: req.method,
@@ -764,24 +1215,31 @@ var getMiddleware = /* @__PURE__ */ __name((app, client) => {
764
1215
  responseSize: res.get("content-length")
765
1216
  });
766
1217
  if ((res.statusCode === 400 || res.statusCode === 422) && res.locals.body) {
767
- const validationErrors = [];
768
- if (validationErrors.length === 0) {
769
- validationErrors.push(...extractExpressValidatorErrors(res.locals.body));
770
- }
771
- if (validationErrors.length === 0) {
772
- validationErrors.push(...extractCelebrateErrors(res.locals.body));
1218
+ let jsonBody;
1219
+ try {
1220
+ jsonBody = JSON.parse(res.locals.body);
1221
+ } catch {
773
1222
  }
774
- if (validationErrors.length === 0) {
775
- validationErrors.push(...extractNestValidationErrors(res.locals.body));
776
- }
777
- validationErrors.forEach((error) => {
778
- client.validationErrorCounter.addValidationError({
779
- consumer: consumer == null ? void 0 : consumer.identifier,
780
- method: req.method,
781
- path: req.route.path,
782
- ...error
1223
+ if (jsonBody) {
1224
+ const validationErrors = [];
1225
+ if (validationErrors.length === 0) {
1226
+ validationErrors.push(...extractExpressValidatorErrors(jsonBody));
1227
+ }
1228
+ if (validationErrors.length === 0) {
1229
+ validationErrors.push(...extractCelebrateErrors(jsonBody));
1230
+ }
1231
+ if (validationErrors.length === 0) {
1232
+ validationErrors.push(...extractNestValidationErrors(jsonBody));
1233
+ }
1234
+ validationErrors.forEach((error) => {
1235
+ client.validationErrorCounter.addValidationError({
1236
+ consumer: consumer == null ? void 0 : consumer.identifier,
1237
+ method: req.method,
1238
+ path: req.route.path,
1239
+ ...error
1240
+ });
783
1241
  });
784
- });
1242
+ }
785
1243
  }
786
1244
  if (res.statusCode === 500 && res.locals.serverError) {
787
1245
  const serverError = res.locals.serverError;
@@ -795,6 +1253,24 @@ var getMiddleware = /* @__PURE__ */ __name((app, client) => {
795
1253
  });
796
1254
  }
797
1255
  }
1256
+ if (client.requestLogger.enabled) {
1257
+ client.requestLogger.logRequest({
1258
+ timestamp: Date.now() / 1e3,
1259
+ method: req.method,
1260
+ path,
1261
+ url: `${req.protocol}://${req.host}${req.originalUrl}`,
1262
+ headers: convertHeaders(req.headers),
1263
+ size: Number(req.get("content-length")),
1264
+ consumer: consumer == null ? void 0 : consumer.identifier,
1265
+ body: convertBody(req.body, req.get("content-type"))
1266
+ }, {
1267
+ statusCode: res.statusCode,
1268
+ responseTime: responseTime / 1e3,
1269
+ headers: convertHeaders(res.getHeaders()),
1270
+ size: Number(res.get("content-length")),
1271
+ body: convertBody(res.locals.body, res.get("content-type"))
1272
+ });
1273
+ }
798
1274
  } catch (error) {
799
1275
  client.logger.error("Error while logging request in Apitally middleware.", {
800
1276
  request: req,