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
@@ -41,7 +41,7 @@ module.exports = __toCommonJS(fastify_exports);
41
41
  var import_fastify_plugin = __toESM(require("fastify-plugin"), 1);
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,395 @@ 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?$/,
302
+ /\/_?health[_-]?checks?$/,
303
+ /\/_?heart[_-]?beats?$/,
304
+ /\/ping$/,
305
+ /\/ready$/,
306
+ /\/live$/
307
+ ];
308
+ var MASK_QUERY_PARAM_PATTERNS = [
309
+ /auth/i,
310
+ /api-?key/i,
311
+ /secret/i,
312
+ /token/i,
313
+ /password/i,
314
+ /pwd/i
315
+ ];
316
+ var MASK_HEADER_PATTERNS = [
317
+ /auth/i,
318
+ /api-?key/i,
319
+ /secret/i,
320
+ /token/i,
321
+ /cookie/i
322
+ ];
323
+ var DEFAULT_CONFIG = {
324
+ enabled: false,
325
+ logQueryParams: true,
326
+ logRequestHeaders: false,
327
+ logRequestBody: false,
328
+ logResponseHeaders: true,
329
+ logResponseBody: false,
330
+ maskQueryParams: [],
331
+ maskHeaders: [],
332
+ excludePaths: []
333
+ };
334
+ var _RequestLogger = class _RequestLogger {
335
+ config;
336
+ enabled;
337
+ suspendUntil = null;
338
+ pendingWrites = [];
339
+ currentFile = null;
340
+ files = [];
341
+ maintainIntervalId;
342
+ constructor(config) {
343
+ this.config = {
344
+ ...DEFAULT_CONFIG,
345
+ ...config
346
+ };
347
+ this.enabled = this.config.enabled && checkWritableFs();
348
+ if (this.enabled) {
349
+ this.maintainIntervalId = setInterval(() => {
350
+ this.maintain();
351
+ }, 1e3);
352
+ }
353
+ }
354
+ shouldExcludePath(urlPath) {
355
+ const patterns = [
356
+ ...this.config.excludePaths,
357
+ ...EXCLUDE_PATH_PATTERNS
358
+ ];
359
+ return matchPatterns(urlPath, patterns);
360
+ }
361
+ shouldMaskQueryParam(name) {
362
+ const patterns = [
363
+ ...this.config.maskQueryParams,
364
+ ...MASK_QUERY_PARAM_PATTERNS
365
+ ];
366
+ return matchPatterns(name, patterns);
367
+ }
368
+ shouldMaskHeader(name) {
369
+ const patterns = [
370
+ ...this.config.maskHeaders,
371
+ ...MASK_HEADER_PATTERNS
372
+ ];
373
+ return matchPatterns(name, patterns);
374
+ }
375
+ hasSupportedContentType(headers) {
376
+ var _a2;
377
+ const contentType = (_a2 = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a2[1];
378
+ return contentType !== void 0 && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
379
+ }
380
+ maskQueryParams(search) {
381
+ const params = new URLSearchParams(search);
382
+ for (const [key] of params) {
383
+ if (this.shouldMaskQueryParam(key)) {
384
+ params.set(key, MASKED);
385
+ }
386
+ }
387
+ return params.toString();
388
+ }
389
+ maskHeaders(headers) {
390
+ return headers.map(([k, v]) => [
391
+ k,
392
+ this.shouldMaskHeader(k) ? MASKED : v
393
+ ]);
394
+ }
395
+ logRequest(request, response) {
396
+ var _a2, _b;
397
+ if (!this.enabled || this.suspendUntil !== null) return;
398
+ const url = new URL(request.url);
399
+ const path = request.path ?? url.pathname;
400
+ if (this.shouldExcludePath(path) || (((_b = (_a2 = this.config).excludeCallback) == null ? void 0 : _b.call(_a2, request, response)) ?? false)) {
401
+ return;
402
+ }
403
+ url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
404
+ request.url = url.toString();
405
+ request.headers = this.config.logRequestHeaders ? this.maskHeaders(request.headers) : [];
406
+ response.headers = this.config.logResponseHeaders ? this.maskHeaders(response.headers) : [];
407
+ if (!this.config.logRequestBody || !this.hasSupportedContentType(request.headers)) {
408
+ request.body = void 0;
409
+ } else if (request.body) {
410
+ if (request.body.length > MAX_BODY_SIZE) {
411
+ request.body = BODY_TOO_LARGE;
412
+ } else if (this.config.maskRequestBodyCallback) {
413
+ try {
414
+ request.body = this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;
415
+ if (request.body.length > MAX_BODY_SIZE) {
416
+ request.body = BODY_TOO_LARGE;
417
+ }
418
+ } catch {
419
+ request.body = void 0;
420
+ }
421
+ }
422
+ }
423
+ if (!this.config.logResponseBody || !this.hasSupportedContentType(response.headers)) {
424
+ response.body = void 0;
425
+ } else if (response.body) {
426
+ if (response.body.length > MAX_BODY_SIZE) {
427
+ response.body = BODY_TOO_LARGE;
428
+ } else if (this.config.maskResponseBodyCallback) {
429
+ try {
430
+ response.body = this.config.maskResponseBodyCallback(request, response) ?? BODY_MASKED;
431
+ if (response.body.length > MAX_BODY_SIZE) {
432
+ response.body = BODY_TOO_LARGE;
433
+ }
434
+ } catch {
435
+ response.body = void 0;
436
+ }
437
+ }
438
+ }
439
+ const item = {
440
+ uuid: (0, import_crypto2.randomUUID)(),
441
+ request: skipEmptyValues(request),
442
+ response: skipEmptyValues(response)
443
+ };
444
+ [
445
+ item.request.body,
446
+ item.response.body
447
+ ].forEach((body) => {
448
+ if (body) {
449
+ body.toJSON = function() {
450
+ return this.toString("base64");
451
+ };
452
+ }
453
+ });
454
+ this.pendingWrites.push(JSON.stringify(item));
455
+ if (this.pendingWrites.length > MAX_PENDING_WRITES) {
456
+ this.pendingWrites.shift();
457
+ }
458
+ }
459
+ async writeToFile() {
460
+ if (!this.enabled || this.pendingWrites.length === 0) {
461
+ return;
462
+ }
463
+ if (!this.currentFile) {
464
+ this.currentFile = new TempGzipFile();
465
+ }
466
+ while (this.pendingWrites.length > 0) {
467
+ const item = this.pendingWrites.shift();
468
+ if (item) {
469
+ await this.currentFile.writeLine(import_buffer2.Buffer.from(item));
470
+ }
471
+ }
472
+ }
473
+ getFile() {
474
+ return this.files.shift();
475
+ }
476
+ retryFileLater(file) {
477
+ this.files.unshift(file);
478
+ }
479
+ async rotateFile() {
480
+ if (this.currentFile) {
481
+ await this.currentFile.close();
482
+ this.files.push(this.currentFile);
483
+ this.currentFile = null;
484
+ }
485
+ }
486
+ async maintain() {
487
+ await this.writeToFile();
488
+ if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {
489
+ await this.rotateFile();
490
+ }
491
+ while (this.files.length > MAX_FILES) {
492
+ const file = this.files.shift();
493
+ file == null ? void 0 : file.delete();
494
+ }
495
+ if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {
496
+ this.suspendUntil = null;
497
+ }
498
+ }
499
+ async clear() {
500
+ this.pendingWrites = [];
501
+ await this.rotateFile();
502
+ this.files.forEach((file) => {
503
+ file.delete();
504
+ });
505
+ this.files = [];
506
+ }
507
+ async close() {
508
+ this.enabled = false;
509
+ await this.clear();
510
+ if (this.maintainIntervalId) {
511
+ clearInterval(this.maintainIntervalId);
512
+ }
513
+ }
514
+ };
515
+ __name(_RequestLogger, "RequestLogger");
516
+ var RequestLogger = _RequestLogger;
517
+ function convertHeaders(headers) {
518
+ if (headers instanceof Headers) {
519
+ return Array.from(headers.entries());
520
+ }
521
+ return Object.entries(headers).flatMap(([key, value]) => {
522
+ if (value === void 0) {
523
+ return [];
524
+ }
525
+ if (Array.isArray(value)) {
526
+ return value.map((v) => [
527
+ key,
528
+ v
529
+ ]);
530
+ }
531
+ return [
532
+ [
533
+ key,
534
+ value.toString()
535
+ ]
536
+ ];
537
+ });
538
+ }
539
+ __name(convertHeaders, "convertHeaders");
540
+ function convertBody(body, contentType) {
541
+ if (!body || !contentType) {
542
+ return;
543
+ }
544
+ try {
545
+ if (contentType.startsWith("application/json")) {
546
+ if (isValidJsonString(body)) {
547
+ return import_buffer2.Buffer.from(body);
548
+ } else {
549
+ return import_buffer2.Buffer.from(JSON.stringify(body));
550
+ }
551
+ }
552
+ if (contentType.startsWith("text/") && typeof body === "string") {
553
+ return import_buffer2.Buffer.from(body);
554
+ }
555
+ } catch (error) {
556
+ return;
557
+ }
558
+ }
559
+ __name(convertBody, "convertBody");
560
+ function isValidJsonString(body) {
561
+ if (typeof body !== "string") {
562
+ return false;
563
+ }
564
+ try {
565
+ JSON.parse(body);
566
+ return true;
567
+ } catch {
568
+ return false;
569
+ }
570
+ }
571
+ __name(isValidJsonString, "isValidJsonString");
572
+ function matchPatterns(value, patterns) {
573
+ return patterns.some((pattern) => {
574
+ return pattern.test(value);
575
+ });
576
+ }
577
+ __name(matchPatterns, "matchPatterns");
578
+ function skipEmptyValues(data) {
579
+ return Object.fromEntries(Object.entries(data).filter(([_, v]) => {
580
+ if (v == null || Number.isNaN(v)) return false;
581
+ if (Array.isArray(v) || import_buffer2.Buffer.isBuffer(v) || typeof v === "string") {
582
+ return v.length > 0;
583
+ }
584
+ return true;
585
+ }));
586
+ }
587
+ __name(skipEmptyValues, "skipEmptyValues");
588
+ function checkWritableFs() {
589
+ try {
590
+ const testPath = (0, import_path2.join)((0, import_os2.tmpdir)(), `apitally-${(0, import_crypto2.randomUUID)()}`);
591
+ (0, import_fs2.writeFileSync)(testPath, "test");
592
+ (0, import_fs2.unlinkSync)(testPath);
593
+ return true;
594
+ } catch (error) {
595
+ return false;
596
+ }
597
+ }
598
+ __name(checkWritableFs, "checkWritableFs");
599
+
600
+ // src/common/serverErrorCounter.ts
601
+ var import_crypto3 = require("crypto");
215
602
  var MAX_MSG_LENGTH = 2048;
216
603
  var MAX_STACKTRACE_LENGTH = 65536;
217
604
  var _ServerErrorCounter = class _ServerErrorCounter {
@@ -263,7 +650,7 @@ var _ServerErrorCounter = class _ServerErrorCounter {
263
650
  serverError.msg.trim(),
264
651
  serverError.traceback.trim()
265
652
  ].join("|");
266
- return (0, import_crypto.createHash)("md5").update(hashInput).digest("hex");
653
+ return (0, import_crypto3.createHash)("md5").update(hashInput).digest("hex");
267
654
  }
268
655
  getTruncatedMessage(msg) {
269
656
  msg = msg.trim();
@@ -309,7 +696,7 @@ __name(_ServerErrorCounter, "ServerErrorCounter");
309
696
  var ServerErrorCounter = _ServerErrorCounter;
310
697
 
311
698
  // src/common/validationErrorCounter.ts
312
- var import_crypto2 = require("crypto");
699
+ var import_crypto4 = require("crypto");
313
700
  var _ValidationErrorCounter = class _ValidationErrorCounter {
314
701
  errorCounts;
315
702
  errorDetails;
@@ -353,7 +740,7 @@ var _ValidationErrorCounter = class _ValidationErrorCounter {
353
740
  validationError.msg.trim(),
354
741
  validationError.type
355
742
  ].join("|");
356
- return (0, import_crypto2.createHash)("md5").update(hashInput).digest("hex");
743
+ return (0, import_crypto4.createHash)("md5").update(hashInput).digest("hex");
357
744
  }
358
745
  };
359
746
  __name(_ValidationErrorCounter, "ValidationErrorCounter");
@@ -382,11 +769,12 @@ var _ApitallyClient = class _ApitallyClient {
382
769
  startupData;
383
770
  startupDataSent = false;
384
771
  requestCounter;
772
+ requestLogger;
385
773
  validationErrorCounter;
386
774
  serverErrorCounter;
387
775
  consumerRegistry;
388
776
  logger;
389
- constructor({ clientId, env = "dev", logger }) {
777
+ constructor({ clientId, env = "dev", requestLoggingConfig, logger }) {
390
778
  if (_ApitallyClient.instance) {
391
779
  throw new Error("Apitally client is already initialized");
392
780
  }
@@ -399,9 +787,10 @@ var _ApitallyClient = class _ApitallyClient {
399
787
  _ApitallyClient.instance = this;
400
788
  this.clientId = clientId;
401
789
  this.env = env;
402
- this.instanceUuid = (0, import_crypto3.randomUUID)();
790
+ this.instanceUuid = (0, import_crypto5.randomUUID)();
403
791
  this.syncDataQueue = [];
404
792
  this.requestCounter = new RequestCounter();
793
+ this.requestLogger = new RequestLogger(requestLoggingConfig);
405
794
  this.validationErrorCounter = new ValidationErrorCounter();
406
795
  this.serverErrorCounter = new ServerErrorCounter();
407
796
  this.consumerRegistry = new ConsumerRegistry();
@@ -423,6 +812,8 @@ var _ApitallyClient = class _ApitallyClient {
423
812
  async handleShutdown() {
424
813
  this.stopSync();
425
814
  await this.sendSyncData();
815
+ await this.sendLogData();
816
+ await this.requestLogger.close();
426
817
  _ApitallyClient.instance = void 0;
427
818
  }
428
819
  getHubUrlPrefix() {
@@ -469,7 +860,8 @@ var _ApitallyClient = class _ApitallyClient {
469
860
  async sync() {
470
861
  try {
471
862
  const promises = [
472
- this.sendSyncData()
863
+ this.sendSyncData(),
864
+ this.sendLogData()
473
865
  ];
474
866
  if (!this.startupDataSent) {
475
867
  promises.push(this.sendStartupData());
@@ -497,7 +889,7 @@ var _ApitallyClient = class _ApitallyClient {
497
889
  this.logger.debug("Sending startup data to Apitally Hub");
498
890
  const payload = {
499
891
  instance_uuid: this.instanceUuid,
500
- message_uuid: (0, import_crypto3.randomUUID)(),
892
+ message_uuid: (0, import_crypto5.randomUUID)(),
501
893
  ...this.startupData
502
894
  };
503
895
  try {
@@ -519,7 +911,7 @@ var _ApitallyClient = class _ApitallyClient {
519
911
  const newPayload = {
520
912
  time_offset: 0,
521
913
  instance_uuid: this.instanceUuid,
522
- message_uuid: (0, import_crypto3.randomUUID)(),
914
+ message_uuid: (0, import_crypto5.randomUUID)(),
523
915
  requests: this.requestCounter.getAndResetRequests(),
524
916
  validation_errors: this.validationErrorCounter.getAndResetValidationErrors(),
525
917
  server_errors: this.serverErrorCounter.getAndResetServerErrors(),
@@ -538,8 +930,7 @@ var _ApitallyClient = class _ApitallyClient {
538
930
  const timeOffset = Date.now() - time;
539
931
  if (timeOffset <= MAX_QUEUE_TIME) {
540
932
  if (i > 0) {
541
- const waitMs = 100 + Math.random() * 200;
542
- await new Promise((resolve) => setTimeout(resolve, waitMs));
933
+ await this.randomDelay();
543
934
  }
544
935
  payload.time_offset = timeOffset / 1e3;
545
936
  await this.sendData("sync", payload);
@@ -558,6 +949,52 @@ var _ApitallyClient = class _ApitallyClient {
558
949
  }
559
950
  }
560
951
  }
952
+ async sendLogData() {
953
+ this.logger.debug("Sending request log data to Apitally Hub");
954
+ await this.requestLogger.rotateFile();
955
+ const fetchWithRetry = (0, import_fetch_retry.default)(fetch, {
956
+ retries: 3,
957
+ retryDelay: 1e3,
958
+ retryOn: [
959
+ 408,
960
+ 429,
961
+ 500,
962
+ 502,
963
+ 503,
964
+ 504
965
+ ]
966
+ });
967
+ let i = 0;
968
+ let logFile;
969
+ while (logFile = this.requestLogger.getFile()) {
970
+ if (i > 0) {
971
+ await this.randomDelay();
972
+ }
973
+ try {
974
+ const response = await fetchWithRetry(`${this.getHubUrlPrefix()}log?uuid=${logFile.uuid}`, {
975
+ method: "POST",
976
+ body: await logFile.getContent()
977
+ });
978
+ if (response.status === 402 && response.headers.has("Retry-After")) {
979
+ const retryAfter = parseInt(response.headers.get("Retry-After") ?? "0");
980
+ if (retryAfter > 0) {
981
+ this.requestLogger.suspendUntil = Date.now() + retryAfter * 1e3;
982
+ this.requestLogger.clear();
983
+ return;
984
+ }
985
+ }
986
+ if (!response.ok) {
987
+ throw new HTTPError(response);
988
+ }
989
+ logFile.delete();
990
+ } catch (error) {
991
+ this.requestLogger.retryFileLater(logFile);
992
+ break;
993
+ }
994
+ i++;
995
+ if (i >= 10) break;
996
+ }
997
+ }
561
998
  handleHubError(error) {
562
999
  if (error instanceof HTTPError) {
563
1000
  if (error.response.status === 404) {
@@ -572,6 +1009,10 @@ var _ApitallyClient = class _ApitallyClient {
572
1009
  }
573
1010
  return false;
574
1011
  }
1012
+ async randomDelay() {
1013
+ const delay = 100 + Math.random() * 200;
1014
+ await new Promise((resolve) => setTimeout(resolve, delay));
1015
+ }
575
1016
  };
576
1017
  __name(_ApitallyClient, "ApitallyClient");
577
1018
  __publicField(_ApitallyClient, "instance");
@@ -625,10 +1066,7 @@ var apitallyPlugin = /* @__PURE__ */ __name(async (fastify, config) => {
625
1066
  await client.handleShutdown();
626
1067
  });
627
1068
  fastify.addHook("onSend", (request, reply, payload, done) => {
628
- try {
629
- reply.payload = JSON.parse(payload);
630
- } catch (error) {
631
- }
1069
+ reply.payload = payload;
632
1070
  done();
633
1071
  });
634
1072
  fastify.addHook("onError", (request, reply, error, done) => {
@@ -638,6 +1076,7 @@ var apitallyPlugin = /* @__PURE__ */ __name(async (fastify, config) => {
638
1076
  done();
639
1077
  });
640
1078
  fastify.addHook("onResponse", (request, reply, done) => {
1079
+ var _a2;
641
1080
  if (request.method.toUpperCase() !== "OPTIONS") {
642
1081
  const consumer = getConsumer(request);
643
1082
  const path = "routeOptions" in request ? request.routeOptions.url : request.routerPath;
@@ -646,26 +1085,33 @@ var apitallyPlugin = /* @__PURE__ */ __name(async (fastify, config) => {
646
1085
  if (Array.isArray(responseSize)) {
647
1086
  responseSize = responseSize[0];
648
1087
  }
1088
+ const responseTime = getResponseTime(reply);
649
1089
  client.consumerRegistry.addOrUpdateConsumer(consumer);
650
1090
  client.requestCounter.addRequest({
651
1091
  consumer: consumer == null ? void 0 : consumer.identifier,
652
1092
  method: request.method,
653
1093
  path,
654
1094
  statusCode: reply.statusCode,
655
- responseTime: getResponseTime(reply),
1095
+ responseTime,
656
1096
  requestSize,
657
1097
  responseSize
658
1098
  });
659
- if ((reply.statusCode === 400 || reply.statusCode === 422) && reply.payload && (!reply.payload.code || reply.payload.code === "FST_ERR_VALIDATION") && typeof reply.payload.message === "string") {
660
- const validationErrors = extractAjvErrors(reply.payload.message);
661
- validationErrors.forEach((error) => {
662
- client.validationErrorCounter.addValidationError({
663
- consumer: consumer == null ? void 0 : consumer.identifier,
664
- method: request.method,
665
- path,
666
- ...error
667
- });
668
- });
1099
+ if ((reply.statusCode === 400 || reply.statusCode === 422) && reply.payload) {
1100
+ try {
1101
+ const parsedPayload = JSON.parse(reply.payload);
1102
+ if ((!parsedPayload.code || parsedPayload.code === "FST_ERR_VALIDATION") && typeof parsedPayload.message === "string") {
1103
+ const validationErrors = extractAjvErrors(parsedPayload.message);
1104
+ validationErrors.forEach((error) => {
1105
+ client.validationErrorCounter.addValidationError({
1106
+ consumer: consumer == null ? void 0 : consumer.identifier,
1107
+ method: request.method,
1108
+ path,
1109
+ ...error
1110
+ });
1111
+ });
1112
+ }
1113
+ } catch (error) {
1114
+ }
669
1115
  }
670
1116
  if (reply.statusCode === 500 && reply.serverError) {
671
1117
  client.serverErrorCounter.addServerError({
@@ -677,6 +1123,24 @@ var apitallyPlugin = /* @__PURE__ */ __name(async (fastify, config) => {
677
1123
  traceback: reply.serverError.stack || ""
678
1124
  });
679
1125
  }
1126
+ if (client.requestLogger.enabled) {
1127
+ client.requestLogger.logRequest({
1128
+ timestamp: Date.now() / 1e3,
1129
+ method: request.method,
1130
+ path,
1131
+ url: `${request.protocol}://${request.host ?? request.hostname}${request.originalUrl ?? request.url}`,
1132
+ headers: convertHeaders(request.headers),
1133
+ size: Number(requestSize),
1134
+ consumer: consumer == null ? void 0 : consumer.identifier,
1135
+ body: convertBody(request.body, request.headers["content-type"])
1136
+ }, {
1137
+ statusCode: reply.statusCode,
1138
+ responseTime: responseTime / 1e3,
1139
+ headers: convertHeaders(reply.getHeaders()),
1140
+ size: Number(responseSize),
1141
+ body: convertBody(reply.payload, (_a2 = reply.getHeader("content-type")) == null ? void 0 : _a2.toString())
1142
+ });
1143
+ }
680
1144
  }
681
1145
  done();
682
1146
  });