Skip to content

Commit

Permalink
feat(backend): add Prometheus metrics (janus-idp#469)
Browse files Browse the repository at this point in the history
* feat(backend): expose prometheus metrics

* feat(backend): expose metrics route only when METRICS_ENABLED=true

* chore: address review comments

* chore: add changeset
  • Loading branch information
kadel committed Sep 6, 2023
1 parent 6374999 commit 28c55dc
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 7 deletions.
7 changes: 7 additions & 0 deletions .changeset/dull-rabbits-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'backend': minor
---

Added `METRICS_ENABLED` that enables Prometheus metrics

When enabled, Prometheus metrics are available at the backed URL in /metrics (http://localhost:7007/metrics)
1 change: 1 addition & 0 deletions app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,4 @@ enabled:
azureDevOps: ${AZURE_ENABLED}
jenkins: ${JENKINS_ENABLED}
permission: ${PERMISSION_ENABLED}
metrics: ${METRICS_ENABLED}
4 changes: 3 additions & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@
"express-promise-router": "4.1.1",
"isolated-vm": "4.6.0",
"pg": "8.11.3",
"winston": "3.10.0"
"winston": "3.10.0",
"express-prom-bundle": "6.6.0",
"prom-client": "14.2.0"
},
"devDependencies": {
"@backstage/cli": "0.22.12",
Expand Down
59 changes: 55 additions & 4 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
loadBackendConfig,
notFoundHandler,
useHotMemoize,
ServiceBuilder,
} from '@backstage/backend-common';
import { TaskScheduler } from '@backstage/backend-tasks';
import { Config } from '@backstage/config';
Expand All @@ -39,6 +40,8 @@ import search from './plugins/search';
import sonarqube from './plugins/sonarqube';
import techdocs from './plugins/techdocs';
import { PluginEnvironment } from './types';
import { metricsHandler } from './metrics';
import { RequestHandler } from 'express';

function makeCreateEnv(config: Config) {
const root = getRootLogger();
Expand Down Expand Up @@ -114,6 +117,34 @@ async function addPlugin(args: AddPlugin | AddOptionalPlugin): Promise<void> {
}
}

type AddRouterBase = {
name: string;
service: ServiceBuilder;
root: string;
router: RequestHandler | ReturnType<typeof Router>;
};

type AddRouterOptional = {
isOptional: true;
config: Config;
} & AddRouterBase;

type AddRouter = {
isOptional?: false;
} & AddRouterBase;

async function addRouter(args: AddRouter | AddRouterOptional): Promise<void> {
const { isOptional, name, service, root, router } = args;

const isRouterEnabled =
!isOptional || args.config.getOptionalBoolean(`enabled.${name}`) || false;

if (isRouterEnabled) {
console.log(`Adding router ${name} to backend...`);
service.addRouter(root, router);
}
}

async function main() {
const config = await loadBackendConfig({
argv: process.argv,
Expand Down Expand Up @@ -215,10 +246,30 @@ async function main() {
// Add backends ABOVE this line; this 404 handler is the catch-all fallback
apiRouter.use(notFoundHandler());

const service = createServiceBuilder(module)
.loadConfig(config)
.addRouter('/api', apiRouter)
.addRouter('', await app(appEnv));
const service = createServiceBuilder(module).loadConfig(config);

// Required routers
await addRouter({
name: 'api',
service,
root: '/api',
router: apiRouter,
});
await addRouter({
name: 'app',
service,
root: '',
router: await app(appEnv),
});

// Optional routers
await addRouter({
name: 'metrics',
config,
service,
root: '',
router: metricsHandler(),
});

await service.start().catch(err => {
console.log(err);
Expand Down
39 changes: 39 additions & 0 deletions packages/backend/src/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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: {} },
});
}
1 change: 1 addition & 0 deletions showcase-docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ The easiest and fastest method for getting started: Backstage Showcase app, runn
- `${GITLAB_ENABLED}` Set to `true` to enable the GitLab Entity backend plugin.
- `${AZURE_ENABLED}` Set to `true` to enable the Azure DevOps Entity backend plugin.
- `${JENKINS_ENABLED}` Set to `true` to enable the Jenkins Entity backend plugin.
- `${METRICS_ENABLED}` Set to `true` to enable Prometheus metrics (metrics will be available on `http://localhost:7007/metrics`).

- Setup the GitHub plugins (GitHub Issues and GitHub Pull Request)

Expand Down
17 changes: 15 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14739,6 +14739,14 @@ express-openapi-validator@^5.0.4:
ono "^7.1.3"
path-to-regexp "^6.2.0"

express-prom-bundle@6.6.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/express-prom-bundle/-/express-prom-bundle-6.6.0.tgz#9c33c1bd1478d70e3961a53aed2d17f15ef821ca"
integrity sha512-tZh2P2p5a8/yxQ5VbRav011Poa4R0mHqdFwn9Swe/obXDe5F0jY9wtRAfNYnqk4LXY7akyvR/nrvAHxQPWUjsQ==
dependencies:
on-finished "^2.3.0"
url-value-parser "^2.0.0"

express-promise-router@4.1.1, express-promise-router@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/express-promise-router/-/express-promise-router-4.1.1.tgz#8fac102060b9bcc868f84d34fbb12fd8fa494291"
Expand Down Expand Up @@ -19555,7 +19563,7 @@ oidc-token-hash@^5.0.3:
resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz#9a229f0a1ce9d4fc89bcaee5478c97a889e7b7b6"
integrity sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==

on-finished@2.4.1:
on-finished@2.4.1, on-finished@^2.3.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
Expand Down Expand Up @@ -20699,7 +20707,7 @@ process@^0.11.10:
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==

prom-client@^14.0.1:
prom-client@14.2.0, prom-client@^14.0.1:
version "14.2.0"
resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-14.2.0.tgz#ca94504e64156f6506574c25fb1c34df7812cf11"
integrity sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA==
Expand Down Expand Up @@ -24021,6 +24029,11 @@ url-template@^2.0.8:
resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21"
integrity sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==

url-value-parser@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/url-value-parser/-/url-value-parser-2.2.0.tgz#f38ae8cd24604ec69bc219d66929ddbbd93a2b32"
integrity sha512-yIQdxJpgkPamPPAPuGdS7Q548rLhny42tg8d4vyTNzFqvOnwqrgHXvgehT09U7fwrzxi3RxCiXjoNUNnNOlQ8A==

url@^0.11.0, url@~0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.1.tgz#26f90f615427eca1b9f4d6a28288c147e2302a32"
Expand Down

0 comments on commit 28c55dc

Please sign in to comment.