diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 1a254d891f..92d301d7e3 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -37,11 +37,13 @@ "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", + "express-prom-bundle": "^6.1.0", "express-promise-router": "^3.0.3", "helmet": "^4.0.0", "knex": "^0.21.1", "lodash": "^4.17.15", "morgan": "^1.10.0", + "prom-client": "^12.0.0", "selfsigned": "^1.10.7", "stoppable": "^1.1.0", "winston": "^3.2.1" diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 4669c00e68..df69387e40 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -37,6 +37,7 @@ import { HttpsSettings, } from './config'; import { createHttpServer, createHttpsServer } from './hostFactory'; +import { metricsHandler } from './metrics'; const DEFAULT_PORT = 7000; // '' is express default, which listens to all interfaces @@ -48,6 +49,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { private logger: Logger | undefined; private corsOptions: cors.CorsOptions | undefined; private httpsSettings: HttpsSettings | undefined; + private enableMetrics: boolean = true; private routers: [string, Router][]; // Reference to the module where builder is created - needed for hot module // reloading @@ -82,6 +84,9 @@ export class ServiceBuilderImpl implements ServiceBuilder { this.httpsSettings = httpsSettings; } + // For now, configuration of metrics is a simple boolean and active by default + this.enableMetrics = backendConfig.getOptionalBoolean('metrics') !== false; + return this; } @@ -131,6 +136,9 @@ export class ServiceBuilderImpl implements ServiceBuilder { } app.use(compression()); app.use(express.json()); + if (this.enableMetrics) { + app.use(metricsHandler()); + } app.use(requestLoggingHandler()); for (const [root, route] of this.routers) { app.use(root, route); diff --git a/packages/backend-common/src/service/lib/metrics.ts b/packages/backend-common/src/service/lib/metrics.ts new file mode 100644 index 0000000000..d7b544765b --- /dev/null +++ b/packages/backend-common/src/service/lib/metrics.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import prom from 'prom-client'; +import promBundle from 'express-prom-bundle'; +import { RequestHandler } from 'express'; + +/** + * 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 + 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: [['^/([^/]*)/.*', '/$1']], + promClient: { collectDefaultMetrics: {} }, + }); +} diff --git a/yarn.lock b/yarn.lock index d56a2cbd2c..59111a320e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6511,6 +6511,11 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +bintrees@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" + integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= + bl@^1.0.0: version "1.2.2" resolved "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" @@ -10135,6 +10140,14 @@ expect@^26.0.1: jest-message-util "^26.0.1" jest-regex-util "^26.0.0" +express-prom-bundle@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-6.1.0.tgz#8fd72e5bedbbd686b8e7c49e0aecbc1b14b28743" + integrity sha512-krlvp5r6sgJ1IwL6M6/coMrNbAlwtpk+uyivfeyRMCupTK4HzIEQHH0gwrNhLiKyPmSbtZcSmtO6s+PRumRp5g== + dependencies: + on-finished "^2.3.0" + url-value-parser "^2.0.0" + express-promise-router@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/express-promise-router/-/express-promise-router-3.0.3.tgz#5e6d22a5a3f013d71833172fe8d7ab780c3f6b70" @@ -16227,7 +16240,7 @@ omggif@1.0.7: resolved "https://registry.npmjs.org/omggif/-/omggif-1.0.7.tgz#59d2eecb0263de84635b3feb887c0c9973f1e49d" integrity sha1-WdLuywJj3oRjWz/riHwMmXPx5J0= -on-finished@~2.3.0: +on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= @@ -17809,6 +17822,13 @@ progress@^2.0.0: resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +prom-client@^12.0.0: + version "12.0.0" + resolved "https://registry.npmjs.org/prom-client/-/prom-client-12.0.0.tgz#9689379b19bd3f6ab88a9866124db9da3d76c6ed" + integrity sha512-JbzzHnw0VDwCvoqf8y1WDtq4wSBAbthMB1pcVI/0lzdqHGJI3KBJDXle70XK+c7Iv93Gihqo0a5LlOn+g8+DrQ== + dependencies: + tdigest "^0.1.1" + promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" @@ -21142,6 +21162,13 @@ tcp-port-used@^1.0.1: debug "4.1.0" is2 "2.0.1" +tdigest@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021" + integrity sha1-Ljyyw56kSeVdHmzZEReszKRYgCE= + dependencies: + bintrees "1.0.1" + telejson@^3.2.0: version "3.3.0" resolved "https://registry.npmjs.org/telejson/-/telejson-3.3.0.tgz#6d814f3c0d254d5c4770085aad063e266b56ad03" @@ -22131,6 +22158,11 @@ url-to-options@^1.0.1: resolved "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= +url-value-parser@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/url-value-parser/-/url-value-parser-2.0.1.tgz#c8179a095ab9ec1f5aa17ca36af5af396b4e95ed" + integrity sha512-bexECeREBIueboLGM3Y1WaAzQkIn+Tca/Xjmjmfd0S/hFHSCEoFkNh0/D0l9G4K74MkEP/lLFRlYnxX3d68Qgw== + url@^0.11.0, url@~0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"