From e2056b47340365efc13443010f8bd8106485b77a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Sat, 21 Nov 2020 19:55:53 +0100 Subject: [PATCH] feat(api): add prometheus metrics Currently we support metrics for the Node.js runtime and HTTP endpoints. --- README.md | 8 +++++ package-lock.json | 58 ++++++++++++++++++++++++++++++++--- package.json | 1 + src/app.module.ts | 2 ++ src/config/config.module.ts | 3 ++ src/metrics/metrics.module.ts | 51 ++++++++++++++++++++++++++++++ 6 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 src/metrics/metrics.module.ts diff --git a/README.md b/README.md index ceafedb..e4528a2 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,14 @@ You can use Sentry to automatically detect and report any exceptions thrown. - `SENTRY_ENABLED`: **false**, Set to `true` to enable Sentry. - `SENTRY_DSN`: _Required_, but only if `SENTRY_ENABLED` is `true`. The [DSN](https://docs.sentry.io/product/sentry-basics/dsn-explainer/) for your Sentry project. +#### Prometheus + +You can use Prometheus to track various metrics about your Listory deployment. + +The metrics will be exposed on the `/api/metrics` endpoint. Make sure that this endpoint is not publicly available in your deployment. + +- `PROMETHEUS_ENABLED`: **false**, Set to `true` to enable Prometheus Metrics. + ## Development ### Configure Spotify API Access diff --git a/package-lock.json b/package-lock.json index ee4c1b1..c348b90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -833,6 +833,17 @@ "minimist": "^1.2.0" } }, + "@digikare/nestjs-prom": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@digikare/nestjs-prom/-/nestjs-prom-1.0.0.tgz", + "integrity": "sha512-iuIdnZlwZ5EVHfcqXRveB+7J/xlv1LAvroW2qxmH9G+NjAHNTaouheDwivyKUfZHaXP9Q3Zp1pN24l3rvZi6+A==", + "requires": { + "@nestjs/testing": "^7.3.2", + "prom-client": "^12.0.0", + "response-time": "^2.3.2", + "url-value-parser": "^2.0.1" + } + }, "@dsherret/to-absolute-glob": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@dsherret/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", @@ -2198,7 +2209,6 @@ "version": "7.5.2", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-7.5.2.tgz", "integrity": "sha512-sFkkS6S97F9dFk8Kk5ksBLJGHQZ7n1S1s3BkHxwHwcJqglcpE45wyhoMQ/YzJP5uPyLkgqCQFT2Hst9ndpBV+Q==", - "dev": true, "requires": { "optional": "0.1.4", "tslib": "2.0.3" @@ -2207,8 +2217,7 @@ "tslib": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", - "dev": true + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" } } }, @@ -4135,6 +4144,11 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" }, + "bintrees": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", + "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -10524,6 +10538,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -10544,8 +10563,7 @@ "optional": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/optional/-/optional-0.1.4.tgz", - "integrity": "sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw==", - "dev": true + "integrity": "sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw==" }, "optionator": { "version": "0.8.3", @@ -11087,6 +11105,14 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "prom-client": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-12.0.0.tgz", + "integrity": "sha512-JbzzHnw0VDwCvoqf8y1WDtq4wSBAbthMB1pcVI/0lzdqHGJI3KBJDXle70XK+c7Iv93Gihqo0a5LlOn+g8+DrQ==", + "requires": { + "tdigest": "^0.1.1" + } + }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -11465,6 +11491,15 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "response-time": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.2.tgz", + "integrity": "sha1-/6cbq5UtYvfB1Jt0NDVfvGjf/Fo=", + "requires": { + "depd": "~1.1.0", + "on-headers": "~1.0.1" + } + }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -12483,6 +12518,14 @@ } } }, + "tdigest": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", + "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=", + "requires": { + "bintrees": "1.0.1" + } + }, "terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -13312,6 +13355,11 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, + "url-value-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/url-value-parser/-/url-value-parser-2.0.1.tgz", + "integrity": "sha512-bexECeREBIueboLGM3Y1WaAzQkIn+Tca/Xjmjmfd0S/hFHSCEoFkNh0/D0l9G4K74MkEP/lLFRlYnxX3d68Qgw==" + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", diff --git a/package.json b/package.json index 3da0b04..8172169 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test:e2e": "jest --config ./apps/listory/test/jest-e2e.json" }, "dependencies": { + "@digikare/nestjs-prom": "^1.0.0", "@hapi/joi": "17.1.1", "@nestjs/common": "7.5.2", "@nestjs/config": "0.5.0", diff --git a/src/app.module.ts b/src/app.module.ts index 7871308..d60eb0e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -9,6 +9,7 @@ import { DatabaseModule } from "./database/database.module"; import { HealthCheckModule } from "./health-check/health-check.module"; import { ListensModule } from "./listens/listens.module"; import { LoggerModule } from "./logger/logger.module"; +import { MetricsModule } from "./metrics/metrics.module"; import { MusicLibraryModule } from "./music-library/music-library.module"; import { ReportsModule } from "./reports/reports.module"; import { SourcesModule } from "./sources/sources.module"; @@ -25,6 +26,7 @@ import { UsersModule } from "./users/users.module"; exclude: ["/api*"], }), RavenModule, + MetricsModule.forRoot(), AuthModule, UsersModule, SourcesModule, diff --git a/src/config/config.module.ts b/src/config/config.module.ts index 0cb7352..0cc4462 100644 --- a/src/config/config.module.ts +++ b/src/config/config.module.ts @@ -42,6 +42,9 @@ import { ConfigModule as NestConfigModule } from "@nestjs/config"; is: Joi.valid(true), then: Joi.required(), }), + + // Prometheus for Metrics (Optional) + PROMETHEUS_ENABLED: Joi.boolean().default(false), }), }), ], diff --git a/src/metrics/metrics.module.ts b/src/metrics/metrics.module.ts new file mode 100644 index 0000000..a89c5af --- /dev/null +++ b/src/metrics/metrics.module.ts @@ -0,0 +1,51 @@ +import { InboundMiddleware, PromModule } from "@digikare/nestjs-prom"; +import { DEFAULT_PROM_OPTIONS } from "@digikare/nestjs-prom/dist/prom.constants"; +import { + DynamicModule, + MiddlewareConsumer, + Module, + NestModule, +} from "@nestjs/common"; + +// Dirty hack because we can not conditionally import modules based on +// injected services and upstream module does not support dynamic configuration +// +// https://github.com/digikare/nestjs-prom/issues/27 +const promEnabled = process.env.PROMETHEUS_ENABLED === "true"; + +@Module({}) +export class MetricsModule implements NestModule { + static forRoot(): DynamicModule { + const module = { + imports: [], + providers: [], + }; + if (promEnabled) { + const promOptions = { + metricPath: "/api/metrics", + withDefaultsMetrics: true, + withDefaultController: true, + }; + + module.imports.push(PromModule.forRoot(promOptions)); + + module.providers.push({ + provide: DEFAULT_PROM_OPTIONS, + useValue: promOptions, + }); + } + + return { + module: MetricsModule, + ...module, + }; + } + + configure(consumer: MiddlewareConsumer) { + if (promEnabled) { + // We register the Middleware ourselves to avoid tracking + // latency for static files served for the frontend. + consumer.apply(InboundMiddleware).forRoutes("/api"); + } + } +}