UNPKG

strapi-prometheus

Version:

A powerful Strapi plugin that adds comprehensive Prometheus metrics to monitor your API performance, system resources, and application behavior using prom-client.

228 lines (227 loc) 7.84 kB
"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 __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 )); const prom = require("prom-client"); const _interopDefault = (e) => e && e.__esModule ? e : { default: e }; const prom__default = /* @__PURE__ */ _interopDefault(prom); const requestDurationSeconds = new prom.Histogram({ name: "http_request_duration_seconds", help: "Duration of HTTP requests in seconds", labelNames: ["origin", "method", "route", "status"], buckets: [ 1e-3, // 1 ms 5e-3, // 5 ms 0.01, // 10 ms 0.05, // 50 ms 0.1, // 100 ms 0.2, // 200 ms 0.5, // 500 ms 1, // 1 second 2, // 2 seconds 5, // 5 seconds 10 // 10 seconds ] }); const requestContentLengthBytes = new prom.Histogram({ name: "http_request_content_length_bytes", help: "Histogram of the size of payloads sent to the server, measured in bytes.", labelNames: ["origin", "method", "route", "status"], buckets: prom.exponentialBuckets(1024, 2, 20) // Buckets starting from 1 KB to 1 GB (1024 * 2^19 = ~536 MB, 2^20 = ~1 GB) }); const responseContentLengthBytes = new prom.Histogram({ name: "http_response_content_length_bytes", help: "Histogram of the size of payloads sent by the server, measured in bytes.", labelNames: ["origin", "method", "route", "status"], buckets: prom.exponentialBuckets(1024, 2, 20) // Buckets starting from 1 KB to 1 GB (1024 * 2^19 = ~536 MB, 2^20 = ~1 GB) }); function getRoutePattern(ctx) { if (ctx._matchedRoute) { return ctx._matchedRoute; } return normalizePath(ctx.path); } function normalizePath(path) { return path.replace(/\/api\/([^\/]+)\/\d+(?:\/.*)?$/, "/api/$1/:id").replace(/\/\d+/g, "/:id").replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "/:uuid").replace(/\/uploads\/[^\/]+\.[a-zA-Z0-9]+/, "/uploads/:file").replace(/\/+/g, "/").replace(/\/$/, "") || "/"; } const metricsMiddleware = async (ctx, next) => { const end = requestDurationSeconds.startTimer(); await next(); const labels = { method: ctx.method, route: getRoutePattern(ctx), origin: ctx.origin || "unknown", status: ctx.status }; ctx.res.once("finish", () => { end(labels); const requestContentLength = ctx.request.get("Content-Length") || ctx.request.get("content-length"); if (requestContentLength) { const parsedRequestContentLength = parseInt(requestContentLength, 10); if (!isNaN(parsedRequestContentLength) && isFinite(parsedRequestContentLength)) { requestContentLengthBytes.observe(labels, parsedRequestContentLength); } } const responseContentLength = ctx.response.get("Content-Length") || ctx.response.get("content-length"); if (responseContentLength) { const parsedResponseContentLength = parseInt(responseContentLength, 10); if (!isNaN(parsedResponseContentLength) && isFinite(parsedResponseContentLength)) { responseContentLengthBytes.observe(labels, parsedResponseContentLength); } } }); }; const versionMetric = new prom.Gauge({ name: "strapi_version_info", help: "Strapi version info", labelNames: ["version", "major", "minor", "patch"] }); const bootstrap$1 = async ({ strapi }) => { const { config: config2 } = strapi.plugin("prometheus"); const labels = config2("labels"); if (labels) prom__default.default.register.setDefaultLabels(config2("labels")); const collectDefaults = config2("collectDefaultMetrics"); if (collectDefaults) prom__default.default.collectDefaultMetrics(collectDefaults); strapi.server.use(metricsMiddleware); const serverConfig = strapi.plugin("prometheus").config("server"); if (typeof serverConfig === "boolean" && !serverConfig) return strapi.server.routes([ { method: "GET", path: "/metrics", handler: async (ctx) => { ctx.response.headers["Content-Type"] = prom__default.default.register.contentType; ctx.body = await prom__default.default.register.metrics(); } } ]); const http = await import("http"); const server = http.createServer(async (req, res) => { if (req.method === "GET" && req.url === serverConfig.path) { const data = await prom__default.default.register.metrics(); res.writeHead(200, { "Content-Type": "text/plain" }); res.end(data); } else { res.statusCode = 404; res.end("Not Found"); } }); server.listen(serverConfig.port, serverConfig.host, () => { strapi.log.info(`Serving metrics on http://${serverConfig.host}:${serverConfig.port}${serverConfig.path}`); }); strapi.plugin("prometheus").destroy = async () => { server.close(); }; const version = require("@strapi/strapi/package.json").version; const [major, minor, patch] = version.split("."); versionMetric.set({ version, major, minor, patch }, 1); }; const lifecycleDurationSeconds = new prom.Histogram({ name: "lifecycle_duration_seconds", help: "Tracks the duration of Strapi lifecycle events in seconds.", labelNames: ["model", "event"], buckets: [ 1e-3, // 1 ms 5e-3, // 5 ms 0.01, // 10 ms 0.05, // 50 ms 0.1, // 100 ms 0.2, // 200 ms 0.5, // 500 ms 1, // 1 second 2, // 2 seconds 5, // 5 seconds 10, // 10 seconds 20, // 20 seconds 30, // 30 seconds 60 // 1 minute ] }); function formatActionName(action) { const modifiedAction = action.replace(/^(before|after)/, ""); return modifiedAction.charAt(0).toLowerCase() + modifiedAction.slice(1); } const bootstrap = ({ strapi }) => { strapi.db.lifecycles.subscribe((event) => { if (event.action.startsWith("before")) { const labels = { event: formatActionName(event.action), model: event.model.singularName }; event.state.end = lifecycleDurationSeconds.startTimer(labels); } if (event.action.startsWith("after") && event.state.end) { event.state.end(); } }); }; const config = { default: { collectDefaultMetrics: { prefix: "" }, labels: [], server: { port: 9e3, host: "0.0.0.0", path: "/metrics" } }, validator(config2) { if (typeof config2.collectDefaultMetrics === "boolean" && config2.collectDefaultMetrics) { throw Error("Invalid collectDefaultMetrics value. Can only be false or DefaultMetricsCollectorConfiguration"); } if (typeof config2.server === "boolean" && config2.server) { throw Error("Invalid server value. Can only be false or { port: number, host: string, path: string }"); } } }; const index = { register: bootstrap$1, bootstrap, config }; module.exports = index; //# sourceMappingURL=index.js.map