mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:21:10 +02:00
♻️ refactor: migrate 6 backends to shared-nestjs-metrics
Replace local MetricsService implementations with @manacore/shared-nestjs-metrics:
- chat, calendar, todo, clock, contacts, skilltree
Removes ~350 LOC of duplicated metrics code:
- Delete local metrics directories (service, module, controller)
- Remove manual metrics middleware from main.ts
- Use MetricsModule.register({ prefix: 'app_' }) pattern
Part of consolidation effort - see docs/CONSOLIDATION_OPPORTUNITIES.md
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f0adea04fd
commit
a1ca002081
43 changed files with 54 additions and 709 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export * from './metrics.module';
|
||||
export * from './metrics.service';
|
||||
export * from './metrics.controller';
|
||||
|
|
@ -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<string> {
|
||||
return this.metricsService.getMetrics();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export * from './metrics.module';
|
||||
export * from './metrics.service';
|
||||
export * from './metrics.controller';
|
||||
|
|
@ -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<string> {
|
||||
return this.metricsService.getMetrics();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export * from './metrics.module';
|
||||
export * from './metrics.service';
|
||||
export * from './metrics.controller';
|
||||
|
|
@ -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<string> {
|
||||
return this.metricsService.getMetrics();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export * from './metrics.module';
|
||||
export * from './metrics.service';
|
||||
export * from './metrics.controller';
|
||||
|
|
@ -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<string> {
|
||||
return this.metricsService.getMetrics();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export * from './metrics.module';
|
||||
export * from './metrics.service';
|
||||
export * from './metrics.controller';
|
||||
|
|
@ -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<string> {
|
||||
return this.metricsService.getMetrics();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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<string> {
|
||||
return this.registry.metrics();
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export * from './metrics.module';
|
||||
export * from './metrics.service';
|
||||
export * from './metrics.controller';
|
||||
|
|
@ -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<void> {
|
||||
res.set('Content-Type', this.metricsService.getContentType());
|
||||
res.send(await this.metricsService.getMetrics());
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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<string>;
|
||||
readonly httpRequestDuration: client.Histogram<string>;
|
||||
|
||||
// Business metrics
|
||||
readonly tasksCreated: client.Counter<string>;
|
||||
readonly tasksCompleted: client.Counter<string>;
|
||||
|
||||
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<string> {
|
||||
return this.register.metrics();
|
||||
}
|
||||
|
||||
getContentType(): string {
|
||||
return this.register.contentType;
|
||||
}
|
||||
}
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue