diff --git a/apps/calendar/apps/backend/package.json b/apps/calendar/apps/backend/package.json index ed42839f0..b1326c4aa 100644 --- a/apps/calendar/apps/backend/package.json +++ b/apps/calendar/apps/backend/package.json @@ -23,6 +23,7 @@ "dependencies": { "@calendar/shared": "workspace:*", "@manacore/shared-nestjs-auth": "workspace:*", + "@manacore/shared-nestjs-metrics": "workspace:*", "@nestjs/common": "^10.4.15", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.15", diff --git a/apps/calendar/apps/backend/src/app.module.ts b/apps/calendar/apps/backend/src/app.module.ts index a11c648e1..7a6a9814b 100644 --- a/apps/calendar/apps/backend/src/app.module.ts +++ b/apps/calendar/apps/backend/src/app.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; +import { MetricsModule } from '@manacore/shared-nestjs-metrics'; import { DatabaseModule } from './db/database.module'; import { HealthModule } from './health/health.module'; import { CalendarModule } from './calendar/calendar.module'; @@ -11,7 +12,6 @@ import { ReminderModule } from './reminder/reminder.module'; import { ShareModule } from './share/share.module'; import { SyncModule } from './sync/sync.module'; import { NetworkModule } from './network/network.module'; -import { MetricsModule } from './metrics'; import { EmailModule } from './email/email.module'; import { NotificationModule } from './notification/notification.module'; @@ -22,7 +22,10 @@ import { NotificationModule } from './notification/notification.module'; envFilePath: '.env', }), ScheduleModule.forRoot(), - MetricsModule, + MetricsModule.register({ + prefix: 'calendar_', + excludePaths: ['/health'], + }), DatabaseModule, HealthModule, EmailModule, diff --git a/apps/calendar/apps/backend/src/main.ts b/apps/calendar/apps/backend/src/main.ts index 7cf6f2ece..408cf64a8 100644 --- a/apps/calendar/apps/backend/src/main.ts +++ b/apps/calendar/apps/backend/src/main.ts @@ -1,48 +1,10 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; import { AppModule } from './app.module'; -import { MetricsService } from './metrics/metrics.service'; - -// Normalize route paths to prevent high cardinality -function normalizeRoute(path: string): string { - return path - .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ':id') - .replace(/\/\d+/g, '/:id'); -} async function bootstrap() { const app = await NestFactory.create(AppModule); - // Get MetricsService for request tracking - const metricsService = app.get(MetricsService); - - // Global Express middleware to track ALL HTTP requests - app.use((req: Request, res: Response, next: NextFunction) => { - if (req.path === '/metrics') { - return next(); - } - - const startTime = Date.now(); - const method = req.method; - const route = normalizeRoute(req.path); - - res.once('finish', () => { - const duration = (Date.now() - startTime) / 1000; - metricsService.httpRequestsTotal.inc({ - method, - route, - status: res.statusCode.toString(), - }); - metricsService.httpRequestDuration.observe( - { method, route, status: res.statusCode.toString() }, - duration - ); - }); - - next(); - }); - // Enable CORS for mobile and web apps const corsOrigins = process.env.CORS_ORIGINS?.split(',').map((origin) => origin.trim()) || [ 'http://localhost:3000', diff --git a/apps/calendar/apps/backend/src/metrics/index.ts b/apps/calendar/apps/backend/src/metrics/index.ts deleted file mode 100644 index 860cd0cdf..000000000 --- a/apps/calendar/apps/backend/src/metrics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './metrics.module'; -export * from './metrics.service'; -export * from './metrics.controller'; diff --git a/apps/calendar/apps/backend/src/metrics/metrics.controller.ts b/apps/calendar/apps/backend/src/metrics/metrics.controller.ts deleted file mode 100644 index eb3168a67..000000000 --- a/apps/calendar/apps/backend/src/metrics/metrics.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get, Header } from '@nestjs/common'; -import { MetricsService } from './metrics.service'; - -@Controller() -export class MetricsController { - constructor(private readonly metricsService: MetricsService) {} - - @Get('metrics') - @Header('Content-Type', 'text/plain; version=0.0.4; charset=utf-8') - async getMetrics(): Promise { - return this.metricsService.getMetrics(); - } -} diff --git a/apps/calendar/apps/backend/src/metrics/metrics.module.ts b/apps/calendar/apps/backend/src/metrics/metrics.module.ts deleted file mode 100644 index 32b20829b..000000000 --- a/apps/calendar/apps/backend/src/metrics/metrics.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { MetricsService } from './metrics.service'; -import { MetricsController } from './metrics.controller'; - -@Global() -@Module({ - controllers: [MetricsController], - providers: [MetricsService], - exports: [MetricsService], -}) -export class MetricsModule {} diff --git a/apps/calendar/apps/backend/src/metrics/metrics.service.ts b/apps/calendar/apps/backend/src/metrics/metrics.service.ts deleted file mode 100644 index c34cf4269..000000000 --- a/apps/calendar/apps/backend/src/metrics/metrics.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import * as client from 'prom-client'; - -@Injectable() -export class MetricsService implements OnModuleInit { - private readonly register: client.Registry; - - // HTTP metrics - readonly httpRequestsTotal: client.Counter; - readonly httpRequestDuration: client.Histogram; - - constructor() { - this.register = new client.Registry(); - - // Add default metrics (CPU, memory, event loop, etc.) - client.collectDefaultMetrics({ - register: this.register, - prefix: 'calendar_', - }); - - // HTTP request counter - this.httpRequestsTotal = new client.Counter({ - name: 'http_requests_total', - help: 'Total number of HTTP requests', - labelNames: ['method', 'route', 'status'], - registers: [this.register], - }); - - // HTTP request duration histogram - this.httpRequestDuration = new client.Histogram({ - name: 'http_request_duration_seconds', - help: 'Duration of HTTP requests in seconds', - labelNames: ['method', 'route', 'status'], - buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5], - registers: [this.register], - }); - } - - onModuleInit() { - // Metrics are ready - } - - async getMetrics(): Promise { - return this.register.metrics(); - } - - getContentType(): string { - return this.register.contentType; - } -} diff --git a/apps/chat/apps/backend/package.json b/apps/chat/apps/backend/package.json index dd2c2e087..361c3e176 100644 --- a/apps/chat/apps/backend/package.json +++ b/apps/chat/apps/backend/package.json @@ -27,6 +27,7 @@ "dependencies": { "@manacore/shared-errors": "workspace:*", "@manacore/shared-nestjs-auth": "workspace:*", + "@manacore/shared-nestjs-metrics": "workspace:*", "@nestjs/common": "^10.4.15", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.15", diff --git a/apps/chat/apps/backend/src/app.module.ts b/apps/chat/apps/backend/src/app.module.ts index 384d57344..720ceb1a9 100644 --- a/apps/chat/apps/backend/src/app.module.ts +++ b/apps/chat/apps/backend/src/app.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { MetricsModule } from '@manacore/shared-nestjs-metrics'; import { DatabaseModule } from './db/database.module'; import { ChatModule } from './chat/chat.module'; import { ConversationModule } from './conversation/conversation.module'; @@ -8,7 +9,6 @@ import { SpaceModule } from './space/space.module'; import { DocumentModule } from './document/document.module'; import { ModelModule } from './model/model.module'; import { HealthModule } from './health/health.module'; -import { MetricsModule } from './metrics'; @Module({ imports: [ @@ -16,7 +16,10 @@ import { MetricsModule } from './metrics'; isGlobal: true, envFilePath: '.env', }), - MetricsModule, + MetricsModule.register({ + prefix: 'chat_', + excludePaths: ['/health'], + }), DatabaseModule, ChatModule, ConversationModule, diff --git a/apps/chat/apps/backend/src/main.ts b/apps/chat/apps/backend/src/main.ts index ed035407b..e0dc3f67e 100644 --- a/apps/chat/apps/backend/src/main.ts +++ b/apps/chat/apps/backend/src/main.ts @@ -1,48 +1,10 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; import { AppModule } from './app.module'; -import { MetricsService } from './metrics/metrics.service'; - -// Normalize route paths to prevent high cardinality -function normalizeRoute(path: string): string { - return path - .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ':id') - .replace(/\/\d+/g, '/:id'); -} async function bootstrap() { const app = await NestFactory.create(AppModule); - // Get MetricsService for request tracking - const metricsService = app.get(MetricsService); - - // Global Express middleware to track ALL HTTP requests - app.use((req: Request, res: Response, next: NextFunction) => { - if (req.path === '/metrics') { - return next(); - } - - const startTime = Date.now(); - const method = req.method; - const route = normalizeRoute(req.path); - - res.once('finish', () => { - const duration = (Date.now() - startTime) / 1000; - metricsService.httpRequestsTotal.inc({ - method, - route, - status: res.statusCode.toString(), - }); - metricsService.httpRequestDuration.observe( - { method, route, status: res.statusCode.toString() }, - duration - ); - }); - - next(); - }); - // Enable CORS for mobile and web apps const corsOrigins = process.env.CORS_ORIGINS?.split(',').map((origin) => origin.trim()) || [ 'http://localhost:3000', diff --git a/apps/chat/apps/backend/src/metrics/index.ts b/apps/chat/apps/backend/src/metrics/index.ts deleted file mode 100644 index 860cd0cdf..000000000 --- a/apps/chat/apps/backend/src/metrics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './metrics.module'; -export * from './metrics.service'; -export * from './metrics.controller'; diff --git a/apps/chat/apps/backend/src/metrics/metrics.controller.ts b/apps/chat/apps/backend/src/metrics/metrics.controller.ts deleted file mode 100644 index eb3168a67..000000000 --- a/apps/chat/apps/backend/src/metrics/metrics.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get, Header } from '@nestjs/common'; -import { MetricsService } from './metrics.service'; - -@Controller() -export class MetricsController { - constructor(private readonly metricsService: MetricsService) {} - - @Get('metrics') - @Header('Content-Type', 'text/plain; version=0.0.4; charset=utf-8') - async getMetrics(): Promise { - return this.metricsService.getMetrics(); - } -} diff --git a/apps/chat/apps/backend/src/metrics/metrics.module.ts b/apps/chat/apps/backend/src/metrics/metrics.module.ts deleted file mode 100644 index 32b20829b..000000000 --- a/apps/chat/apps/backend/src/metrics/metrics.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { MetricsService } from './metrics.service'; -import { MetricsController } from './metrics.controller'; - -@Global() -@Module({ - controllers: [MetricsController], - providers: [MetricsService], - exports: [MetricsService], -}) -export class MetricsModule {} diff --git a/apps/chat/apps/backend/src/metrics/metrics.service.ts b/apps/chat/apps/backend/src/metrics/metrics.service.ts deleted file mode 100644 index 0e4a11910..000000000 --- a/apps/chat/apps/backend/src/metrics/metrics.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import * as client from 'prom-client'; - -@Injectable() -export class MetricsService implements OnModuleInit { - private readonly register: client.Registry; - - // HTTP metrics - readonly httpRequestsTotal: client.Counter; - readonly httpRequestDuration: client.Histogram; - - constructor() { - this.register = new client.Registry(); - - // Add default metrics (CPU, memory, event loop, etc.) - client.collectDefaultMetrics({ - register: this.register, - prefix: 'chat_', - }); - - // HTTP request counter - this.httpRequestsTotal = new client.Counter({ - name: 'http_requests_total', - help: 'Total number of HTTP requests', - labelNames: ['method', 'route', 'status'], - registers: [this.register], - }); - - // HTTP request duration histogram - this.httpRequestDuration = new client.Histogram({ - name: 'http_request_duration_seconds', - help: 'Duration of HTTP requests in seconds', - labelNames: ['method', 'route', 'status'], - buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5], - registers: [this.register], - }); - } - - onModuleInit() { - // Metrics are ready - } - - async getMetrics(): Promise { - return this.register.metrics(); - } - - getContentType(): string { - return this.register.contentType; - } -} diff --git a/apps/clock/apps/backend/package.json b/apps/clock/apps/backend/package.json index 5f30e18eb..fc76610b4 100644 --- a/apps/clock/apps/backend/package.json +++ b/apps/clock/apps/backend/package.json @@ -20,6 +20,7 @@ "dependencies": { "@clock/shared": "workspace:*", "@manacore/shared-nestjs-auth": "workspace:*", + "@manacore/shared-nestjs-metrics": "workspace:*", "@nestjs/common": "^10.4.15", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.15", diff --git a/apps/clock/apps/backend/src/app.module.ts b/apps/clock/apps/backend/src/app.module.ts index 678ac2eb4..2a0d736d1 100644 --- a/apps/clock/apps/backend/src/app.module.ts +++ b/apps/clock/apps/backend/src/app.module.ts @@ -1,13 +1,13 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; +import { MetricsModule } from '@manacore/shared-nestjs-metrics'; import { DatabaseModule } from './db/database.module'; import { HealthModule } from './health/health.module'; import { AlarmModule } from './alarm/alarm.module'; import { TimerModule } from './timer/timer.module'; import { WorldClockModule } from './world-clock/world-clock.module'; import { PresetModule } from './preset/preset.module'; -import { MetricsModule } from './metrics'; @Module({ imports: [ @@ -16,7 +16,10 @@ import { MetricsModule } from './metrics'; envFilePath: '.env', }), ScheduleModule.forRoot(), - MetricsModule, + MetricsModule.register({ + prefix: 'clock_', + excludePaths: ['/health'], + }), DatabaseModule, HealthModule, AlarmModule, diff --git a/apps/clock/apps/backend/src/main.ts b/apps/clock/apps/backend/src/main.ts index bb35eca6e..19536e827 100644 --- a/apps/clock/apps/backend/src/main.ts +++ b/apps/clock/apps/backend/src/main.ts @@ -1,48 +1,10 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; import { AppModule } from './app.module'; -import { MetricsService } from './metrics/metrics.service'; - -// Normalize route paths to prevent high cardinality -function normalizeRoute(path: string): string { - return path - .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ':id') - .replace(/\/\d+/g, '/:id'); -} async function bootstrap() { const app = await NestFactory.create(AppModule); - // Get MetricsService for request tracking - const metricsService = app.get(MetricsService); - - // Global Express middleware to track ALL HTTP requests - app.use((req: Request, res: Response, next: NextFunction) => { - if (req.path === '/metrics') { - return next(); - } - - const startTime = Date.now(); - const method = req.method; - const route = normalizeRoute(req.path); - - res.once('finish', () => { - const duration = (Date.now() - startTime) / 1000; - metricsService.httpRequestsTotal.inc({ - method, - route, - status: res.statusCode.toString(), - }); - metricsService.httpRequestDuration.observe( - { method, route, status: res.statusCode.toString() }, - duration - ); - }); - - next(); - }); - // Enable CORS for mobile and web apps const corsOrigins = process.env.CORS_ORIGINS?.split(',').map((origin) => origin.trim()) || [ 'http://localhost:3000', diff --git a/apps/clock/apps/backend/src/metrics/index.ts b/apps/clock/apps/backend/src/metrics/index.ts deleted file mode 100644 index 860cd0cdf..000000000 --- a/apps/clock/apps/backend/src/metrics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './metrics.module'; -export * from './metrics.service'; -export * from './metrics.controller'; diff --git a/apps/clock/apps/backend/src/metrics/metrics.controller.ts b/apps/clock/apps/backend/src/metrics/metrics.controller.ts deleted file mode 100644 index eb3168a67..000000000 --- a/apps/clock/apps/backend/src/metrics/metrics.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get, Header } from '@nestjs/common'; -import { MetricsService } from './metrics.service'; - -@Controller() -export class MetricsController { - constructor(private readonly metricsService: MetricsService) {} - - @Get('metrics') - @Header('Content-Type', 'text/plain; version=0.0.4; charset=utf-8') - async getMetrics(): Promise { - return this.metricsService.getMetrics(); - } -} diff --git a/apps/clock/apps/backend/src/metrics/metrics.module.ts b/apps/clock/apps/backend/src/metrics/metrics.module.ts deleted file mode 100644 index 32b20829b..000000000 --- a/apps/clock/apps/backend/src/metrics/metrics.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { MetricsService } from './metrics.service'; -import { MetricsController } from './metrics.controller'; - -@Global() -@Module({ - controllers: [MetricsController], - providers: [MetricsService], - exports: [MetricsService], -}) -export class MetricsModule {} diff --git a/apps/clock/apps/backend/src/metrics/metrics.service.ts b/apps/clock/apps/backend/src/metrics/metrics.service.ts deleted file mode 100644 index 6f3913cf2..000000000 --- a/apps/clock/apps/backend/src/metrics/metrics.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import * as client from 'prom-client'; - -@Injectable() -export class MetricsService implements OnModuleInit { - private readonly register: client.Registry; - - // HTTP metrics - readonly httpRequestsTotal: client.Counter; - readonly httpRequestDuration: client.Histogram; - - constructor() { - this.register = new client.Registry(); - - // Add default metrics (CPU, memory, event loop, etc.) - client.collectDefaultMetrics({ - register: this.register, - prefix: 'clock_', - }); - - // HTTP request counter - this.httpRequestsTotal = new client.Counter({ - name: 'http_requests_total', - help: 'Total number of HTTP requests', - labelNames: ['method', 'route', 'status'], - registers: [this.register], - }); - - // HTTP request duration histogram - this.httpRequestDuration = new client.Histogram({ - name: 'http_request_duration_seconds', - help: 'Duration of HTTP requests in seconds', - labelNames: ['method', 'route', 'status'], - buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5], - registers: [this.register], - }); - } - - onModuleInit() { - // Metrics are ready - } - - async getMetrics(): Promise { - return this.register.metrics(); - } - - getContentType(): string { - return this.register.contentType; - } -} diff --git a/apps/contacts/apps/backend/package.json b/apps/contacts/apps/backend/package.json index 8b793dcad..3666833bf 100644 --- a/apps/contacts/apps/backend/package.json +++ b/apps/contacts/apps/backend/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "@manacore/shared-nestjs-auth": "workspace:*", + "@manacore/shared-nestjs-metrics": "workspace:*", "@manacore/shared-storage": "workspace:*", "@nestjs/common": "^10.4.15", "@nestjs/config": "^3.3.0", diff --git a/apps/contacts/apps/backend/src/app.module.ts b/apps/contacts/apps/backend/src/app.module.ts index da037ba22..cf36fd6e2 100644 --- a/apps/contacts/apps/backend/src/app.module.ts +++ b/apps/contacts/apps/backend/src/app.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { MetricsModule } from '@manacore/shared-nestjs-metrics'; import { DatabaseModule } from './db/database.module'; import { ContactModule } from './contact/contact.module'; import { TagModule } from './tag/tag.module'; @@ -13,7 +14,6 @@ import { DuplicatesModule } from './duplicates/duplicates.module'; import { PhotoModule } from './photo/photo.module'; import { BatchModule } from './batch/batch.module'; import { NetworkModule } from './network/network.module'; -import { MetricsModule } from './metrics'; @Module({ imports: [ @@ -21,7 +21,10 @@ import { MetricsModule } from './metrics'; isGlobal: true, envFilePath: '.env', }), - MetricsModule, + MetricsModule.register({ + prefix: 'contacts_', + excludePaths: ['/health'], + }), DatabaseModule, ContactModule, TagModule, diff --git a/apps/contacts/apps/backend/src/main.ts b/apps/contacts/apps/backend/src/main.ts index ef7042c47..d77ea9f05 100644 --- a/apps/contacts/apps/backend/src/main.ts +++ b/apps/contacts/apps/backend/src/main.ts @@ -1,48 +1,10 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; import { AppModule } from './app.module'; -import { MetricsService } from './metrics/metrics.service'; - -// Normalize route paths to prevent high cardinality -function normalizeRoute(path: string): string { - return path - .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ':id') - .replace(/\/\d+/g, '/:id'); -} async function bootstrap() { const app = await NestFactory.create(AppModule); - // Get MetricsService for request tracking - const metricsService = app.get(MetricsService); - - // Global Express middleware to track ALL HTTP requests - app.use((req: Request, res: Response, next: NextFunction) => { - if (req.path === '/metrics') { - return next(); - } - - const startTime = Date.now(); - const method = req.method; - const route = normalizeRoute(req.path); - - res.once('finish', () => { - const duration = (Date.now() - startTime) / 1000; - metricsService.httpRequestsTotal.inc({ - method, - route, - status: res.statusCode.toString(), - }); - metricsService.httpRequestDuration.observe( - { method, route, status: res.statusCode.toString() }, - duration - ); - }); - - next(); - }); - // Enable CORS for mobile and web apps const corsOrigins = process.env.CORS_ORIGINS?.split(',').map((origin) => origin.trim()) || [ 'http://localhost:3000', diff --git a/apps/contacts/apps/backend/src/metrics/index.ts b/apps/contacts/apps/backend/src/metrics/index.ts deleted file mode 100644 index 860cd0cdf..000000000 --- a/apps/contacts/apps/backend/src/metrics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './metrics.module'; -export * from './metrics.service'; -export * from './metrics.controller'; diff --git a/apps/contacts/apps/backend/src/metrics/metrics.controller.ts b/apps/contacts/apps/backend/src/metrics/metrics.controller.ts deleted file mode 100644 index eb3168a67..000000000 --- a/apps/contacts/apps/backend/src/metrics/metrics.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get, Header } from '@nestjs/common'; -import { MetricsService } from './metrics.service'; - -@Controller() -export class MetricsController { - constructor(private readonly metricsService: MetricsService) {} - - @Get('metrics') - @Header('Content-Type', 'text/plain; version=0.0.4; charset=utf-8') - async getMetrics(): Promise { - return this.metricsService.getMetrics(); - } -} diff --git a/apps/contacts/apps/backend/src/metrics/metrics.module.ts b/apps/contacts/apps/backend/src/metrics/metrics.module.ts deleted file mode 100644 index 32b20829b..000000000 --- a/apps/contacts/apps/backend/src/metrics/metrics.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { MetricsService } from './metrics.service'; -import { MetricsController } from './metrics.controller'; - -@Global() -@Module({ - controllers: [MetricsController], - providers: [MetricsService], - exports: [MetricsService], -}) -export class MetricsModule {} diff --git a/apps/contacts/apps/backend/src/metrics/metrics.service.ts b/apps/contacts/apps/backend/src/metrics/metrics.service.ts deleted file mode 100644 index dfffa3962..000000000 --- a/apps/contacts/apps/backend/src/metrics/metrics.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import * as client from 'prom-client'; - -@Injectable() -export class MetricsService implements OnModuleInit { - private readonly register: client.Registry; - - // HTTP metrics - readonly httpRequestsTotal: client.Counter; - readonly httpRequestDuration: client.Histogram; - - constructor() { - this.register = new client.Registry(); - - // Add default metrics (CPU, memory, event loop, etc.) - client.collectDefaultMetrics({ - register: this.register, - prefix: 'contacts_', - }); - - // HTTP request counter - this.httpRequestsTotal = new client.Counter({ - name: 'http_requests_total', - help: 'Total number of HTTP requests', - labelNames: ['method', 'route', 'status'], - registers: [this.register], - }); - - // HTTP request duration histogram - this.httpRequestDuration = new client.Histogram({ - name: 'http_request_duration_seconds', - help: 'Duration of HTTP requests in seconds', - labelNames: ['method', 'route', 'status'], - buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5], - registers: [this.register], - }); - } - - onModuleInit() { - // Metrics are ready - } - - async getMetrics(): Promise { - return this.register.metrics(); - } - - getContentType(): string { - return this.register.contentType; - } -} diff --git a/apps/skilltree/apps/backend/package.json b/apps/skilltree/apps/backend/package.json index 2f1dd3c59..489a9021e 100644 --- a/apps/skilltree/apps/backend/package.json +++ b/apps/skilltree/apps/backend/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@manacore/shared-nestjs-auth": "workspace:*", + "@manacore/shared-nestjs-metrics": "workspace:*", "@nestjs/common": "^10.4.9", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.9", diff --git a/apps/skilltree/apps/backend/src/app.module.ts b/apps/skilltree/apps/backend/src/app.module.ts index 1cc59968e..5a5bf55b2 100644 --- a/apps/skilltree/apps/backend/src/app.module.ts +++ b/apps/skilltree/apps/backend/src/app.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { MetricsModule } from '@manacore/shared-nestjs-metrics'; import { DatabaseModule } from './db/database.module'; import { HealthModule } from './health/health.module'; -import { MetricsModule } from './metrics'; import { SkillModule } from './skill/skill.module'; import { ActivityModule } from './activity/activity.module'; @@ -12,7 +12,10 @@ import { ActivityModule } from './activity/activity.module'; isGlobal: true, envFilePath: '.env', }), - MetricsModule, + MetricsModule.register({ + prefix: 'skilltree_', + excludePaths: ['/health'], + }), DatabaseModule, HealthModule, SkillModule, diff --git a/apps/skilltree/apps/backend/src/main.ts b/apps/skilltree/apps/backend/src/main.ts index 3cd031213..659f37cab 100644 --- a/apps/skilltree/apps/backend/src/main.ts +++ b/apps/skilltree/apps/backend/src/main.ts @@ -1,49 +1,11 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe, Logger } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; import { AppModule } from './app.module'; -import { MetricsService } from './metrics/metrics.service'; - -// Normalize route paths to prevent high cardinality -function normalizeRoute(path: string): string { - return path - .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ':id') - .replace(/\/\d+/g, '/:id'); -} async function bootstrap() { const logger = new Logger('Bootstrap'); const app = await NestFactory.create(AppModule); - // Get MetricsService for request tracking - const metricsService = app.get(MetricsService); - - // Global Express middleware to track ALL HTTP requests - app.use((req: Request, res: Response, next: NextFunction) => { - if (req.path === '/metrics') { - return next(); - } - - const startTime = Date.now(); - const method = req.method; - const route = normalizeRoute(req.path); - - res.once('finish', () => { - const duration = (Date.now() - startTime) / 1000; - metricsService.httpRequestsTotal.inc({ - method, - route, - status: res.statusCode.toString(), - }); - metricsService.httpRequestDuration.observe( - { method, route, status: res.statusCode.toString() }, - duration - ); - }); - - next(); - }); - // Enable CORS app.enableCors({ origin: (origin, callback) => { diff --git a/apps/skilltree/apps/backend/src/metrics/index.ts b/apps/skilltree/apps/backend/src/metrics/index.ts deleted file mode 100644 index 860cd0cdf..000000000 --- a/apps/skilltree/apps/backend/src/metrics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './metrics.module'; -export * from './metrics.service'; -export * from './metrics.controller'; diff --git a/apps/skilltree/apps/backend/src/metrics/metrics.controller.ts b/apps/skilltree/apps/backend/src/metrics/metrics.controller.ts deleted file mode 100644 index 4ee665772..000000000 --- a/apps/skilltree/apps/backend/src/metrics/metrics.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get, Header } from '@nestjs/common'; -import { MetricsService } from './metrics.service'; - -@Controller('metrics') -export class MetricsController { - constructor(private metricsService: MetricsService) {} - - @Get() - @Header('Content-Type', 'text/plain') - async getMetrics(): Promise { - return this.metricsService.getMetrics(); - } -} diff --git a/apps/skilltree/apps/backend/src/metrics/metrics.module.ts b/apps/skilltree/apps/backend/src/metrics/metrics.module.ts deleted file mode 100644 index 6c262956a..000000000 --- a/apps/skilltree/apps/backend/src/metrics/metrics.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { MetricsController } from './metrics.controller'; -import { MetricsService } from './metrics.service'; - -@Global() -@Module({ - controllers: [MetricsController], - providers: [MetricsService], - exports: [MetricsService], -}) -export class MetricsModule {} diff --git a/apps/skilltree/apps/backend/src/metrics/metrics.service.ts b/apps/skilltree/apps/backend/src/metrics/metrics.service.ts deleted file mode 100644 index 0b9d56083..000000000 --- a/apps/skilltree/apps/backend/src/metrics/metrics.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { collectDefaultMetrics, Counter, Histogram, Registry } from 'prom-client'; - -@Injectable() -export class MetricsService implements OnModuleInit { - private registry: Registry; - - public httpRequestsTotal: Counter; - public httpRequestDuration: Histogram; - - constructor() { - this.registry = new Registry(); - - this.httpRequestsTotal = new Counter({ - name: 'http_requests_total', - help: 'Total number of HTTP requests', - labelNames: ['method', 'route', 'status'], - registers: [this.registry], - }); - - this.httpRequestDuration = new Histogram({ - name: 'http_request_duration_seconds', - help: 'Duration of HTTP requests in seconds', - labelNames: ['method', 'route', 'status'], - buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5], - registers: [this.registry], - }); - } - - onModuleInit() { - collectDefaultMetrics({ register: this.registry }); - } - - async getMetrics(): Promise { - return this.registry.metrics(); - } -} diff --git a/apps/todo/apps/backend/package.json b/apps/todo/apps/backend/package.json index 4aff1041f..771ddb1d1 100644 --- a/apps/todo/apps/backend/package.json +++ b/apps/todo/apps/backend/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "@manacore/shared-nestjs-auth": "workspace:*", + "@manacore/shared-nestjs-metrics": "workspace:*", "@nestjs/common": "^10.4.9", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.9", diff --git a/apps/todo/apps/backend/src/app.module.ts b/apps/todo/apps/backend/src/app.module.ts index 702d910cd..d78c21394 100644 --- a/apps/todo/apps/backend/src/app.module.ts +++ b/apps/todo/apps/backend/src/app.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; +import { MetricsModule } from '@manacore/shared-nestjs-metrics'; import { DatabaseModule } from './db/database.module'; import { HealthModule } from './health/health.module'; import { ProjectModule } from './project/project.module'; @@ -9,7 +10,6 @@ import { LabelModule } from './label/label.module'; import { ReminderModule } from './reminder/reminder.module'; import { KanbanModule } from './kanban/kanban.module'; import { NetworkModule } from './network/network.module'; -import { MetricsModule } from './metrics'; @Module({ imports: [ @@ -18,7 +18,10 @@ import { MetricsModule } from './metrics'; envFilePath: '.env', }), ScheduleModule.forRoot(), - MetricsModule, + MetricsModule.register({ + prefix: 'todo_', + excludePaths: ['/health'], + }), DatabaseModule, HealthModule, ProjectModule, diff --git a/apps/todo/apps/backend/src/main.ts b/apps/todo/apps/backend/src/main.ts index 273da3d06..75c1b53fc 100644 --- a/apps/todo/apps/backend/src/main.ts +++ b/apps/todo/apps/backend/src/main.ts @@ -1,51 +1,11 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe, Logger } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; import { AppModule } from './app.module'; -import { MetricsService } from './metrics/metrics.service'; - -// Normalize route paths to prevent high cardinality -function normalizeRoute(path: string): string { - return path - .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ':id') - .replace(/\/\d+/g, '/:id'); -} async function bootstrap() { const logger = new Logger('Bootstrap'); const app = await NestFactory.create(AppModule); - // Get MetricsService for request tracking - const metricsService = app.get(MetricsService); - - // Global Express middleware to track ALL HTTP requests - // This runs before guards/interceptors, so it catches auth failures etc. - app.use((req: Request, res: Response, next: NextFunction) => { - // Skip metrics endpoint - if (req.path === '/metrics') { - return next(); - } - - const startTime = Date.now(); - const method = req.method; - const route = normalizeRoute(req.path); - - res.once('finish', () => { - const duration = (Date.now() - startTime) / 1000; - metricsService.httpRequestsTotal.inc({ - method, - route, - status: res.statusCode.toString(), - }); - metricsService.httpRequestDuration.observe( - { method, route, status: res.statusCode.toString() }, - duration - ); - }); - - next(); - }); - // Enable CORS for all platforms app.enableCors({ origin: (origin, callback) => { diff --git a/apps/todo/apps/backend/src/metrics/index.ts b/apps/todo/apps/backend/src/metrics/index.ts deleted file mode 100644 index 860cd0cdf..000000000 --- a/apps/todo/apps/backend/src/metrics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './metrics.module'; -export * from './metrics.service'; -export * from './metrics.controller'; diff --git a/apps/todo/apps/backend/src/metrics/metrics.controller.ts b/apps/todo/apps/backend/src/metrics/metrics.controller.ts deleted file mode 100644 index 60825246a..000000000 --- a/apps/todo/apps/backend/src/metrics/metrics.controller.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Controller, Get, Header, Res } from '@nestjs/common'; -import { Response } from 'express'; -import { MetricsService } from './metrics.service'; - -@Controller() -export class MetricsController { - constructor(private readonly metricsService: MetricsService) {} - - @Get('metrics') - async getMetrics(@Res() res: Response): Promise { - res.set('Content-Type', this.metricsService.getContentType()); - res.send(await this.metricsService.getMetrics()); - } -} diff --git a/apps/todo/apps/backend/src/metrics/metrics.module.ts b/apps/todo/apps/backend/src/metrics/metrics.module.ts deleted file mode 100644 index 32b20829b..000000000 --- a/apps/todo/apps/backend/src/metrics/metrics.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { MetricsService } from './metrics.service'; -import { MetricsController } from './metrics.controller'; - -@Global() -@Module({ - controllers: [MetricsController], - providers: [MetricsService], - exports: [MetricsService], -}) -export class MetricsModule {} diff --git a/apps/todo/apps/backend/src/metrics/metrics.service.ts b/apps/todo/apps/backend/src/metrics/metrics.service.ts deleted file mode 100644 index 015f64b2f..000000000 --- a/apps/todo/apps/backend/src/metrics/metrics.service.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import * as client from 'prom-client'; - -@Injectable() -export class MetricsService implements OnModuleInit { - private readonly register: client.Registry; - - // HTTP metrics - readonly httpRequestsTotal: client.Counter; - readonly httpRequestDuration: client.Histogram; - - // Business metrics - readonly tasksCreated: client.Counter; - readonly tasksCompleted: client.Counter; - - constructor() { - this.register = new client.Registry(); - - // Add default metrics (CPU, memory, event loop, etc.) - client.collectDefaultMetrics({ - register: this.register, - prefix: 'todo_', - }); - - // HTTP request counter - this.httpRequestsTotal = new client.Counter({ - name: 'http_requests_total', - help: 'Total number of HTTP requests', - labelNames: ['method', 'route', 'status'], - registers: [this.register], - }); - - // HTTP request duration histogram - this.httpRequestDuration = new client.Histogram({ - name: 'http_request_duration_seconds', - help: 'Duration of HTTP requests in seconds', - labelNames: ['method', 'route', 'status'], - buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5], - registers: [this.register], - }); - - // Business metrics - this.tasksCreated = new client.Counter({ - name: 'todo_tasks_created_total', - help: 'Total number of tasks created', - registers: [this.register], - }); - - this.tasksCompleted = new client.Counter({ - name: 'todo_tasks_completed_total', - help: 'Total number of tasks completed', - registers: [this.register], - }); - } - - onModuleInit() { - // Metrics are ready - } - - async getMetrics(): Promise { - return this.register.metrics(); - } - - getContentType(): string { - return this.register.contentType; - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9be323785..86b1639b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: '@manacore/shared-nestjs-auth': specifier: workspace:* version: link:../../../../packages/shared-nestjs-auth + '@manacore/shared-nestjs-metrics': + specifier: workspace:* + version: link:../../../../packages/shared-nestjs-metrics '@nestjs/common': specifier: ^10.4.15 version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -379,6 +382,9 @@ importers: '@manacore/shared-nestjs-auth': specifier: workspace:* version: link:../../../../packages/shared-nestjs-auth + '@manacore/shared-nestjs-metrics': + specifier: workspace:* + version: link:../../../../packages/shared-nestjs-metrics '@nestjs/common': specifier: ^10.4.15 version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -715,6 +721,9 @@ importers: '@manacore/shared-nestjs-auth': specifier: workspace:* version: link:../../../../packages/shared-nestjs-auth + '@manacore/shared-nestjs-metrics': + specifier: workspace:* + version: link:../../../../packages/shared-nestjs-metrics '@nestjs/common': specifier: ^10.4.15 version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -979,6 +988,9 @@ importers: '@manacore/shared-nestjs-auth': specifier: workspace:* version: link:../../../../packages/shared-nestjs-auth + '@manacore/shared-nestjs-metrics': + specifier: workspace:* + version: link:../../../../packages/shared-nestjs-metrics '@manacore/shared-storage': specifier: workspace:* version: link:../../../../packages/shared-storage @@ -3642,6 +3654,9 @@ importers: '@manacore/shared-nestjs-auth': specifier: workspace:* version: link:../../../../packages/shared-nestjs-auth + '@manacore/shared-nestjs-metrics': + specifier: workspace:* + version: link:../../../../packages/shared-nestjs-metrics '@nestjs/common': specifier: ^10.4.9 version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -3999,6 +4014,9 @@ importers: '@manacore/shared-nestjs-auth': specifier: workspace:* version: link:../../../../packages/shared-nestjs-auth + '@manacore/shared-nestjs-metrics': + specifier: workspace:* + version: link:../../../../packages/shared-nestjs-metrics '@nestjs/common': specifier: ^10.4.9 version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)