apitally
Version:
Simple API monitoring & analytics for REST APIs built with Express, Fastify, NestJS, AdonisJS, Hono, H3, Elysia, Hapi, and Koa.
151 lines • 6.39 kB
JavaScript
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
import { definePlugin, onError, onRequest, onResponse } from "h3";
import { AsyncLocalStorage } from "node:async_hooks";
import { performance } from "node:perf_hooks";
import { ApitallyClient } from "../common/client.js";
import { consumerFromStringOrObject } from "../common/consumerRegistry.js";
import { mergeHeaders, parseContentLength } from "../common/headers.js";
import { convertHeaders } from "../common/requestLogger.js";
import { captureResponse } from "../common/response.js";
import { patchConsole, patchWinston } from "../loggers/index.js";
import { getAppInfo } from "./utils.js";
const REQUEST_TIMESTAMP_SYMBOL = Symbol("apitally.requestTimestamp");
const REQUEST_BODY_SYMBOL = Symbol("apitally.requestBody");
const jsonHeaders = new Headers({
"content-type": "application/json;charset=UTF-8"
});
const apitallyPlugin = definePlugin((app, config) => {
const client = new ApitallyClient(config);
const logsContext = new AsyncLocalStorage();
const setStartupData = /* @__PURE__ */ __name((attempt = 1) => {
const appInfo = getAppInfo(app, config.appVersion);
if (appInfo.paths.length > 0 || attempt >= 10) {
client.setStartupData(appInfo);
client.startSync();
} else {
setTimeout(() => setStartupData(attempt + 1), 500);
}
}, "setStartupData");
setTimeout(() => setStartupData(), 500);
if (client.requestLogger.config.captureLogs) {
patchConsole(logsContext);
patchWinston(logsContext);
}
const handleResponse = /* @__PURE__ */ __name(async (event, response, error) => {
var _a, _b;
if (event.req.method.toUpperCase() === "OPTIONS") {
return response;
}
const startTime = event.context[REQUEST_TIMESTAMP_SYMBOL];
const path = (_a = event.context.matchedRoute) == null ? void 0 : _a.route;
const consumer = getConsumer(event);
client.consumerRegistry.addOrUpdateConsumer(consumer);
if (!response) {
response = new Response(null, {
status: (error == null ? void 0 : error.status) || 500,
statusText: (error == null ? void 0 : error.statusText) || "Internal Server Error",
headers: (error == null ? void 0 : error.headers) ? mergeHeaders(jsonHeaders, error.headers) : jsonHeaders
});
}
const [newResponse, responsePromise] = captureResponse(response, {
captureBody: client.requestLogger.enabled && client.requestLogger.config.logResponseBody,
maxBodySize: client.requestLogger.maxBodySize
});
responsePromise.then(async (capturedResponse) => {
const responseTime = startTime ? performance.now() - startTime : 0;
const responseSize = capturedResponse.completed ? capturedResponse.size : void 0;
const requestSize = parseContentLength(event.req.headers.get("content-length"));
if (path) {
client.requestCounter.addRequest({
consumer: consumer == null ? void 0 : consumer.identifier,
method: event.req.method,
path,
statusCode: response.status,
responseTime,
requestSize,
responseSize
});
}
if (client.requestLogger.enabled) {
const logs = logsContext.getStore();
client.requestLogger.logRequest({
timestamp: (Date.now() - responseTime) / 1e3,
method: event.req.method,
path,
url: event.req.url,
headers: convertHeaders(Object.fromEntries(event.req.headers.entries())),
size: requestSize,
consumer: consumer == null ? void 0 : consumer.identifier,
body: event.context[REQUEST_BODY_SYMBOL]
}, {
statusCode: response.status,
responseTime: responseTime / 1e3,
headers: convertHeaders(Object.fromEntries(response.headers.entries())),
size: responseSize,
body: capturedResponse.body
}, (error == null ? void 0 : error.cause) instanceof Error ? error.cause : void 0, logs);
}
});
if (path && (error == null ? void 0 : error.status) === 400 && error.data && error.data.name === "ZodError") {
const zodError = error.data;
(_b = zodError.issues) == null ? void 0 : _b.forEach((issue) => {
client.validationErrorCounter.addValidationError({
consumer: consumer == null ? void 0 : consumer.identifier,
method: event.req.method,
path,
loc: issue.path.join("."),
msg: issue.message,
type: issue.code
});
});
}
if (path && (error == null ? void 0 : error.status) === 500 && error.cause instanceof Error) {
client.serverErrorCounter.addServerError({
consumer: consumer == null ? void 0 : consumer.identifier,
method: event.req.method,
path,
type: error.cause.name,
msg: error.cause.message,
traceback: error.cause.stack || ""
});
}
return newResponse;
}, "handleResponse");
app.use(onRequest(async (event) => {
logsContext.enterWith([]);
event.context[REQUEST_TIMESTAMP_SYMBOL] = performance.now();
const requestContentType = event.req.headers.get("content-type");
const requestSize = parseContentLength(event.req.headers.get("content-length")) ?? 0;
if (client.requestLogger.enabled && client.requestLogger.config.logRequestBody && client.requestLogger.isSupportedContentType(requestContentType) && requestSize <= client.requestLogger.maxBodySize) {
const clonedRequest = event.req.clone();
const requestBody = Buffer.from(await clonedRequest.arrayBuffer());
event.context[REQUEST_BODY_SYMBOL] = requestBody;
}
})).use(onResponse((response, event) => {
if (client.isEnabled()) {
return handleResponse(event, response, void 0);
}
})).use(onError((error, event) => {
if (client.isEnabled()) {
handleResponse(event, void 0, error);
}
}));
});
function setConsumer(event, consumer) {
event.context.apitallyConsumer = consumer || void 0;
}
__name(setConsumer, "setConsumer");
function getConsumer(event) {
const consumer = event.context.apitallyConsumer;
if (consumer) {
return consumerFromStringOrObject(consumer);
}
return null;
}
__name(getConsumer, "getConsumer");
export {
apitallyPlugin,
setConsumer
};
//# sourceMappingURL=plugin.js.map