apitally 0.15.2 → 0.16.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.
- package/README.md +2 -7
- package/dist/adonisjs/configure.cjs +33 -2
- package/dist/adonisjs/configure.cjs.map +1 -1
- package/dist/adonisjs/configure.js +33 -2
- package/dist/adonisjs/configure.js.map +1 -1
- package/dist/adonisjs/index.cjs +33 -2
- package/dist/adonisjs/index.cjs.map +1 -1
- package/dist/adonisjs/index.js +33 -2
- package/dist/adonisjs/index.js.map +1 -1
- package/dist/adonisjs/middleware.cjs.map +1 -1
- package/dist/adonisjs/middleware.js.map +1 -1
- package/dist/adonisjs/provider.cjs +118 -46
- package/dist/adonisjs/provider.cjs.map +1 -1
- package/dist/adonisjs/provider.js +118 -46
- package/dist/adonisjs/provider.js.map +1 -1
- package/dist/common/client.cjs +118 -46
- package/dist/common/client.cjs.map +1 -1
- package/dist/common/client.js +118 -46
- package/dist/common/client.js.map +1 -1
- package/dist/common/requestLogger.cjs +118 -46
- package/dist/common/requestLogger.cjs.map +1 -1
- package/dist/common/requestLogger.d.cts +5 -0
- package/dist/common/requestLogger.d.ts +5 -0
- package/dist/common/requestLogger.js +118 -46
- package/dist/common/requestLogger.js.map +1 -1
- package/dist/express/index.cjs +118 -46
- package/dist/express/index.cjs.map +1 -1
- package/dist/express/index.js +118 -46
- package/dist/express/index.js.map +1 -1
- package/dist/express/middleware.cjs +118 -46
- package/dist/express/middleware.cjs.map +1 -1
- package/dist/express/middleware.js +118 -46
- package/dist/express/middleware.js.map +1 -1
- package/dist/fastify/index.cjs +118 -46
- package/dist/fastify/index.cjs.map +1 -1
- package/dist/fastify/index.js +118 -46
- package/dist/fastify/index.js.map +1 -1
- package/dist/fastify/plugin.cjs +118 -46
- package/dist/fastify/plugin.cjs.map +1 -1
- package/dist/fastify/plugin.js +118 -46
- package/dist/fastify/plugin.js.map +1 -1
- package/dist/h3/index.cjs +118 -46
- package/dist/h3/index.cjs.map +1 -1
- package/dist/h3/index.js +118 -46
- package/dist/h3/index.js.map +1 -1
- package/dist/h3/plugin.cjs +118 -46
- package/dist/h3/plugin.cjs.map +1 -1
- package/dist/h3/plugin.js +118 -46
- package/dist/h3/plugin.js.map +1 -1
- package/dist/hono/index.cjs +118 -46
- package/dist/hono/index.cjs.map +1 -1
- package/dist/hono/index.js +118 -46
- package/dist/hono/index.js.map +1 -1
- package/dist/hono/middleware.cjs +118 -46
- package/dist/hono/middleware.cjs.map +1 -1
- package/dist/hono/middleware.js +118 -46
- package/dist/hono/middleware.js.map +1 -1
- package/dist/koa/index.cjs +118 -46
- package/dist/koa/index.cjs.map +1 -1
- package/dist/koa/index.js +118 -46
- package/dist/koa/index.js.map +1 -1
- package/dist/koa/middleware.cjs +118 -46
- package/dist/koa/middleware.cjs.map +1 -1
- package/dist/koa/middleware.js +118 -46
- package/dist/koa/middleware.js.map +1 -1
- package/dist/nestjs/index.cjs +118 -46
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.js +118 -46
- package/dist/nestjs/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -206,6 +206,16 @@ var MASK_HEADER_PATTERNS = [
|
|
|
206
206
|
/token/i,
|
|
207
207
|
/cookie/i
|
|
208
208
|
];
|
|
209
|
+
var MASK_BODY_FIELD_PATTERNS = [
|
|
210
|
+
/password/i,
|
|
211
|
+
/pwd/i,
|
|
212
|
+
/token/i,
|
|
213
|
+
/secret/i,
|
|
214
|
+
/auth/i,
|
|
215
|
+
/card[-_ ]?number/i,
|
|
216
|
+
/ccv/i,
|
|
217
|
+
/ssn/i
|
|
218
|
+
];
|
|
209
219
|
var DEFAULT_CONFIG = {
|
|
210
220
|
enabled: false,
|
|
211
221
|
logQueryParams: true,
|
|
@@ -216,6 +226,7 @@ var DEFAULT_CONFIG = {
|
|
|
216
226
|
logException: true,
|
|
217
227
|
maskQueryParams: [],
|
|
218
228
|
maskHeaders: [],
|
|
229
|
+
maskBodyFields: [],
|
|
219
230
|
excludePaths: []
|
|
220
231
|
};
|
|
221
232
|
var _RequestLogger = class _RequestLogger {
|
|
@@ -266,11 +277,23 @@ var _RequestLogger = class _RequestLogger {
|
|
|
266
277
|
];
|
|
267
278
|
return matchPatterns(name, patterns);
|
|
268
279
|
}
|
|
280
|
+
shouldMaskBodyField(name) {
|
|
281
|
+
const patterns = [
|
|
282
|
+
...this.config.maskBodyFields,
|
|
283
|
+
...MASK_BODY_FIELD_PATTERNS
|
|
284
|
+
];
|
|
285
|
+
return matchPatterns(name, patterns);
|
|
286
|
+
}
|
|
269
287
|
hasSupportedContentType(headers) {
|
|
270
288
|
var _a;
|
|
271
289
|
const contentType = (_a = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a[1];
|
|
272
290
|
return this.isSupportedContentType(contentType);
|
|
273
291
|
}
|
|
292
|
+
hasJsonContentType(headers) {
|
|
293
|
+
var _a;
|
|
294
|
+
const contentType = (_a = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a[1];
|
|
295
|
+
return contentType ? /\bjson\b/i.test(contentType) : null;
|
|
296
|
+
}
|
|
274
297
|
isSupportedContentType(contentType) {
|
|
275
298
|
return typeof contentType === "string" && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
|
|
276
299
|
}
|
|
@@ -289,6 +312,72 @@ var _RequestLogger = class _RequestLogger {
|
|
|
289
312
|
this.shouldMaskHeader(k) ? MASKED : v
|
|
290
313
|
]);
|
|
291
314
|
}
|
|
315
|
+
maskBody(data) {
|
|
316
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) {
|
|
317
|
+
const result = {};
|
|
318
|
+
for (const [key, value] of Object.entries(data)) {
|
|
319
|
+
if (typeof value === "string" && this.shouldMaskBodyField(key)) {
|
|
320
|
+
result[key] = MASKED;
|
|
321
|
+
} else {
|
|
322
|
+
result[key] = this.maskBody(value);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
if (Array.isArray(data)) {
|
|
328
|
+
return data.map((item) => this.maskBody(item));
|
|
329
|
+
}
|
|
330
|
+
return data;
|
|
331
|
+
}
|
|
332
|
+
applyMasking(item) {
|
|
333
|
+
if (this.config.maskRequestBodyCallback && item.request.body && item.request.body !== BODY_TOO_LARGE) {
|
|
334
|
+
try {
|
|
335
|
+
const maskedBody = this.config.maskRequestBodyCallback(item.request);
|
|
336
|
+
item.request.body = maskedBody ?? BODY_MASKED;
|
|
337
|
+
} catch {
|
|
338
|
+
item.request.body = void 0;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (this.config.maskResponseBodyCallback && item.response.body && item.response.body !== BODY_TOO_LARGE) {
|
|
342
|
+
try {
|
|
343
|
+
const maskedBody = this.config.maskResponseBodyCallback(item.request, item.response);
|
|
344
|
+
item.response.body = maskedBody ?? BODY_MASKED;
|
|
345
|
+
} catch {
|
|
346
|
+
item.response.body = void 0;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (item.request.body && item.request.body.length > MAX_BODY_SIZE) {
|
|
350
|
+
item.request.body = BODY_TOO_LARGE;
|
|
351
|
+
}
|
|
352
|
+
if (item.response.body && item.response.body.length > MAX_BODY_SIZE) {
|
|
353
|
+
item.response.body = BODY_TOO_LARGE;
|
|
354
|
+
}
|
|
355
|
+
for (const key of [
|
|
356
|
+
"request",
|
|
357
|
+
"response"
|
|
358
|
+
]) {
|
|
359
|
+
const bodyData = item[key].body;
|
|
360
|
+
if (!bodyData || bodyData === BODY_TOO_LARGE || bodyData === BODY_MASKED) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const headers = item[key].headers;
|
|
364
|
+
const hasJsonContent = this.hasJsonContentType(headers);
|
|
365
|
+
if (hasJsonContent === null || hasJsonContent) {
|
|
366
|
+
try {
|
|
367
|
+
const parsedBody = JSON.parse(bodyData.toString());
|
|
368
|
+
const maskedBody = this.maskBody(parsedBody);
|
|
369
|
+
item[key].body = import_buffer2.Buffer.from(JSON.stringify(maskedBody));
|
|
370
|
+
} catch {
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
item.request.headers = this.config.logRequestHeaders ? this.maskHeaders(item.request.headers) : [];
|
|
375
|
+
item.response.headers = this.config.logResponseHeaders ? this.maskHeaders(item.response.headers) : [];
|
|
376
|
+
const url = new URL(item.request.url);
|
|
377
|
+
url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
|
|
378
|
+
item.request.url = url.toString();
|
|
379
|
+
return item;
|
|
380
|
+
}
|
|
292
381
|
logRequest(request, response, error) {
|
|
293
382
|
var _a, _b, _c;
|
|
294
383
|
if (!this.enabled || this.suspendUntil !== null) return;
|
|
@@ -298,64 +387,30 @@ var _RequestLogger = class _RequestLogger {
|
|
|
298
387
|
if (this.shouldExcludePath(path) || this.shouldExcludeUserAgent(userAgent) || (((_c = (_b = this.config).excludeCallback) == null ? void 0 : _c.call(_b, request, response)) ?? false)) {
|
|
299
388
|
return;
|
|
300
389
|
}
|
|
301
|
-
url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
|
|
302
|
-
request.url = url.toString();
|
|
303
390
|
if (!this.config.logRequestBody || !this.hasSupportedContentType(request.headers)) {
|
|
304
391
|
request.body = void 0;
|
|
305
|
-
} else if (request.body) {
|
|
306
|
-
if (request.body.length > MAX_BODY_SIZE) {
|
|
307
|
-
request.body = BODY_TOO_LARGE;
|
|
308
|
-
} else if (this.config.maskRequestBodyCallback) {
|
|
309
|
-
try {
|
|
310
|
-
request.body = this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;
|
|
311
|
-
if (request.body.length > MAX_BODY_SIZE) {
|
|
312
|
-
request.body = BODY_TOO_LARGE;
|
|
313
|
-
}
|
|
314
|
-
} catch {
|
|
315
|
-
request.body = void 0;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
392
|
}
|
|
319
393
|
if (!this.config.logResponseBody || !this.hasSupportedContentType(response.headers)) {
|
|
320
394
|
response.body = void 0;
|
|
321
|
-
} else if (response.body) {
|
|
322
|
-
if (response.body.length > MAX_BODY_SIZE) {
|
|
323
|
-
response.body = BODY_TOO_LARGE;
|
|
324
|
-
} else if (this.config.maskResponseBodyCallback) {
|
|
325
|
-
try {
|
|
326
|
-
response.body = this.config.maskResponseBodyCallback(request, response) ?? BODY_MASKED;
|
|
327
|
-
if (response.body.length > MAX_BODY_SIZE) {
|
|
328
|
-
response.body = BODY_TOO_LARGE;
|
|
329
|
-
}
|
|
330
|
-
} catch {
|
|
331
|
-
response.body = void 0;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
395
|
}
|
|
335
|
-
request.
|
|
336
|
-
|
|
396
|
+
if (request.size !== void 0 && request.size < 0) {
|
|
397
|
+
request.size = void 0;
|
|
398
|
+
}
|
|
399
|
+
if (response.size !== void 0 && response.size < 0) {
|
|
400
|
+
response.size = void 0;
|
|
401
|
+
}
|
|
337
402
|
const item = {
|
|
338
403
|
uuid: (0, import_crypto2.randomUUID)(),
|
|
339
|
-
request
|
|
340
|
-
response
|
|
404
|
+
request,
|
|
405
|
+
response,
|
|
341
406
|
exception: error && this.config.logException ? {
|
|
342
407
|
type: error.name,
|
|
343
408
|
message: truncateExceptionMessage(error.message),
|
|
344
409
|
stacktrace: truncateExceptionStackTrace(error.stack || ""),
|
|
345
410
|
sentryEventId: getSentryEventId()
|
|
346
|
-
} :
|
|
411
|
+
} : void 0
|
|
347
412
|
};
|
|
348
|
-
|
|
349
|
-
item.request.body,
|
|
350
|
-
item.response.body
|
|
351
|
-
].forEach((body) => {
|
|
352
|
-
if (body) {
|
|
353
|
-
body.toJSON = function() {
|
|
354
|
-
return this.toString("base64");
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
this.pendingWrites.push(JSON.stringify(item));
|
|
413
|
+
this.pendingWrites.push(item);
|
|
359
414
|
if (this.pendingWrites.length > MAX_PENDING_WRITES) {
|
|
360
415
|
this.pendingWrites.shift();
|
|
361
416
|
}
|
|
@@ -369,9 +424,26 @@ var _RequestLogger = class _RequestLogger {
|
|
|
369
424
|
this.currentFile = new TempGzipFile();
|
|
370
425
|
}
|
|
371
426
|
while (this.pendingWrites.length > 0) {
|
|
372
|
-
|
|
427
|
+
let item = this.pendingWrites.shift();
|
|
373
428
|
if (item) {
|
|
374
|
-
|
|
429
|
+
item = this.applyMasking(item);
|
|
430
|
+
const finalItem = {
|
|
431
|
+
uuid: item.uuid,
|
|
432
|
+
request: skipEmptyValues(item.request),
|
|
433
|
+
response: skipEmptyValues(item.response),
|
|
434
|
+
exception: item.exception
|
|
435
|
+
};
|
|
436
|
+
[
|
|
437
|
+
finalItem.request.body,
|
|
438
|
+
finalItem.response.body
|
|
439
|
+
].forEach((body) => {
|
|
440
|
+
if (body) {
|
|
441
|
+
body.toJSON = function() {
|
|
442
|
+
return this.toString("base64");
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
await this.currentFile.writeLine(import_buffer2.Buffer.from(JSON.stringify(finalItem)));
|
|
375
447
|
}
|
|
376
448
|
}
|
|
377
449
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/common/requestLogger.ts","../../src/common/sentry.ts","../../src/common/serverErrorCounter.ts","../../src/common/tempGzipFile.ts"],"sourcesContent":["import AsyncLock from \"async-lock\";\nimport { Buffer } from \"buffer\";\nimport { randomUUID } from \"crypto\";\nimport { unlinkSync, writeFileSync } from \"fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"http\";\nimport { tmpdir } from \"os\";\nimport { join } from \"path\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport {\n truncateExceptionMessage,\n truncateExceptionStackTrace,\n} from \"./serverErrorCounter.js\";\nimport TempGzipFile from \"./tempGzipFile.js\";\n\nconst MAX_BODY_SIZE = 50_000; // 50 KB (uncompressed)\nconst MAX_FILE_SIZE = 1_000_000; // 1 MB (compressed)\nconst MAX_FILES = 50;\nconst MAX_PENDING_WRITES = 100;\nconst BODY_TOO_LARGE = Buffer.from(\"<body too large>\");\nconst BODY_MASKED = Buffer.from(\"<masked>\");\nconst MASKED = \"******\";\nconst ALLOWED_CONTENT_TYPES = [\n \"application/json\",\n \"application/problem+json\",\n \"application/vnd.api+json\",\n \"text/plain\",\n];\nconst EXCLUDE_PATH_PATTERNS = [\n /\\/_?healthz?$/i,\n /\\/_?health[_-]?checks?$/i,\n /\\/_?heart[_-]?beats?$/i,\n /\\/ping$/i,\n /\\/ready$/i,\n /\\/live$/i,\n];\nconst EXCLUDE_USER_AGENT_PATTERNS = [\n /health[-_ ]?check/i,\n /microsoft-azure-application-lb/i,\n /googlehc/i,\n /kube-probe/i,\n];\nconst MASK_QUERY_PARAM_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /password/i,\n /pwd/i,\n];\nconst MASK_HEADER_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /cookie/i,\n];\n\nexport type Request = {\n timestamp: number;\n method: string;\n path?: string;\n url: string;\n headers: [string, string][];\n size?: number;\n consumer?: string;\n body?: Buffer;\n};\n\nexport type Response = {\n statusCode: number;\n responseTime: number;\n headers: [string, string][];\n size?: number;\n body?: Buffer;\n};\n\nexport type RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: RegExp[];\n maskRequestBodyCallback?: (request: Request) => Buffer | null | undefined;\n maskResponseBodyCallback?: (\n request: Request,\n response: Response,\n ) => Buffer | null | undefined;\n excludePaths: RegExp[];\n excludeCallback?: (request: Request, response: Response) => boolean;\n};\n\nconst DEFAULT_CONFIG: RequestLoggingConfig = {\n enabled: false,\n logQueryParams: true,\n logRequestHeaders: false,\n logRequestBody: false,\n logResponseHeaders: true,\n logResponseBody: false,\n logException: true,\n maskQueryParams: [],\n maskHeaders: [],\n excludePaths: [],\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: string[] = [];\n private currentFile: TempGzipFile | null = null;\n private files: TempGzipFile[] = [];\n private maintainIntervalId?: NodeJS.Timeout;\n private lock = new AsyncLock();\n\n constructor(config?: Partial<RequestLoggingConfig>) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.enabled = this.config.enabled && checkWritableFs();\n\n if (this.enabled) {\n this.maintainIntervalId = setInterval(() => {\n this.maintain();\n }, 1000);\n }\n }\n\n get maxBodySize() {\n return MAX_BODY_SIZE;\n }\n\n private shouldExcludePath(urlPath: string) {\n const patterns = [...this.config.excludePaths, ...EXCLUDE_PATH_PATTERNS];\n return matchPatterns(urlPath, patterns);\n }\n\n private shouldExcludeUserAgent(userAgent?: string) {\n return userAgent\n ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS)\n : false;\n }\n\n private shouldMaskQueryParam(name: string) {\n const patterns = [\n ...this.config.maskQueryParams,\n ...MASK_QUERY_PARAM_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskHeader(name: string) {\n const patterns = [...this.config.maskHeaders, ...MASK_HEADER_PATTERNS];\n return matchPatterns(name, patterns);\n }\n\n private hasSupportedContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return this.isSupportedContentType(contentType);\n }\n\n public isSupportedContentType(contentType?: string | null) {\n return (\n typeof contentType === \"string\" &&\n ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t))\n );\n }\n\n private maskQueryParams(search: string) {\n const params = new URLSearchParams(search);\n for (const [key] of params) {\n if (this.shouldMaskQueryParam(key)) {\n params.set(key, MASKED);\n }\n }\n return params.toString();\n }\n\n private maskHeaders(headers: [string, string][]): [string, string][] {\n return headers.map(([k, v]) => [k, this.shouldMaskHeader(k) ? MASKED : v]);\n }\n\n logRequest(request: Request, response: Response, error?: Error) {\n if (!this.enabled || this.suspendUntil !== null) return;\n\n const url = new URL(request.url);\n const path = request.path ?? url.pathname;\n const userAgent = request.headers.find(\n ([k]) => k.toLowerCase() === \"user-agent\",\n )?.[1];\n\n if (\n this.shouldExcludePath(path) ||\n this.shouldExcludeUserAgent(userAgent) ||\n (this.config.excludeCallback?.(request, response) ?? false)\n ) {\n return;\n }\n\n // Process query params\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n request.url = url.toString();\n\n // Process request body\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n } else if (request.body) {\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n } else if (this.config.maskRequestBodyCallback) {\n try {\n request.body =\n this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n }\n } catch {\n request.body = undefined;\n }\n }\n }\n\n // Process response body\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n } else if (response.body) {\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n } else if (this.config.maskResponseBodyCallback) {\n try {\n response.body =\n this.config.maskResponseBodyCallback(request, response) ??\n BODY_MASKED;\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n }\n } catch {\n response.body = undefined;\n }\n }\n }\n\n // Process headers\n request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(request.headers)\n : [];\n response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(response.headers)\n : [];\n\n const item = {\n uuid: randomUUID(),\n request: skipEmptyValues(request),\n response: skipEmptyValues(response),\n exception:\n error && this.config.logException\n ? {\n type: error.name,\n message: truncateExceptionMessage(error.message),\n stacktrace: truncateExceptionStackTrace(error.stack || \"\"),\n sentryEventId: getSentryEventId(),\n }\n : null,\n };\n [item.request.body, item.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Different return type\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n this.pendingWrites.push(JSON.stringify(item));\n\n if (this.pendingWrites.length > MAX_PENDING_WRITES) {\n this.pendingWrites.shift();\n }\n }\n\n async writeToFile() {\n if (!this.enabled || this.pendingWrites.length === 0) {\n return;\n }\n return this.lock.acquire(\"file\", async () => {\n if (!this.currentFile) {\n this.currentFile = new TempGzipFile();\n }\n while (this.pendingWrites.length > 0) {\n const item = this.pendingWrites.shift();\n if (item) {\n await this.currentFile.writeLine(Buffer.from(item));\n }\n }\n });\n }\n\n getFile() {\n return this.files.shift();\n }\n\n retryFileLater(file: TempGzipFile) {\n this.files.unshift(file);\n }\n\n async rotateFile() {\n return this.lock.acquire(\"file\", async () => {\n if (this.currentFile) {\n await this.currentFile.close();\n this.files.push(this.currentFile);\n this.currentFile = null;\n }\n });\n }\n\n async maintain() {\n await this.writeToFile();\n if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {\n await this.rotateFile();\n }\n while (this.files.length > MAX_FILES) {\n const file = this.files.shift();\n file?.delete();\n }\n if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {\n this.suspendUntil = null;\n }\n }\n\n async clear() {\n this.pendingWrites = [];\n await this.rotateFile();\n this.files.forEach((file) => {\n file.delete();\n });\n this.files = [];\n }\n\n async close() {\n this.enabled = false;\n await this.clear();\n if (this.maintainIntervalId) {\n clearInterval(this.maintainIntervalId);\n }\n }\n}\n\nexport function convertHeaders(\n headers:\n | Headers\n | IncomingHttpHeaders\n | OutgoingHttpHeaders\n | Record<string, string | string[] | number | undefined>,\n) {\n if (headers instanceof Headers) {\n return Array.from(headers.entries());\n }\n return Object.entries(headers).flatMap(([key, value]) => {\n if (value === undefined) {\n return [];\n }\n if (Array.isArray(value)) {\n return value.map((v) => [key, v]);\n }\n return [[key, value.toString()]];\n }) as [string, string][];\n}\n\nexport function convertBody(body: any, contentType?: string | null) {\n if (!body || !contentType) {\n return;\n }\n try {\n if (contentType.startsWith(\"application/json\")) {\n if (isValidJsonString(body)) {\n return Buffer.from(body);\n } else {\n return Buffer.from(JSON.stringify(body));\n }\n }\n if (contentType.startsWith(\"text/\") && typeof body === \"string\") {\n return Buffer.from(body);\n }\n } catch (error) {\n return;\n }\n}\n\nfunction isValidJsonString(body: any) {\n if (typeof body !== \"string\") {\n return false;\n }\n try {\n JSON.parse(body);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction matchPatterns(value: string, patterns: RegExp[]) {\n return patterns.some((pattern) => {\n return pattern.test(value);\n });\n}\n\nfunction skipEmptyValues<T extends Record<string, any>>(data: T) {\n return Object.fromEntries(\n Object.entries(data).filter(([_, v]) => {\n if (v == null || Number.isNaN(v)) return false;\n if (Array.isArray(v) || Buffer.isBuffer(v) || typeof v === \"string\") {\n return v.length > 0;\n }\n return true;\n }),\n ) as Partial<T>;\n}\n\nfunction checkWritableFs() {\n try {\n const testPath = join(tmpdir(), `apitally-${randomUUID()}`);\n writeFileSync(testPath, \"test\");\n unlinkSync(testPath);\n return true;\n } catch (error) {\n return false;\n }\n}\n","import type * as Sentry from \"@sentry/node\";\n\nlet sentry: typeof Sentry | undefined;\n\n// Initialize Sentry when the module is loaded\n(async () => {\n try {\n sentry = await import(\"@sentry/node\");\n } catch (e) {\n // Sentry SDK is not installed, ignore\n }\n})();\n\n/**\n * Returns the last Sentry event ID if available\n */\nexport function getSentryEventId(): string | undefined {\n if (sentry && sentry.lastEventId) {\n return sentry.lastEventId();\n }\n return undefined;\n}\n","import { createHash } from \"crypto\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport { ConsumerMethodPath, ServerError, ServerErrorsItem } from \"./types.js\";\n\nconst MAX_MSG_LENGTH = 2048;\nconst MAX_STACKTRACE_LENGTH = 65536;\n\nexport default class ServerErrorCounter {\n private errorCounts: Map<string, number>;\n private errorDetails: Map<string, ConsumerMethodPath & ServerError>;\n private sentryEventIds: Map<string, string>;\n\n constructor() {\n this.errorCounts = new Map();\n this.errorDetails = new Map();\n this.sentryEventIds = new Map();\n }\n\n public addServerError(serverError: ConsumerMethodPath & ServerError) {\n const key = this.getKey(serverError);\n if (!this.errorDetails.has(key)) {\n this.errorDetails.set(key, serverError);\n }\n this.errorCounts.set(key, (this.errorCounts.get(key) || 0) + 1);\n\n const sentryEventId = getSentryEventId();\n if (sentryEventId) {\n this.sentryEventIds.set(key, sentryEventId);\n }\n }\n\n public getAndResetServerErrors() {\n const data: Array<ServerErrorsItem> = [];\n this.errorCounts.forEach((count, key) => {\n const serverError = this.errorDetails.get(key);\n if (serverError) {\n data.push({\n consumer: serverError.consumer || null,\n method: serverError.method,\n path: serverError.path,\n type: serverError.type,\n msg: truncateExceptionMessage(serverError.msg),\n traceback: truncateExceptionStackTrace(serverError.traceback),\n sentry_event_id: this.sentryEventIds.get(key) || null,\n error_count: count,\n });\n }\n });\n this.errorCounts.clear();\n this.errorDetails.clear();\n return data;\n }\n\n private getKey(serverError: ConsumerMethodPath & ServerError) {\n const hashInput = [\n serverError.consumer || \"\",\n serverError.method.toUpperCase(),\n serverError.path,\n serverError.type,\n serverError.msg.trim(),\n serverError.traceback.trim(),\n ].join(\"|\");\n return createHash(\"md5\").update(hashInput).digest(\"hex\");\n }\n}\n\nexport function truncateExceptionMessage(msg: string) {\n if (msg.length <= MAX_MSG_LENGTH) {\n return msg;\n }\n const suffix = \"... (truncated)\";\n const cutoff = MAX_MSG_LENGTH - suffix.length;\n return msg.substring(0, cutoff) + suffix;\n}\n\nexport function truncateExceptionStackTrace(stack: string) {\n const suffix = \"... (truncated) ...\";\n const cutoff = MAX_STACKTRACE_LENGTH - suffix.length;\n const lines = stack.trim().split(\"\\n\");\n const truncatedLines: string[] = [];\n let length = 0;\n for (const line of lines) {\n if (length + line.length + 1 > cutoff) {\n truncatedLines.push(suffix);\n break;\n }\n truncatedLines.push(line);\n length += line.length + 1;\n }\n return truncatedLines.join(\"\\n\");\n}\n","import { Buffer } from \"buffer\";\nimport { randomUUID } from \"crypto\";\nimport { createWriteStream, readFile, unlinkSync, WriteStream } from \"fs\";\nimport { tmpdir } from \"os\";\nimport { join } from \"path\";\nimport { createGzip, Gzip } from \"zlib\";\n\nexport default class TempGzipFile {\n public uuid: string;\n private filePath: string;\n private gzip: Gzip;\n private writeStream: WriteStream;\n private readyPromise: Promise<void>;\n private closedPromise: Promise<void>;\n\n constructor() {\n this.uuid = randomUUID();\n this.filePath = join(tmpdir(), `apitally-${this.uuid}.gz`);\n this.writeStream = createWriteStream(this.filePath);\n this.readyPromise = new Promise<void>((resolve, reject) => {\n this.writeStream.once(\"ready\", resolve);\n this.writeStream.once(\"error\", reject);\n });\n this.closedPromise = new Promise<void>((resolve, reject) => {\n this.writeStream.once(\"close\", resolve);\n this.writeStream.once(\"error\", reject);\n });\n this.gzip = createGzip();\n this.gzip.pipe(this.writeStream);\n }\n\n get size() {\n return this.writeStream.bytesWritten;\n }\n\n async writeLine(data: Buffer) {\n await this.readyPromise;\n return new Promise<void>((resolve, reject) => {\n this.gzip.write(Buffer.concat([data, Buffer.from(\"\\n\")]), (error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n }\n\n async getContent() {\n return new Promise<Buffer>((resolve, reject) => {\n readFile(this.filePath, (error, data) => {\n if (error) {\n reject(error);\n } else {\n resolve(data);\n }\n });\n });\n }\n\n async close() {\n await new Promise<void>((resolve) => {\n this.gzip.end(() => {\n resolve();\n });\n });\n await this.closedPromise;\n }\n\n async delete() {\n await this.close();\n unlinkSync(this.filePath);\n }\n}\n"],"mappings":"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","names":["import_buffer","import_crypto","import_fs","import_os","import_path","sentry","e","getSentryEventId","lastEventId","undefined","MAX_MSG_LENGTH","MAX_STACKTRACE_LENGTH","truncateExceptionMessage","msg","length","MAX_MSG_LENGTH","suffix","cutoff","substring","truncateExceptionStackTrace","stack","MAX_STACKTRACE_LENGTH","lines","trim","split","truncatedLines","line","push","join","TempGzipFile","uuid","filePath","gzip","writeStream","readyPromise","closedPromise","randomUUID","join","tmpdir","createWriteStream","Promise","resolve","reject","once","createGzip","pipe","size","bytesWritten","writeLine","data","write","Buffer","concat","from","error","getContent","readFile","close","end","delete","unlinkSync","MAX_BODY_SIZE","MAX_FILE_SIZE","MAX_FILES","MAX_PENDING_WRITES","BODY_TOO_LARGE","Buffer","from","BODY_MASKED","MASKED","ALLOWED_CONTENT_TYPES","EXCLUDE_PATH_PATTERNS","EXCLUDE_USER_AGENT_PATTERNS","MASK_QUERY_PARAM_PATTERNS","MASK_HEADER_PATTERNS","DEFAULT_CONFIG","enabled","logQueryParams","logRequestHeaders","logRequestBody","logResponseHeaders","logResponseBody","logException","maskQueryParams","maskHeaders","excludePaths","RequestLogger","config","suspendUntil","pendingWrites","currentFile","files","maintainIntervalId","lock","AsyncLock","checkWritableFs","setInterval","maintain","maxBodySize","shouldExcludePath","urlPath","patterns","matchPatterns","shouldExcludeUserAgent","userAgent","shouldMaskQueryParam","name","shouldMaskHeader","hasSupportedContentType","headers","contentType","find","k","toLowerCase","isSupportedContentType","some","t","startsWith","search","params","URLSearchParams","key","set","toString","map","v","logRequest","request","response","error","url","URL","path","pathname","excludeCallback","body","undefined","length","maskRequestBodyCallback","maskResponseBodyCallback","item","uuid","randomUUID","skipEmptyValues","exception","type","message","truncateExceptionMessage","stacktrace","truncateExceptionStackTrace","stack","sentryEventId","getSentryEventId","forEach","toJSON","push","JSON","stringify","shift","writeToFile","acquire","TempGzipFile","writeLine","getFile","retryFileLater","file","unshift","rotateFile","close","size","delete","Date","now","clear","clearInterval","convertHeaders","Headers","Array","entries","Object","flatMap","value","isArray","convertBody","isValidJsonString","parse","pattern","test","data","fromEntries","filter","_","Number","isNaN","isBuffer","testPath","join","tmpdir","writeFileSync","unlinkSync"]}
|
|
1
|
+
{"version":3,"sources":["../../src/common/requestLogger.ts","../../src/common/sentry.ts","../../src/common/serverErrorCounter.ts","../../src/common/tempGzipFile.ts"],"sourcesContent":["import AsyncLock from \"async-lock\";\nimport { Buffer } from \"buffer\";\nimport { randomUUID } from \"crypto\";\nimport { unlinkSync, writeFileSync } from \"fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"http\";\nimport { tmpdir } from \"os\";\nimport { join } from \"path\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport {\n truncateExceptionMessage,\n truncateExceptionStackTrace,\n} from \"./serverErrorCounter.js\";\nimport TempGzipFile from \"./tempGzipFile.js\";\n\nconst MAX_BODY_SIZE = 50_000; // 50 KB (uncompressed)\nconst MAX_FILE_SIZE = 1_000_000; // 1 MB (compressed)\nconst MAX_FILES = 50;\nconst MAX_PENDING_WRITES = 100;\nconst BODY_TOO_LARGE = Buffer.from(\"<body too large>\");\nconst BODY_MASKED = Buffer.from(\"<masked>\");\nconst MASKED = \"******\";\nconst ALLOWED_CONTENT_TYPES = [\n \"application/json\",\n \"application/problem+json\",\n \"application/vnd.api+json\",\n \"text/plain\",\n];\nconst EXCLUDE_PATH_PATTERNS = [\n /\\/_?healthz?$/i,\n /\\/_?health[_-]?checks?$/i,\n /\\/_?heart[_-]?beats?$/i,\n /\\/ping$/i,\n /\\/ready$/i,\n /\\/live$/i,\n];\nconst EXCLUDE_USER_AGENT_PATTERNS = [\n /health[-_ ]?check/i,\n /microsoft-azure-application-lb/i,\n /googlehc/i,\n /kube-probe/i,\n];\nconst MASK_QUERY_PARAM_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /password/i,\n /pwd/i,\n];\nconst MASK_HEADER_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /cookie/i,\n];\nconst MASK_BODY_FIELD_PATTERNS = [\n /password/i,\n /pwd/i,\n /token/i,\n /secret/i,\n /auth/i,\n /card[-_ ]?number/i,\n /ccv/i,\n /ssn/i,\n];\n\nexport type Request = {\n timestamp: number;\n method: string;\n path?: string;\n url: string;\n headers: [string, string][];\n size?: number;\n consumer?: string;\n body?: Buffer;\n};\n\nexport type Response = {\n statusCode: number;\n responseTime: number;\n headers: [string, string][];\n size?: number;\n body?: Buffer;\n};\n\nexport type RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: RegExp[];\n maskBodyFields: RegExp[];\n maskRequestBodyCallback?: (request: Request) => Buffer | null | undefined;\n maskResponseBodyCallback?: (\n request: Request,\n response: Response,\n ) => Buffer | null | undefined;\n excludePaths: RegExp[];\n excludeCallback?: (request: Request, response: Response) => boolean;\n};\n\nconst DEFAULT_CONFIG: RequestLoggingConfig = {\n enabled: false,\n logQueryParams: true,\n logRequestHeaders: false,\n logRequestBody: false,\n logResponseHeaders: true,\n logResponseBody: false,\n logException: true,\n maskQueryParams: [],\n maskHeaders: [],\n maskBodyFields: [],\n excludePaths: [],\n};\n\ntype RequestLogItem = {\n uuid: string;\n request: Request;\n response: Response;\n exception?: {\n type: string;\n message: string;\n stacktrace: string;\n sentryEventId?: string;\n };\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: RequestLogItem[] = [];\n private currentFile: TempGzipFile | null = null;\n private files: TempGzipFile[] = [];\n private maintainIntervalId?: NodeJS.Timeout;\n private lock = new AsyncLock();\n\n constructor(config?: Partial<RequestLoggingConfig>) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.enabled = this.config.enabled && checkWritableFs();\n\n if (this.enabled) {\n this.maintainIntervalId = setInterval(() => {\n this.maintain();\n }, 1000);\n }\n }\n\n get maxBodySize() {\n return MAX_BODY_SIZE;\n }\n\n private shouldExcludePath(urlPath: string) {\n const patterns = [...this.config.excludePaths, ...EXCLUDE_PATH_PATTERNS];\n return matchPatterns(urlPath, patterns);\n }\n\n private shouldExcludeUserAgent(userAgent?: string) {\n return userAgent\n ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS)\n : false;\n }\n\n private shouldMaskQueryParam(name: string) {\n const patterns = [\n ...this.config.maskQueryParams,\n ...MASK_QUERY_PARAM_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskHeader(name: string) {\n const patterns = [...this.config.maskHeaders, ...MASK_HEADER_PATTERNS];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskBodyField(name: string) {\n const patterns = [\n ...this.config.maskBodyFields,\n ...MASK_BODY_FIELD_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private hasSupportedContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return this.isSupportedContentType(contentType);\n }\n\n private hasJsonContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return contentType ? /\\bjson\\b/i.test(contentType) : null;\n }\n\n public isSupportedContentType(contentType?: string | null) {\n return (\n typeof contentType === \"string\" &&\n ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t))\n );\n }\n\n private maskQueryParams(search: string) {\n const params = new URLSearchParams(search);\n for (const [key] of params) {\n if (this.shouldMaskQueryParam(key)) {\n params.set(key, MASKED);\n }\n }\n return params.toString();\n }\n\n private maskHeaders(headers: [string, string][]): [string, string][] {\n return headers.map(([k, v]) => [k, this.shouldMaskHeader(k) ? MASKED : v]);\n }\n\n private maskBody(data: any): any {\n if (typeof data === \"object\" && data !== null && !Array.isArray(data)) {\n const result: any = {};\n for (const [key, value] of Object.entries(data)) {\n if (typeof value === \"string\" && this.shouldMaskBodyField(key)) {\n result[key] = MASKED;\n } else {\n result[key] = this.maskBody(value);\n }\n }\n return result;\n }\n if (Array.isArray(data)) {\n return data.map((item) => this.maskBody(item));\n }\n return data;\n }\n\n private applyMasking(item: RequestLogItem) {\n // Apply user-provided maskRequestBodyCallback function\n if (\n this.config.maskRequestBodyCallback &&\n item.request.body &&\n item.request.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskRequestBodyCallback(item.request);\n item.request.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.request.body = undefined;\n }\n }\n\n // Apply user-provided maskResponseBodyCallback function\n if (\n this.config.maskResponseBodyCallback &&\n item.response.body &&\n item.response.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskResponseBodyCallback(\n item.request,\n item.response,\n );\n item.response.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.response.body = undefined;\n }\n }\n\n // Check request and response body sizes\n if (item.request.body && item.request.body.length > MAX_BODY_SIZE) {\n item.request.body = BODY_TOO_LARGE;\n }\n if (item.response.body && item.response.body.length > MAX_BODY_SIZE) {\n item.response.body = BODY_TOO_LARGE;\n }\n\n // Mask request and response body fields\n for (const key of [\"request\", \"response\"] as const) {\n const bodyData = item[key].body;\n if (\n !bodyData ||\n bodyData === BODY_TOO_LARGE ||\n bodyData === BODY_MASKED\n ) {\n continue;\n }\n\n const headers = item[key].headers;\n const hasJsonContent = this.hasJsonContentType(headers);\n if (hasJsonContent === null || hasJsonContent) {\n try {\n const parsedBody = JSON.parse(bodyData.toString());\n const maskedBody = this.maskBody(parsedBody);\n item[key].body = Buffer.from(JSON.stringify(maskedBody));\n } catch {\n // If parsing fails, leave body as is\n }\n }\n }\n\n // Mask request and response headers\n item.request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(item.request.headers)\n : [];\n item.response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(item.response.headers)\n : [];\n\n // Mask query params\n const url = new URL(item.request.url);\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n item.request.url = url.toString();\n\n return item;\n }\n\n logRequest(request: Request, response: Response, error?: Error) {\n if (!this.enabled || this.suspendUntil !== null) return;\n\n const url = new URL(request.url);\n const path = request.path ?? url.pathname;\n const userAgent = request.headers.find(\n ([k]) => k.toLowerCase() === \"user-agent\",\n )?.[1];\n\n if (\n this.shouldExcludePath(path) ||\n this.shouldExcludeUserAgent(userAgent) ||\n (this.config.excludeCallback?.(request, response) ?? false)\n ) {\n return;\n }\n\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n }\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n }\n\n if (request.size !== undefined && request.size < 0) {\n request.size = undefined;\n }\n if (response.size !== undefined && response.size < 0) {\n response.size = undefined;\n }\n\n const item: RequestLogItem = {\n uuid: randomUUID(),\n request: request,\n response: response,\n exception:\n error && this.config.logException\n ? {\n type: error.name,\n message: truncateExceptionMessage(error.message),\n stacktrace: truncateExceptionStackTrace(error.stack || \"\"),\n sentryEventId: getSentryEventId(),\n }\n : undefined,\n };\n this.pendingWrites.push(item);\n\n if (this.pendingWrites.length > MAX_PENDING_WRITES) {\n this.pendingWrites.shift();\n }\n }\n\n async writeToFile() {\n if (!this.enabled || this.pendingWrites.length === 0) {\n return;\n }\n return this.lock.acquire(\"file\", async () => {\n if (!this.currentFile) {\n this.currentFile = new TempGzipFile();\n }\n while (this.pendingWrites.length > 0) {\n let item = this.pendingWrites.shift();\n if (item) {\n item = this.applyMasking(item);\n\n const finalItem = {\n uuid: item.uuid,\n request: skipEmptyValues(item.request),\n response: skipEmptyValues(item.response),\n exception: item.exception,\n };\n\n // Set up body serialization for JSON\n [finalItem.request.body, finalItem.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Override Buffer's default JSON serialization\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n\n await this.currentFile.writeLine(\n Buffer.from(JSON.stringify(finalItem)),\n );\n }\n }\n });\n }\n\n getFile() {\n return this.files.shift();\n }\n\n retryFileLater(file: TempGzipFile) {\n this.files.unshift(file);\n }\n\n async rotateFile() {\n return this.lock.acquire(\"file\", async () => {\n if (this.currentFile) {\n await this.currentFile.close();\n this.files.push(this.currentFile);\n this.currentFile = null;\n }\n });\n }\n\n async maintain() {\n await this.writeToFile();\n if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {\n await this.rotateFile();\n }\n while (this.files.length > MAX_FILES) {\n const file = this.files.shift();\n file?.delete();\n }\n if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {\n this.suspendUntil = null;\n }\n }\n\n async clear() {\n this.pendingWrites = [];\n await this.rotateFile();\n this.files.forEach((file) => {\n file.delete();\n });\n this.files = [];\n }\n\n async close() {\n this.enabled = false;\n await this.clear();\n if (this.maintainIntervalId) {\n clearInterval(this.maintainIntervalId);\n }\n }\n}\n\nexport function convertHeaders(\n headers:\n | Headers\n | IncomingHttpHeaders\n | OutgoingHttpHeaders\n | Record<string, string | string[] | number | undefined>,\n) {\n if (headers instanceof Headers) {\n return Array.from(headers.entries());\n }\n return Object.entries(headers).flatMap(([key, value]) => {\n if (value === undefined) {\n return [];\n }\n if (Array.isArray(value)) {\n return value.map((v) => [key, v]);\n }\n return [[key, value.toString()]];\n }) as [string, string][];\n}\n\nexport function convertBody(body: any, contentType?: string | null) {\n if (!body || !contentType) {\n return;\n }\n try {\n if (contentType.startsWith(\"application/json\")) {\n if (isValidJsonString(body)) {\n return Buffer.from(body);\n } else {\n return Buffer.from(JSON.stringify(body));\n }\n }\n if (contentType.startsWith(\"text/\") && typeof body === \"string\") {\n return Buffer.from(body);\n }\n } catch (error) {\n return;\n }\n}\n\nfunction isValidJsonString(body: any) {\n if (typeof body !== \"string\") {\n return false;\n }\n try {\n JSON.parse(body);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction matchPatterns(value: string, patterns: RegExp[]) {\n return patterns.some((pattern) => {\n return pattern.test(value);\n });\n}\n\nfunction skipEmptyValues<T extends Record<string, any>>(data: T) {\n return Object.fromEntries(\n Object.entries(data).filter(([_, v]) => {\n if (v == null || Number.isNaN(v)) return false;\n if (Array.isArray(v) || Buffer.isBuffer(v) || typeof v === \"string\") {\n return v.length > 0;\n }\n return true;\n }),\n ) as Partial<T>;\n}\n\nfunction checkWritableFs() {\n try {\n const testPath = join(tmpdir(), `apitally-${randomUUID()}`);\n writeFileSync(testPath, \"test\");\n unlinkSync(testPath);\n return true;\n } catch (error) {\n return false;\n }\n}\n","import type * as Sentry from \"@sentry/node\";\n\nlet sentry: typeof Sentry | undefined;\n\n// Initialize Sentry when the module is loaded\n(async () => {\n try {\n sentry = await import(\"@sentry/node\");\n } catch (e) {\n // Sentry SDK is not installed, ignore\n }\n})();\n\n/**\n * Returns the last Sentry event ID if available\n */\nexport function getSentryEventId(): string | undefined {\n if (sentry && sentry.lastEventId) {\n return sentry.lastEventId();\n }\n return undefined;\n}\n","import { createHash } from \"crypto\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport { ConsumerMethodPath, ServerError, ServerErrorsItem } from \"./types.js\";\n\nconst MAX_MSG_LENGTH = 2048;\nconst MAX_STACKTRACE_LENGTH = 65536;\n\nexport default class ServerErrorCounter {\n private errorCounts: Map<string, number>;\n private errorDetails: Map<string, ConsumerMethodPath & ServerError>;\n private sentryEventIds: Map<string, string>;\n\n constructor() {\n this.errorCounts = new Map();\n this.errorDetails = new Map();\n this.sentryEventIds = new Map();\n }\n\n public addServerError(serverError: ConsumerMethodPath & ServerError) {\n const key = this.getKey(serverError);\n if (!this.errorDetails.has(key)) {\n this.errorDetails.set(key, serverError);\n }\n this.errorCounts.set(key, (this.errorCounts.get(key) || 0) + 1);\n\n const sentryEventId = getSentryEventId();\n if (sentryEventId) {\n this.sentryEventIds.set(key, sentryEventId);\n }\n }\n\n public getAndResetServerErrors() {\n const data: Array<ServerErrorsItem> = [];\n this.errorCounts.forEach((count, key) => {\n const serverError = this.errorDetails.get(key);\n if (serverError) {\n data.push({\n consumer: serverError.consumer || null,\n method: serverError.method,\n path: serverError.path,\n type: serverError.type,\n msg: truncateExceptionMessage(serverError.msg),\n traceback: truncateExceptionStackTrace(serverError.traceback),\n sentry_event_id: this.sentryEventIds.get(key) || null,\n error_count: count,\n });\n }\n });\n this.errorCounts.clear();\n this.errorDetails.clear();\n return data;\n }\n\n private getKey(serverError: ConsumerMethodPath & ServerError) {\n const hashInput = [\n serverError.consumer || \"\",\n serverError.method.toUpperCase(),\n serverError.path,\n serverError.type,\n serverError.msg.trim(),\n serverError.traceback.trim(),\n ].join(\"|\");\n return createHash(\"md5\").update(hashInput).digest(\"hex\");\n }\n}\n\nexport function truncateExceptionMessage(msg: string) {\n if (msg.length <= MAX_MSG_LENGTH) {\n return msg;\n }\n const suffix = \"... (truncated)\";\n const cutoff = MAX_MSG_LENGTH - suffix.length;\n return msg.substring(0, cutoff) + suffix;\n}\n\nexport function truncateExceptionStackTrace(stack: string) {\n const suffix = \"... (truncated) ...\";\n const cutoff = MAX_STACKTRACE_LENGTH - suffix.length;\n const lines = stack.trim().split(\"\\n\");\n const truncatedLines: string[] = [];\n let length = 0;\n for (const line of lines) {\n if (length + line.length + 1 > cutoff) {\n truncatedLines.push(suffix);\n break;\n }\n truncatedLines.push(line);\n length += line.length + 1;\n }\n return truncatedLines.join(\"\\n\");\n}\n","import { Buffer } from \"buffer\";\nimport { randomUUID } from \"crypto\";\nimport { createWriteStream, readFile, unlinkSync, WriteStream } from \"fs\";\nimport { tmpdir } from \"os\";\nimport { join } from \"path\";\nimport { createGzip, Gzip } from \"zlib\";\n\nexport default class TempGzipFile {\n public uuid: string;\n private filePath: string;\n private gzip: Gzip;\n private writeStream: WriteStream;\n private readyPromise: Promise<void>;\n private closedPromise: Promise<void>;\n\n constructor() {\n this.uuid = randomUUID();\n this.filePath = join(tmpdir(), `apitally-${this.uuid}.gz`);\n this.writeStream = createWriteStream(this.filePath);\n this.readyPromise = new Promise<void>((resolve, reject) => {\n this.writeStream.once(\"ready\", resolve);\n this.writeStream.once(\"error\", reject);\n });\n this.closedPromise = new Promise<void>((resolve, reject) => {\n this.writeStream.once(\"close\", resolve);\n this.writeStream.once(\"error\", reject);\n });\n this.gzip = createGzip();\n this.gzip.pipe(this.writeStream);\n }\n\n get size() {\n return this.writeStream.bytesWritten;\n }\n\n async writeLine(data: Buffer) {\n await this.readyPromise;\n return new Promise<void>((resolve, reject) => {\n this.gzip.write(Buffer.concat([data, Buffer.from(\"\\n\")]), (error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n }\n\n async getContent() {\n return new Promise<Buffer>((resolve, reject) => {\n readFile(this.filePath, (error, data) => {\n if (error) {\n reject(error);\n } else {\n resolve(data);\n }\n });\n });\n }\n\n async close() {\n await new Promise<void>((resolve) => {\n this.gzip.end(() => {\n resolve();\n });\n });\n await this.closedPromise;\n }\n\n async delete() {\n await this.close();\n unlinkSync(this.filePath);\n }\n}\n"],"mappings":"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","names":["import_buffer","import_crypto","import_fs","import_os","import_path","sentry","e","getSentryEventId","lastEventId","undefined","MAX_MSG_LENGTH","MAX_STACKTRACE_LENGTH","truncateExceptionMessage","msg","length","MAX_MSG_LENGTH","suffix","cutoff","substring","truncateExceptionStackTrace","stack","MAX_STACKTRACE_LENGTH","lines","trim","split","truncatedLines","line","push","join","TempGzipFile","uuid","filePath","gzip","writeStream","readyPromise","closedPromise","randomUUID","join","tmpdir","createWriteStream","Promise","resolve","reject","once","createGzip","pipe","size","bytesWritten","writeLine","data","write","Buffer","concat","from","error","getContent","readFile","close","end","delete","unlinkSync","MAX_BODY_SIZE","MAX_FILE_SIZE","MAX_FILES","MAX_PENDING_WRITES","BODY_TOO_LARGE","Buffer","from","BODY_MASKED","MASKED","ALLOWED_CONTENT_TYPES","EXCLUDE_PATH_PATTERNS","EXCLUDE_USER_AGENT_PATTERNS","MASK_QUERY_PARAM_PATTERNS","MASK_HEADER_PATTERNS","MASK_BODY_FIELD_PATTERNS","DEFAULT_CONFIG","enabled","logQueryParams","logRequestHeaders","logRequestBody","logResponseHeaders","logResponseBody","logException","maskQueryParams","maskHeaders","maskBodyFields","excludePaths","RequestLogger","config","suspendUntil","pendingWrites","currentFile","files","maintainIntervalId","lock","AsyncLock","checkWritableFs","setInterval","maintain","maxBodySize","shouldExcludePath","urlPath","patterns","matchPatterns","shouldExcludeUserAgent","userAgent","shouldMaskQueryParam","name","shouldMaskHeader","shouldMaskBodyField","hasSupportedContentType","headers","contentType","find","k","toLowerCase","isSupportedContentType","hasJsonContentType","test","some","t","startsWith","search","params","URLSearchParams","key","set","toString","map","v","maskBody","data","Array","isArray","result","value","Object","entries","item","applyMasking","maskRequestBodyCallback","request","body","maskedBody","undefined","maskResponseBodyCallback","response","length","bodyData","hasJsonContent","parsedBody","JSON","parse","stringify","url","URL","logRequest","error","path","pathname","excludeCallback","size","uuid","randomUUID","exception","type","message","truncateExceptionMessage","stacktrace","truncateExceptionStackTrace","stack","sentryEventId","getSentryEventId","push","shift","writeToFile","acquire","TempGzipFile","finalItem","skipEmptyValues","forEach","toJSON","writeLine","getFile","retryFileLater","file","unshift","rotateFile","close","delete","Date","now","clear","clearInterval","convertHeaders","Headers","flatMap","convertBody","isValidJsonString","pattern","fromEntries","filter","_","Number","isNaN","isBuffer","testPath","join","tmpdir","writeFileSync","unlinkSync"]}
|
|
@@ -29,6 +29,7 @@ type RequestLoggingConfig = {
|
|
|
29
29
|
logException: boolean;
|
|
30
30
|
maskQueryParams: RegExp[];
|
|
31
31
|
maskHeaders: RegExp[];
|
|
32
|
+
maskBodyFields: RegExp[];
|
|
32
33
|
maskRequestBodyCallback?: (request: Request) => Buffer | null | undefined;
|
|
33
34
|
maskResponseBodyCallback?: (request: Request, response: Response) => Buffer | null | undefined;
|
|
34
35
|
excludePaths: RegExp[];
|
|
@@ -49,10 +50,14 @@ declare class RequestLogger {
|
|
|
49
50
|
private shouldExcludeUserAgent;
|
|
50
51
|
private shouldMaskQueryParam;
|
|
51
52
|
private shouldMaskHeader;
|
|
53
|
+
private shouldMaskBodyField;
|
|
52
54
|
private hasSupportedContentType;
|
|
55
|
+
private hasJsonContentType;
|
|
53
56
|
isSupportedContentType(contentType?: string | null): boolean;
|
|
54
57
|
private maskQueryParams;
|
|
55
58
|
private maskHeaders;
|
|
59
|
+
private maskBody;
|
|
60
|
+
private applyMasking;
|
|
56
61
|
logRequest(request: Request, response: Response, error?: Error): void;
|
|
57
62
|
writeToFile(): Promise<void>;
|
|
58
63
|
getFile(): TempGzipFile | undefined;
|
|
@@ -29,6 +29,7 @@ type RequestLoggingConfig = {
|
|
|
29
29
|
logException: boolean;
|
|
30
30
|
maskQueryParams: RegExp[];
|
|
31
31
|
maskHeaders: RegExp[];
|
|
32
|
+
maskBodyFields: RegExp[];
|
|
32
33
|
maskRequestBodyCallback?: (request: Request) => Buffer | null | undefined;
|
|
33
34
|
maskResponseBodyCallback?: (request: Request, response: Response) => Buffer | null | undefined;
|
|
34
35
|
excludePaths: RegExp[];
|
|
@@ -49,10 +50,14 @@ declare class RequestLogger {
|
|
|
49
50
|
private shouldExcludeUserAgent;
|
|
50
51
|
private shouldMaskQueryParam;
|
|
51
52
|
private shouldMaskHeader;
|
|
53
|
+
private shouldMaskBodyField;
|
|
52
54
|
private hasSupportedContentType;
|
|
55
|
+
private hasJsonContentType;
|
|
53
56
|
isSupportedContentType(contentType?: string | null): boolean;
|
|
54
57
|
private maskQueryParams;
|
|
55
58
|
private maskHeaders;
|
|
59
|
+
private maskBody;
|
|
60
|
+
private applyMasking;
|
|
56
61
|
logRequest(request: Request, response: Response, error?: Error): void;
|
|
57
62
|
writeToFile(): Promise<void>;
|
|
58
63
|
getFile(): TempGzipFile | undefined;
|
|
@@ -172,6 +172,16 @@ var MASK_HEADER_PATTERNS = [
|
|
|
172
172
|
/token/i,
|
|
173
173
|
/cookie/i
|
|
174
174
|
];
|
|
175
|
+
var MASK_BODY_FIELD_PATTERNS = [
|
|
176
|
+
/password/i,
|
|
177
|
+
/pwd/i,
|
|
178
|
+
/token/i,
|
|
179
|
+
/secret/i,
|
|
180
|
+
/auth/i,
|
|
181
|
+
/card[-_ ]?number/i,
|
|
182
|
+
/ccv/i,
|
|
183
|
+
/ssn/i
|
|
184
|
+
];
|
|
175
185
|
var DEFAULT_CONFIG = {
|
|
176
186
|
enabled: false,
|
|
177
187
|
logQueryParams: true,
|
|
@@ -182,6 +192,7 @@ var DEFAULT_CONFIG = {
|
|
|
182
192
|
logException: true,
|
|
183
193
|
maskQueryParams: [],
|
|
184
194
|
maskHeaders: [],
|
|
195
|
+
maskBodyFields: [],
|
|
185
196
|
excludePaths: []
|
|
186
197
|
};
|
|
187
198
|
var _RequestLogger = class _RequestLogger {
|
|
@@ -232,11 +243,23 @@ var _RequestLogger = class _RequestLogger {
|
|
|
232
243
|
];
|
|
233
244
|
return matchPatterns(name, patterns);
|
|
234
245
|
}
|
|
246
|
+
shouldMaskBodyField(name) {
|
|
247
|
+
const patterns = [
|
|
248
|
+
...this.config.maskBodyFields,
|
|
249
|
+
...MASK_BODY_FIELD_PATTERNS
|
|
250
|
+
];
|
|
251
|
+
return matchPatterns(name, patterns);
|
|
252
|
+
}
|
|
235
253
|
hasSupportedContentType(headers) {
|
|
236
254
|
var _a;
|
|
237
255
|
const contentType = (_a = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a[1];
|
|
238
256
|
return this.isSupportedContentType(contentType);
|
|
239
257
|
}
|
|
258
|
+
hasJsonContentType(headers) {
|
|
259
|
+
var _a;
|
|
260
|
+
const contentType = (_a = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a[1];
|
|
261
|
+
return contentType ? /\bjson\b/i.test(contentType) : null;
|
|
262
|
+
}
|
|
240
263
|
isSupportedContentType(contentType) {
|
|
241
264
|
return typeof contentType === "string" && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
|
|
242
265
|
}
|
|
@@ -255,6 +278,72 @@ var _RequestLogger = class _RequestLogger {
|
|
|
255
278
|
this.shouldMaskHeader(k) ? MASKED : v
|
|
256
279
|
]);
|
|
257
280
|
}
|
|
281
|
+
maskBody(data) {
|
|
282
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) {
|
|
283
|
+
const result = {};
|
|
284
|
+
for (const [key, value] of Object.entries(data)) {
|
|
285
|
+
if (typeof value === "string" && this.shouldMaskBodyField(key)) {
|
|
286
|
+
result[key] = MASKED;
|
|
287
|
+
} else {
|
|
288
|
+
result[key] = this.maskBody(value);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
if (Array.isArray(data)) {
|
|
294
|
+
return data.map((item) => this.maskBody(item));
|
|
295
|
+
}
|
|
296
|
+
return data;
|
|
297
|
+
}
|
|
298
|
+
applyMasking(item) {
|
|
299
|
+
if (this.config.maskRequestBodyCallback && item.request.body && item.request.body !== BODY_TOO_LARGE) {
|
|
300
|
+
try {
|
|
301
|
+
const maskedBody = this.config.maskRequestBodyCallback(item.request);
|
|
302
|
+
item.request.body = maskedBody ?? BODY_MASKED;
|
|
303
|
+
} catch {
|
|
304
|
+
item.request.body = void 0;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (this.config.maskResponseBodyCallback && item.response.body && item.response.body !== BODY_TOO_LARGE) {
|
|
308
|
+
try {
|
|
309
|
+
const maskedBody = this.config.maskResponseBodyCallback(item.request, item.response);
|
|
310
|
+
item.response.body = maskedBody ?? BODY_MASKED;
|
|
311
|
+
} catch {
|
|
312
|
+
item.response.body = void 0;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (item.request.body && item.request.body.length > MAX_BODY_SIZE) {
|
|
316
|
+
item.request.body = BODY_TOO_LARGE;
|
|
317
|
+
}
|
|
318
|
+
if (item.response.body && item.response.body.length > MAX_BODY_SIZE) {
|
|
319
|
+
item.response.body = BODY_TOO_LARGE;
|
|
320
|
+
}
|
|
321
|
+
for (const key of [
|
|
322
|
+
"request",
|
|
323
|
+
"response"
|
|
324
|
+
]) {
|
|
325
|
+
const bodyData = item[key].body;
|
|
326
|
+
if (!bodyData || bodyData === BODY_TOO_LARGE || bodyData === BODY_MASKED) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const headers = item[key].headers;
|
|
330
|
+
const hasJsonContent = this.hasJsonContentType(headers);
|
|
331
|
+
if (hasJsonContent === null || hasJsonContent) {
|
|
332
|
+
try {
|
|
333
|
+
const parsedBody = JSON.parse(bodyData.toString());
|
|
334
|
+
const maskedBody = this.maskBody(parsedBody);
|
|
335
|
+
item[key].body = Buffer3.from(JSON.stringify(maskedBody));
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
item.request.headers = this.config.logRequestHeaders ? this.maskHeaders(item.request.headers) : [];
|
|
341
|
+
item.response.headers = this.config.logResponseHeaders ? this.maskHeaders(item.response.headers) : [];
|
|
342
|
+
const url = new URL(item.request.url);
|
|
343
|
+
url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
|
|
344
|
+
item.request.url = url.toString();
|
|
345
|
+
return item;
|
|
346
|
+
}
|
|
258
347
|
logRequest(request, response, error) {
|
|
259
348
|
var _a, _b, _c;
|
|
260
349
|
if (!this.enabled || this.suspendUntil !== null) return;
|
|
@@ -264,64 +353,30 @@ var _RequestLogger = class _RequestLogger {
|
|
|
264
353
|
if (this.shouldExcludePath(path) || this.shouldExcludeUserAgent(userAgent) || (((_c = (_b = this.config).excludeCallback) == null ? void 0 : _c.call(_b, request, response)) ?? false)) {
|
|
265
354
|
return;
|
|
266
355
|
}
|
|
267
|
-
url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
|
|
268
|
-
request.url = url.toString();
|
|
269
356
|
if (!this.config.logRequestBody || !this.hasSupportedContentType(request.headers)) {
|
|
270
357
|
request.body = void 0;
|
|
271
|
-
} else if (request.body) {
|
|
272
|
-
if (request.body.length > MAX_BODY_SIZE) {
|
|
273
|
-
request.body = BODY_TOO_LARGE;
|
|
274
|
-
} else if (this.config.maskRequestBodyCallback) {
|
|
275
|
-
try {
|
|
276
|
-
request.body = this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;
|
|
277
|
-
if (request.body.length > MAX_BODY_SIZE) {
|
|
278
|
-
request.body = BODY_TOO_LARGE;
|
|
279
|
-
}
|
|
280
|
-
} catch {
|
|
281
|
-
request.body = void 0;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
358
|
}
|
|
285
359
|
if (!this.config.logResponseBody || !this.hasSupportedContentType(response.headers)) {
|
|
286
360
|
response.body = void 0;
|
|
287
|
-
} else if (response.body) {
|
|
288
|
-
if (response.body.length > MAX_BODY_SIZE) {
|
|
289
|
-
response.body = BODY_TOO_LARGE;
|
|
290
|
-
} else if (this.config.maskResponseBodyCallback) {
|
|
291
|
-
try {
|
|
292
|
-
response.body = this.config.maskResponseBodyCallback(request, response) ?? BODY_MASKED;
|
|
293
|
-
if (response.body.length > MAX_BODY_SIZE) {
|
|
294
|
-
response.body = BODY_TOO_LARGE;
|
|
295
|
-
}
|
|
296
|
-
} catch {
|
|
297
|
-
response.body = void 0;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
361
|
}
|
|
301
|
-
request.
|
|
302
|
-
|
|
362
|
+
if (request.size !== void 0 && request.size < 0) {
|
|
363
|
+
request.size = void 0;
|
|
364
|
+
}
|
|
365
|
+
if (response.size !== void 0 && response.size < 0) {
|
|
366
|
+
response.size = void 0;
|
|
367
|
+
}
|
|
303
368
|
const item = {
|
|
304
369
|
uuid: randomUUID2(),
|
|
305
|
-
request
|
|
306
|
-
response
|
|
370
|
+
request,
|
|
371
|
+
response,
|
|
307
372
|
exception: error && this.config.logException ? {
|
|
308
373
|
type: error.name,
|
|
309
374
|
message: truncateExceptionMessage(error.message),
|
|
310
375
|
stacktrace: truncateExceptionStackTrace(error.stack || ""),
|
|
311
376
|
sentryEventId: getSentryEventId()
|
|
312
|
-
} :
|
|
377
|
+
} : void 0
|
|
313
378
|
};
|
|
314
|
-
|
|
315
|
-
item.request.body,
|
|
316
|
-
item.response.body
|
|
317
|
-
].forEach((body) => {
|
|
318
|
-
if (body) {
|
|
319
|
-
body.toJSON = function() {
|
|
320
|
-
return this.toString("base64");
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
this.pendingWrites.push(JSON.stringify(item));
|
|
379
|
+
this.pendingWrites.push(item);
|
|
325
380
|
if (this.pendingWrites.length > MAX_PENDING_WRITES) {
|
|
326
381
|
this.pendingWrites.shift();
|
|
327
382
|
}
|
|
@@ -335,9 +390,26 @@ var _RequestLogger = class _RequestLogger {
|
|
|
335
390
|
this.currentFile = new TempGzipFile();
|
|
336
391
|
}
|
|
337
392
|
while (this.pendingWrites.length > 0) {
|
|
338
|
-
|
|
393
|
+
let item = this.pendingWrites.shift();
|
|
339
394
|
if (item) {
|
|
340
|
-
|
|
395
|
+
item = this.applyMasking(item);
|
|
396
|
+
const finalItem = {
|
|
397
|
+
uuid: item.uuid,
|
|
398
|
+
request: skipEmptyValues(item.request),
|
|
399
|
+
response: skipEmptyValues(item.response),
|
|
400
|
+
exception: item.exception
|
|
401
|
+
};
|
|
402
|
+
[
|
|
403
|
+
finalItem.request.body,
|
|
404
|
+
finalItem.response.body
|
|
405
|
+
].forEach((body) => {
|
|
406
|
+
if (body) {
|
|
407
|
+
body.toJSON = function() {
|
|
408
|
+
return this.toString("base64");
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
await this.currentFile.writeLine(Buffer3.from(JSON.stringify(finalItem)));
|
|
341
413
|
}
|
|
342
414
|
}
|
|
343
415
|
});
|