UNPKG

apitally

Version:

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

324 lines 11.3 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); 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 { getPackageVersion } from "../common/packageVersions.js"; import { convertBody, convertHeaders } from "../common/requestLogger.js"; import { patchConsole, patchNestLogger, patchPinoLogger, patchWinston } from "../loggers/index.js"; import { getEndpoints, getRouterInfo, parseExpressPath, parseExpressPathRegExp } from "./utils.js"; function useApitally(app, config) { const client = new ApitallyClient(config); const middleware = getMiddleware(app, client); app.use(middleware); const setStartupData = /* @__PURE__ */ __name((attempt = 1) => { const appInfo = getAppInfo(app, config.basePath, config.appVersion); if (appInfo.paths.length > 0 || attempt >= 10) { client.setStartupData(appInfo); client.startSync(); } else { setTimeout(() => setStartupData(attempt + 1), 500); } }, "setStartupData"); setTimeout(() => setStartupData(), 500); } __name(useApitally, "useApitally"); function getMiddleware(app, client) { let errorHandlerConfigured = false; const logsContext = new AsyncLocalStorage(); if (client.requestLogger.config.captureLogs) { patchConsole(logsContext); patchWinston(logsContext); patchNestLogger(logsContext); } return async (req, res, next) => { if (!client.isEnabled() || req.method.toUpperCase() === "OPTIONS") { next(); return; } if (!errorHandlerConfigured) { app.use((err, req2, res2, next2) => { res2.locals.serverError = err; next2(err); }); errorHandlerConfigured = true; } if (client.requestLogger.config.captureLogs && "log" in req) { await patchPinoLogger(req.log, logsContext); } logsContext.run([], () => { try { const startTime = performance.now(); const originalSend = res.send; res.send = (body) => { const contentType = res.get("content-type"); if (client.requestLogger.isSupportedContentType(contentType)) { res.locals.body = body; } return originalSend.call(res, body); }; res.once("finish", () => { try { const responseTime = performance.now() - startTime; const path = getRoutePath(req); const consumer = getConsumer(req); client.consumerRegistry.addOrUpdateConsumer(consumer); const requestSize = parseContentLength(req.get("content-length")); const responseSize = parseContentLength(res.get("content-length")); if (path) { client.requestCounter.addRequest({ consumer: consumer == null ? void 0 : consumer.identifier, method: req.method, path, statusCode: res.statusCode, responseTime, requestSize, responseSize }); if ((res.statusCode === 400 || res.statusCode === 422) && res.locals.body) { let jsonBody; try { jsonBody = JSON.parse(res.locals.body); } catch { } if (jsonBody) { const validationErrors = []; if (validationErrors.length === 0) { validationErrors.push(...extractExpressValidatorErrors(jsonBody)); } if (validationErrors.length === 0) { validationErrors.push(...extractCelebrateErrors(jsonBody)); } if (validationErrors.length === 0) { validationErrors.push(...extractNestValidationErrors(jsonBody)); } validationErrors.forEach((error) => { client.validationErrorCounter.addValidationError({ consumer: consumer == null ? void 0 : consumer.identifier, method: req.method, path, ...error }); }); } } if (res.statusCode === 500 && res.locals.serverError) { const serverError = res.locals.serverError; client.serverErrorCounter.addServerError({ consumer: consumer == null ? void 0 : consumer.identifier, method: req.method, path, type: serverError.name, msg: serverError.message, traceback: serverError.stack || "" }); } } if (client.requestLogger.enabled) { const logs = logsContext.getStore(); client.requestLogger.logRequest({ timestamp: Date.now() / 1e3, method: req.method, path, url: `${req.protocol}://${req.host}${req.originalUrl}`, headers: convertHeaders(req.headers), size: requestSize, consumer: consumer == null ? void 0 : consumer.identifier, body: convertBody(req.body, req.get("content-type")) }, { statusCode: res.statusCode, responseTime: responseTime / 1e3, headers: convertHeaders(res.getHeaders()), size: responseSize, body: convertBody(res.locals.body, res.get("content-type")) }, res.locals.serverError, logs); } } catch (error) { client.logger.error("Error while logging request in Apitally middleware.", { request: req, response: res, error }); } }); } catch (error) { client.logger.error("Error in Apitally middleware.", { request: req, response: res, error }); } finally { next(); } }); }; } __name(getMiddleware, "getMiddleware"); function getRoutePath(req) { if (!req.route) { return; } if (req.baseUrl) { const routerInfo = getRouterInfo(req.app); if (routerInfo.stack) { const routerPath = getRouterPath(routerInfo.stack, req.baseUrl); return req.route.path === "/" ? routerPath : routerPath + req.route.path; } } return req.route.path; } __name(getRoutePath, "getRoutePath"); function getRouterPath(stack, baseUrl) { var _a; const routerPaths = []; while (stack && stack.length > 0) { const routerLayer = stack.find((layer) => { var _a2; return layer.name === "router" && layer.path && (baseUrl.startsWith(layer.path) || ((_a2 = layer.regexp) == null ? void 0 : _a2.test(baseUrl))); }); if (routerLayer) { if (routerLayer.regexp && routerLayer.keys && routerLayer.keys.length > 0) { const parsedPath = parseExpressPathRegExp(routerLayer.regexp, routerLayer.keys); routerPaths.push("/" + parsedPath); } else if (routerLayer.params && Object.keys(routerLayer.params).length > 0) { const parsedPath = parseExpressPath(routerLayer.path, routerLayer.params); routerPaths.push(parsedPath); } else { routerPaths.push(routerLayer.path); } stack = (_a = routerLayer.handle) == null ? void 0 : _a.stack; baseUrl = baseUrl.slice(routerLayer.path.length); } else { break; } } return routerPaths.filter((path) => path !== "/").join(""); } __name(getRouterPath, "getRouterPath"); function setConsumer(req, consumer) { req.apitallyConsumer = consumer || void 0; } __name(setConsumer, "setConsumer"); function getConsumer(req) { if (req.apitallyConsumer) { return consumerFromStringOrObject(req.apitallyConsumer); } else if (req.consumerIdentifier) { process.emitWarning("The consumerIdentifier property on the request object is deprecated. Use apitallyConsumer instead.", "DeprecationWarning"); return consumerFromStringOrObject(req.consumerIdentifier); } return null; } __name(getConsumer, "getConsumer"); function extractExpressValidatorErrors(responseBody) { try { const errors = []; if (responseBody && responseBody.errors && Array.isArray(responseBody.errors)) { responseBody.errors.forEach((error) => { if (error.location && error.path && error.msg && error.type) { errors.push({ loc: `${error.location}.${error.path}`, msg: error.msg, type: error.type }); } }); } return errors; } catch (error) { return []; } } __name(extractExpressValidatorErrors, "extractExpressValidatorErrors"); function extractCelebrateErrors(responseBody) { try { const errors = []; if (responseBody && responseBody.validation) { Object.values(responseBody.validation).forEach((error) => { if (error.source && error.keys && Array.isArray(error.keys) && error.message) { error.keys.forEach((key) => { errors.push({ loc: `${error.source}.${key}`, msg: subsetJoiMessage(error.message, key), type: "" }); }); } }); } return errors; } catch (error) { return []; } } __name(extractCelebrateErrors, "extractCelebrateErrors"); function extractNestValidationErrors(responseBody) { try { const errors = []; if (responseBody && Array.isArray(responseBody.message)) { responseBody.message.forEach((message) => { errors.push({ loc: "", msg: message, type: "" }); }); } return errors; } catch (error) { return []; } } __name(extractNestValidationErrors, "extractNestValidationErrors"); function subsetJoiMessage(message, key) { const messageWithKey = message.split(". ").find((message2) => message2.includes(`"${key}"`)); return messageWithKey ? messageWithKey : message; } __name(subsetJoiMessage, "subsetJoiMessage"); function getAppInfo(app, basePath, appVersion) { const versions = [ [ "nodejs", process.version.replace(/^v/, "") ] ]; const expressVersion = getPackageVersion("express"); const nestjsVersion = getPackageVersion("@nestjs/core"); const apitallyVersion = getPackageVersion("../.."); if (expressVersion) { versions.push([ "express", expressVersion ]); } if (nestjsVersion) { versions.push([ "nestjs", nestjsVersion ]); } if (apitallyVersion) { versions.push([ "apitally", apitallyVersion ]); } if (appVersion) { versions.push([ "app", appVersion ]); } return { paths: getEndpoints(app, basePath || ""), versions: Object.fromEntries(versions), client: "js:express" }; } __name(getAppInfo, "getAppInfo"); export { setConsumer, useApitally }; //# sourceMappingURL=middleware.js.map