From 13b6feb22f53a102de7284aa4035e61745f60af8 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 1 Sep 2026 20:48:27 +0200 Subject: [PATCH 1/8] chore: Made schedule creation a success log and not an error --- src/util.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util.ts b/src/util.ts index cac52ed..bb0e781 100644 --- a/src/util.ts +++ b/src/util.ts @@ -9,7 +9,7 @@ import { SystemType } from '@/types/common/system-types'; import { TokenType } from '@/types/common/token-types'; import { config, disabledFeatures } from '@/config-manager'; import { PasswordResetToken } from '@/models/password-reset-token'; -import { LOG_ERROR } from '@/logger'; +import { LOG_ERROR, LOG_SUCCESS } from '@/logger'; import type { IncomingHttpHeaders } from 'node:http'; import type { ParsedQs } from 'qs'; import type mongoose from 'mongoose'; @@ -379,5 +379,5 @@ function scheduledTask(schedule: string, name: string, fn: () => void | Promise< start: true }); - LOG_ERROR(`Added schedule ${name} for ${schedule}`); + LOG_SUCCESS(`Added schedule ${name} for ${schedule}`); } From ba245c0f9345b48b55d9f65df57e9b8117790d33 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 1 Sep 2026 20:49:00 +0200 Subject: [PATCH 2/8] fix: remove deletion workflow from boot --- src/server.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/server.ts b/src/server.ts index 0914cd8..cc40298 100644 --- a/src/server.ts +++ b/src/server.ts @@ -116,8 +116,6 @@ async function main(): Promise { startProvisioner(); - await checkMarkedDeletions(); - setupScheduledTasks(); app.listen(config.http.port, () => { From a622772eedb942c734aed8ca574a582d8b23e114 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 1 Sep 2026 20:54:37 +0200 Subject: [PATCH 3/8] feat: add healthcheck endpoint --- src/server.ts | 4 +++- src/services/healthz.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/services/healthz.ts diff --git a/src/server.ts b/src/server.ts index cc40298..1029f97 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,7 +3,7 @@ import morgan from 'morgan'; import xmlbuilder from 'xmlbuilder'; import xmlparser from '@/middleware/xml-parser'; import { connect as connectCache } from '@/cache'; -import { checkMarkedDeletions, connect as connectDatabase } from '@/database'; +import { connect as connectDatabase } from '@/database'; import { startGRPCServer } from '@/services/grpc/server'; import { fullUrl, getValueFromHeaders, setupScheduledTasks } from '@/util'; import { LOG_INFO, LOG_SUCCESS, LOG_WARN } from '@/logger'; @@ -15,6 +15,7 @@ import datastore from '@/services/datastore'; import api from '@/services/api'; import localcdn from '@/services/local-cdn'; import assets from '@/services/assets'; +import healthz from '@/services/healthz'; import { config, disabledFeatures } from '@/config-manager'; import { startProvisioner } from '@/provisioning'; @@ -51,6 +52,7 @@ app.use(nasc); app.use(api); app.use(localcdn); app.use(assets); +app.use(healthz); if (!disabledFeatures.datastore) { app.use(datastore); diff --git a/src/services/healthz.ts b/src/services/healthz.ts new file mode 100644 index 0000000..a875ac8 --- /dev/null +++ b/src/services/healthz.ts @@ -0,0 +1,9 @@ +import express from 'express'; + +const router = express.Router(); + +router.get('/healthz', (req, res) => { + res.status(200).send('OK'); +}); + +export default router; From 5b3b5e4151372d5022e347743100e5ae1cc84e2d Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 1 Sep 2026 21:02:47 +0200 Subject: [PATCH 4/8] fix: made error logs include the stacktrace --- src/server.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index 1029f97..88f8f6a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,3 +1,4 @@ +import { format } from 'node:util'; import express from 'express'; import morgan from 'morgan'; import xmlbuilder from 'xmlbuilder'; @@ -96,7 +97,7 @@ app.use((error: any, request: express.Request, response: express.Response, _next deviceID = 'Unknown'; } - LOG_WARN(`HTTP ${status} at ${url} from ${deviceID}: ${error.message}`); + LOG_WARN(`HTTP ${status} at ${url} from ${deviceID}: ${format(error)}`); response.status(status).json({ app: 'api', From 8a1964fab09f70a9373cb5d5a9866f15c1e088f8 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 1 Sep 2026 21:15:36 +0200 Subject: [PATCH 5/8] feat: add support for prometheus metrics --- package-lock.json | 81 ++++++++++++++++++++++++++++++++------ package.json | 1 + src/config-manager.ts | 4 ++ src/metrics.ts | 47 ++++++++++++++++++++++ src/server.ts | 5 +++ src/types/common/config.ts | 4 ++ 6 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 src/metrics.ts diff --git a/package-lock.json b/package-lock.json index cc39094..9c6a503 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "ejs": "^3.1.10", "email-validator": "^2.0.4", "express": "^4.17.1", + "express-prom-bundle": "^7.0.2", "express-rate-limit": "^6.7.0", "fs-extra": "^8.1.0", "got": "^11.8.2", @@ -1636,6 +1637,16 @@ "node": ">=6" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pretendonetwork/eslint-config": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@pretendonetwork/eslint-config/-/eslint-config-0.2.0.tgz", @@ -1965,7 +1976,6 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -1998,7 +2008,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -2035,7 +2044,6 @@ "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -2048,7 +2056,6 @@ "version": "4.19.9", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -2085,7 +2092,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -2131,7 +2137,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, "node_modules/@types/morgan": { @@ -2184,14 +2189,12 @@ "version": "6.15.1", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/responselike": { @@ -2207,7 +2210,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -2217,7 +2219,6 @@ "version": "1.15.10", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -2229,7 +2230,6 @@ "version": "0.17.6", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -3302,6 +3302,13 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "license": "MIT", + "peer": true + }, "node_modules/bit-buffer": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/bit-buffer/-/bit-buffer-0.2.5.tgz", @@ -5308,6 +5315,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-prom-bundle": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-7.0.2.tgz", + "integrity": "sha512-ffFV4HGHvCKnkNJFqm42sYztRJE5mLgOj8MpGey1HOatuFhtcwXoBD2m5gca7ZbcyjkIf7lOH5ZdrhlrBf0sGw==", + "license": "MIT", + "dependencies": { + "@types/express": "^4.17.21", + "express": "^4.18.2", + "on-finished": "^2.3.0", + "url-value-parser": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "prom-client": ">=15.0.0" + } + }, "node_modules/express-rate-limit": { "version": "6.11.2", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.11.2.tgz", @@ -8154,6 +8179,21 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "deprecated": "prom-client has been replaced by @prometheus-io/client", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -9328,6 +9368,16 @@ "node": ">=10" } }, + "node_modules/tdigest": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.3.tgz", + "integrity": "sha512-zbRt+lT+/H4fRItHshczHErVCQnitJk8MfMT24MqFJf3YL7SJJPqGIGeuOdvxXxM/AHFzKBl7WoyaYwqO9s3Kw==", + "license": "MIT", + "peer": true, + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/tga": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/tga/-/tga-1.0.7.tgz", @@ -9813,6 +9863,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url-value-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/url-value-parser/-/url-value-parser-2.2.0.tgz", + "integrity": "sha512-yIQdxJpgkPamPPAPuGdS7Q548rLhny42tg8d4vyTNzFqvOnwqrgHXvgehT09U7fwrzxi3RxCiXjoNUNnNOlQ8A==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 3d00970..a389e69 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "ejs": "^3.1.10", "email-validator": "^2.0.4", "express": "^4.17.1", + "express-prom-bundle": "^7.0.2", "express-rate-limit": "^6.7.0", "fs-extra": "^8.1.0", "got": "^11.8.2", diff --git a/src/config-manager.ts b/src/config-manager.ts index 3499f05..7c0ef58 100644 --- a/src/config-manager.ts +++ b/src/config-manager.ts @@ -38,6 +38,10 @@ export const config: Config = { http: { port: Number(process.env.PN_ACT_CONFIG_HTTP_PORT || '') }, + metrics: { + enabled: process.env.PN_ACT_CONFIG_METRICS_ENABLED === 'true', + port: Number(process.env.PN_ACT_CONFIG_METRICS_PORT || '') + }, mongoose: { connection_string: process.env.PN_ACT_CONFIG_MONGO_CONNECTION_STRING || '', options: mongooseConnectOptions diff --git a/src/metrics.ts b/src/metrics.ts new file mode 100644 index 0000000..5991220 --- /dev/null +++ b/src/metrics.ts @@ -0,0 +1,47 @@ +import { format } from 'node:util'; +import expressMetrics from 'express-prom-bundle'; +import express from 'express'; +import { LOG_ERROR, LOG_INFO, LOG_SUCCESS } from '@/logger'; +import { config } from '@/config-manager'; +import type { Express, NextFunction, Request, Response } from 'express'; + +export function registerMetrics(app: Express): Express { + const metrics = express(); + + if (config.metrics.enabled) { + LOG_INFO('Setting up metrics'); + app.use(expressMetrics({ + // * Include full express and nodejs metrics + includeMethod: true, + includePath: true, + urlValueParser: { + minBase64Length: 15 + }, + promClient: { + collectDefaultMetrics: {} + }, + + // * Keep metrics on a different app (so they aren't exposed) + autoregister: false, + metricsApp: metrics + })); + } + + metrics.use((error: Error, req: Request, res: Response, _next: NextFunction) => { + LOG_ERROR(`Request failed (metrics): ${format(error)}`); + res.sendStatus(500); + }); + + return metrics; +} + +export function listenMetrics(metricsApp: Express): void { + if (!config.metrics.enabled) { + return; + } + + const port = config.metrics.port; + metricsApp.listen(port, () => { + LOG_SUCCESS(`Metrics HTTP server started on port ${port}`); + }); +} diff --git a/src/server.ts b/src/server.ts index 88f8f6a..289004f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -19,6 +19,7 @@ import assets from '@/services/assets'; import healthz from '@/services/healthz'; import { config, disabledFeatures } from '@/config-manager'; import { startProvisioner } from '@/provisioning'; +import { listenMetrics, registerMetrics } from '@/metrics'; process.title = 'Pretendo - Account'; process.on('uncaughtException', (err, origin) => { @@ -31,6 +32,9 @@ process.on('SIGTERM', () => { const app = express(); +// * Metrics has to happen first so we can measure the other middleware +const metricsApp = registerMetrics(app); + // * START APPLICATION app.set('view engine', 'ejs'); app.set('views', __dirname + '/views'); @@ -124,6 +128,7 @@ async function main(): Promise { app.listen(config.http.port, () => { LOG_SUCCESS(`HTTP server started on port ${config.http.port}`); }); + listenMetrics(metricsApp); } main().catch(console.error); diff --git a/src/types/common/config.ts b/src/types/common/config.ts index 0eab22f..ec100e1 100644 --- a/src/types/common/config.ts +++ b/src/types/common/config.ts @@ -9,6 +9,10 @@ export interface Config { http: { port: number; }; + metrics: { + enabled: boolean; + port: number; + }; mongoose: { connection_string: string; options: mongoose.ConnectOptions; From 7babb418dfa765ccdd789c6d9781fb89f9bc212e Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 1 Sep 2026 21:29:29 +0200 Subject: [PATCH 6/8] feat: implement PNID and NEXToken metrics --- src/metrics.ts | 28 ++++++++++++++++++++++++++++ src/models/pnid.ts | 3 +++ 2 files changed, 31 insertions(+) diff --git a/src/metrics.ts b/src/metrics.ts index 5991220..d2b2f9f 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -1,10 +1,38 @@ import { format } from 'node:util'; +import { Gauge } from 'prom-client'; import expressMetrics from 'express-prom-bundle'; import express from 'express'; import { LOG_ERROR, LOG_INFO, LOG_SUCCESS } from '@/logger'; import { config } from '@/config-manager'; +import { PNID } from '@/models/pnid'; +import { NEXToken } from '@/models/nex-token'; import type { Express, NextFunction, Request, Response } from 'express'; +export const pnidTotalGauge = new Gauge({ + name: 'pn_account_pnid_total', + help: 'Total number of registered PNIDs', + async collect(): Promise { + // * Aggregations are faster on large collections + const [result] = await PNID.aggregate<{ n: number } | undefined>([ + { $match: { deleted: false } }, + { $count: 'n' } + ]); + this.set(result?.n ?? 0); + } +}); + +export const nexTokenTotalGauge = new Gauge({ + name: 'pn_account_nex_token_total', + help: 'Total number of NEX tokens', + async collect(): Promise { + // * Aggregations are faster on large collections + const [result] = await NEXToken.aggregate<{ n: number } | undefined>([ + { $count: 'n' } + ]); + this.set(result?.n ?? 0); + } +}); + export function registerMetrics(app: Express): Express { const metrics = express(); diff --git a/src/models/pnid.ts b/src/models/pnid.ts index 81d523d..0b2d8ca 100644 --- a/src/models/pnid.ts +++ b/src/models/pnid.ts @@ -144,6 +144,9 @@ PNIDSchema.index({ 'pid': 1, 'username': 1, 'connections.discord.id': 1 }); PNIDSchema.plugin(uniqueValidator, { message: '{PATH} already in use.' }); +// * Used by metrics +PNIDSchema.index({ deleted: 1 }); + /* According to http://pf2m.com/tools/rank.php Nintendo PID's start at 1,800,000,000 and count down with each account This means the max PID is 1799999999 and hard-limits the number of potential accounts to 1,800,000,000 From e4d89506593d0914fceedba20a3e40bf7f3de76f Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 1 Sep 2026 21:33:46 +0200 Subject: [PATCH 7/8] fix: Remove deprecated usage of request.host --- src/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.ts b/src/util.ts index bb0e781..dc8a74d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -78,7 +78,7 @@ export function createServiceToken(server: HydratedServerDocument, options: Serv export function fullUrl(request: express.Request): string { const protocol = request.protocol; - const host = request.host; + const host = request.hostname; const opath = request.originalUrl; return `${protocol}://${host}${opath}`; From 4bcfb7b184be9e8c5c54b489c25bb709f6da487b Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 1 Sep 2026 21:42:58 +0200 Subject: [PATCH 8/8] chore: add documentation of new config fields --- SETUP.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SETUP.md b/SETUP.md index 1672729..e0a4570 100644 --- a/SETUP.md +++ b/SETUP.md @@ -96,4 +96,6 @@ Configurations are loaded through environment variables. `.env` files are suppor | `PN_ACT_CONFIG_GRPC_MIIVERSE_HOST` | Used to remove Miiverse user data during account deletion | No | | `PN_ACT_CONFIG_GRPC_MIIVERSE_PORT` | Used to remove Miiverse user data during account deletion | No | | `PN_ACT_CONFIG_GRPC_MIIVERSE_KEY_API` | Used to remove Miiverse user data during account deletion | No | -| `PN_ACT_PROVISIONING_SERVER_CONFIG` | Specify a path to a JSON file containing a list of servers to provision automatically to the DB | Yes | \ No newline at end of file +| `PN_ACT_PROVISIONING_SERVER_CONFIG` | Specify a path to a JSON file containing a list of servers to provision automatically to the DB | Yes | +| `PN_ACT_CONFIG_METRICS_ENABLED` | Set to `true` to enable the metrics server, uses the metrics port | Yes | +| `PN_ACT_CONFIG_METRICS_PORT` | The HTTP port the metrics server listens on | Yes |