apitally
Version:
Simple API monitoring & analytics for REST APIs built with Express, Fastify, NestJS, AdonisJS, Hono, H3, Elysia, Hapi, and Koa.
609 lines (604 loc) • 18.1 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/common/requestLogger.ts
var requestLogger_exports = {};
__export(requestLogger_exports, {
convertBody: () => convertBody,
convertHeaders: () => convertHeaders,
default: () => RequestLogger
});
module.exports = __toCommonJS(requestLogger_exports);
var import_async_lock = __toESM(require("async-lock"), 1);
var import_node_buffer2 = require("buffer");
var import_node_crypto3 = require("crypto");
var import_node_fs2 = require("fs");
var import_node_os2 = require("os");
var import_node_path2 = require("path");
// src/common/sentry.ts
var sentry;
(async () => {
try {
sentry = await import("@sentry/node");
} catch (e) {
}
})();
function getSentryEventId() {
if (sentry && sentry.lastEventId) {
return sentry.lastEventId();
}
return void 0;
}
__name(getSentryEventId, "getSentryEventId");
// src/common/serverErrorCounter.ts
var import_node_crypto = require("crypto");
var MAX_MSG_LENGTH = 2048;
var MAX_STACKTRACE_LENGTH = 65536;
function truncateExceptionMessage(msg) {
if (msg.length <= MAX_MSG_LENGTH) {
return msg;
}
const suffix = "... (truncated)";
const cutoff = MAX_MSG_LENGTH - suffix.length;
return msg.substring(0, cutoff) + suffix;
}
__name(truncateExceptionMessage, "truncateExceptionMessage");
function truncateExceptionStackTrace(stack) {
const suffix = "... (truncated) ...";
const cutoff = MAX_STACKTRACE_LENGTH - suffix.length;
const lines = stack.trim().split("\n");
const truncatedLines = [];
let length = 0;
for (const line of lines) {
if (length + line.length + 1 > cutoff) {
truncatedLines.push(suffix);
break;
}
truncatedLines.push(line);
length += line.length + 1;
}
return truncatedLines.join("\n");
}
__name(truncateExceptionStackTrace, "truncateExceptionStackTrace");
// src/common/tempGzipFile.ts
var import_node_buffer = require("buffer");
var import_node_crypto2 = require("crypto");
var import_node_fs = require("fs");
var import_promises = require("fs/promises");
var import_node_os = require("os");
var import_node_path = require("path");
var import_node_zlib = require("zlib");
var _TempGzipFile = class _TempGzipFile {
uuid;
filePath;
gzip;
writeStream;
readyPromise;
closedPromise;
constructor() {
this.uuid = (0, import_node_crypto2.randomUUID)();
this.filePath = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `apitally-${this.uuid}.gz`);
this.writeStream = (0, import_node_fs.createWriteStream)(this.filePath);
this.readyPromise = new Promise((resolve, reject) => {
this.writeStream.once("ready", resolve);
this.writeStream.once("error", reject);
});
this.closedPromise = new Promise((resolve, reject) => {
this.writeStream.once("close", resolve);
this.writeStream.once("error", reject);
});
this.gzip = (0, import_node_zlib.createGzip)();
this.gzip.pipe(this.writeStream);
}
get size() {
return this.writeStream.bytesWritten;
}
async writeLine(data) {
await this.readyPromise;
return new Promise((resolve, reject) => {
this.gzip.write(import_node_buffer.Buffer.concat([
data,
import_node_buffer.Buffer.from("\n")
]), (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
async getContent() {
return new Promise((resolve, reject) => {
(0, import_node_fs.readFile)(this.filePath, (error, data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}
async close() {
await new Promise((resolve) => {
this.gzip.end(() => {
resolve();
});
});
await this.closedPromise;
}
async delete() {
await this.close();
await (0, import_promises.unlink)(this.filePath);
}
};
__name(_TempGzipFile, "TempGzipFile");
var TempGzipFile = _TempGzipFile;
// src/common/requestLogger.ts
var MAX_BODY_SIZE = 5e4;
var MAX_FILE_SIZE = 1e6;
var MAX_FILES = 50;
var MAX_PENDING_WRITES = 100;
var MAX_LOG_MSG_LENGTH = 2048;
var BODY_TOO_LARGE = import_node_buffer2.Buffer.from("<body too large>");
var BODY_MASKED = import_node_buffer2.Buffer.from("<masked>");
var MASKED = "******";
var ALLOWED_CONTENT_TYPES = [
"application/json",
"application/problem+json",
"application/vnd.api+json",
"text/plain",
"text/html"
];
var EXCLUDE_PATH_PATTERNS = [
/\/_?healthz?$/i,
/\/_?health[_-]?checks?$/i,
/\/_?heart[_-]?beats?$/i,
/\/ping$/i,
/\/ready$/i,
/\/live$/i
];
var EXCLUDE_USER_AGENT_PATTERNS = [
/health[-_ ]?check/i,
/microsoft-azure-application-lb/i,
/googlehc/i,
/kube-probe/i
];
var MASK_QUERY_PARAM_PATTERNS = [
/auth/i,
/api-?key/i,
/secret/i,
/token/i,
/password/i,
/pwd/i
];
var MASK_HEADER_PATTERNS = [
/auth/i,
/api-?key/i,
/secret/i,
/token/i,
/cookie/i
];
var MASK_BODY_FIELD_PATTERNS = [
/password/i,
/pwd/i,
/token/i,
/secret/i,
/auth/i,
/card[-_ ]?number/i,
/ccv/i,
/ssn/i
];
var DEFAULT_CONFIG = {
enabled: false,
logQueryParams: true,
logRequestHeaders: false,
logRequestBody: false,
logResponseHeaders: true,
logResponseBody: false,
logException: true,
captureLogs: false,
maskQueryParams: [],
maskHeaders: [],
maskBodyFields: [],
excludePaths: []
};
var _RequestLogger = class _RequestLogger {
config;
enabled;
suspendUntil = null;
pendingWrites = [];
currentFile = null;
files = [];
maintainIntervalId;
lock = new import_async_lock.default();
constructor(config) {
this.config = {
...DEFAULT_CONFIG,
...config
};
this.enabled = this.config.enabled && checkWritableFs();
if (this.enabled) {
this.maintainIntervalId = setInterval(() => {
this.maintain();
}, 1e3);
}
}
get maxBodySize() {
return MAX_BODY_SIZE;
}
shouldExcludePath(urlPath) {
const patterns = [
...this.config.excludePaths,
...EXCLUDE_PATH_PATTERNS
];
return matchPatterns(urlPath, patterns);
}
shouldExcludeUserAgent(userAgent) {
return userAgent ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS) : false;
}
shouldMaskQueryParam(name) {
const patterns = [
...this.config.maskQueryParams,
...MASK_QUERY_PARAM_PATTERNS
];
return matchPatterns(name, patterns);
}
shouldMaskHeader(name) {
const patterns = [
...this.config.maskHeaders,
...MASK_HEADER_PATTERNS
];
return matchPatterns(name, patterns);
}
shouldMaskBodyField(name) {
const patterns = [
...this.config.maskBodyFields,
...MASK_BODY_FIELD_PATTERNS
];
return matchPatterns(name, patterns);
}
hasSupportedContentType(headers) {
var _a;
const contentType = (_a = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a[1];
return this.isSupportedContentType(contentType);
}
hasJsonContentType(headers) {
var _a;
const contentType = (_a = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a[1];
return contentType ? /\bjson\b/i.test(contentType) : null;
}
isSupportedContentType(contentType) {
return typeof contentType === "string" && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
}
maskQueryParams(search) {
const params = new URLSearchParams(search);
for (const [key] of params) {
if (this.shouldMaskQueryParam(key)) {
params.set(key, MASKED);
}
}
return params.toString();
}
maskHeaders(headers) {
return headers.map(([k, v]) => [
k,
this.shouldMaskHeader(k) ? MASKED : v
]);
}
maskBody(data) {
if (typeof data === "object" && data !== null && !Array.isArray(data)) {
const result = {};
for (const [key, value] of Object.entries(data)) {
if (typeof value === "string" && this.shouldMaskBodyField(key)) {
result[key] = MASKED;
} else {
result[key] = this.maskBody(value);
}
}
return result;
}
if (Array.isArray(data)) {
return data.map((item) => this.maskBody(item));
}
return data;
}
applyMasking(item) {
if (this.config.maskRequestBodyCallback && item.request.body && item.request.body !== BODY_TOO_LARGE) {
try {
const maskedBody = this.config.maskRequestBodyCallback(item.request);
item.request.body = maskedBody ?? BODY_MASKED;
} catch {
item.request.body = void 0;
}
}
if (this.config.maskResponseBodyCallback && item.response.body && item.response.body !== BODY_TOO_LARGE) {
try {
const maskedBody = this.config.maskResponseBodyCallback(item.request, item.response);
item.response.body = maskedBody ?? BODY_MASKED;
} catch {
item.response.body = void 0;
}
}
if (item.request.body && item.request.body.length > MAX_BODY_SIZE) {
item.request.body = BODY_TOO_LARGE;
}
if (item.response.body && item.response.body.length > MAX_BODY_SIZE) {
item.response.body = BODY_TOO_LARGE;
}
for (const key of [
"request",
"response"
]) {
const bodyData = item[key].body;
if (!bodyData || bodyData === BODY_TOO_LARGE || bodyData === BODY_MASKED) {
continue;
}
const headers = item[key].headers;
const hasJsonContent = this.hasJsonContentType(headers);
if (hasJsonContent === null || hasJsonContent) {
try {
const parsedBody = JSON.parse(bodyData.toString());
const maskedBody = this.maskBody(parsedBody);
item[key].body = import_node_buffer2.Buffer.from(JSON.stringify(maskedBody));
} catch {
}
}
}
item.request.headers = this.config.logRequestHeaders ? this.maskHeaders(item.request.headers) : [];
item.response.headers = this.config.logResponseHeaders ? this.maskHeaders(item.response.headers) : [];
const url = new URL(item.request.url);
url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
item.request.url = url.toString();
return item;
}
logRequest(request, response, error, logs) {
var _a, _b, _c;
if (!this.enabled || this.suspendUntil !== null) return;
const url = new URL(request.url);
const path = request.path ?? url.pathname;
const userAgent = (_a = request.headers.find(([k]) => k.toLowerCase() === "user-agent")) == null ? void 0 : _a[1];
if (this.shouldExcludePath(path) || this.shouldExcludeUserAgent(userAgent) || (((_c = (_b = this.config).excludeCallback) == null ? void 0 : _c.call(_b, request, response)) ?? false)) {
return;
}
if (!this.config.logRequestBody || !this.hasSupportedContentType(request.headers)) {
request.body = void 0;
}
if (!this.config.logResponseBody || !this.hasSupportedContentType(response.headers)) {
response.body = void 0;
}
if (request.size !== void 0 && request.size < 0) {
request.size = void 0;
}
if (response.size !== void 0 && response.size < 0) {
response.size = void 0;
}
const item = {
uuid: (0, import_node_crypto3.randomUUID)(),
request,
response,
exception: error && this.config.logException ? {
type: error.name,
message: truncateExceptionMessage(error.message),
stacktrace: truncateExceptionStackTrace(error.stack || ""),
sentryEventId: getSentryEventId()
} : void 0
};
if (logs && logs.length > 0) {
item.logs = logs.map((log) => ({
timestamp: log.timestamp,
logger: log.logger,
level: log.level,
message: truncateLogMessage(log.message)
}));
}
this.pendingWrites.push(item);
if (this.pendingWrites.length > MAX_PENDING_WRITES) {
this.pendingWrites.shift();
}
}
async writeToFile() {
if (!this.enabled || this.pendingWrites.length === 0) {
return;
}
return this.lock.acquire("file", async () => {
if (!this.currentFile) {
this.currentFile = new TempGzipFile();
}
while (this.pendingWrites.length > 0) {
let item = this.pendingWrites.shift();
if (item) {
item = this.applyMasking(item);
const finalItem = {
uuid: item.uuid,
request: skipEmptyValues(item.request),
response: skipEmptyValues(item.response),
exception: item.exception,
logs: item.logs
};
[
finalItem.request.body,
finalItem.response.body
].forEach((body) => {
if (body) {
body.toJSON = function() {
return this.toString("base64");
};
}
});
await this.currentFile.writeLine(import_node_buffer2.Buffer.from(JSON.stringify(finalItem)));
}
}
});
}
getFile() {
return this.files.shift();
}
retryFileLater(file) {
this.files.unshift(file);
}
async rotateFile() {
return this.lock.acquire("file", async () => {
if (this.currentFile) {
await this.currentFile.close();
this.files.push(this.currentFile);
this.currentFile = null;
}
});
}
async maintain() {
await this.writeToFile();
if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {
await this.rotateFile();
}
while (this.files.length > MAX_FILES) {
const file = this.files.shift();
file == null ? void 0 : file.delete();
}
if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {
this.suspendUntil = null;
}
}
async clear() {
this.pendingWrites = [];
await this.rotateFile();
this.files.forEach((file) => {
file.delete();
});
this.files = [];
}
async close() {
this.enabled = false;
await this.clear();
if (this.maintainIntervalId) {
clearInterval(this.maintainIntervalId);
}
}
};
__name(_RequestLogger, "RequestLogger");
var RequestLogger = _RequestLogger;
function convertHeaders(headers) {
if (!headers) {
return [];
}
if (headers instanceof Headers) {
return Array.from(headers.entries());
}
return Object.entries(headers).flatMap(([key, value]) => {
if (value === void 0) {
return [];
}
if (Array.isArray(value)) {
return value.map((v) => [
key,
v
]);
}
return [
[
key,
value.toString()
]
];
});
}
__name(convertHeaders, "convertHeaders");
function convertBody(body, contentType) {
if (!body || !contentType) {
return;
}
try {
if (contentType.startsWith("application/json")) {
if (isValidJsonString(body)) {
return import_node_buffer2.Buffer.from(body);
} else {
return import_node_buffer2.Buffer.from(JSON.stringify(body));
}
}
if (contentType.startsWith("text/") && typeof body === "string") {
return import_node_buffer2.Buffer.from(body);
}
} catch (error) {
return;
}
}
__name(convertBody, "convertBody");
function isValidJsonString(body) {
if (typeof body !== "string") {
return false;
}
try {
JSON.parse(body);
return true;
} catch {
return false;
}
}
__name(isValidJsonString, "isValidJsonString");
function matchPatterns(value, patterns) {
return patterns.some((pattern) => {
return pattern.test(value);
});
}
__name(matchPatterns, "matchPatterns");
function skipEmptyValues(data) {
return Object.fromEntries(Object.entries(data).filter(([_, v]) => {
if (v == null || Number.isNaN(v)) return false;
if (Array.isArray(v) || import_node_buffer2.Buffer.isBuffer(v) || typeof v === "string") {
return v.length > 0;
}
return true;
}));
}
__name(skipEmptyValues, "skipEmptyValues");
function truncateLogMessage(msg) {
if (msg.length > MAX_LOG_MSG_LENGTH) {
const suffix = "... (truncated)";
return msg.slice(0, MAX_LOG_MSG_LENGTH - suffix.length) + suffix;
}
return msg;
}
__name(truncateLogMessage, "truncateLogMessage");
function checkWritableFs() {
try {
const testPath = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `apitally-${(0, import_node_crypto3.randomUUID)()}`);
(0, import_node_fs2.writeFileSync)(testPath, "test");
(0, import_node_fs2.unlinkSync)(testPath);
return true;
} catch (error) {
return false;
}
}
__name(checkWritableFs, "checkWritableFs");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
convertBody,
convertHeaders
});
//# sourceMappingURL=requestLogger.cjs.map