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.

1 lines 12.4 kB
{"version":3,"file":"index.mjs","sources":["../../server/src/middlewares/metrics.ts","../../server/src/register.ts","../../server/src/bootstrap.ts","../../server/src/config/index.ts","../../server/src/index.ts"],"sourcesContent":["import { Counter, exponentialBuckets, Gauge, Histogram } from \"prom-client\";\n\nconst requestDurationSeconds = new Histogram({\n name: 'http_request_duration_seconds',\n help: 'Duration of HTTP requests in seconds',\n labelNames: ['origin', 'method', 'route', 'status'],\n buckets: [\n 0.001, // 1 ms\n 0.005, // 5 ms\n 0.01, // 10 ms\n 0.05, // 50 ms\n 0.1, // 100 ms\n 0.2, // 200 ms\n 0.5, // 500 ms\n 1, // 1 second\n 2, // 2 seconds\n 5, // 5 seconds\n 10, // 10 seconds\n ]\n});\n\nconst requestContentLengthBytes = new Histogram({\n name: 'http_request_content_length_bytes',\n help: 'Histogram of the size of payloads sent to the server, measured in bytes.',\n labelNames: ['origin', 'method', 'route', 'status'],\n buckets: exponentialBuckets(1024, 2, 20) // Buckets starting from 1 KB to 1 GB (1024 * 2^19 = ~536 MB, 2^20 = ~1 GB)\n});\n\nconst responseContentLengthBytes = new Histogram({\n name: 'http_response_content_length_bytes',\n help: 'Histogram of the size of payloads sent by the server, measured in bytes.',\n labelNames: ['origin', 'method', 'route', 'status'],\n buckets: exponentialBuckets(1024, 2, 20) // Buckets starting from 1 KB to 1 GB (1024 * 2^19 = ~536 MB, 2^20 = ~1 GB)\n});\n\nfunction getRoutePattern(ctx: any): string {\n // First try the matched route (available after routing)\n if (ctx._matchedRoute) {\n return ctx._matchedRoute;\n }\n\n // Fallback to normalized path\n return normalizePath(ctx.path);\n}\n\n/**\n * Normalize path to reduce metric cardinality when matched route isn't available\n */\nfunction normalizePath(path: string): string {\n return path\n // Strapi API routes: /api/articles/123 -> /api/articles/:id\n .replace(/\\/api\\/([^\\/]+)\\/\\d+(?:\\/.*)?$/, '/api/$1/:id')\n // Generic numeric IDs\n .replace(/\\/\\d+/g, '/:id')\n // UUIDs\n .replace(/\\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '/:uuid')\n // File uploads\n .replace(/\\/uploads\\/[^\\/]+\\.[a-zA-Z0-9]+/, '/uploads/:file')\n // Clean up\n .replace(/\\/+/g, '/')\n .replace(/\\/$/, '') || '/';\n}\n\n\nexport default async (ctx, next) => {\n const end = requestDurationSeconds.startTimer();\n\n await next();\n\n // Create final labels with all information\n const labels = {\n method: ctx.method,\n route: getRoutePattern(ctx),\n origin: ctx.origin || 'unknown',\n status: ctx.status\n };\n\n ctx.res.once('finish', () => {\n end(labels);\n\n const requestContentLength = ctx.request.get('Content-Length') || ctx.request.get('content-length');\n if (requestContentLength) {\n const parsedRequestContentLength = parseInt(requestContentLength, 10);\n if (!isNaN(parsedRequestContentLength) && isFinite(parsedRequestContentLength)) {\n requestContentLengthBytes.observe(labels, parsedRequestContentLength);\n }\n }\n\n const responseContentLength = ctx.response.get('Content-Length') || ctx.response.get('content-length');\n if (responseContentLength) {\n const parsedResponseContentLength = parseInt(responseContentLength, 10);\n if (!isNaN(parsedResponseContentLength) && isFinite(parsedResponseContentLength)) {\n responseContentLengthBytes.observe(labels, parsedResponseContentLength);\n }\n }\n });\n};\n","import type { Core } from '@strapi/strapi';\nimport prom, { Gauge } from 'prom-client';\nimport metricsMiddleware from './middlewares/metrics';\n\nconst versionMetric = new Gauge({\n name: 'strapi_version_info',\n help: 'Strapi version info',\n labelNames: ['version','major','minor','patch']\n});\n\nconst bootstrap = async ({ strapi }: { strapi: Core.Strapi }) => {\n const { config } = strapi.plugin('prometheus');\n\n const labels = config('labels');\n if (labels) prom.register.setDefaultLabels(config('labels'));\n\n const collectDefaults = config('collectDefaultMetrics');\n if (collectDefaults) prom.collectDefaultMetrics(collectDefaults);\n\n strapi.server.use(metricsMiddleware);\n\n const serverConfig: false | { port: number, host: string, path: string } = strapi.plugin('prometheus').config('server');\n\n if (typeof serverConfig === 'boolean' && !serverConfig) return strapi.server.routes([\n {\n method: 'GET',\n path: '/metrics',\n handler: async (ctx) => {\n ctx.response.headers['Content-Type'] = prom.register.contentType;\n ctx.body = await prom.register.metrics()\n }\n }\n ])\n\n const http = await import('http');\n\n const server = http.createServer(async (req, res) => {\n if (req.method === 'GET' && req.url === serverConfig.path) {\n // Replace this with your logic to generate metrics data\n const data = await prom.register.metrics()\n res.writeHead(200, { 'Content-Type': 'text/plain' });\n res.end(data);\n } else {\n res.statusCode = 404;\n res.end('Not Found');\n }\n });\n\n server.listen(serverConfig.port, serverConfig.host, () => {\n strapi.log.info(`Serving metrics on http://${serverConfig.host}:${serverConfig.port}${serverConfig.path}`);\n });\n\n strapi.plugin('prometheus').destroy = async () => {\n server.close();\n };\n\n const version = require('@strapi/strapi/package.json').version;\n const [major, minor, patch] = version.split('.')\n versionMetric.set({version,major,minor,patch}, 1)\n};\n\nexport default bootstrap;\n","import { Core } from '@strapi/strapi';\nimport { Histogram } from 'prom-client';\n\n\nconst lifecycleDurationSeconds = new Histogram({\n name: 'lifecycle_duration_seconds',\n help: 'Tracks the duration of Strapi lifecycle events in seconds.',\n labelNames: ['model', 'event'],\n buckets: [\n 0.001, // 1 ms\n 0.005, // 5 ms\n 0.01, // 10 ms\n 0.05, // 50 ms\n 0.1, // 100 ms\n 0.2, // 200 ms\n 0.5, // 500 ms\n 1, // 1 second\n 2, // 2 seconds\n 5, // 5 seconds\n 10, // 10 seconds\n 20, // 20 seconds\n 30, // 30 seconds\n 60 // 1 minute\n ]\n});\n\nfunction formatActionName(action: string): string {\n // Remove 'before' or 'after' from the start of the action name\n const modifiedAction = action.replace(/^(before|after)/, '');\n\n // Lowercase the first letter of the remaining string\n return modifiedAction.charAt(0).toLowerCase() + modifiedAction.slice(1);\n}\n\n\nexport default ({ strapi }: { strapi: Core.Strapi }) => {\n strapi.db!.lifecycles.subscribe((event) => {\n if (event.action.startsWith('before')) {\n const labels = {\n event: formatActionName(event.action),\n model: event.model.singularName,\n };\n\n event.state.end = lifecycleDurationSeconds.startTimer(labels);\n }\n\n if (event.action.startsWith('after') && event.state.end) {\n (event.state.end as (labels?: Partial<Record<\"model\" | \"hook\", string | number>>) => number)()\n }\n });\n};\n","import { DefaultMetricsCollectorConfiguration, RegistryContentType } from \"prom-client\";\n\nexport interface Config {\n labels: object;\n collectDefaultMetrics: false | DefaultMetricsCollectorConfiguration<RegistryContentType>\n server: false | { port: number, host: string, path: string }\n}\n\nexport default {\n default: {\n collectDefaultMetrics: { prefix: '' },\n labels: [],\n server: {\n port: 9000,\n host: '0.0.0.0',\n path: '/metrics'\n }\n } as Config,\n validator(config: Config) {\n if (typeof config.collectDefaultMetrics === 'boolean' && config.collectDefaultMetrics) {\n throw Error('Invalid collectDefaultMetrics value. Can only be false or DefaultMetricsCollectorConfiguration');\n }\n\n if (typeof config.server === 'boolean' && config.server) {\n throw Error('Invalid server value. Can only be false or { port: number, host: string, path: string }');\n }\n },\n};\n","/**\n * Application methods\n */\nimport register from './register';\nimport bootstrap from './bootstrap';\n\n/**\n * Plugin server methods\n */\nimport config from './config';\n\nexport default { register, bootstrap, config };\n"],"names":["bootstrap","config","register"],"mappings":"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"}