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
@@ -13,7 +13,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
13
13
  import fp from "fastify-plugin";
14
14
 
15
15
  // src/common/client.ts
16
- import { randomUUID } from "crypto";
16
+ import { randomUUID as randomUUID3 } from "crypto";
17
17
  import fetchRetry from "fetch-retry";
18
18
 
19
19
  // src/common/consumerRegistry.ts
@@ -182,6 +182,403 @@ var _RequestCounter = class _RequestCounter {
182
182
  __name(_RequestCounter, "RequestCounter");
183
183
  var RequestCounter = _RequestCounter;
184
184
 
185
+ // src/common/requestLogger.ts
186
+ import { Buffer as Buffer3 } from "buffer";
187
+ import { randomUUID as randomUUID2 } from "crypto";
188
+ import { unlinkSync as unlinkSync2, writeFileSync } from "fs";
189
+ import { tmpdir as tmpdir2 } from "os";
190
+ import { join as join2 } from "path";
191
+
192
+ // src/common/tempGzipFile.ts
193
+ import { Buffer as Buffer2 } from "buffer";
194
+ import { randomUUID } from "crypto";
195
+ import { createWriteStream, readFile, unlinkSync } from "fs";
196
+ import { tmpdir } from "os";
197
+ import { join } from "path";
198
+ import { createGzip } from "zlib";
199
+ var _TempGzipFile = class _TempGzipFile {
200
+ uuid;
201
+ filePath;
202
+ gzip;
203
+ writeStream;
204
+ readyPromise;
205
+ constructor() {
206
+ this.uuid = randomUUID();
207
+ this.filePath = join(tmpdir(), `apitally-${this.uuid}.gz`);
208
+ this.writeStream = createWriteStream(this.filePath);
209
+ this.readyPromise = new Promise((resolve, reject) => {
210
+ this.writeStream.once("ready", resolve);
211
+ this.writeStream.once("error", reject);
212
+ });
213
+ this.gzip = createGzip();
214
+ this.gzip.pipe(this.writeStream);
215
+ }
216
+ get size() {
217
+ return this.writeStream.bytesWritten;
218
+ }
219
+ async writeLine(data) {
220
+ await this.readyPromise;
221
+ return new Promise((resolve, reject) => {
222
+ this.gzip.write(Buffer2.concat([
223
+ data,
224
+ Buffer2.from("\n")
225
+ ]), (error) => {
226
+ if (error) {
227
+ reject(error);
228
+ } else {
229
+ resolve();
230
+ }
231
+ });
232
+ });
233
+ }
234
+ async getContent() {
235
+ return new Promise((resolve, reject) => {
236
+ readFile(this.filePath, (error, data) => {
237
+ if (error) {
238
+ reject(error);
239
+ } else {
240
+ resolve(data);
241
+ }
242
+ });
243
+ });
244
+ }
245
+ async close() {
246
+ return new Promise((resolve) => {
247
+ this.gzip.end(() => {
248
+ resolve();
249
+ });
250
+ });
251
+ }
252
+ async delete() {
253
+ await this.close();
254
+ unlinkSync(this.filePath);
255
+ }
256
+ };
257
+ __name(_TempGzipFile, "TempGzipFile");
258
+ var TempGzipFile = _TempGzipFile;
259
+
260
+ // src/common/requestLogger.ts
261
+ var MAX_BODY_SIZE = 5e4;
262
+ var MAX_FILE_SIZE = 1e6;
263
+ var MAX_FILES = 50;
264
+ var MAX_PENDING_WRITES = 100;
265
+ var BODY_TOO_LARGE = Buffer3.from("<body too large>");
266
+ var BODY_MASKED = Buffer3.from("<masked>");
267
+ var MASKED = "******";
268
+ var ALLOWED_CONTENT_TYPES = [
269
+ "application/json",
270
+ "text/plain"
271
+ ];
272
+ var EXCLUDE_PATH_PATTERNS = [
273
+ /\/_?healthz?$/i,
274
+ /\/_?health[_-]?checks?$/i,
275
+ /\/_?heart[_-]?beats?$/i,
276
+ /\/ping$/i,
277
+ /\/ready$/i,
278
+ /\/live$/i
279
+ ];
280
+ var EXCLUDE_USER_AGENT_PATTERNS = [
281
+ /health[-_ ]?check/i,
282
+ /microsoft-azure-application-lb/i,
283
+ /googlehc/i,
284
+ /kube-probe/i
285
+ ];
286
+ var MASK_QUERY_PARAM_PATTERNS = [
287
+ /auth/i,
288
+ /api-?key/i,
289
+ /secret/i,
290
+ /token/i,
291
+ /password/i,
292
+ /pwd/i
293
+ ];
294
+ var MASK_HEADER_PATTERNS = [
295
+ /auth/i,
296
+ /api-?key/i,
297
+ /secret/i,
298
+ /token/i,
299
+ /cookie/i
300
+ ];
301
+ var DEFAULT_CONFIG = {
302
+ enabled: false,
303
+ logQueryParams: true,
304
+ logRequestHeaders: false,
305
+ logRequestBody: false,
306
+ logResponseHeaders: true,
307
+ logResponseBody: false,
308
+ maskQueryParams: [],
309
+ maskHeaders: [],
310
+ excludePaths: []
311
+ };
312
+ var _RequestLogger = class _RequestLogger {
313
+ config;
314
+ enabled;
315
+ suspendUntil = null;
316
+ pendingWrites = [];
317
+ currentFile = null;
318
+ files = [];
319
+ maintainIntervalId;
320
+ constructor(config) {
321
+ this.config = {
322
+ ...DEFAULT_CONFIG,
323
+ ...config
324
+ };
325
+ this.enabled = this.config.enabled && checkWritableFs();
326
+ if (this.enabled) {
327
+ this.maintainIntervalId = setInterval(() => {
328
+ this.maintain();
329
+ }, 1e3);
330
+ }
331
+ }
332
+ shouldExcludePath(urlPath) {
333
+ const patterns = [
334
+ ...this.config.excludePaths,
335
+ ...EXCLUDE_PATH_PATTERNS
336
+ ];
337
+ return matchPatterns(urlPath, patterns);
338
+ }
339
+ shouldExcludeUserAgent(userAgent) {
340
+ return userAgent ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS) : false;
341
+ }
342
+ shouldMaskQueryParam(name) {
343
+ const patterns = [
344
+ ...this.config.maskQueryParams,
345
+ ...MASK_QUERY_PARAM_PATTERNS
346
+ ];
347
+ return matchPatterns(name, patterns);
348
+ }
349
+ shouldMaskHeader(name) {
350
+ const patterns = [
351
+ ...this.config.maskHeaders,
352
+ ...MASK_HEADER_PATTERNS
353
+ ];
354
+ return matchPatterns(name, patterns);
355
+ }
356
+ hasSupportedContentType(headers) {
357
+ var _a2;
358
+ const contentType = (_a2 = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a2[1];
359
+ return contentType !== void 0 && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
360
+ }
361
+ maskQueryParams(search) {
362
+ const params = new URLSearchParams(search);
363
+ for (const [key] of params) {
364
+ if (this.shouldMaskQueryParam(key)) {
365
+ params.set(key, MASKED);
366
+ }
367
+ }
368
+ return params.toString();
369
+ }
370
+ maskHeaders(headers) {
371
+ return headers.map(([k, v]) => [
372
+ k,
373
+ this.shouldMaskHeader(k) ? MASKED : v
374
+ ]);
375
+ }
376
+ logRequest(request, response) {
377
+ var _a2, _b, _c;
378
+ if (!this.enabled || this.suspendUntil !== null) return;
379
+ const url = new URL(request.url);
380
+ const path = request.path ?? url.pathname;
381
+ const userAgent = (_a2 = request.headers.find(([k]) => k.toLowerCase() === "user-agent")) == null ? void 0 : _a2[1];
382
+ if (this.shouldExcludePath(path) || this.shouldExcludeUserAgent(userAgent) || (((_c = (_b = this.config).excludeCallback) == null ? void 0 : _c.call(_b, request, response)) ?? false)) {
383
+ return;
384
+ }
385
+ url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
386
+ request.url = url.toString();
387
+ request.headers = this.config.logRequestHeaders ? this.maskHeaders(request.headers) : [];
388
+ response.headers = this.config.logResponseHeaders ? this.maskHeaders(response.headers) : [];
389
+ if (!this.config.logRequestBody || !this.hasSupportedContentType(request.headers)) {
390
+ request.body = void 0;
391
+ } else if (request.body) {
392
+ if (request.body.length > MAX_BODY_SIZE) {
393
+ request.body = BODY_TOO_LARGE;
394
+ } else if (this.config.maskRequestBodyCallback) {
395
+ try {
396
+ request.body = this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;
397
+ if (request.body.length > MAX_BODY_SIZE) {
398
+ request.body = BODY_TOO_LARGE;
399
+ }
400
+ } catch {
401
+ request.body = void 0;
402
+ }
403
+ }
404
+ }
405
+ if (!this.config.logResponseBody || !this.hasSupportedContentType(response.headers)) {
406
+ response.body = void 0;
407
+ } else if (response.body) {
408
+ if (response.body.length > MAX_BODY_SIZE) {
409
+ response.body = BODY_TOO_LARGE;
410
+ } else if (this.config.maskResponseBodyCallback) {
411
+ try {
412
+ response.body = this.config.maskResponseBodyCallback(request, response) ?? BODY_MASKED;
413
+ if (response.body.length > MAX_BODY_SIZE) {
414
+ response.body = BODY_TOO_LARGE;
415
+ }
416
+ } catch {
417
+ response.body = void 0;
418
+ }
419
+ }
420
+ }
421
+ const item = {
422
+ uuid: randomUUID2(),
423
+ request: skipEmptyValues(request),
424
+ response: skipEmptyValues(response)
425
+ };
426
+ [
427
+ item.request.body,
428
+ item.response.body
429
+ ].forEach((body) => {
430
+ if (body) {
431
+ body.toJSON = function() {
432
+ return this.toString("base64");
433
+ };
434
+ }
435
+ });
436
+ this.pendingWrites.push(JSON.stringify(item));
437
+ if (this.pendingWrites.length > MAX_PENDING_WRITES) {
438
+ this.pendingWrites.shift();
439
+ }
440
+ }
441
+ async writeToFile() {
442
+ if (!this.enabled || this.pendingWrites.length === 0) {
443
+ return;
444
+ }
445
+ if (!this.currentFile) {
446
+ this.currentFile = new TempGzipFile();
447
+ }
448
+ while (this.pendingWrites.length > 0) {
449
+ const item = this.pendingWrites.shift();
450
+ if (item) {
451
+ await this.currentFile.writeLine(Buffer3.from(item));
452
+ }
453
+ }
454
+ }
455
+ getFile() {
456
+ return this.files.shift();
457
+ }
458
+ retryFileLater(file) {
459
+ this.files.unshift(file);
460
+ }
461
+ async rotateFile() {
462
+ if (this.currentFile) {
463
+ await this.currentFile.close();
464
+ this.files.push(this.currentFile);
465
+ this.currentFile = null;
466
+ }
467
+ }
468
+ async maintain() {
469
+ await this.writeToFile();
470
+ if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {
471
+ await this.rotateFile();
472
+ }
473
+ while (this.files.length > MAX_FILES) {
474
+ const file = this.files.shift();
475
+ file == null ? void 0 : file.delete();
476
+ }
477
+ if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {
478
+ this.suspendUntil = null;
479
+ }
480
+ }
481
+ async clear() {
482
+ this.pendingWrites = [];
483
+ await this.rotateFile();
484
+ this.files.forEach((file) => {
485
+ file.delete();
486
+ });
487
+ this.files = [];
488
+ }
489
+ async close() {
490
+ this.enabled = false;
491
+ await this.clear();
492
+ if (this.maintainIntervalId) {
493
+ clearInterval(this.maintainIntervalId);
494
+ }
495
+ }
496
+ };
497
+ __name(_RequestLogger, "RequestLogger");
498
+ var RequestLogger = _RequestLogger;
499
+ function convertHeaders(headers) {
500
+ if (headers instanceof Headers) {
501
+ return Array.from(headers.entries());
502
+ }
503
+ return Object.entries(headers).flatMap(([key, value]) => {
504
+ if (value === void 0) {
505
+ return [];
506
+ }
507
+ if (Array.isArray(value)) {
508
+ return value.map((v) => [
509
+ key,
510
+ v
511
+ ]);
512
+ }
513
+ return [
514
+ [
515
+ key,
516
+ value.toString()
517
+ ]
518
+ ];
519
+ });
520
+ }
521
+ __name(convertHeaders, "convertHeaders");
522
+ function convertBody(body, contentType) {
523
+ if (!body || !contentType) {
524
+ return;
525
+ }
526
+ try {
527
+ if (contentType.startsWith("application/json")) {
528
+ if (isValidJsonString(body)) {
529
+ return Buffer3.from(body);
530
+ } else {
531
+ return Buffer3.from(JSON.stringify(body));
532
+ }
533
+ }
534
+ if (contentType.startsWith("text/") && typeof body === "string") {
535
+ return Buffer3.from(body);
536
+ }
537
+ } catch (error) {
538
+ return;
539
+ }
540
+ }
541
+ __name(convertBody, "convertBody");
542
+ function isValidJsonString(body) {
543
+ if (typeof body !== "string") {
544
+ return false;
545
+ }
546
+ try {
547
+ JSON.parse(body);
548
+ return true;
549
+ } catch {
550
+ return false;
551
+ }
552
+ }
553
+ __name(isValidJsonString, "isValidJsonString");
554
+ function matchPatterns(value, patterns) {
555
+ return patterns.some((pattern) => {
556
+ return pattern.test(value);
557
+ });
558
+ }
559
+ __name(matchPatterns, "matchPatterns");
560
+ function skipEmptyValues(data) {
561
+ return Object.fromEntries(Object.entries(data).filter(([_, v]) => {
562
+ if (v == null || Number.isNaN(v)) return false;
563
+ if (Array.isArray(v) || Buffer3.isBuffer(v) || typeof v === "string") {
564
+ return v.length > 0;
565
+ }
566
+ return true;
567
+ }));
568
+ }
569
+ __name(skipEmptyValues, "skipEmptyValues");
570
+ function checkWritableFs() {
571
+ try {
572
+ const testPath = join2(tmpdir2(), `apitally-${randomUUID2()}`);
573
+ writeFileSync(testPath, "test");
574
+ unlinkSync2(testPath);
575
+ return true;
576
+ } catch (error) {
577
+ return false;
578
+ }
579
+ }
580
+ __name(checkWritableFs, "checkWritableFs");
581
+
185
582
  // src/common/serverErrorCounter.ts
186
583
  import { createHash } from "crypto";
187
584
  var MAX_MSG_LENGTH = 2048;
@@ -354,11 +751,12 @@ var _ApitallyClient = class _ApitallyClient {
354
751
  startupData;
355
752
  startupDataSent = false;
356
753
  requestCounter;
754
+ requestLogger;
357
755
  validationErrorCounter;
358
756
  serverErrorCounter;
359
757
  consumerRegistry;
360
758
  logger;
361
- constructor({ clientId, env = "dev", logger }) {
759
+ constructor({ clientId, env = "dev", requestLoggingConfig, logger }) {
362
760
  if (_ApitallyClient.instance) {
363
761
  throw new Error("Apitally client is already initialized");
364
762
  }
@@ -371,9 +769,10 @@ var _ApitallyClient = class _ApitallyClient {
371
769
  _ApitallyClient.instance = this;
372
770
  this.clientId = clientId;
373
771
  this.env = env;
374
- this.instanceUuid = randomUUID();
772
+ this.instanceUuid = randomUUID3();
375
773
  this.syncDataQueue = [];
376
774
  this.requestCounter = new RequestCounter();
775
+ this.requestLogger = new RequestLogger(requestLoggingConfig);
377
776
  this.validationErrorCounter = new ValidationErrorCounter();
378
777
  this.serverErrorCounter = new ServerErrorCounter();
379
778
  this.consumerRegistry = new ConsumerRegistry();
@@ -395,6 +794,8 @@ var _ApitallyClient = class _ApitallyClient {
395
794
  async handleShutdown() {
396
795
  this.stopSync();
397
796
  await this.sendSyncData();
797
+ await this.sendLogData();
798
+ await this.requestLogger.close();
398
799
  _ApitallyClient.instance = void 0;
399
800
  }
400
801
  getHubUrlPrefix() {
@@ -441,7 +842,8 @@ var _ApitallyClient = class _ApitallyClient {
441
842
  async sync() {
442
843
  try {
443
844
  const promises = [
444
- this.sendSyncData()
845
+ this.sendSyncData(),
846
+ this.sendLogData()
445
847
  ];
446
848
  if (!this.startupDataSent) {
447
849
  promises.push(this.sendStartupData());
@@ -469,7 +871,7 @@ var _ApitallyClient = class _ApitallyClient {
469
871
  this.logger.debug("Sending startup data to Apitally Hub");
470
872
  const payload = {
471
873
  instance_uuid: this.instanceUuid,
472
- message_uuid: randomUUID(),
874
+ message_uuid: randomUUID3(),
473
875
  ...this.startupData
474
876
  };
475
877
  try {
@@ -491,7 +893,7 @@ var _ApitallyClient = class _ApitallyClient {
491
893
  const newPayload = {
492
894
  time_offset: 0,
493
895
  instance_uuid: this.instanceUuid,
494
- message_uuid: randomUUID(),
896
+ message_uuid: randomUUID3(),
495
897
  requests: this.requestCounter.getAndResetRequests(),
496
898
  validation_errors: this.validationErrorCounter.getAndResetValidationErrors(),
497
899
  server_errors: this.serverErrorCounter.getAndResetServerErrors(),
@@ -510,8 +912,7 @@ var _ApitallyClient = class _ApitallyClient {
510
912
  const timeOffset = Date.now() - time;
511
913
  if (timeOffset <= MAX_QUEUE_TIME) {
512
914
  if (i > 0) {
513
- const waitMs = 100 + Math.random() * 200;
514
- await new Promise((resolve) => setTimeout(resolve, waitMs));
915
+ await this.randomDelay();
515
916
  }
516
917
  payload.time_offset = timeOffset / 1e3;
517
918
  await this.sendData("sync", payload);
@@ -530,6 +931,52 @@ var _ApitallyClient = class _ApitallyClient {
530
931
  }
531
932
  }
532
933
  }
934
+ async sendLogData() {
935
+ this.logger.debug("Sending request log data to Apitally Hub");
936
+ await this.requestLogger.rotateFile();
937
+ const fetchWithRetry = fetchRetry(fetch, {
938
+ retries: 3,
939
+ retryDelay: 1e3,
940
+ retryOn: [
941
+ 408,
942
+ 429,
943
+ 500,
944
+ 502,
945
+ 503,
946
+ 504
947
+ ]
948
+ });
949
+ let i = 0;
950
+ let logFile;
951
+ while (logFile = this.requestLogger.getFile()) {
952
+ if (i > 0) {
953
+ await this.randomDelay();
954
+ }
955
+ try {
956
+ const response = await fetchWithRetry(`${this.getHubUrlPrefix()}log?uuid=${logFile.uuid}`, {
957
+ method: "POST",
958
+ body: await logFile.getContent()
959
+ });
960
+ if (response.status === 402 && response.headers.has("Retry-After")) {
961
+ const retryAfter = parseInt(response.headers.get("Retry-After") ?? "0");
962
+ if (retryAfter > 0) {
963
+ this.requestLogger.suspendUntil = Date.now() + retryAfter * 1e3;
964
+ this.requestLogger.clear();
965
+ return;
966
+ }
967
+ }
968
+ if (!response.ok) {
969
+ throw new HTTPError(response);
970
+ }
971
+ logFile.delete();
972
+ } catch (error) {
973
+ this.requestLogger.retryFileLater(logFile);
974
+ break;
975
+ }
976
+ i++;
977
+ if (i >= 10) break;
978
+ }
979
+ }
533
980
  handleHubError(error) {
534
981
  if (error instanceof HTTPError) {
535
982
  if (error.response.status === 404) {
@@ -544,6 +991,10 @@ var _ApitallyClient = class _ApitallyClient {
544
991
  }
545
992
  return false;
546
993
  }
994
+ async randomDelay() {
995
+ const delay = 100 + Math.random() * 200;
996
+ await new Promise((resolve) => setTimeout(resolve, delay));
997
+ }
547
998
  };
548
999
  __name(_ApitallyClient, "ApitallyClient");
549
1000
  __publicField(_ApitallyClient, "instance");
@@ -596,10 +1047,7 @@ var apitallyPlugin = /* @__PURE__ */ __name(async (fastify, config) => {
596
1047
  await client.handleShutdown();
597
1048
  });
598
1049
  fastify.addHook("onSend", (request, reply, payload, done) => {
599
- try {
600
- reply.payload = JSON.parse(payload);
601
- } catch (error) {
602
- }
1050
+ reply.payload = payload;
603
1051
  done();
604
1052
  });
605
1053
  fastify.addHook("onError", (request, reply, error, done) => {
@@ -609,6 +1057,7 @@ var apitallyPlugin = /* @__PURE__ */ __name(async (fastify, config) => {
609
1057
  done();
610
1058
  });
611
1059
  fastify.addHook("onResponse", (request, reply, done) => {
1060
+ var _a2;
612
1061
  if (request.method.toUpperCase() !== "OPTIONS") {
613
1062
  const consumer = getConsumer(request);
614
1063
  const path = "routeOptions" in request ? request.routeOptions.url : request.routerPath;
@@ -617,26 +1066,33 @@ var apitallyPlugin = /* @__PURE__ */ __name(async (fastify, config) => {
617
1066
  if (Array.isArray(responseSize)) {
618
1067
  responseSize = responseSize[0];
619
1068
  }
1069
+ const responseTime = getResponseTime(reply);
620
1070
  client.consumerRegistry.addOrUpdateConsumer(consumer);
621
1071
  client.requestCounter.addRequest({
622
1072
  consumer: consumer == null ? void 0 : consumer.identifier,
623
1073
  method: request.method,
624
1074
  path,
625
1075
  statusCode: reply.statusCode,
626
- responseTime: getResponseTime(reply),
1076
+ responseTime,
627
1077
  requestSize,
628
1078
  responseSize
629
1079
  });
630
- if ((reply.statusCode === 400 || reply.statusCode === 422) && reply.payload && (!reply.payload.code || reply.payload.code === "FST_ERR_VALIDATION") && typeof reply.payload.message === "string") {
631
- const validationErrors = extractAjvErrors(reply.payload.message);
632
- validationErrors.forEach((error) => {
633
- client.validationErrorCounter.addValidationError({
634
- consumer: consumer == null ? void 0 : consumer.identifier,
635
- method: request.method,
636
- path,
637
- ...error
638
- });
639
- });
1080
+ if ((reply.statusCode === 400 || reply.statusCode === 422) && reply.payload) {
1081
+ try {
1082
+ const parsedPayload = JSON.parse(reply.payload);
1083
+ if ((!parsedPayload.code || parsedPayload.code === "FST_ERR_VALIDATION") && typeof parsedPayload.message === "string") {
1084
+ const validationErrors = extractAjvErrors(parsedPayload.message);
1085
+ validationErrors.forEach((error) => {
1086
+ client.validationErrorCounter.addValidationError({
1087
+ consumer: consumer == null ? void 0 : consumer.identifier,
1088
+ method: request.method,
1089
+ path,
1090
+ ...error
1091
+ });
1092
+ });
1093
+ }
1094
+ } catch (error) {
1095
+ }
640
1096
  }
641
1097
  if (reply.statusCode === 500 && reply.serverError) {
642
1098
  client.serverErrorCounter.addServerError({
@@ -648,6 +1104,24 @@ var apitallyPlugin = /* @__PURE__ */ __name(async (fastify, config) => {
648
1104
  traceback: reply.serverError.stack || ""
649
1105
  });
650
1106
  }
1107
+ if (client.requestLogger.enabled) {
1108
+ client.requestLogger.logRequest({
1109
+ timestamp: Date.now() / 1e3,
1110
+ method: request.method,
1111
+ path,
1112
+ url: `${request.protocol}://${request.host ?? request.hostname}${request.originalUrl ?? request.url}`,
1113
+ headers: convertHeaders(request.headers),
1114
+ size: Number(requestSize),
1115
+ consumer: consumer == null ? void 0 : consumer.identifier,
1116
+ body: convertBody(request.body, request.headers["content-type"])
1117
+ }, {
1118
+ statusCode: reply.statusCode,
1119
+ responseTime: responseTime / 1e3,
1120
+ headers: convertHeaders(reply.getHeaders()),
1121
+ size: Number(responseSize),
1122
+ body: convertBody(reply.payload, (_a2 = reply.getHeader("content-type")) == null ? void 0 : _a2.toString())
1123
+ });
1124
+ }
651
1125
  }
652
1126
  done();
653
1127
  });