apitally 0.9.5 → 0.11.0

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 +398 -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 +394 -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 +419 -0
  13. package/dist/common/requestLogger.cjs.map +1 -0
  14. package/dist/common/requestLogger.d.cts +63 -0
  15. package/dist/common/requestLogger.d.ts +63 -0
  16. package/dist/common/requestLogger.js +395 -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 +500 -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 +496 -30
  36. package/dist/express/index.js.map +1 -1
  37. package/dist/express/middleware.cjs +500 -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 +496 -30
  42. package/dist/express/middleware.js.map +1 -1
  43. package/dist/fastify/index.cjs +491 -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 +487 -23
  48. package/dist/fastify/index.js.map +1 -1
  49. package/dist/fastify/plugin.cjs +491 -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 +487 -23
  54. package/dist/fastify/plugin.js.map +1 -1
  55. package/dist/hono/index.cjs +460 -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 +456 -11
  60. package/dist/hono/index.js.map +1 -1
  61. package/dist/hono/middleware.cjs +460 -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 +456 -11
  66. package/dist/hono/middleware.js.map +1 -1
  67. package/dist/koa/index.cjs +475 -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 +471 -11
  72. package/dist/koa/index.js.map +1 -1
  73. package/dist/koa/middleware.cjs +475 -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 +471 -11
  78. package/dist/koa/middleware.js.map +1 -1
  79. package/dist/nestjs/index.cjs +536 -41
  80. package/dist/nestjs/index.cjs.map +1 -1
  81. package/dist/nestjs/index.d.cts +10 -2
  82. package/dist/nestjs/index.d.ts +10 -2
  83. package/dist/nestjs/index.js +534 -37
  84. package/dist/nestjs/index.js.map +1 -1
  85. package/package.json +8 -2
@@ -39,7 +39,7 @@ module.exports = __toCommonJS(plugin_exports);
39
39
  var import_fastify_plugin = __toESM(require("fastify-plugin"), 1);
40
40
 
41
41
  // src/common/client.ts
42
- var import_crypto3 = require("crypto");
42
+ var import_crypto5 = require("crypto");
43
43
  var import_fetch_retry = __toESM(require("fetch-retry"), 1);
44
44
 
45
45
  // src/common/consumerRegistry.ts
@@ -208,8 +208,395 @@ var _RequestCounter = class _RequestCounter {
208
208
  __name(_RequestCounter, "RequestCounter");
209
209
  var RequestCounter = _RequestCounter;
210
210
 
211
- // src/common/serverErrorCounter.ts
211
+ // src/common/requestLogger.ts
212
+ var import_buffer2 = require("buffer");
213
+ var import_crypto2 = require("crypto");
214
+ var import_fs2 = require("fs");
215
+ var import_os2 = require("os");
216
+ var import_path2 = require("path");
217
+
218
+ // src/common/tempGzipFile.ts
219
+ var import_buffer = require("buffer");
212
220
  var import_crypto = require("crypto");
221
+ var import_fs = require("fs");
222
+ var import_os = require("os");
223
+ var import_path = require("path");
224
+ var import_zlib = require("zlib");
225
+ var _TempGzipFile = class _TempGzipFile {
226
+ uuid;
227
+ filePath;
228
+ gzip;
229
+ writeStream;
230
+ readyPromise;
231
+ constructor() {
232
+ this.uuid = (0, import_crypto.randomUUID)();
233
+ this.filePath = (0, import_path.join)((0, import_os.tmpdir)(), `apitally-${this.uuid}.gz`);
234
+ this.writeStream = (0, import_fs.createWriteStream)(this.filePath);
235
+ this.readyPromise = new Promise((resolve, reject) => {
236
+ this.writeStream.once("ready", resolve);
237
+ this.writeStream.once("error", reject);
238
+ });
239
+ this.gzip = (0, import_zlib.createGzip)();
240
+ this.gzip.pipe(this.writeStream);
241
+ }
242
+ get size() {
243
+ return this.writeStream.bytesWritten;
244
+ }
245
+ async writeLine(data) {
246
+ await this.readyPromise;
247
+ return new Promise((resolve, reject) => {
248
+ this.gzip.write(import_buffer.Buffer.concat([
249
+ data,
250
+ import_buffer.Buffer.from("\n")
251
+ ]), (error) => {
252
+ if (error) {
253
+ reject(error);
254
+ } else {
255
+ resolve();
256
+ }
257
+ });
258
+ });
259
+ }
260
+ async getContent() {
261
+ return new Promise((resolve, reject) => {
262
+ (0, import_fs.readFile)(this.filePath, (error, data) => {
263
+ if (error) {
264
+ reject(error);
265
+ } else {
266
+ resolve(data);
267
+ }
268
+ });
269
+ });
270
+ }
271
+ async close() {
272
+ return new Promise((resolve) => {
273
+ this.gzip.end(() => {
274
+ resolve();
275
+ });
276
+ });
277
+ }
278
+ async delete() {
279
+ await this.close();
280
+ (0, import_fs.unlinkSync)(this.filePath);
281
+ }
282
+ };
283
+ __name(_TempGzipFile, "TempGzipFile");
284
+ var TempGzipFile = _TempGzipFile;
285
+
286
+ // src/common/requestLogger.ts
287
+ var MAX_BODY_SIZE = 5e4;
288
+ var MAX_FILE_SIZE = 1e6;
289
+ var MAX_FILES = 50;
290
+ var MAX_PENDING_WRITES = 100;
291
+ var BODY_TOO_LARGE = import_buffer2.Buffer.from("<body too large>");
292
+ var BODY_MASKED = import_buffer2.Buffer.from("<masked>");
293
+ var MASKED = "******";
294
+ var ALLOWED_CONTENT_TYPES = [
295
+ "application/json",
296
+ "text/plain"
297
+ ];
298
+ var EXCLUDE_PATH_PATTERNS = [
299
+ /\/_?healthz?$/,
300
+ /\/_?health[_-]?checks?$/,
301
+ /\/_?heart[_-]?beats?$/,
302
+ /\/ping$/,
303
+ /\/ready$/,
304
+ /\/live$/
305
+ ];
306
+ var MASK_QUERY_PARAM_PATTERNS = [
307
+ /auth/i,
308
+ /api-?key/i,
309
+ /secret/i,
310
+ /token/i,
311
+ /password/i,
312
+ /pwd/i
313
+ ];
314
+ var MASK_HEADER_PATTERNS = [
315
+ /auth/i,
316
+ /api-?key/i,
317
+ /secret/i,
318
+ /token/i,
319
+ /cookie/i
320
+ ];
321
+ var DEFAULT_CONFIG = {
322
+ enabled: false,
323
+ logQueryParams: true,
324
+ logRequestHeaders: false,
325
+ logRequestBody: false,
326
+ logResponseHeaders: true,
327
+ logResponseBody: false,
328
+ maskQueryParams: [],
329
+ maskHeaders: [],
330
+ excludePaths: []
331
+ };
332
+ var _RequestLogger = class _RequestLogger {
333
+ config;
334
+ enabled;
335
+ suspendUntil = null;
336
+ pendingWrites = [];
337
+ currentFile = null;
338
+ files = [];
339
+ maintainIntervalId;
340
+ constructor(config) {
341
+ this.config = {
342
+ ...DEFAULT_CONFIG,
343
+ ...config
344
+ };
345
+ this.enabled = this.config.enabled && checkWritableFs();
346
+ if (this.enabled) {
347
+ this.maintainIntervalId = setInterval(() => {
348
+ this.maintain();
349
+ }, 1e3);
350
+ }
351
+ }
352
+ shouldExcludePath(urlPath) {
353
+ const patterns = [
354
+ ...this.config.excludePaths,
355
+ ...EXCLUDE_PATH_PATTERNS
356
+ ];
357
+ return matchPatterns(urlPath, patterns);
358
+ }
359
+ shouldMaskQueryParam(name) {
360
+ const patterns = [
361
+ ...this.config.maskQueryParams,
362
+ ...MASK_QUERY_PARAM_PATTERNS
363
+ ];
364
+ return matchPatterns(name, patterns);
365
+ }
366
+ shouldMaskHeader(name) {
367
+ const patterns = [
368
+ ...this.config.maskHeaders,
369
+ ...MASK_HEADER_PATTERNS
370
+ ];
371
+ return matchPatterns(name, patterns);
372
+ }
373
+ hasSupportedContentType(headers) {
374
+ var _a2;
375
+ const contentType = (_a2 = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a2[1];
376
+ return contentType !== void 0 && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
377
+ }
378
+ maskQueryParams(search) {
379
+ const params = new URLSearchParams(search);
380
+ for (const [key] of params) {
381
+ if (this.shouldMaskQueryParam(key)) {
382
+ params.set(key, MASKED);
383
+ }
384
+ }
385
+ return params.toString();
386
+ }
387
+ maskHeaders(headers) {
388
+ return headers.map(([k, v]) => [
389
+ k,
390
+ this.shouldMaskHeader(k) ? MASKED : v
391
+ ]);
392
+ }
393
+ logRequest(request, response) {
394
+ var _a2, _b;
395
+ if (!this.enabled || this.suspendUntil !== null) return;
396
+ const url = new URL(request.url);
397
+ const path = request.path ?? url.pathname;
398
+ if (this.shouldExcludePath(path) || (((_b = (_a2 = this.config).excludeCallback) == null ? void 0 : _b.call(_a2, request, response)) ?? false)) {
399
+ return;
400
+ }
401
+ url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
402
+ request.url = url.toString();
403
+ request.headers = this.config.logRequestHeaders ? this.maskHeaders(request.headers) : [];
404
+ response.headers = this.config.logResponseHeaders ? this.maskHeaders(response.headers) : [];
405
+ if (!this.config.logRequestBody || !this.hasSupportedContentType(request.headers)) {
406
+ request.body = void 0;
407
+ } else if (request.body) {
408
+ if (request.body.length > MAX_BODY_SIZE) {
409
+ request.body = BODY_TOO_LARGE;
410
+ } else if (this.config.maskRequestBodyCallback) {
411
+ try {
412
+ request.body = this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;
413
+ if (request.body.length > MAX_BODY_SIZE) {
414
+ request.body = BODY_TOO_LARGE;
415
+ }
416
+ } catch {
417
+ request.body = void 0;
418
+ }
419
+ }
420
+ }
421
+ if (!this.config.logResponseBody || !this.hasSupportedContentType(response.headers)) {
422
+ response.body = void 0;
423
+ } else if (response.body) {
424
+ if (response.body.length > MAX_BODY_SIZE) {
425
+ response.body = BODY_TOO_LARGE;
426
+ } else if (this.config.maskResponseBodyCallback) {
427
+ try {
428
+ response.body = this.config.maskResponseBodyCallback(request, response) ?? BODY_MASKED;
429
+ if (response.body.length > MAX_BODY_SIZE) {
430
+ response.body = BODY_TOO_LARGE;
431
+ }
432
+ } catch {
433
+ response.body = void 0;
434
+ }
435
+ }
436
+ }
437
+ const item = {
438
+ uuid: (0, import_crypto2.randomUUID)(),
439
+ request: skipEmptyValues(request),
440
+ response: skipEmptyValues(response)
441
+ };
442
+ [
443
+ item.request.body,
444
+ item.response.body
445
+ ].forEach((body) => {
446
+ if (body) {
447
+ body.toJSON = function() {
448
+ return this.toString("base64");
449
+ };
450
+ }
451
+ });
452
+ this.pendingWrites.push(JSON.stringify(item));
453
+ if (this.pendingWrites.length > MAX_PENDING_WRITES) {
454
+ this.pendingWrites.shift();
455
+ }
456
+ }
457
+ async writeToFile() {
458
+ if (!this.enabled || this.pendingWrites.length === 0) {
459
+ return;
460
+ }
461
+ if (!this.currentFile) {
462
+ this.currentFile = new TempGzipFile();
463
+ }
464
+ while (this.pendingWrites.length > 0) {
465
+ const item = this.pendingWrites.shift();
466
+ if (item) {
467
+ await this.currentFile.writeLine(import_buffer2.Buffer.from(item));
468
+ }
469
+ }
470
+ }
471
+ getFile() {
472
+ return this.files.shift();
473
+ }
474
+ retryFileLater(file) {
475
+ this.files.unshift(file);
476
+ }
477
+ async rotateFile() {
478
+ if (this.currentFile) {
479
+ await this.currentFile.close();
480
+ this.files.push(this.currentFile);
481
+ this.currentFile = null;
482
+ }
483
+ }
484
+ async maintain() {
485
+ await this.writeToFile();
486
+ if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {
487
+ await this.rotateFile();
488
+ }
489
+ while (this.files.length > MAX_FILES) {
490
+ const file = this.files.shift();
491
+ file == null ? void 0 : file.delete();
492
+ }
493
+ if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {
494
+ this.suspendUntil = null;
495
+ }
496
+ }
497
+ async clear() {
498
+ this.pendingWrites = [];
499
+ await this.rotateFile();
500
+ this.files.forEach((file) => {
501
+ file.delete();
502
+ });
503
+ this.files = [];
504
+ }
505
+ async close() {
506
+ this.enabled = false;
507
+ await this.clear();
508
+ if (this.maintainIntervalId) {
509
+ clearInterval(this.maintainIntervalId);
510
+ }
511
+ }
512
+ };
513
+ __name(_RequestLogger, "RequestLogger");
514
+ var RequestLogger = _RequestLogger;
515
+ function convertHeaders(headers) {
516
+ if (headers instanceof Headers) {
517
+ return Array.from(headers.entries());
518
+ }
519
+ return Object.entries(headers).flatMap(([key, value]) => {
520
+ if (value === void 0) {
521
+ return [];
522
+ }
523
+ if (Array.isArray(value)) {
524
+ return value.map((v) => [
525
+ key,
526
+ v
527
+ ]);
528
+ }
529
+ return [
530
+ [
531
+ key,
532
+ value.toString()
533
+ ]
534
+ ];
535
+ });
536
+ }
537
+ __name(convertHeaders, "convertHeaders");
538
+ function convertBody(body, contentType) {
539
+ if (!body || !contentType) {
540
+ return;
541
+ }
542
+ try {
543
+ if (contentType.startsWith("application/json")) {
544
+ if (isValidJsonString(body)) {
545
+ return import_buffer2.Buffer.from(body);
546
+ } else {
547
+ return import_buffer2.Buffer.from(JSON.stringify(body));
548
+ }
549
+ }
550
+ if (contentType.startsWith("text/") && typeof body === "string") {
551
+ return import_buffer2.Buffer.from(body);
552
+ }
553
+ } catch (error) {
554
+ return;
555
+ }
556
+ }
557
+ __name(convertBody, "convertBody");
558
+ function isValidJsonString(body) {
559
+ if (typeof body !== "string") {
560
+ return false;
561
+ }
562
+ try {
563
+ JSON.parse(body);
564
+ return true;
565
+ } catch {
566
+ return false;
567
+ }
568
+ }
569
+ __name(isValidJsonString, "isValidJsonString");
570
+ function matchPatterns(value, patterns) {
571
+ return patterns.some((pattern) => {
572
+ return pattern.test(value);
573
+ });
574
+ }
575
+ __name(matchPatterns, "matchPatterns");
576
+ function skipEmptyValues(data) {
577
+ return Object.fromEntries(Object.entries(data).filter(([_, v]) => {
578
+ if (v == null || Number.isNaN(v)) return false;
579
+ if (Array.isArray(v) || import_buffer2.Buffer.isBuffer(v) || typeof v === "string") {
580
+ return v.length > 0;
581
+ }
582
+ return true;
583
+ }));
584
+ }
585
+ __name(skipEmptyValues, "skipEmptyValues");
586
+ function checkWritableFs() {
587
+ try {
588
+ const testPath = (0, import_path2.join)((0, import_os2.tmpdir)(), `apitally-${(0, import_crypto2.randomUUID)()}`);
589
+ (0, import_fs2.writeFileSync)(testPath, "test");
590
+ (0, import_fs2.unlinkSync)(testPath);
591
+ return true;
592
+ } catch (error) {
593
+ return false;
594
+ }
595
+ }
596
+ __name(checkWritableFs, "checkWritableFs");
597
+
598
+ // src/common/serverErrorCounter.ts
599
+ var import_crypto3 = require("crypto");
213
600
  var MAX_MSG_LENGTH = 2048;
214
601
  var MAX_STACKTRACE_LENGTH = 65536;
215
602
  var _ServerErrorCounter = class _ServerErrorCounter {
@@ -261,7 +648,7 @@ var _ServerErrorCounter = class _ServerErrorCounter {
261
648
  serverError.msg.trim(),
262
649
  serverError.traceback.trim()
263
650
  ].join("|");
264
- return (0, import_crypto.createHash)("md5").update(hashInput).digest("hex");
651
+ return (0, import_crypto3.createHash)("md5").update(hashInput).digest("hex");
265
652
  }
266
653
  getTruncatedMessage(msg) {
267
654
  msg = msg.trim();
@@ -307,7 +694,7 @@ __name(_ServerErrorCounter, "ServerErrorCounter");
307
694
  var ServerErrorCounter = _ServerErrorCounter;
308
695
 
309
696
  // src/common/validationErrorCounter.ts
310
- var import_crypto2 = require("crypto");
697
+ var import_crypto4 = require("crypto");
311
698
  var _ValidationErrorCounter = class _ValidationErrorCounter {
312
699
  errorCounts;
313
700
  errorDetails;
@@ -351,7 +738,7 @@ var _ValidationErrorCounter = class _ValidationErrorCounter {
351
738
  validationError.msg.trim(),
352
739
  validationError.type
353
740
  ].join("|");
354
- return (0, import_crypto2.createHash)("md5").update(hashInput).digest("hex");
741
+ return (0, import_crypto4.createHash)("md5").update(hashInput).digest("hex");
355
742
  }
356
743
  };
357
744
  __name(_ValidationErrorCounter, "ValidationErrorCounter");
@@ -380,11 +767,12 @@ var _ApitallyClient = class _ApitallyClient {
380
767
  startupData;
381
768
  startupDataSent = false;
382
769
  requestCounter;
770
+ requestLogger;
383
771
  validationErrorCounter;
384
772
  serverErrorCounter;
385
773
  consumerRegistry;
386
774
  logger;
387
- constructor({ clientId, env = "dev", logger }) {
775
+ constructor({ clientId, env = "dev", requestLoggingConfig, logger }) {
388
776
  if (_ApitallyClient.instance) {
389
777
  throw new Error("Apitally client is already initialized");
390
778
  }
@@ -397,9 +785,10 @@ var _ApitallyClient = class _ApitallyClient {
397
785
  _ApitallyClient.instance = this;
398
786
  this.clientId = clientId;
399
787
  this.env = env;
400
- this.instanceUuid = (0, import_crypto3.randomUUID)();
788
+ this.instanceUuid = (0, import_crypto5.randomUUID)();
401
789
  this.syncDataQueue = [];
402
790
  this.requestCounter = new RequestCounter();
791
+ this.requestLogger = new RequestLogger(requestLoggingConfig);
403
792
  this.validationErrorCounter = new ValidationErrorCounter();
404
793
  this.serverErrorCounter = new ServerErrorCounter();
405
794
  this.consumerRegistry = new ConsumerRegistry();
@@ -421,6 +810,8 @@ var _ApitallyClient = class _ApitallyClient {
421
810
  async handleShutdown() {
422
811
  this.stopSync();
423
812
  await this.sendSyncData();
813
+ await this.sendLogData();
814
+ await this.requestLogger.close();
424
815
  _ApitallyClient.instance = void 0;
425
816
  }
426
817
  getHubUrlPrefix() {
@@ -467,7 +858,8 @@ var _ApitallyClient = class _ApitallyClient {
467
858
  async sync() {
468
859
  try {
469
860
  const promises = [
470
- this.sendSyncData()
861
+ this.sendSyncData(),
862
+ this.sendLogData()
471
863
  ];
472
864
  if (!this.startupDataSent) {
473
865
  promises.push(this.sendStartupData());
@@ -495,7 +887,7 @@ var _ApitallyClient = class _ApitallyClient {
495
887
  this.logger.debug("Sending startup data to Apitally Hub");
496
888
  const payload = {
497
889
  instance_uuid: this.instanceUuid,
498
- message_uuid: (0, import_crypto3.randomUUID)(),
890
+ message_uuid: (0, import_crypto5.randomUUID)(),
499
891
  ...this.startupData
500
892
  };
501
893
  try {
@@ -517,7 +909,7 @@ var _ApitallyClient = class _ApitallyClient {
517
909
  const newPayload = {
518
910
  time_offset: 0,
519
911
  instance_uuid: this.instanceUuid,
520
- message_uuid: (0, import_crypto3.randomUUID)(),
912
+ message_uuid: (0, import_crypto5.randomUUID)(),
521
913
  requests: this.requestCounter.getAndResetRequests(),
522
914
  validation_errors: this.validationErrorCounter.getAndResetValidationErrors(),
523
915
  server_errors: this.serverErrorCounter.getAndResetServerErrors(),
@@ -536,8 +928,7 @@ var _ApitallyClient = class _ApitallyClient {
536
928
  const timeOffset = Date.now() - time;
537
929
  if (timeOffset <= MAX_QUEUE_TIME) {
538
930
  if (i > 0) {
539
- const waitMs = 100 + Math.random() * 200;
540
- await new Promise((resolve) => setTimeout(resolve, waitMs));
931
+ await this.randomDelay();
541
932
  }
542
933
  payload.time_offset = timeOffset / 1e3;
543
934
  await this.sendData("sync", payload);
@@ -556,6 +947,52 @@ var _ApitallyClient = class _ApitallyClient {
556
947
  }
557
948
  }
558
949
  }
950
+ async sendLogData() {
951
+ this.logger.debug("Sending request log data to Apitally Hub");
952
+ await this.requestLogger.rotateFile();
953
+ const fetchWithRetry = (0, import_fetch_retry.default)(fetch, {
954
+ retries: 3,
955
+ retryDelay: 1e3,
956
+ retryOn: [
957
+ 408,
958
+ 429,
959
+ 500,
960
+ 502,
961
+ 503,
962
+ 504
963
+ ]
964
+ });
965
+ let i = 0;
966
+ let logFile;
967
+ while (logFile = this.requestLogger.getFile()) {
968
+ if (i > 0) {
969
+ await this.randomDelay();
970
+ }
971
+ try {
972
+ const response = await fetchWithRetry(`${this.getHubUrlPrefix()}log?uuid=${logFile.uuid}`, {
973
+ method: "POST",
974
+ body: await logFile.getContent()
975
+ });
976
+ if (response.status === 402 && response.headers.has("Retry-After")) {
977
+ const retryAfter = parseInt(response.headers.get("Retry-After") ?? "0");
978
+ if (retryAfter > 0) {
979
+ this.requestLogger.suspendUntil = Date.now() + retryAfter * 1e3;
980
+ this.requestLogger.clear();
981
+ return;
982
+ }
983
+ }
984
+ if (!response.ok) {
985
+ throw new HTTPError(response);
986
+ }
987
+ logFile.delete();
988
+ } catch (error) {
989
+ this.requestLogger.retryFileLater(logFile);
990
+ break;
991
+ }
992
+ i++;
993
+ if (i >= 10) break;
994
+ }
995
+ }
559
996
  handleHubError(error) {
560
997
  if (error instanceof HTTPError) {
561
998
  if (error.response.status === 404) {
@@ -570,6 +1007,10 @@ var _ApitallyClient = class _ApitallyClient {
570
1007
  }
571
1008
  return false;
572
1009
  }
1010
+ async randomDelay() {
1011
+ const delay = 100 + Math.random() * 200;
1012
+ await new Promise((resolve) => setTimeout(resolve, delay));
1013
+ }
573
1014
  };
574
1015
  __name(_ApitallyClient, "ApitallyClient");
575
1016
  __publicField(_ApitallyClient, "instance");
@@ -623,10 +1064,7 @@ var apitallyPlugin = /* @__PURE__ */ __name(async (fastify, config) => {
623
1064
  await client.handleShutdown();
624
1065
  });
625
1066
  fastify.addHook("onSend", (request, reply, payload, done) => {
626
- try {
627
- reply.payload = JSON.parse(payload);
628
- } catch (error) {
629
- }
1067
+ reply.payload = payload;
630
1068
  done();
631
1069
  });
632
1070
  fastify.addHook("onError", (request, reply, error, done) => {
@@ -636,6 +1074,7 @@ var apitallyPlugin = /* @__PURE__ */ __name(async (fastify, config) => {
636
1074
  done();
637
1075
  });
638
1076
  fastify.addHook("onResponse", (request, reply, done) => {
1077
+ var _a2;
639
1078
  if (request.method.toUpperCase() !== "OPTIONS") {
640
1079
  const consumer = getConsumer(request);
641
1080
  const path = "routeOptions" in request ? request.routeOptions.url : request.routerPath;
@@ -644,26 +1083,33 @@ var apitallyPlugin = /* @__PURE__ */ __name(async (fastify, config) => {
644
1083
  if (Array.isArray(responseSize)) {
645
1084
  responseSize = responseSize[0];
646
1085
  }
1086
+ const responseTime = getResponseTime(reply);
647
1087
  client.consumerRegistry.addOrUpdateConsumer(consumer);
648
1088
  client.requestCounter.addRequest({
649
1089
  consumer: consumer == null ? void 0 : consumer.identifier,
650
1090
  method: request.method,
651
1091
  path,
652
1092
  statusCode: reply.statusCode,
653
- responseTime: getResponseTime(reply),
1093
+ responseTime,
654
1094
  requestSize,
655
1095
  responseSize
656
1096
  });
657
- if ((reply.statusCode === 400 || reply.statusCode === 422) && reply.payload && (!reply.payload.code || reply.payload.code === "FST_ERR_VALIDATION") && typeof reply.payload.message === "string") {
658
- const validationErrors = extractAjvErrors(reply.payload.message);
659
- validationErrors.forEach((error) => {
660
- client.validationErrorCounter.addValidationError({
661
- consumer: consumer == null ? void 0 : consumer.identifier,
662
- method: request.method,
663
- path,
664
- ...error
665
- });
666
- });
1097
+ if ((reply.statusCode === 400 || reply.statusCode === 422) && reply.payload) {
1098
+ try {
1099
+ const parsedPayload = JSON.parse(reply.payload);
1100
+ if ((!parsedPayload.code || parsedPayload.code === "FST_ERR_VALIDATION") && typeof parsedPayload.message === "string") {
1101
+ const validationErrors = extractAjvErrors(parsedPayload.message);
1102
+ validationErrors.forEach((error) => {
1103
+ client.validationErrorCounter.addValidationError({
1104
+ consumer: consumer == null ? void 0 : consumer.identifier,
1105
+ method: request.method,
1106
+ path,
1107
+ ...error
1108
+ });
1109
+ });
1110
+ }
1111
+ } catch (error) {
1112
+ }
667
1113
  }
668
1114
  if (reply.statusCode === 500 && reply.serverError) {
669
1115
  client.serverErrorCounter.addServerError({
@@ -675,6 +1121,24 @@ var apitallyPlugin = /* @__PURE__ */ __name(async (fastify, config) => {
675
1121
  traceback: reply.serverError.stack || ""
676
1122
  });
677
1123
  }
1124
+ if (client.requestLogger.enabled) {
1125
+ client.requestLogger.logRequest({
1126
+ timestamp: Date.now() / 1e3,
1127
+ method: request.method,
1128
+ path,
1129
+ url: `${request.protocol}://${request.host ?? request.hostname}${request.originalUrl ?? request.url}`,
1130
+ headers: convertHeaders(request.headers),
1131
+ size: Number(requestSize),
1132
+ consumer: consumer == null ? void 0 : consumer.identifier,
1133
+ body: convertBody(request.body, request.headers["content-type"])
1134
+ }, {
1135
+ statusCode: reply.statusCode,
1136
+ responseTime: responseTime / 1e3,
1137
+ headers: convertHeaders(reply.getHeaders()),
1138
+ size: Number(responseSize),
1139
+ body: convertBody(reply.payload, (_a2 = reply.getHeader("content-type")) == null ? void 0 : _a2.toString())
1140
+ });
1141
+ }
678
1142
  }
679
1143
  done();
680
1144
  });