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.
205 lines (204 loc) • 6.46 kB
JavaScript
import prom, { Histogram, exponentialBuckets, Gauge } from "prom-client";
const requestDurationSeconds = new 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 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: exponentialBuckets(1024, 2, 20)
// Buckets starting from 1 KB to 1 GB (1024 * 2^19 = ~536 MB, 2^20 = ~1 GB)
});
const responseContentLengthBytes = new 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: 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 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.register.setDefaultLabels(config2("labels"));
const collectDefaults = config2("collectDefaultMetrics");
if (collectDefaults)
prom.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.register.contentType;
ctx.body = await prom.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.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 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 };
export {
index as default
};
//# sourceMappingURL=index.mjs.map