UNPKG

apitally

Version:

Simple API monitoring & analytics for REST APIs built with Express, Fastify, NestJS, AdonisJS, Hono, H3, Elysia, Hapi, and Koa.

210 lines 9.32 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); import { StatusMap, ValidationError } from "elysia"; 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 { 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 START_TIME_SYMBOL = Symbol("apitally.startTime"); const REQUEST_BODY_SYMBOL = Symbol("apitally.requestBody"); const RESPONSE_SYMBOL = Symbol("apitally.response"); const RESPONSE_PROMISE_SYMBOL = Symbol("apitally.responsePromise"); const ERROR_SYMBOL = Symbol("apitally.error"); const CLIENT_SYMBOL = Symbol("apitally.client"); const REQUEST_SYMBOL = Symbol("apitally.request"); function apitallyPlugin(config) { const client = new ApitallyClient(config); const logsContext = new AsyncLocalStorage(); if (client.requestLogger.enabled && client.requestLogger.config.captureLogs) { patchConsole(logsContext); patchWinston(logsContext); } return (app) => { const handler = app["~adapter"].handler; if (!handler.mapResponse.name.startsWith("wrapped")) { const originalMapResponse = handler.mapResponse; const originalMapCompactResponse = handler.mapCompactResponse; const originalMapEarlyResponse = handler.mapEarlyResponse; const captureMappedResponse = /* @__PURE__ */ __name((originalResponse, mappedResponse, set) => { const request = set == null ? void 0 : set[REQUEST_SYMBOL]; if (request instanceof Request && mappedResponse instanceof Response && !(RESPONSE_SYMBOL in request) && CLIENT_SYMBOL in request) { if (typeof originalResponse === "string") { const responseBody = Buffer.from(originalResponse); request[RESPONSE_SYMBOL] = mappedResponse; request[RESPONSE_PROMISE_SYMBOL] = Promise.resolve({ body: responseBody, size: responseBody.length, completed: true }); } else { const client2 = request[CLIENT_SYMBOL]; const [newResponse, responsePromise] = captureResponse(mappedResponse, { captureBody: client2.requestLogger.enabled && client2.requestLogger.config.logResponseBody, maxBodySize: client2.requestLogger.maxBodySize }); request[RESPONSE_SYMBOL] = newResponse; request[RESPONSE_PROMISE_SYMBOL] = responsePromise; return newResponse; } } return mappedResponse; }, "captureMappedResponse"); handler.mapResponse = /* @__PURE__ */ __name(function wrappedMapResponse(response, set) { const mappedResponse = originalMapResponse(response, set); const newResponse = captureMappedResponse(response, mappedResponse, set); return newResponse; }, "wrappedMapResponse"); handler.mapCompactResponse = /* @__PURE__ */ __name(function wrappedMapCompactResponse(response) { const mappedResponse = originalMapCompactResponse(response); const newResponse = captureMappedResponse(response, mappedResponse, void 0); return newResponse; }, "wrappedMapCompactResponse"); handler.mapEarlyResponse = /* @__PURE__ */ __name(function wrappedMapEarlyResponse(response, set) { const mappedResponse = originalMapEarlyResponse(response, set); const newResponse = captureMappedResponse(response, mappedResponse, set); return newResponse; }, "wrappedMapEarlyResponse"); } return app.decorate("apitally", {}).onStart(() => { const appInfo = getAppInfo(app, config.appVersion); client.setStartupData(appInfo); client.startSync(); }).onStop(async () => { await client.handleShutdown(); }).onRequest(async ({ request, set }) => { if (!client.isEnabled() || request.method.toUpperCase() === "OPTIONS") { return; } request[CLIENT_SYMBOL] = client; request[START_TIME_SYMBOL] = performance.now(); set[REQUEST_SYMBOL] = request; logsContext.enterWith([]); if (client.requestLogger.enabled && client.requestLogger.config.logRequestBody) { const contentType = request.headers.get("content-type"); const requestSize = parseContentLength(request.headers.get("content-length")) ?? 0; if (client.requestLogger.isSupportedContentType(contentType) && requestSize <= client.requestLogger.maxBodySize) { try { request[REQUEST_BODY_SYMBOL] = Buffer.from(await request.clone().arrayBuffer()); } catch (error) { } } } }).onAfterResponse(async ({ request, set, route, apitally }) => { if (!client.isEnabled() || request.method.toUpperCase() === "OPTIONS") { return; } const startTime = request[START_TIME_SYMBOL]; const responseTime = startTime ? performance.now() - startTime : 0; const requestBody = request[REQUEST_BODY_SYMBOL]; const requestSize = parseContentLength(request.headers.get("content-length")) ?? (requestBody == null ? void 0 : requestBody.length); let responsePromise = request[RESPONSE_PROMISE_SYMBOL]; let response = request[RESPONSE_SYMBOL]; const error = request[ERROR_SYMBOL]; if (!response && error && "toResponse" in error && typeof error.toResponse === "function") { try { response = error.toResponse(); const errorResponseBody = Buffer.from(await response.arrayBuffer()); responsePromise = Promise.resolve({ body: errorResponseBody, size: errorResponseBody.length, completed: true }); } catch (error2) { } } const statusCode = (response == null ? void 0 : response.status) ?? getStatusCode(set) ?? 200; if (!response) { response = new Response(null, { status: statusCode, statusText: "", headers: new Headers() }); responsePromise = Promise.resolve({ body: void 0, size: 0, completed: true }); } const consumer = apitally.consumer ? consumerFromStringOrObject(apitally.consumer) : null; client.consumerRegistry.addOrUpdateConsumer(consumer); responsePromise == null ? void 0 : responsePromise.then(async (capturedResponse) => { const responseHeaders = (response == null ? void 0 : response.headers) ?? set.headers; const responseSize = capturedResponse.completed ? capturedResponse.size : void 0; client.requestCounter.addRequest({ consumer: consumer == null ? void 0 : consumer.identifier, method: request.method, path: route, statusCode, responseTime, requestSize, responseSize }); if (client.requestLogger.enabled) { const logs = logsContext.getStore(); client.requestLogger.logRequest({ timestamp: (Date.now() - responseTime) / 1e3, method: request.method, path: route, url: request.url, headers: convertHeaders(Object.fromEntries(request.headers.entries())), size: requestSize, consumer: consumer == null ? void 0 : consumer.identifier, body: requestBody }, { statusCode, responseTime: responseTime / 1e3, headers: convertHeaders(responseHeaders), size: responseSize, body: capturedResponse.body }, error, logs); } }); if ((statusCode === 400 || statusCode === 422) && error instanceof ValidationError) { try { const parsedMessage = JSON.parse(error.message); client.validationErrorCounter.addValidationError({ consumer: consumer == null ? void 0 : consumer.identifier, method: request.method, path: route, loc: (parsedMessage.on ?? "") + "." + (parsedMessage.property ?? ""), msg: parsedMessage.message, type: "" }); } catch (error2) { } } if (statusCode === 500 && error) { client.serverErrorCounter.addServerError({ consumer: consumer == null ? void 0 : consumer.identifier, method: request.method, path: route, type: error.name, msg: error.message, traceback: error.stack || "" }); } }).onError(({ request, error }) => { if (client.isEnabled() && error instanceof Error) { request[ERROR_SYMBOL] = error; } }); }; } __name(apitallyPlugin, "apitallyPlugin"); function getStatusCode(set) { if (typeof set.status === "number") { return set.status; } else if (typeof set.status === "string") { return StatusMap[set.status]; } } __name(getStatusCode, "getStatusCode"); export { apitallyPlugin as default }; //# sourceMappingURL=plugin.js.map