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