UNPKG

apitally

Version:

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

273 lines 9.16 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); import fp from "fastify-plugin"; import { AsyncLocalStorage, AsyncResource } from "node:async_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"; const LOGS_SYMBOL = Symbol("apitally.logs"); const ASYNC_RESOURCE_SYMBOL = Symbol("apitally.logsContextAsyncResource"); const apitallyPlugin = /* @__PURE__ */ __name(async (fastify, config) => { const client = new ApitallyClient(config); const routes = []; const logsContext = new AsyncLocalStorage(); if (client.requestLogger.config.captureLogs) { patchConsole(logsContext); patchWinston(logsContext); patchPinoLogger(fastify.log, logsContext); patchNestLogger(logsContext); } fastify.decorateRequest("apitallyConsumer", null); fastify.decorateRequest("consumerIdentifier", null); fastify.decorateReply("payload", null); fastify.addHook("onRoute", (routeOptions) => { const methods = Array.isArray(routeOptions.method) ? routeOptions.method : [ routeOptions.method ]; methods.forEach((method) => { if (![ "HEAD", "OPTIONS" ].includes(method.toUpperCase())) { routes.push({ method: method.toUpperCase(), path: routeOptions.url }); } }); }); fastify.addHook("onReady", () => { client.setStartupData(getAppInfo(routes, config.appVersion)); client.startSync(); }); fastify.addHook("onClose", async () => { await client.handleShutdown(); }); fastify.addHook("onRequest", (request, reply, done) => { if (client.isEnabled()) { const logs = []; request[LOGS_SYMBOL] = logs; logsContext.run(logs, () => { const asyncResource = new AsyncResource("ApitallyLogsContext"); request[ASYNC_RESOURCE_SYMBOL] = asyncResource; asyncResource.runInAsyncScope(done, request.raw); }); } else { done(); } }); fastify.addHook("preValidation", (request, reply, done) => { if (client.isEnabled() && request[ASYNC_RESOURCE_SYMBOL]) { const asyncResource = request[ASYNC_RESOURCE_SYMBOL]; asyncResource.runInAsyncScope(done, request.raw); } else { done(); } }); fastify.addHook("onSend", (request, reply, payload, done) => { const contentType = reply.getHeader("content-type"); if (client.requestLogger.isSupportedContentType(contentType)) { reply.payload = payload; } done(); }); fastify.addHook("onError", (request, reply, error, done) => { if (!error.statusCode || error.statusCode === 500) { reply.serverError = error; } done(); }); fastify.addHook("onResponse", (request, reply, done) => { var _a; if (client.isEnabled() && request.method.toUpperCase() !== "OPTIONS") { const consumer = getConsumer(request); const path = "routeOptions" in request ? request.routeOptions.url : request.routerPath; const requestSize = parseContentLength(request.headers["content-length"]); const responseSize = parseContentLength(reply.getHeader("content-length")); const responseTime = getResponseTime(reply); client.consumerRegistry.addOrUpdateConsumer(consumer); client.requestCounter.addRequest({ consumer: consumer == null ? void 0 : consumer.identifier, method: request.method, path, statusCode: reply.statusCode, responseTime, requestSize, responseSize }); if ((reply.statusCode === 400 || reply.statusCode === 422) && reply.payload) { try { const parsedPayload = JSON.parse(reply.payload); const validationErrors = []; if ((!parsedPayload.code || parsedPayload.code === "FST_ERR_VALIDATION") && typeof parsedPayload.message === "string") { validationErrors.push(...extractAjvErrors(parsedPayload.message)); } else if (Array.isArray(parsedPayload.message)) { validationErrors.push(...extractNestValidationErrors(parsedPayload.message)); } validationErrors.forEach((error) => { client.validationErrorCounter.addValidationError({ consumer: consumer == null ? void 0 : consumer.identifier, method: request.method, path, ...error }); }); } catch (error) { } } if (reply.statusCode === 500 && reply.serverError) { client.serverErrorCounter.addServerError({ consumer: consumer == null ? void 0 : consumer.identifier, method: request.method, path, type: reply.serverError.name, msg: reply.serverError.message, traceback: reply.serverError.stack || "" }); } if (client.requestLogger.enabled) { const logs = request[LOGS_SYMBOL]; client.requestLogger.logRequest({ timestamp: Date.now() / 1e3, method: request.method, path, url: `${request.protocol}://${request.host ?? request.hostname}${request.originalUrl ?? request.url}`, headers: convertHeaders(request.headers), size: requestSize, consumer: consumer == null ? void 0 : consumer.identifier, body: convertBody(request.body, request.headers["content-type"]) }, { statusCode: reply.statusCode, responseTime: responseTime / 1e3, headers: convertHeaders(reply.getHeaders()), size: responseSize, body: convertBody(reply.payload, (_a = reply.getHeader("content-type")) == null ? void 0 : _a.toString()) }, reply.serverError, logs); } } done(); }); }, "apitallyPlugin"); function getAppInfo(routes, appVersion) { const versions = [ [ "nodejs", process.version.replace(/^v/, "") ] ]; const fastifyVersion = getPackageVersion("fastify"); const pinoVersion = getPackageVersion("pino"); const nestjsVersion = getPackageVersion("@nestjs/core"); const apitallyVersion = getPackageVersion("../.."); if (fastifyVersion) { versions.push([ "fastify", fastifyVersion ]); } if (pinoVersion) { versions.push([ "pino", pinoVersion ]); } if (nestjsVersion) { versions.push([ "nestjs", nestjsVersion ]); } if (apitallyVersion) { versions.push([ "apitally", apitallyVersion ]); } if (appVersion) { versions.push([ "app", appVersion ]); } return { paths: routes, versions: Object.fromEntries(versions), client: "js:fastify" }; } __name(getAppInfo, "getAppInfo"); function setConsumer(request, consumer) { request.apitallyConsumer = consumer || void 0; } __name(setConsumer, "setConsumer"); function getConsumer(request) { if (request.apitallyConsumer) { return consumerFromStringOrObject(request.apitallyConsumer); } else if (request.consumerIdentifier) { process.emitWarning("The consumerIdentifier property on the request object is deprecated. Use apitallyConsumer instead.", "DeprecationWarning"); return consumerFromStringOrObject(request.consumerIdentifier); } return null; } __name(getConsumer, "getConsumer"); function getResponseTime(reply) { if (reply.elapsedTime !== void 0) { return reply.elapsedTime; } else if (reply.getResponseTime !== void 0) { return reply.getResponseTime(); } return 0; } __name(getResponseTime, "getResponseTime"); function extractAjvErrors(message) { try { const regex = /(?<=^|, )((?:headers|params|query|querystring|body)[/.][^ ]+)(?= )/g; const matches = []; let match; while ((match = regex.exec(message)) !== null) { matches.push({ match: match[0], index: match.index }); } return matches.map((m, i) => { const endIndex = i + 1 < matches.length ? matches[i + 1].index - 2 : message.length; const matchSplit = m.match.split(/[/.]/); if (matchSplit[0] === "querystring") { matchSplit[0] = "query"; } return { loc: matchSplit.join("."), msg: message.substring(m.index, endIndex), type: "" }; }); } catch (error) { return []; } } __name(extractAjvErrors, "extractAjvErrors"); function extractNestValidationErrors(message) { try { return message.filter((msg) => typeof msg === "string").map((msg) => ({ loc: "", msg, type: "" })); } catch (error) { return []; } } __name(extractNestValidationErrors, "extractNestValidationErrors"); var plugin_default = fp(apitallyPlugin, { name: "apitally" }); export { apitallyPlugin, plugin_default as default, setConsumer }; //# sourceMappingURL=plugin.js.map