mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-10-01 14:48:25 -05:00
Merge pull request #358 from PretendoNetwork/feat/healthcheck-metrics-and-more
Healthchecks, metrics & more
This commit is contained in:
4
SETUP.md
4
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 |
|
||||
| `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 |
|
||||
|
||||
81
package-lock.json
generated
81
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
75
src/metrics.ts
Normal file
75
src/metrics.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
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<void> {
|
||||
// * 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<void> {
|
||||
// * 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();
|
||||
|
||||
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}`);
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { format } from 'node:util';
|
||||
import express from 'express';
|
||||
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,8 +16,10 @@ 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';
|
||||
import { listenMetrics, registerMetrics } from '@/metrics';
|
||||
|
||||
process.title = 'Pretendo - Account';
|
||||
process.on('uncaughtException', (err, origin) => {
|
||||
@@ -29,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');
|
||||
@@ -51,6 +57,7 @@ app.use(nasc);
|
||||
app.use(api);
|
||||
app.use(localcdn);
|
||||
app.use(assets);
|
||||
app.use(healthz);
|
||||
|
||||
if (!disabledFeatures.datastore) {
|
||||
app.use(datastore);
|
||||
@@ -94,7 +101,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',
|
||||
@@ -116,13 +123,12 @@ async function main(): Promise<void> {
|
||||
|
||||
startProvisioner();
|
||||
|
||||
await checkMarkedDeletions();
|
||||
|
||||
setupScheduledTasks();
|
||||
|
||||
app.listen(config.http.port, () => {
|
||||
LOG_SUCCESS(`HTTP server started on port ${config.http.port}`);
|
||||
});
|
||||
listenMetrics(metricsApp);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
9
src/services/healthz.ts
Normal file
9
src/services/healthz.ts
Normal file
@@ -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;
|
||||
@@ -9,6 +9,10 @@ export interface Config {
|
||||
http: {
|
||||
port: number;
|
||||
};
|
||||
metrics: {
|
||||
enabled: boolean;
|
||||
port: number;
|
||||
};
|
||||
mongoose: {
|
||||
connection_string: string;
|
||||
options: mongoose.ConnectOptions;
|
||||
|
||||
@@ -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';
|
||||
@@ -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}`;
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user