mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:41:08 +02:00
feat(metrics): add Prometheus metrics to all backends
- Add metrics module to calendar, chat, clock, contacts backends - Add metrics module to mana-core-auth service - Expose /metrics endpoint for Prometheus scraping - Track HTTP requests, response times, and custom business metrics Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
75ffd504bc
commit
1c5a1b8442
35 changed files with 611 additions and 14 deletions
|
|
@ -32,6 +32,7 @@
|
|||
"drizzle-orm": "^0.38.3",
|
||||
"ical.js": "^2.1.0",
|
||||
"postgres": "^3.4.5",
|
||||
"prom-client": "^15.1.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"tsdav": "^2.1.1"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { EventTagGroupModule } from './event-tag-group/event-tag-group.module';
|
|||
import { ReminderModule } from './reminder/reminder.module';
|
||||
import { ShareModule } from './share/share.module';
|
||||
import { NetworkModule } from './network/network.module';
|
||||
import { MetricsModule } from './metrics';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -18,6 +19,7 @@ import { NetworkModule } from './network/network.module';
|
|||
envFilePath: '.env',
|
||||
}),
|
||||
ScheduleModule.forRoot(),
|
||||
MetricsModule,
|
||||
DatabaseModule,
|
||||
HealthModule,
|
||||
CalendarModule,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,48 @@
|
|||
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',
|
||||
|
|
@ -30,8 +68,10 @@ async function bootstrap() {
|
|||
})
|
||||
);
|
||||
|
||||
// Set global prefix for API routes
|
||||
app.setGlobalPrefix('api/v1');
|
||||
// Set global prefix for API routes (exclude metrics endpoint)
|
||||
app.setGlobalPrefix('api/v1', {
|
||||
exclude: ['metrics', 'health'],
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3014;
|
||||
await app.listen(port);
|
||||
|
|
|
|||
3
apps/calendar/apps/backend/src/metrics/index.ts
Normal file
3
apps/calendar/apps/backend/src/metrics/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './metrics.module';
|
||||
export * from './metrics.service';
|
||||
export * from './metrics.controller';
|
||||
13
apps/calendar/apps/backend/src/metrics/metrics.controller.ts
Normal file
13
apps/calendar/apps/backend/src/metrics/metrics.controller.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
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<string> {
|
||||
return this.metricsService.getMetrics();
|
||||
}
|
||||
}
|
||||
11
apps/calendar/apps/backend/src/metrics/metrics.module.ts
Normal file
11
apps/calendar/apps/backend/src/metrics/metrics.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
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 {}
|
||||
50
apps/calendar/apps/backend/src/metrics/metrics.service.ts
Normal file
50
apps/calendar/apps/backend/src/metrics/metrics.service.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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<string>;
|
||||
readonly httpRequestDuration: client.Histogram<string>;
|
||||
|
||||
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<string> {
|
||||
return this.register.metrics();
|
||||
}
|
||||
|
||||
getContentType(): string {
|
||||
return this.register.contentType;
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@
|
|||
"drizzle-orm": "^0.38.3",
|
||||
"openai": "^4.77.0",
|
||||
"postgres": "^3.4.5",
|
||||
"prom-client": "^15.1.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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: [
|
||||
|
|
@ -15,6 +16,7 @@ import { HealthModule } from './health/health.module';
|
|||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
MetricsModule,
|
||||
DatabaseModule,
|
||||
ChatModule,
|
||||
ConversationModule,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,48 @@
|
|||
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',
|
||||
|
|
@ -22,9 +60,6 @@ async function bootstrap() {
|
|||
credentials: true,
|
||||
});
|
||||
|
||||
// Global exception filter will be added later via module
|
||||
// app.useGlobalFilters(new AppExceptionFilter());
|
||||
|
||||
// Enable validation
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
|
|
@ -34,8 +69,10 @@ async function bootstrap() {
|
|||
})
|
||||
);
|
||||
|
||||
// Set global prefix for API routes
|
||||
app.setGlobalPrefix('api/v1');
|
||||
// Set global prefix for API routes (exclude metrics endpoint)
|
||||
app.setGlobalPrefix('api/v1', {
|
||||
exclude: ['metrics', 'health'],
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3002;
|
||||
await app.listen(port);
|
||||
|
|
|
|||
3
apps/chat/apps/backend/src/metrics/index.ts
Normal file
3
apps/chat/apps/backend/src/metrics/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './metrics.module';
|
||||
export * from './metrics.service';
|
||||
export * from './metrics.controller';
|
||||
13
apps/chat/apps/backend/src/metrics/metrics.controller.ts
Normal file
13
apps/chat/apps/backend/src/metrics/metrics.controller.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
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<string> {
|
||||
return this.metricsService.getMetrics();
|
||||
}
|
||||
}
|
||||
11
apps/chat/apps/backend/src/metrics/metrics.module.ts
Normal file
11
apps/chat/apps/backend/src/metrics/metrics.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
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 {}
|
||||
50
apps/chat/apps/backend/src/metrics/metrics.service.ts
Normal file
50
apps/chat/apps/backend/src/metrics/metrics.service.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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<string>;
|
||||
readonly httpRequestDuration: client.Histogram<string>;
|
||||
|
||||
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<string> {
|
||||
return this.register.metrics();
|
||||
}
|
||||
|
||||
getContentType(): string {
|
||||
return this.register.contentType;
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@
|
|||
"drizzle-kit": "^0.30.2",
|
||||
"drizzle-orm": "^0.38.3",
|
||||
"postgres": "^3.4.5",
|
||||
"prom-client": "^15.1.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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: [
|
||||
|
|
@ -15,6 +16,7 @@ import { PresetModule } from './preset/preset.module';
|
|||
envFilePath: '.env',
|
||||
}),
|
||||
ScheduleModule.forRoot(),
|
||||
MetricsModule,
|
||||
DatabaseModule,
|
||||
HealthModule,
|
||||
AlarmModule,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,48 @@
|
|||
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',
|
||||
|
|
@ -30,8 +68,10 @@ async function bootstrap() {
|
|||
})
|
||||
);
|
||||
|
||||
// Set global prefix for API routes
|
||||
app.setGlobalPrefix('api/v1');
|
||||
// Set global prefix for API routes (exclude metrics endpoint)
|
||||
app.setGlobalPrefix('api/v1', {
|
||||
exclude: ['metrics', 'health'],
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3017;
|
||||
await app.listen(port);
|
||||
|
|
|
|||
3
apps/clock/apps/backend/src/metrics/index.ts
Normal file
3
apps/clock/apps/backend/src/metrics/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './metrics.module';
|
||||
export * from './metrics.service';
|
||||
export * from './metrics.controller';
|
||||
13
apps/clock/apps/backend/src/metrics/metrics.controller.ts
Normal file
13
apps/clock/apps/backend/src/metrics/metrics.controller.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
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<string> {
|
||||
return this.metricsService.getMetrics();
|
||||
}
|
||||
}
|
||||
11
apps/clock/apps/backend/src/metrics/metrics.module.ts
Normal file
11
apps/clock/apps/backend/src/metrics/metrics.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
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 {}
|
||||
50
apps/clock/apps/backend/src/metrics/metrics.service.ts
Normal file
50
apps/clock/apps/backend/src/metrics/metrics.service.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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<string>;
|
||||
readonly httpRequestDuration: client.Histogram<string>;
|
||||
|
||||
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<string> {
|
||||
return this.register.metrics();
|
||||
}
|
||||
|
||||
getContentType(): string {
|
||||
return this.register.contentType;
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@
|
|||
"dependencies": {
|
||||
"@manacore/shared-nestjs-auth": "workspace:*",
|
||||
"@manacore/shared-storage": "workspace:*",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
|
|
@ -32,7 +31,9 @@
|
|||
"drizzle-kit": "^0.30.2",
|
||||
"drizzle-orm": "^0.38.3",
|
||||
"googleapis": "^144.0.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"postgres": "^3.4.5",
|
||||
"prom-client": "^15.1.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"vcard4": "^4.0.2"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ 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: [
|
||||
|
|
@ -20,6 +21,7 @@ import { NetworkModule } from './network/network.module';
|
|||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
MetricsModule,
|
||||
DatabaseModule,
|
||||
ContactModule,
|
||||
TagModule,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,48 @@
|
|||
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',
|
||||
|
|
@ -30,8 +68,10 @@ async function bootstrap() {
|
|||
})
|
||||
);
|
||||
|
||||
// Set global prefix for API routes
|
||||
app.setGlobalPrefix('api/v1');
|
||||
// Set global prefix for API routes (exclude metrics endpoint)
|
||||
app.setGlobalPrefix('api/v1', {
|
||||
exclude: ['metrics', 'health'],
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3015;
|
||||
await app.listen(port);
|
||||
|
|
|
|||
3
apps/contacts/apps/backend/src/metrics/index.ts
Normal file
3
apps/contacts/apps/backend/src/metrics/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './metrics.module';
|
||||
export * from './metrics.service';
|
||||
export * from './metrics.controller';
|
||||
13
apps/contacts/apps/backend/src/metrics/metrics.controller.ts
Normal file
13
apps/contacts/apps/backend/src/metrics/metrics.controller.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
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<string> {
|
||||
return this.metricsService.getMetrics();
|
||||
}
|
||||
}
|
||||
11
apps/contacts/apps/backend/src/metrics/metrics.module.ts
Normal file
11
apps/contacts/apps/backend/src/metrics/metrics.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
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 {}
|
||||
50
apps/contacts/apps/backend/src/metrics/metrics.service.ts
Normal file
50
apps/contacts/apps/backend/src/metrics/metrics.service.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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<string>;
|
||||
readonly httpRequestDuration: client.Histogram<string>;
|
||||
|
||||
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<string> {
|
||||
return this.register.metrics();
|
||||
}
|
||||
|
||||
getContentType(): string {
|
||||
return this.register.contentType;
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@
|
|||
"jsonwebtoken": "^9.0.2",
|
||||
"nanoid": "^5.0.9",
|
||||
"postgres": "^3.4.5",
|
||||
"prom-client": "^15.1.0",
|
||||
"redis": "^4.7.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { SettingsModule } from './settings/settings.module';
|
|||
import { TagsModule } from './tags/tags.module';
|
||||
import { AiModule } from './ai/ai.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { MetricsModule } from './metrics';
|
||||
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||
|
||||
@Module({
|
||||
|
|
@ -25,6 +26,7 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
|||
limit: 100, // 100 requests per minute
|
||||
},
|
||||
]),
|
||||
MetricsModule,
|
||||
AiModule,
|
||||
AuthModule,
|
||||
CreditsModule,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,53 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cookieParser from 'cookie-parser';
|
||||
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);
|
||||
|
||||
const configService = app.get(ConfigService);
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
// Security middleware - configure helmet to allow CORS
|
||||
app.use(
|
||||
helmet({
|
||||
|
|
@ -41,8 +79,10 @@ async function bootstrap() {
|
|||
})
|
||||
);
|
||||
|
||||
// Global prefix
|
||||
app.setGlobalPrefix('api/v1');
|
||||
// Global prefix (exclude metrics endpoint)
|
||||
app.setGlobalPrefix('api/v1', {
|
||||
exclude: ['metrics', 'health'],
|
||||
});
|
||||
|
||||
const port = configService.get<number>('port') || 3001;
|
||||
await app.listen(port);
|
||||
|
|
|
|||
3
services/mana-core-auth/src/metrics/index.ts
Normal file
3
services/mana-core-auth/src/metrics/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './metrics.module';
|
||||
export * from './metrics.service';
|
||||
export * from './metrics.controller';
|
||||
13
services/mana-core-auth/src/metrics/metrics.controller.ts
Normal file
13
services/mana-core-auth/src/metrics/metrics.controller.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
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<string> {
|
||||
return this.metricsService.getMetrics();
|
||||
}
|
||||
}
|
||||
11
services/mana-core-auth/src/metrics/metrics.module.ts
Normal file
11
services/mana-core-auth/src/metrics/metrics.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
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 {}
|
||||
50
services/mana-core-auth/src/metrics/metrics.service.ts
Normal file
50
services/mana-core-auth/src/metrics/metrics.service.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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<string>;
|
||||
readonly httpRequestDuration: client.Histogram<string>;
|
||||
|
||||
constructor() {
|
||||
this.register = new client.Registry();
|
||||
|
||||
// Add default metrics (CPU, memory, event loop, etc.)
|
||||
client.collectDefaultMetrics({
|
||||
register: this.register,
|
||||
prefix: 'auth_',
|
||||
});
|
||||
|
||||
// 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<string> {
|
||||
return this.register.metrics();
|
||||
}
|
||||
|
||||
getContentType(): string {
|
||||
return this.register.contentType;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue