diff --git a/README.md b/README.md index 6df09e1..91be1ae 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ Backend for movie-web - [ ] consume provider metrics - [ ] DELETE user - should delete all associated data - [ ] prometheus metrics - - [ ] requests - - [ ] user count + - [X] requests + - [X] user count - [ ] provider metrics - [ ] ratelimits (stored in redis) - [X] switch to pnpm diff --git a/src/main.ts b/src/main.ts index 46cd827..bb51828 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,5 @@ -import { setupFastify } from '@/modules/fastify'; +import { setupFastify, startFastify } from '@/modules/fastify'; +import { setupMetrics } from '@/modules/metrics'; import { setupMikroORM } from '@/modules/mikro'; import { scopedLogger } from '@/services/logger'; @@ -9,8 +10,11 @@ async function bootstrap(): Promise { evt: 'setup', }); + const app = await setupFastify(); await setupMikroORM(); - await setupFastify(); + await setupMetrics(app); + + await startFastify(app); log.info(`App setup, ready to accept connections`, { evt: 'success', diff --git a/src/modules/fastify/index.ts b/src/modules/fastify/index.ts index f1587cc..b4fa39d 100644 --- a/src/modules/fastify/index.ts +++ b/src/modules/fastify/index.ts @@ -72,9 +72,14 @@ export async function setupFastify(): Promise { }, ); + if (!exportedApp) throw new Error('no app to export in fastify'); + return exportedApp; +} + +export function startFastify(app: FastifyInstance) { // listen to port log.info(`listening to port`, { evt: 'setup-listen' }); - return new Promise((resolve) => { + return new Promise((resolve) => { app.listen( { port: conf.server.port, @@ -91,7 +96,7 @@ export async function setupFastify(): Promise { log.info(`fastify setup successfully`, { evt: 'setup-success', }); - resolve(exportedApp as FastifyInstance); + resolve(); }, ); }); diff --git a/src/modules/fastify/routes.ts b/src/modules/fastify/routes.ts index 00487f1..7832d34 100644 --- a/src/modules/fastify/routes.ts +++ b/src/modules/fastify/routes.ts @@ -9,11 +9,8 @@ import { userProgressRouter } from '@/routes/users/progress'; import { userSessionsRouter } from '@/routes/users/sessions'; import { userSettingsRouter } from '@/routes/users/settings'; import { FastifyInstance } from 'fastify'; -import metricsPlugin from 'fastify-metrics'; export async function setupRoutes(app: FastifyInstance) { - await app.register(metricsPlugin, { endpoint: '/metrics' }); - await app.register(manageAuthRouter.register); await app.register(loginAuthRouter.register); await app.register(userSessionsRouter.register); diff --git a/src/modules/metrics/index.ts b/src/modules/metrics/index.ts new file mode 100644 index 0000000..11b5b3d --- /dev/null +++ b/src/modules/metrics/index.ts @@ -0,0 +1,39 @@ +import { getORM } from '@/modules/mikro'; +import { FastifyInstance } from 'fastify'; +import { Counter } from 'prom-client'; +import metricsPlugin from 'fastify-metrics'; +import { updateMetrics } from '@/modules/metrics/update'; +import { scopedLogger } from '@/services/logger'; + +const log = scopedLogger('metrics'); + +export type Metrics = { + user: Counter<'namespace'>; +}; + +let metrics: null | Metrics = null; + +export function getMetrics() { + if (!metrics) throw new Error('metrics not initialized'); + return metrics; +} + +export async function setupMetrics(app: FastifyInstance) { + log.info(`Setting up metrics...`, { evt: 'start' }); + + await app.register(metricsPlugin, { endpoint: '/metrics' }); + + metrics = { + user: new Counter({ + name: 'user_count', + help: 'user_help', + labelNames: ['namespace'], + }), + }; + + const orm = getORM(); + const em = orm.em.fork(); + log.info(`Syncing up metrics...`, { evt: 'sync' }); + await updateMetrics(em, metrics); + log.info(`Metrics initialized!`, { evt: 'end' }); +} diff --git a/src/modules/metrics/update.ts b/src/modules/metrics/update.ts new file mode 100644 index 0000000..60668ac --- /dev/null +++ b/src/modules/metrics/update.ts @@ -0,0 +1,22 @@ +import { User } from '@/db/models/User'; +import { Metrics } from '@/modules/metrics'; +import { EntityManager } from '@mikro-orm/postgresql'; + +export async function updateMetrics(em: EntityManager, metrics: Metrics) { + const users = await em + .createQueryBuilder(User) + .groupBy('namespace') + .count() + .select(['namespace', 'count']) + .execute< + { + namespace: string; + count: string; + }[] + >(); + + metrics.user.reset(); + users.forEach((v) => { + metrics?.user.inc({ namespace: v.namespace }, Number(v.count)); + }); +} diff --git a/src/routes/auth/manage.ts b/src/routes/auth/manage.ts index a506a00..a4c3760 100644 --- a/src/routes/auth/manage.ts +++ b/src/routes/auth/manage.ts @@ -1,5 +1,6 @@ import { formatSession } from '@/db/models/Session'; import { User, formatUser } from '@/db/models/User'; +import { getMetrics } from '@/modules/metrics'; import { assertCaptcha } from '@/services/captcha'; import { handle } from '@/services/handler'; import { makeRouter } from '@/services/router'; @@ -37,6 +38,7 @@ export const manageAuthRouter = makeRouter((app) => { ); await em.persistAndFlush([user, session]); + getMetrics().user.inc({ namespace: body.namespace }, 1); return { user: formatUser(user),