diff --git a/app/packages/backend/package.json b/app/packages/backend/package.json index 6f8caf5c0..60e68d816 100644 --- a/app/packages/backend/package.json +++ b/app/packages/backend/package.json @@ -48,7 +48,9 @@ "express-promise-router": "^4.1.0", "node-gyp": "^9.0.0", "pg": "^8.11.3", - "winston": "^3.2.1" + "winston": "^3.2.1", + "express-prom-bundle": "^7.0.0", + "prom-client": "^15.0.0" }, "devDependencies": { "@backstage/cli": "^0.25.0", @@ -60,4 +62,4 @@ "files": [ "dist" ] -} +} \ No newline at end of file diff --git a/app/packages/backend/src/index.ts b/app/packages/backend/src/index.ts index e44f19597..a366447c9 100644 --- a/app/packages/backend/src/index.ts +++ b/app/packages/backend/src/index.ts @@ -35,6 +35,7 @@ import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; import kubernetes from './plugins/kubernetes'; import todo from './plugins/todo'; +import { metricsHandler } from './metrics'; function makeCreateEnv(config: Config) { const root = getRootLogger(); @@ -110,7 +111,8 @@ async function main() { const service = createServiceBuilder(module) .loadConfig(config) -.addRouter('', await healthcheck(healthcheckEnv)) + .addRouter('', await healthcheck(healthcheckEnv)) + .addRouter('', metricsHandler()) .addRouter('/api', apiRouter) .addRouter('', await app(appEnv)); diff --git a/app/packages/backend/src/metrics.ts b/app/packages/backend/src/metrics.ts new file mode 100644 index 000000000..49afdb695 --- /dev/null +++ b/app/packages/backend/src/metrics.ts @@ -0,0 +1,40 @@ +// packages/backend/src/metrics.ts +import { useHotCleanup } from '@backstage/backend-common'; +import { RequestHandler } from 'express'; +import promBundle from 'express-prom-bundle'; +import prom from 'prom-client'; +import * as url from 'url'; + +const rootRegEx = new RegExp('^/([^/]*)/.*'); +const apiRegEx = new RegExp('^/api/([^/]*)/.*'); + +export function normalizePath(req: any): string { + const path = url.parse(req.originalUrl || req.url).pathname || '/'; + + // Capture /api/ and the plugin name + if (apiRegEx.test(path)) { + return path.replace(apiRegEx, '/api/$1'); + } + + // Only the first path segment at root level + return path.replace(rootRegEx, '/$1'); +} + +/** + * Adds a /metrics endpoint, register default runtime metrics and instrument the router. + */ +export function metricsHandler(): RequestHandler { + // We can only initialize the metrics once and have to clean them up between hot reloads + useHotCleanup(module, () => prom.register.clear()); + + return promBundle({ + includeMethod: true, + includePath: true, + // Using includePath alone is problematic, as it will include path labels with high + // cardinality (e.g. path params). Instead we would have to template them. However, this + // is difficult, as every backend plugin might use different routes. Instead we only take + // the first directory of the path, to have at least an idea how each plugin performs: + normalizePath, + promClient: { collectDefaultMetrics: {} }, + }); +}